Repository: vitessio/vitess (Go)

Issue:
Bug Report: Shard ranges for 103 & 107 shards end in -ff
### Overview of the Issue

There is a bug in the way the shard ranges are generated for 103 & 107 shards.
For example.
```
vtctldclient --server=$host:15999 GenerateShardRanges 102 | tail -n 2
  "fd-"
]
```
```
vtctldclient --server=$host:15999 GenerateShardRanges 2 | tail -n 2
  "80-"
]
```
You can see that the ending ranges for 102 & 2 are what we typically expect.

But when you use 103 or 107, you get the ending range ending in `-ff`. 
```
vtctldclient --server=$host:15999 GenerateShardRanges 103 | tail -n 2
  "fd-ff"
]

vtctldclient --server=$host:15999 GenerateShardRanges 107 | tail -n 2
  "fd-ff"
]
```

I have not deployed a keyspace with 103 or 107 shards, so not sure how the keyspace would work, but the output from the range generation logic seems to give unexpected results.


### Reproduction Steps

```
Using the latest main, create a test cluster.
$ minikube start --kubernetes-version=v1.27.11 --cpus=4 --memory=11000 --disk-size=32g
$ kubectl apply -f examples/operator/operator.yaml
$ kubectl apply -f examples/operator/101_initial_cluster.yaml

And hit the vtctld service with the vtctldclient command shared above.
```


### Binary Version

```sh
$ /vt/bin/vtgate --version
vtgate version Version: 20.0.0-SNAPSHOT (Git revision bec711bb837582ae32d81fa997ea30e08e1c5598 branch 'main') built on Fri May 31 19:18:34 UTC 2024 by vitess@buildkitsandbox using go1.22.3 linux/amd64
```



### Operating System and Environment details

```sh
NA
```


### Log Fragments

```sh
NA
```



Discussion:
This seems important to fix before v20 because vtop is affected. I'll check if I can repro with small numbers like 3 and 5.
There's something special about 103 and 107 :)
Works fine with 3,5,7,9,55,101,105,109,111.
Given that, I'd say that this isn't critical to fix, though certainly nice to fix.

```
% for i in 3 5 7 9 101 105 109 111
for> do
for> vtctldclient GenerateShardRanges $i | tail -n 2 | head -n 1
for> done
  "aa-"
  "cc-"
  "db-"
  "e3-"
  "fd-"
  "fd-"
  "fd-"
  "fd-"
```
> There's something special about 103 and 107 :) Works fine with 3,5,7,9,55,101,105,109,111. Given that, I'd say that this isn't critical to fix, though certainly nice to fix.
> 
> ```
> % for i in 3 5 7 9 101 105 109 111
> for> do
> for> vtctldclient GenerateShardRanges $i | tail -n 2 | head -n 1
> for> done
>   "aa-"
>   "cc-"
>   "db-"
>   "e3-"
>   "fd-"
>   "fd-"
>   "fd-"
>   "fd-"
> ```

Yes, its just 103 & 107 that are effected. I ran tests upto 128 shards.



