@@ -1091,6 +1091,148 @@ public void shouldHandleEdgeCaseWithMoreStandbyReplicasThanAvailableClients() {
1091
1091
assertEquals (numTasks , allStandbyTasks .size ());
1092
1092
}
1093
1093
1094
+ @ Test
1095
+ public void shouldReassignTasksWhenNewNodeJoinsWithExistingActiveAndStandbyAssignments () {
1096
+ // Initial setup: Node 1 has active tasks 0,1 and standby tasks 2,3
1097
+ // Node 2 has active tasks 2,3 and standby tasks 0,1
1098
+ final AssignmentMemberSpec memberSpec1 = createAssignmentMemberSpec ("process1" ,
1099
+ mkMap (mkEntry ("test-subtopology" , Sets .newSet (0 , 1 ))),
1100
+ mkMap (mkEntry ("test-subtopology" , Sets .newSet (2 , 3 ))));
1101
+
1102
+ final AssignmentMemberSpec memberSpec2 = createAssignmentMemberSpec ("process2" ,
1103
+ mkMap (mkEntry ("test-subtopology" , Sets .newSet (2 , 3 ))),
1104
+ mkMap (mkEntry ("test-subtopology" , Sets .newSet (0 , 1 ))));
1105
+
1106
+ // Node 3 joins as new client
1107
+ final AssignmentMemberSpec memberSpec3 = createAssignmentMemberSpec ("process3" );
1108
+
1109
+ final Map <String , AssignmentMemberSpec > members = mkMap (
1110
+ mkEntry ("member1" , memberSpec1 ), mkEntry ("member2" , memberSpec2 ), mkEntry ("member3" , memberSpec3 ));
1111
+
1112
+ final GroupAssignment result = assignor .assign (
1113
+ new GroupSpecImpl (members , mkMap (mkEntry (NUM_STANDBY_REPLICAS_CONFIG , "1" ))),
1114
+ new TopologyDescriberImpl (4 , true , List .of ("test-subtopology" ))
1115
+ );
1116
+
1117
+ // Verify all active tasks are assigned
1118
+ final Set <Integer > allAssignedActiveTasks = new HashSet <>();
1119
+ allAssignedActiveTasks .addAll (getAllActiveTaskIds (result , "member1" ));
1120
+ allAssignedActiveTasks .addAll (getAllActiveTaskIds (result , "member2" ));
1121
+ allAssignedActiveTasks .addAll (getAllActiveTaskIds (result , "member3" ));
1122
+ assertEquals (Sets .newSet (0 , 1 , 2 , 3 ), allAssignedActiveTasks );
1123
+
1124
+ // Verify all standby tasks are assigned
1125
+ final Set <Integer > allAssignedStandbyTasks = new HashSet <>();
1126
+ allAssignedStandbyTasks .addAll (getAllStandbyTaskIds (result , "member1" ));
1127
+ allAssignedStandbyTasks .addAll (getAllStandbyTaskIds (result , "member2" ));
1128
+ allAssignedStandbyTasks .addAll (getAllStandbyTaskIds (result , "member3" ));
1129
+ assertEquals (Sets .newSet (0 , 1 , 2 , 3 ), allAssignedStandbyTasks );
1130
+
1131
+ // Verify each member has 1-2 active tasks and at most 3 tasks total
1132
+ assertTrue (getAllActiveTaskIds (result , "member1" ).size () >= 1 && getAllActiveTaskIds (result , "member1" ).size () <= 2 );
1133
+ assertTrue (getAllActiveTaskIds (result , "member1" ).size () + getAllStandbyTaskIds (result , "member1" ).size () <= 3 );
1134
+
1135
+ assertTrue (getAllActiveTaskIds (result , "member2" ).size () >= 1 && getAllActiveTaskIds (result , "member2" ).size () <= 2 );
1136
+ assertTrue (getAllActiveTaskIds (result , "member2" ).size () + getAllStandbyTaskIds (result , "member2" ).size () <= 3 );
1137
+
1138
+ assertTrue (getAllActiveTaskIds (result , "member3" ).size () >= 1 && getAllActiveTaskIds (result , "member3" ).size () <= 2 );
1139
+ assertTrue (getAllActiveTaskIds (result , "member3" ).size () + getAllStandbyTaskIds (result , "member3" ).size () <= 3 );
1140
+ }
1141
+
1142
+ @ Test
1143
+ public void shouldRangeAssignTasksWhenScalingUp () {
1144
+ // Two clients, the second one is new
1145
+ final AssignmentMemberSpec memberSpec1 = createAssignmentMemberSpec ("process1" ,
1146
+ Map .of ("test-subtopology1" , Set .of (0 , 1 ), "test-subtopology2" , Set .of (0 , 1 )),
1147
+ Map .of ());
1148
+ final AssignmentMemberSpec memberSpec2 = createAssignmentMemberSpec ("process2" );
1149
+ final Map <String , AssignmentMemberSpec > members = mkMap (
1150
+ mkEntry ("member1" , memberSpec1 ), mkEntry ("member2" , memberSpec2 ));
1151
+
1152
+ // Two subtopologies with 2 tasks each (4 tasks total) with standby replicas enabled
1153
+ final GroupAssignment result = assignor .assign (
1154
+ new GroupSpecImpl (members , mkMap (mkEntry (NUM_STANDBY_REPLICAS_CONFIG , String .valueOf (1 )))),
1155
+ new TopologyDescriberImpl (2 , true , Arrays .asList ("test-subtopology1" , "test-subtopology2" ))
1156
+ );
1157
+
1158
+ // Each client should get one task from each subtopology
1159
+ final MemberAssignment testMember1 = result .members ().get ("member1" );
1160
+ assertNotNull (testMember1 );
1161
+ assertEquals (1 , testMember1 .activeTasks ().get ("test-subtopology1" ).size ());
1162
+ assertEquals (1 , testMember1 .activeTasks ().get ("test-subtopology2" ).size ());
1163
+
1164
+ final MemberAssignment testMember2 = result .members ().get ("member2" );
1165
+ assertNotNull (testMember2 );
1166
+ assertEquals (1 , testMember2 .activeTasks ().get ("test-subtopology1" ).size ());
1167
+ assertEquals (1 , testMember2 .activeTasks ().get ("test-subtopology2" ).size ());
1168
+
1169
+ // Verify all tasks are assigned exactly once
1170
+ final Set <Integer > allSubtopology1Tasks = new HashSet <>();
1171
+ allSubtopology1Tasks .addAll (testMember1 .activeTasks ().get ("test-subtopology1" ));
1172
+ allSubtopology1Tasks .addAll (testMember2 .activeTasks ().get ("test-subtopology1" ));
1173
+ assertEquals (Sets .newSet (0 , 1 ), allSubtopology1Tasks );
1174
+
1175
+ final Set <Integer > allSubtopology2Tasks = new HashSet <>();
1176
+ allSubtopology2Tasks .addAll (testMember1 .activeTasks ().get ("test-subtopology2" ));
1177
+ allSubtopology2Tasks .addAll (testMember2 .activeTasks ().get ("test-subtopology2" ));
1178
+ assertEquals (Sets .newSet (0 , 1 ), allSubtopology2Tasks );
1179
+
1180
+ // Each client should get one task from each subtopology
1181
+ assertNotNull (testMember1 );
1182
+ assertEquals (1 , testMember1 .standbyTasks ().get ("test-subtopology1" ).size ());
1183
+ assertEquals (1 , testMember1 .standbyTasks ().get ("test-subtopology2" ).size ());
1184
+
1185
+ assertNotNull (testMember2 );
1186
+ assertEquals (1 , testMember2 .standbyTasks ().get ("test-subtopology1" ).size ());
1187
+ assertEquals (1 , testMember2 .standbyTasks ().get ("test-subtopology2" ).size ());
1188
+ }
1189
+
1190
+ @ Test
1191
+ public void shouldRangeAssignTasksWhenStartingEmpty () {
1192
+ // Two clients starting empty (no previous tasks)
1193
+ final AssignmentMemberSpec memberSpec1 = createAssignmentMemberSpec ("process1" );
1194
+ final AssignmentMemberSpec memberSpec2 = createAssignmentMemberSpec ("process2" );
1195
+ final Map <String , AssignmentMemberSpec > members = mkMap (
1196
+ mkEntry ("member1" , memberSpec1 ), mkEntry ("member2" , memberSpec2 ));
1197
+
1198
+ // Two subtopologies with 2 tasks each (4 tasks total) with standby replicas enabled
1199
+ final GroupAssignment result = assignor .assign (
1200
+ new GroupSpecImpl (members , mkMap (mkEntry (NUM_STANDBY_REPLICAS_CONFIG , String .valueOf (1 )))),
1201
+ new TopologyDescriberImpl (2 , true , Arrays .asList ("test-subtopology1" , "test-subtopology2" ))
1202
+ );
1203
+
1204
+ // Each client should get one task from each subtopology
1205
+ final MemberAssignment testMember1 = result .members ().get ("member1" );
1206
+ assertNotNull (testMember1 );
1207
+ assertEquals (1 , testMember1 .activeTasks ().get ("test-subtopology1" ).size ());
1208
+ assertEquals (1 , testMember1 .activeTasks ().get ("test-subtopology2" ).size ());
1209
+
1210
+ final MemberAssignment testMember2 = result .members ().get ("member2" );
1211
+ assertNotNull (testMember2 );
1212
+ assertEquals (1 , testMember2 .activeTasks ().get ("test-subtopology1" ).size ());
1213
+ assertEquals (1 , testMember2 .activeTasks ().get ("test-subtopology2" ).size ());
1214
+
1215
+ // Verify all tasks are assigned exactly once
1216
+ final Set <Integer > allSubtopology1Tasks = new HashSet <>();
1217
+ allSubtopology1Tasks .addAll (testMember1 .activeTasks ().get ("test-subtopology1" ));
1218
+ allSubtopology1Tasks .addAll (testMember2 .activeTasks ().get ("test-subtopology1" ));
1219
+ assertEquals (Sets .newSet (0 , 1 ), allSubtopology1Tasks );
1220
+
1221
+ final Set <Integer > allSubtopology2Tasks = new HashSet <>();
1222
+ allSubtopology2Tasks .addAll (testMember1 .activeTasks ().get ("test-subtopology2" ));
1223
+ allSubtopology2Tasks .addAll (testMember2 .activeTasks ().get ("test-subtopology2" ));
1224
+ assertEquals (Sets .newSet (0 , 1 ), allSubtopology2Tasks );
1225
+
1226
+ // Each client should get one task from each subtopology
1227
+ assertNotNull (testMember1 );
1228
+ assertEquals (1 , testMember1 .standbyTasks ().get ("test-subtopology1" ).size ());
1229
+ assertEquals (1 , testMember1 .standbyTasks ().get ("test-subtopology2" ).size ());
1230
+
1231
+ assertNotNull (testMember2 );
1232
+ assertEquals (1 , testMember2 .standbyTasks ().get ("test-subtopology1" ).size ());
1233
+ assertEquals (1 , testMember2 .standbyTasks ().get ("test-subtopology2" ).size ());
1234
+ }
1235
+
1094
1236
1095
1237
private int getAllActiveTaskCount (GroupAssignment result , String ... memberIds ) {
1096
1238
int size = 0 ;
0 commit comments