1919import org .junit .Test ;
2020import org .junit .runner .RunWith ;
2121import org .mockito .junit .MockitoJUnitRunner ;
22+ import org .thingsboard .mqtt .broker .adaptor .ProtoConverter ;
2223import org .thingsboard .mqtt .broker .common .data .BrokerConstants ;
24+ import org .thingsboard .mqtt .broker .exception .QueuePersistenceException ;
2325import org .thingsboard .mqtt .broker .gen .queue .RetainedMsgProto ;
2426import org .thingsboard .mqtt .broker .queue .TbQueueAdmin ;
27+ import org .thingsboard .mqtt .broker .queue .TbQueueControlledOffsetConsumer ;
2528import org .thingsboard .mqtt .broker .queue .cluster .ServiceInfoProvider ;
2629import org .thingsboard .mqtt .broker .queue .common .DefaultTbQueueMsgHeaders ;
2730import org .thingsboard .mqtt .broker .queue .common .TbProtoQueueMsg ;
2831import org .thingsboard .mqtt .broker .queue .provider .RetainedMsgQueueFactory ;
2932import org .thingsboard .mqtt .broker .service .stats .RetainedMsgConsumerStats ;
3033import org .thingsboard .mqtt .broker .service .stats .StatsManager ;
3134
35+ import java .util .ArrayList ;
3236import java .util .List ;
37+ import java .util .Map ;
3338
39+ import static org .junit .Assert .assertEquals ;
3440import static org .junit .Assert .assertThrows ;
41+ import static org .junit .Assert .assertTrue ;
42+ import static org .mockito .ArgumentMatchers .any ;
3543import static org .mockito .ArgumentMatchers .anyInt ;
44+ import static org .mockito .ArgumentMatchers .anyLong ;
45+ import static org .mockito .ArgumentMatchers .anyString ;
46+ import static org .mockito .Mockito .doAnswer ;
3647import static org .mockito .Mockito .mock ;
3748import static org .mockito .Mockito .never ;
3849import static org .mockito .Mockito .verify ;
@@ -43,6 +54,8 @@ public class RetainedMsgConsumerImplTest {
4354
4455 private RetainedMsgConsumerStats stats ;
4556 private RetainedMsgChangesCallback callback ;
57+ private RetainedMsgQueueFactory queueFactory ;
58+ private RetainedMsgPersistenceService persistenceService ;
4659 private RetainedMsgConsumerImpl consumer ;
4760
4861 @ Before
@@ -51,10 +64,12 @@ public void setUp() {
5164 stats = mock (RetainedMsgConsumerStats .class );
5265 when (statsManager .getRetainedMsgConsumerStats ()).thenReturn (stats );
5366 callback = mock (RetainedMsgChangesCallback .class );
67+ queueFactory = mock (RetainedMsgQueueFactory .class );
68+ persistenceService = mock (RetainedMsgPersistenceService .class );
5469 consumer = new RetainedMsgConsumerImpl (
55- mock ( RetainedMsgQueueFactory . class ) ,
70+ queueFactory ,
5671 mock (ServiceInfoProvider .class ),
57- mock ( RetainedMsgPersistenceService . class ) ,
72+ persistenceService ,
5873 mock (TbQueueAdmin .class ),
5974 statsManager );
6075 }
@@ -86,6 +101,40 @@ public void givenMidPackFailure_whenProcessPack_thenNoCounterAdvances() {
86101 verify (stats , never ()).log (anyInt (), anyInt ());
87102 }
88103
104+ @ Test
105+ @ SuppressWarnings ("unchecked" )
106+ public void givenFirstMarkerWriteFailsOnFreshTopic_whenInitLoad_thenRetriesAndCompletes () throws Exception {
107+ // On a fresh Kafka cluster the first write to the just-created topic can fail while the partition leader has not
108+ // applied the new topic metadata yet ("This server does not host this topic-partition"). initLoad must retry the
109+ // marker write instead of failing broker startup.
110+ TbQueueControlledOffsetConsumer <TbProtoQueueMsg <RetainedMsgProto >> queueConsumer = mock (TbQueueControlledOffsetConsumer .class );
111+ when (queueFactory .createConsumer (any (), any ())).thenReturn (queueConsumer );
112+ consumer .init ();
113+
114+ List <String > persistedTopics = new ArrayList <>();
115+ doAnswer (invocation -> {
116+ persistedTopics .add (invocation .getArgument (0 ));
117+ if (persistedTopics .size () == 1 ) {
118+ throw new QueuePersistenceException ("Failed to update retained msg" );
119+ }
120+ return null ;
121+ }).when (persistenceService ).persistRetainedMsgSync (anyString (), any ());
122+ when (queueConsumer .poll (anyLong ())).thenAnswer (invocation -> List .of (markerMsg (persistedTopics .get (0 ))));
123+
124+ Map <String , RetainedMsg > result = consumer .initLoad ();
125+
126+ assertTrue (result .isEmpty ());
127+ // failed marker write + retried marker write (same key, so the consumer still recognizes it) + marker clear
128+ assertEquals (3 , persistedTopics .size ());
129+ assertEquals (persistedTopics .get (0 ), persistedTopics .get (1 ));
130+ assertEquals (persistedTopics .get (0 ), persistedTopics .get (2 ));
131+ }
132+
133+ private static TbProtoQueueMsg <RetainedMsgProto > markerMsg (String topic ) {
134+ RetainedMsgProto proto = ProtoConverter .convertToRetainedMsgProto (new RetainedMsg (topic , BrokerConstants .DUMMY_PAYLOAD , 0 ));
135+ return new TbProtoQueueMsg <>(topic , proto , new DefaultTbQueueMsgHeaders ());
136+ }
137+
89138 private static TbProtoQueueMsg <RetainedMsgProto > clearedMsg () {
90139 // Default RetainedMsgProto has an empty payload + qos 0, which the consumer treats as a cleared (tombstone) msg.
91140 DefaultTbQueueMsgHeaders headers = new DefaultTbQueueMsgHeaders ();
0 commit comments