@@ -12,13 +12,15 @@ import {
12
12
GlideString ,
13
13
ObjectType ,
14
14
ProtocolVersion ,
15
+ GlideClusterClientConfiguration ,
15
16
} from ".." ;
16
17
import { ValkeyCluster } from "../../utils/TestUtils.js" ;
17
18
import {
18
19
flushAndCloseClient ,
19
20
getClientConfigurationOption ,
20
21
getServerVersion ,
21
22
parseEndpoints ,
23
+ waitForClusterReady as isClusterReadyWithExpectedNodeCount ,
22
24
} from "./TestUtilities" ;
23
25
24
26
const TIMEOUT = 50000 ;
@@ -376,6 +378,166 @@ describe("Scan GlideClusterClient", () => {
376
378
} ,
377
379
TIMEOUT ,
378
380
) ;
381
+
382
+ it . each ( [ ProtocolVersion . RESP2 , ProtocolVersion . RESP3 ] ) (
383
+ `GlideClusterClient scan with allowNonCoveredSlots %p` ,
384
+ async ( protocol ) => {
385
+ const testCluster = await ValkeyCluster . createCluster (
386
+ true ,
387
+ 3 ,
388
+ 0 ,
389
+ getServerVersion ,
390
+ ) ;
391
+ const config : GlideClusterClientConfiguration = {
392
+ addresses : testCluster
393
+ . getAddresses ( )
394
+ . map ( ( [ host , port ] ) => ( { host, port } ) ) ,
395
+ protocol,
396
+ } ;
397
+ const testClient = await GlideClusterClient . createClient ( config ) ;
398
+
399
+ try {
400
+ for ( let i = 0 ; i < 10000 ; i ++ ) {
401
+ const result = await testClient . set ( `${ uuidv4 ( ) } ` , "value" ) ;
402
+ expect ( result ) . toBe ( "OK" ) ;
403
+ }
404
+
405
+ // Perform an initial scan to ensure all works as expected
406
+ let cursor = new ClusterScanCursor ( ) ;
407
+ let result = await testClient . scan ( cursor ) ;
408
+ cursor = result [ 0 ] ;
409
+ expect ( cursor . isFinished ( ) ) . toBe ( false ) ;
410
+
411
+ // Set 'cluster-require-full-coverage' to 'no' to allow operations with missing slots
412
+ await testClient . configSet ( {
413
+ "cluster-require-full-coverage" : "no" ,
414
+ } ) ;
415
+
416
+ // Forget one server to simulate a node failure
417
+ const addresses = testCluster . getAddresses ( ) ;
418
+ const addressToForget = addresses [ 0 ] ;
419
+ const allOtherAddresses = addresses . slice ( 1 ) ;
420
+ const idToForget = await testClient . customCommand (
421
+ [ "CLUSTER" , "MYID" ] ,
422
+ {
423
+ route : {
424
+ type : "routeByAddress" ,
425
+ host : addressToForget [ 0 ] ,
426
+ port : addressToForget [ 1 ] ,
427
+ } ,
428
+ } ,
429
+ ) ;
430
+
431
+ for ( const address of allOtherAddresses ) {
432
+ await testClient . customCommand (
433
+ [ "CLUSTER" , "FORGET" , idToForget as string ] ,
434
+ {
435
+ route : {
436
+ type : "routeByAddress" ,
437
+ host : address [ 0 ] ,
438
+ port : address [ 1 ] ,
439
+ } ,
440
+ } ,
441
+ ) ;
442
+ }
443
+
444
+ // Wait for the cluster to stabilize after forgetting a node
445
+ const ready = await isClusterReadyWithExpectedNodeCount (
446
+ testClient ,
447
+ allOtherAddresses . length ,
448
+ ) ;
449
+ expect ( ready ) . toBe ( true ) ;
450
+
451
+ // Attempt to scan without 'allowNonCoveredSlots', expecting an error
452
+ // Since it might take time for the inner core to forget the missing node,
453
+ // we retry the scan until the expected error is thrown.
454
+
455
+ const maxRetries = 10 ;
456
+ let retries = 0 ;
457
+ let errorReceived = false ;
458
+
459
+ while ( retries < maxRetries && ! errorReceived ) {
460
+ retries ++ ;
461
+ cursor = new ClusterScanCursor ( ) ;
462
+
463
+ try {
464
+ while ( ! cursor . isFinished ( ) ) {
465
+ result = await testClient . scan ( cursor ) ;
466
+ cursor = result [ 0 ] ;
467
+ }
468
+
469
+ // If scan completes without error, wait and retry
470
+ await new Promise ( ( resolve ) =>
471
+ setTimeout ( resolve , 1000 ) ,
472
+ ) ;
473
+ } catch ( error ) {
474
+ if (
475
+ error instanceof Error &&
476
+ error . message . includes (
477
+ "Could not find an address covering a slot, SCAN operation cannot continue" ,
478
+ )
479
+ ) {
480
+ // Expected error occurred
481
+ errorReceived = true ;
482
+ } else {
483
+ // Unexpected error, rethrow
484
+ throw error ;
485
+ }
486
+ }
487
+ }
488
+
489
+ expect ( errorReceived ) . toBe ( true ) ;
490
+
491
+ // Perform scan with 'allowNonCoveredSlots: true'
492
+ cursor = new ClusterScanCursor ( ) ;
493
+
494
+ while ( ! cursor . isFinished ( ) ) {
495
+ result = await testClient . scan ( cursor , {
496
+ allowNonCoveredSlots : true ,
497
+ } ) ;
498
+ cursor = result [ 0 ] ;
499
+ }
500
+
501
+ expect ( cursor . isFinished ( ) ) . toBe ( true ) ;
502
+
503
+ // Get keys using 'KEYS *' from the remaining nodes
504
+ const keys : GlideString [ ] = [ ] ;
505
+
506
+ for ( const address of allOtherAddresses ) {
507
+ const result = await testClient . customCommand (
508
+ [ "KEYS" , "*" ] ,
509
+ {
510
+ route : {
511
+ type : "routeByAddress" ,
512
+ host : address [ 0 ] ,
513
+ port : address [ 1 ] ,
514
+ } ,
515
+ } ,
516
+ ) ;
517
+ keys . push ( ...( result as GlideString [ ] ) ) ;
518
+ }
519
+
520
+ // Scan again with 'allowNonCoveredSlots: true' and collect results
521
+ cursor = new ClusterScanCursor ( ) ;
522
+ const results : GlideString [ ] = [ ] ;
523
+
524
+ while ( ! cursor . isFinished ( ) ) {
525
+ result = await testClient . scan ( cursor , {
526
+ allowNonCoveredSlots : true ,
527
+ } ) ;
528
+ results . push ( ...result [ 1 ] ) ;
529
+ cursor = result [ 0 ] ;
530
+ }
531
+
532
+ // Compare the sets of keys obtained from 'KEYS *' and 'SCAN'
533
+ expect ( new Set ( results ) ) . toEqual ( new Set ( keys ) ) ;
534
+ } finally {
535
+ testClient . close ( ) ;
536
+ await testCluster . close ( ) ;
537
+ }
538
+ } ,
539
+ TIMEOUT ,
540
+ ) ;
379
541
} ) ;
380
542
381
543
//standalone tests
0 commit comments