Patch:
diff --git a/go/vt/key/key.go b/go/vt/key/key.go
index 82852daa16e..09d4bad2975 100644
--- a/go/vt/key/key.go
+++ b/go/vt/key/key.go
@@ -438,7 +438,7 @@ func GenerateShardRanges(shards int) ([]string, error) {
 	realEnd := float64(0)
 	shardRanges := make([]string, 0, shards)
 
-	for i := 1; i <= shards; i++ {
+	for i := 1; i < shards; i++ {
 		realEnd = float64(i) * size
 
 		end = int(realEnd)
@@ -446,5 +446,7 @@ func GenerateShardRanges(shards int) ([]string, error) {
 		start = end
 	}
 
+	shardRanges = append(shardRanges, rangeFormatter(start, maxShards))
+
 	return shardRanges, nil
 }


Test patch:
diff --git a/go/vt/key/key_test.go b/go/vt/key/key_test.go
index 84d4365ff0e..00ba4477e2b 100644
--- a/go/vt/key/key_test.go
+++ b/go/vt/key/key_test.go
@@ -18,6 +18,7 @@ package key
 
 import (
 	"encoding/hex"
+	"fmt"
 	"strings"
 	"testing"
 
@@ -1523,6 +1524,24 @@ func TestGenerateShardRanges(t *testing.T) {
 			[]string{"-24", "24-49", "49-6d", "6d-92", "92-b6", "b6-db", "db-"},
 			false,
 		},
+		{
+			"works for 49 shards",
+			args{49},
+			[]string{"-05", "05-0a", "0a-0f", "0f-14", "14-1a", "1a-1f", "1f-24", "24-29", "29-2f", "2f-34", "34-39", "39-3e", "3e-43", "43-49", "49-4e", "4e-53", "53-58", "58-5e", "5e-63", "63-68", "68-6d", "6d-72", "72-78", "78-7d", "7d-82", "82-87", "87-8d", "8d-92", "92-97", "97-9c", "9c-a1", "a1-a7", "a7-ac", "ac-b1", "b1-b6", "b6-bc", "bc-c1", "c1-c6", "c6-cb", "cb-d0", "d0-d6", "d6-db", "db-e0", "e0-e5", "e5-eb", "eb-f0", "f0-f5", "f5-fa", "fa-"},
+			false,
+		},
+		{
+			"works for 103 shards",
+			args{103},
+			[]string{"-02", "02-04", "04-07", "07-09", "09-0c", "0c-0e", "0e-11", "11-13", "13-16", "16-18", "18-1b", "1b-1d", "1d-20", "20-22", "22-25", "25-27", "27-2a", "2a-2c", "2c-2f", "2f-31", "31-34", "34-36", "36-39", "39-3b", "3b-3e", "3e-40", "40-43", "43-45", "45-48", "48-4a", "4a-4d", "4d-4f", "4f-52", "52-54", "54-56", "56-59", "59-5b", "5b-5e", "5e-60", "60-63", "63-65", "65-68", "68-6a", "6a-6d", "6d-6f", "6f-72", "72-74", "74-77", "77-79", "79-7c", "7c-7e", "7e-81", "81-83", "83-86", "86-88", "88-8b", "8b-8d", "8d-90", "90-92", "92-95", "95-97", "97-9a", "9a-9c", "9c-9f", "9f-a1", "a1-a4", "a4-a6", "a6-a9", "a9-ab", "ab-ad", "ad-b0", "b0-b2", "b2-b5", "b5-b7", "b7-ba", "ba-bc", "bc-bf", "bf-c1", "c1-c4", "c4-c6", "c6-c9", "c9-cb", "cb-ce", "ce-d0", "d0-d3", "d3-d5", "d5-d8", "d8-da", "da-dd", "dd-df", "df-e2", "e2-e4", "e4-e7", "e7-e9", "e9-ec", "ec-ee", "ee-f1", "f1-f3", "f3-f6", "f6-f8", "f8-fb", "fb-fd", "fd-"},
+			false,
+		},
+		{
+			"works for 107 shards",
+			args{107},
+			[]string{"-02", "02-04", "04-07", "07-09", "09-0b", "0b-0e", "0e-10", "10-13", "13-15", "15-17", "17-1a", "1a-1c", "1c-1f", "1f-21", "21-23", "23-26", "26-28", "28-2b", "2b-2d", "2d-2f", "2f-32", "32-34", "34-37", "37-39", "39-3b", "3b-3e", "3e-40", "40-42", "42-45", "45-47", "47-4a", "4a-4c", "4c-4e", "4e-51", "51-53", "53-56", "56-58", "58-5a", "5a-5d", "5d-5f", "5f-62", "62-64", "64-66", "66-69", "69-6b", "6b-6e", "6e-70", "70-72", "72-75", "75-77", "77-7a", "7a-7c", "7c-7e", "7e-81", "81-83", "83-85", "85-88", "88-8a", "8a-8d", "8d-8f", "8f-91", "91-94", "94-96", "96-99", "99-9b", "9b-9d", "9d-a0", "a0-a2", "a2-a5", "a5-a7", "a7-a9", "a9-ac", "ac-ae", "ae-b1", "b1-b3", "b3-b5", "b5-b8", "b8-ba", "ba-bd", "bd-bf", "bf-c1", "c1-c4", "c4-c6", "c6-c8", "c8-cb", "cb-cd", "cd-d0", "d0-d2", "d2-d4", "d4-d7", "d7-d9", "d9-dc", "dc-de", "de-e0", "e0-e3", "e3-e5", "e5-e8", "e8-ea", "ea-ec", "ec-ef", "ef-f1", "f1-f4", "f4-f6", "f6-f8", "f8-fb", "fb-fd", "fd-"},
+			false,
+		},
 		{
 			"works for large number of shards",
 			args{256},
@@ -1545,6 +1564,29 @@ func TestGenerateShardRanges(t *testing.T) {
 	}
 }
 
+func TestGenerateShardRangesForManyShards(t *testing.T) {
+	for i := 1; i <= 1024; i++ {
+		t.Run(fmt.Sprintf("shards=%d", i), func(t *testing.T) {
+			ranges, err := GenerateShardRanges(i)
+
+			require.NoError(t, err)
+			require.Len(t, ranges, i)
+
+			// verify that the shards are contiguous and non-overlapping
+			for j := 1; j < len(ranges); j++ {
+				prevEnd := ranges[j-1][strings.Index(ranges[j-1], "-")+1:]
+				currStart := ranges[j][:strings.Index(ranges[j], "-")]
+
+				require.Equal(t, prevEnd, currStart, "Shards %d and %d are not contiguous: %s != %s", j-1, j, prevEnd, currStart)
+			}
+
+			// verify that the shards cover the full keyspace
+			require.True(t, strings.HasPrefix(ranges[0], "-"), "First shard does not start with a hyphen: %s", ranges[0])
+			require.True(t, strings.HasSuffix(ranges[len(ranges)-1], "-"), "Last shard does not end with a hyphen: %s", ranges[len(ranges)-1])
+		})
+	}
+}
+
 func TestShardCalculatorForShardsGreaterThan512(t *testing.T) {
 	got, err := GenerateShardRanges(512)
 	assert.NoError(t, err)