@@ -667,4 +667,172 @@ struct HashSlotShardMapTests {
667
667
#expect( shardNodes. replicas. contains ( expectedReplica1) )
668
668
#expect( shardNodes. replicas. contains ( expectedReplica2) )
669
669
}
670
+
671
+ func makeExampleCusterWithNShardsAndMReplicasPerShard( shards: Int , replicas: Int ) -> ValkeyClusterDescription {
672
+ let defaultRangeSize = Int ( HashSlot . max. rawValue + 1 ) / shards
673
+ var range : ClosedRange < HashSlot > = 0 ... 0
674
+
675
+ var result = [ ValkeyClusterDescription . Shard] ( )
676
+ var nodeIndex = 1
677
+
678
+ for i in 0 ..< shards {
679
+ if i == 0 {
680
+ if shards == 1 {
681
+ range = HashSlot . min... HashSlot . max
682
+ } else {
683
+ range = HashSlot . min... ( range. upperBound. advanced ( by: defaultRangeSize - 1 ) )
684
+ }
685
+ } else if i == shards - 1 {
686
+ range = ( range. upperBound. advanced ( by: 1 ) ) ... HashSlot . max
687
+ } else {
688
+ range = ( range. upperBound. advanced ( by: 1 ) ) ... ( range. upperBound. advanced ( by: defaultRangeSize - 1 ) )
689
+ }
690
+
691
+ var shard = ValkeyClusterDescription . Shard ( slots: [ range] , nodes: [ ] )
692
+ for _ in 0 ..< ( replicas + 1 ) {
693
+ defer { nodeIndex += 1 }
694
+ shard. nodes. append (
695
+ . init(
696
+ id: " node- \( nodeIndex) " ,
697
+ port: nil ,
698
+ tlsPort: 6379 ,
699
+ ip: " 192.168.64. \( nodeIndex) " ,
700
+ hostname: " node- \( nodeIndex) .valkey.io " ,
701
+ endpoint: " node- \( nodeIndex) .valkey.io " ,
702
+ role: . replica,
703
+ replicationOffset: 14 ,
704
+ health: . online
705
+ )
706
+ )
707
+ }
708
+ let primaryIndex = shard. nodes. indices. randomElement ( ) !
709
+ shard. nodes [ primaryIndex] . role = . master
710
+
711
+ result. append ( shard)
712
+ }
713
+
714
+ return ValkeyClusterDescription ( result)
715
+ }
716
+
717
+ @Test ( " Case 1: MovedError specifies the already exisiting shard primary node " )
718
+ func movedErrorSpecifiesTheAlreadyExisitingShardPrimaryNode( ) throws {
719
+ let clusterDescription = self . makeExampleCusterWithNShardsAndMReplicasPerShard ( shards: 3 , replicas: 1 )
720
+
721
+ var map = HashSlotShardMap ( )
722
+ map. updateCluster ( clusterDescription. shards)
723
+
724
+ let ogShard = try map. nodeID ( for: CollectionOfOne ( 2 ) )
725
+ let update = map. updateSlots ( with: ValkeyMovedError ( slot: 2 , endpoint: ogShard. master. endpoint, port: ogShard. master. port) )
726
+ #expect( update == . updatedSlotToExistingNode)
727
+ let updatedShard = try map. nodeID ( for: CollectionOfOne ( 2 ) )
728
+ #expect( updatedShard == ogShard)
729
+ }
730
+
731
+ @Test ( " Case 2: MovedError specifies a previous shard replica node " )
732
+ func movedErrorSpecifiesAPreviousShardReplicaNode( ) throws {
733
+ let clusterDescription = self . makeExampleCusterWithNShardsAndMReplicasPerShard ( shards: 3 , replicas: 3 )
734
+
735
+ var map = HashSlotShardMap ( )
736
+ map. updateCluster ( clusterDescription. shards)
737
+
738
+ let ogShard = try map. nodeID ( for: CollectionOfOne ( 2 ) )
739
+ let luckyReplica = ogShard. replicas. randomElement ( ) !
740
+
741
+ let update = map. updateSlots ( with: ValkeyMovedError ( slot: 2 , endpoint: luckyReplica. endpoint, port: luckyReplica. port) )
742
+ #expect( update == . updatedSlotToExistingNode)
743
+ let updatedShard = try map. nodeID ( for: CollectionOfOne ( 2 ) )
744
+ #expect( updatedShard. master == luckyReplica)
745
+ #expect( updatedShard != ogShard)
746
+
747
+ // test neighboring hashes have seen an update as well
748
+ let updatedShard1 = try map. nodeID ( for: CollectionOfOne ( 1 ) )
749
+ let updatedShard3 = try map. nodeID ( for: CollectionOfOne ( 3 ) )
750
+
751
+ #expect( updatedShard == updatedShard1)
752
+ #expect( updatedShard == updatedShard3)
753
+ }
754
+
755
+ @Test ( " Case 3: MovedError specifies another shards primary node " )
756
+ func movedErrorSpecifiesOtherShardPrimaryNode( ) throws {
757
+ let clusterDescription = self . makeExampleCusterWithNShardsAndMReplicasPerShard ( shards: 3 , replicas: 3 )
758
+
759
+ var map = HashSlotShardMap ( )
760
+ map. updateCluster ( clusterDescription. shards)
761
+
762
+ let ogShard = try map. nodeID ( for: CollectionOfOne ( 2 ) )
763
+ let otherShard = try map. nodeID ( for: CollectionOfOne ( . max) )
764
+ let newPrimary = otherShard. master
765
+
766
+ let update = map. updateSlots ( with: ValkeyMovedError ( slot: 2 , endpoint: newPrimary. endpoint, port: newPrimary. port) )
767
+ #expect( update == . updatedSlotToExistingNode)
768
+ let updatedShard = try map. nodeID ( for: CollectionOfOne ( 2 ) )
769
+ #expect( updatedShard == otherShard)
770
+
771
+ // test neighboring hashes have not been updated
772
+ let updatedShard1 = try map. nodeID ( for: CollectionOfOne ( 1 ) )
773
+ let updatedShard3 = try map. nodeID ( for: CollectionOfOne ( 3 ) )
774
+
775
+ #expect( ogShard == updatedShard1)
776
+ #expect( ogShard == updatedShard3)
777
+ }
778
+
779
+ @Test ( " Case 4: MovedError specifies another shards replica node " )
780
+ func movedErrorSpecifiesOtherShardReplicaNode( ) throws {
781
+ let clusterDescription = self . makeExampleCusterWithNShardsAndMReplicasPerShard ( shards: 3 , replicas: 3 )
782
+
783
+ var map = HashSlotShardMap ( )
784
+ map. updateCluster ( clusterDescription. shards)
785
+
786
+ let ogShard = try map. nodeID ( for: CollectionOfOne ( 2 ) )
787
+ let otherShard = try map. nodeID ( for: CollectionOfOne ( . max) )
788
+ let newPrimary = otherShard. replicas. randomElement ( ) !
789
+
790
+ let update = map. updateSlots ( with: ValkeyMovedError ( slot: 2 , endpoint: newPrimary. endpoint, port: newPrimary. port) )
791
+ #expect( update == . updatedSlotToExistingNode)
792
+ let updatedShard = try map. nodeID ( for: CollectionOfOne ( 2 ) )
793
+ #expect( updatedShard. master == newPrimary)
794
+ #expect( updatedShard. replicas. isEmpty)
795
+ #expect( updatedShard != ogShard)
796
+
797
+ // test neighboring hashes have not been updated
798
+ let updatedShard1 = try map. nodeID ( for: CollectionOfOne ( 1 ) )
799
+ let updatedShard3 = try map. nodeID ( for: CollectionOfOne ( 3 ) )
800
+
801
+ #expect( ogShard == updatedShard1)
802
+ #expect( ogShard == updatedShard3)
803
+
804
+ // test other shard has been updated and new primary replica has been removed there
805
+ let otherShardUpdated = try map. nodeID ( for: CollectionOfOne ( . max) )
806
+ #expect( !otherShardUpdated. replicas. contains ( newPrimary) )
807
+ }
808
+
809
+ @Test ( " Case 5: MovedError specifies previously unknown node " )
810
+ func movedErrorSpecifiesPreviouslyUnknownNode( ) throws {
811
+ let clusterDescription = self . makeExampleCusterWithNShardsAndMReplicasPerShard ( shards: 3 , replicas: 3 )
812
+
813
+ var map = HashSlotShardMap ( )
814
+ map. updateCluster ( clusterDescription. shards)
815
+
816
+ let ogShard = try map. nodeID ( for: CollectionOfOne ( 2 ) )
817
+ let otherShard = try map. nodeID ( for: CollectionOfOne ( . max) )
818
+ let newPrimary = ValkeyNodeID ( endpoint: " new.valkey.io " , port: 6379 )
819
+
820
+ let update = map. updateSlots ( with: ValkeyMovedError ( slot: 2 , endpoint: newPrimary. endpoint, port: newPrimary. port) )
821
+ #expect( update == . updatedSlotToUnknownNode)
822
+ let updatedShard = try map. nodeID ( for: CollectionOfOne ( 2 ) )
823
+ #expect( updatedShard. master == newPrimary)
824
+ #expect( updatedShard. replicas. isEmpty)
825
+ #expect( updatedShard != ogShard)
826
+
827
+ // test neighboring hashes have not been updated
828
+ let updatedShard1 = try map. nodeID ( for: CollectionOfOne ( 1 ) )
829
+ let updatedShard3 = try map. nodeID ( for: CollectionOfOne ( 3 ) )
830
+
831
+ #expect( ogShard == updatedShard1)
832
+ #expect( ogShard == updatedShard3)
833
+
834
+ // test other shard has been updated and new primary replica has been removed there
835
+ let otherShardUpdated = try map. nodeID ( for: CollectionOfOne ( . max) )
836
+ #expect( !otherShardUpdated. replicas. contains ( newPrimary) )
837
+ }
670
838
}
0 commit comments