@@ -226,6 +226,61 @@ super_stream_partition_sac_test(_) ->
226226
227227 ok .
228228
229+ % % Regression test for a stuck-group state. When the arrival of the stepping-down
230+ % % consumer's consumer_update response triggers command_activate_consumer and
231+ % % evaluate_active_consumer picks the same consumer that just stepped down,
232+ % % the coordinator must still notify that consumer so its client learns it is
233+ % % active again. Without the notification, the coordinator believes the group
234+ % % has an active member while the client has been told active=false, and the
235+ % % group is silently stranded.
236+ super_stream_partition_sac_re_notifies_stepping_down_consumer_re_selected_as_active_test (_ ) ->
237+ Stream = <<" stream" >>,
238+ ConsumerName = <<" app" >>,
239+ ConnectionPid = self (),
240+ GroupId = {<<" /" >>, Stream , ConsumerName },
241+ % % Partition index chosen so that evaluate_active_consumer picks the first
242+ % % consumer once three have joined (3 rem 3 = 0), which is precisely the
243+ % % one deactivated by the two-consumer rebalance below.
244+ PartitionIndex = 3 ,
245+
246+ Command0 = register_consumer_command (Stream , PartitionIndex , ConsumerName , ConnectionPid , 0 ),
247+ State0 = state (),
248+ {State1 , {ok , true }, Effects1 } = ? MOD :apply (Command0 , State0 ),
249+ assertSendMessageActivateEffect (ConnectionPid , 0 , Stream , ConsumerName , true , Effects1 ),
250+
251+ Command1 = register_consumer_command (Stream , PartitionIndex , ConsumerName , ConnectionPid , 1 ),
252+ {State2 , {ok , false }, Effects2 } = ? MOD :apply (Command1 , State1 ),
253+ % % 3 rem 2 = 1 selects subscription 1, so subscription 0 is asked to step down.
254+ assertSendMessageSteppingDownEffect (ConnectionPid , 0 , Stream , ConsumerName , Effects2 ),
255+
256+ Command2 = register_consumer_command (Stream , PartitionIndex , ConsumerName , ConnectionPid , 2 ),
257+ {#? STATE {groups = #{GroupId := # group {consumers = Consumers3 }}} = State3 ,
258+ {ok , false }, Effects3 } = ? MOD :apply (Command2 , State2 ),
259+ % % 3 rem 3 = 0 selects subscription 0, which is still lookup_active_consumer
260+ % % because is_active({_, deactivating}) is true, so no rebalance happens.
261+ assertCsrsEqual ([csr (ConnectionPid , 0 , deactivating ),
262+ csr (ConnectionPid , 1 , waiting ),
263+ csr (ConnectionPid , 2 , waiting )],
264+ Consumers3 ),
265+ assertEmpty (Effects3 ),
266+
267+ % % Simulate the arrival of subscription 0's consumer_update response, which
268+ % % causes the stream reader to fire command_activate_consumer.
269+ Command3 = activate_consumer_command (Stream , ConsumerName ),
270+ {#? STATE {groups = #{GroupId := # group {consumers = Consumers4 }}},
271+ ok , Effects4 } = ? MOD :apply (Command3 , State3 ),
272+
273+ % % Subscription 0 is selected as active again by 3 rem 3 = 0.
274+ assertCsrsEqual ([csr (ConnectionPid , 0 , active ),
275+ csr (ConnectionPid , 1 , waiting ),
276+ csr (ConnectionPid , 2 , waiting )],
277+ Consumers4 ),
278+ % % Subscription 0's client was previously told active=false and must be
279+ % % notified it is active again.
280+ assertSendMessageActivateEffect (ConnectionPid , 0 , Stream , ConsumerName , true , Effects4 ),
281+
282+ ok .
283+
229284ensure_monitors_test (_ ) ->
230285 GroupId = {<<" /" >>, <<" stream" >>, <<" app" >>},
231286 Group = grp ([csr (self (), 0 , true ), csr (self (), 1 , false )]),
0 commit comments