@@ -18,71 +18,226 @@ package sessionbalancer
1818
1919import (
2020 "context"
21+ _ "embed"
22+ "fmt"
23+ "slices"
2124 "testing"
25+ "time"
2226
2327 "github.com/stretchr/testify/require"
2428
2529 "vitess.io/vitess/go/mysql"
30+ "vitess.io/vitess/go/test/endtoend/cluster"
2631)
2732
33+ const (
34+ cell = "test_misc"
35+ keyspace = "uks"
36+ )
37+
38+ //go:embed uschema.sql
39+ var uschemaSQL string
40+
41+ func createCluster (t * testing.T , replicaCount int ) (* cluster.LocalProcessCluster , * mysql.ConnParams ) {
42+ t .Helper ()
43+
44+ // Create a new clusterInstance
45+ clusterInstance := cluster .NewCluster (cell , "localhost" )
46+
47+ // Start topo server
48+ err := clusterInstance .StartTopo ()
49+ require .NoError (t , err , "Failed to start topo server" )
50+
51+ // Enable session balancer in vtgate
52+ clusterInstance .VtGateExtraArgs = append (clusterInstance .VtGateExtraArgs ,
53+ "--enable-balancer" ,
54+ "--balancer-vtgate-cells" , clusterInstance .Cell ,
55+ "--balancer-type" , "session" )
56+
57+ ks := cluster.Keyspace {
58+ Name : keyspace ,
59+ SchemaSQL : uschemaSQL ,
60+ }
61+ err = clusterInstance .StartUnshardedKeyspace (ks , replicaCount , false )
62+ require .NoError (t , err , "Failed to start keyspace" )
63+
64+ err = clusterInstance .StartVtgate ()
65+ require .NoError (t , err , "Failed to start vtgate" )
66+
67+ vtParams := clusterInstance .GetVTParams (keyspace )
68+
69+ return clusterInstance , & vtParams
70+ }
71+
2872// TestSessionBalancer validates that the session balancer consistently routes
2973// queries for the same session to the same tablet.
3074func TestSessionBalancer (t * testing.T ) {
75+ cluster , vtParams := createCluster (t , 2 )
76+ defer cluster .Teardown ()
77+
78+ // Get connections that route to different tablets
79+ conns , ids := connections (t , vtParams , "replica" , 2 )
80+
81+ for _ , conn := range conns {
82+ defer conn .Close ()
83+ }
84+
85+ // Validate that each connection consistently returns the same server ID
86+ for range 20 {
87+ for i , conn := range conns {
88+ id := serverID (t , conn )
89+ require .Equal (t , ids [i ], id )
90+ }
91+ }
92+ }
93+
94+ // TestSessionBalancerRemoveTablet validates that when a tablet is killed,
95+ // connections that were using that tablet get rerouted to remaining tablets.
96+ func TestSessionBalancerRemoveTablet (t * testing.T ) {
97+ cluster , vtParams := createCluster (t , 2 )
98+ defer cluster .Teardown ()
99+
31100 // Get connections that route to different tablets
32- conn1 , conn2 , id1 , id2 := connections (t )
101+ conns , _ := connections (t , vtParams , "replica" , 2 )
102+ conn1 , conn2 := conns [0 ], conns [1 ]
33103
34104 defer conn1 .Close ()
35105 defer conn2 .Close ()
36106
37- // Validate that each connection consistently returns the same server ID
107+ tablets := tablets (t , cluster , "replica" )
108+ require .NotNil (t , tablets )
109+ require .Len (t , tablets , 2 )
110+
111+ err := tablets [0 ].VttabletProcess .TearDown ()
112+ require .NoError (t , err )
113+
114+ time .Sleep (10 * time .Millisecond )
115+
38116 for range 20 {
39117 newID1 := serverID (t , conn1 )
40- require .Equal (t , id1 , newID1 )
41-
42118 newID2 := serverID (t , conn2 )
43- require .Equal (t , id2 , newID2 )
44119
45- require .NotEqual (t , newID2 , newID1 )
120+ require .Equal (t , newID1 , newID2 )
121+ }
122+ }
123+
124+ // TestSessionBalancerAddTablet validates that when a new tablet is added,
125+ // new connections get routed to the new tablet.
126+ func TestSessionBalancerAddTablet (t * testing.T ) {
127+ cluster , vtParams := createCluster (t , 3 )
128+ defer cluster .Teardown ()
129+
130+ // Get 3 connections that route to different tablets
131+ conns , ids := connections (t , vtParams , "replica" , 3 )
132+ for _ , conn := range conns {
133+ defer conn .Close ()
134+ }
135+
136+ tablets := tablets (t , cluster , "replica" )
137+ require .NotNil (t , tablets )
138+ require .Len (t , tablets , 3 )
139+
140+ // Start with only 2 tablets serving
141+ tablet := tablets [2 ]
142+ err := tablet .VttabletProcess .TearDown ()
143+ require .NoError (t , err )
144+
145+ time .Sleep (10 * time .Millisecond )
146+
147+ // Find the connection that moved
148+ var conn * mysql.Conn
149+ for i , c := range conns {
150+ newID := serverID (t , c )
151+ if newID != ids [i ] {
152+ conn = c
153+ break
154+ }
155+ }
156+
157+ require .NotNil (t , conn , "One connection should've moved tablets" )
158+
159+ // Start up the tablet again
160+ err = tablet .RestartOnlyTablet ()
161+ require .NoError (t , err )
162+
163+ time .Sleep (10 * time .Millisecond )
164+
165+ // All connections should route to the same IDs again
166+ for range 20 {
167+ for i , conn := range conns {
168+ id := ids [i ]
169+ newID := serverID (t , conn )
170+ require .Equal (t , id , newID )
171+ }
46172 }
47173}
48174
49- // connections returns two connections that should route to different servers .
50- func connections (t * testing.T ) ( conn1 , conn2 * mysql.Conn , id1 , id2 string ) {
175+ // connections returns the specified number of connections that should route to different tablets .
176+ func connections (t * testing.T , vtParams * mysql.ConnParams , tabletType string , numConnections int ) ([] * mysql. Conn , [] string ) {
51177 t .Helper ()
52178
53- // Keep creating connections until we find two that route to different server IDs
54- vtParams .DbName = uks + "@replica"
55- for {
56- conn1 , err := mysql .Connect (context .Background (), & vtParams )
57- require .NoError (t , err )
179+ vtParams .DbName = fmt .Sprintf ("%s@%s" , keyspace , tabletType )
180+
181+ conns := make ([]* mysql.Conn , 0 , numConnections )
182+ ids := make ([]string , 0 , numConnections )
58183
59- conn2 , err = mysql .Connect (context .Background (), & vtParams )
184+ // Keep creating connections until we have the required number with different server IDs
185+ for range 20 {
186+ conn , err := mysql .Connect (context .Background (), vtParams )
60187 require .NoError (t , err )
61188
62- id1 = serverID (t , conn1 )
63- id2 = serverID (t , conn2 )
189+ id := serverID (t , conn )
190+ newID := ! slices .Contains (ids , id )
191+
192+ // If we found a new tablet, add it to the list of connections
193+ if newID {
194+ conns = append (conns , conn )
195+ ids = append (ids , id )
196+
197+ if len (conns ) == numConnections {
198+ return conns , ids
199+ }
64200
65- // Break if we found connections with different server IDs
66- if id1 != id2 {
67- return conn1 , conn2 , id1 , id2
201+ continue
68202 }
69203
70- // If not, close the connections and try again
71- conn1 .Close ()
72- conn2 .Close ()
204+ conn .Close ()
73205 }
206+
207+ t .Fatalf ("could not create %d connections with different tablet connections" , numConnections )
208+ return nil , nil
74209}
75210
76- // serverID runs a `SELECT @@server_id` on the given connection and returns the
77- // server's ID.
211+ // serverID runs a `SELECT @@server_id` on the given connection and returns the server's ID.
78212func serverID (t * testing.T , conn * mysql.Conn ) string {
79213 t .Helper ()
80214
81- result1 , err := conn .ExecuteFetch ("SELECT @@server_uuid " , 1 , false )
215+ result1 , err := conn .ExecuteFetch ("SELECT @@server_id " , 1 , false )
82216 require .NoError (t , err )
83217
84218 tablet1Bytes , err := result1 .Rows [0 ][0 ].ToBytes ()
85219 require .NoError (t , err )
86220
87221 return string (tablet1Bytes )
88222}
223+
224+ func tablets (t * testing.T , clusterInstance * cluster.LocalProcessCluster , tabletType string ) []* cluster.Vttablet {
225+ t .Helper ()
226+
227+ if len (clusterInstance .Keyspaces ) == 0 {
228+ return nil
229+ }
230+
231+ if len (clusterInstance .Keyspaces [0 ].Shards ) == 0 {
232+ return nil
233+ }
234+
235+ tablets := make ([]* cluster.Vttablet , 0 , 2 )
236+ for _ , tablet := range clusterInstance .Keyspaces [0 ].Shards [0 ].Vttablets {
237+ if tablet .Type == tabletType {
238+ tablets = append (tablets , tablet )
239+ }
240+ }
241+
242+ return tablets
243+ }
0 commit comments