2525import org .apache .fluss .server .log .FetchParams ;
2626import org .apache .fluss .server .log .LogOffsetMetadata ;
2727import org .apache .fluss .server .metrics .group .TestingMetricGroups ;
28+ import org .apache .fluss .server .replica .Replica ;
2829import org .apache .fluss .server .replica .ReplicaTestBase ;
2930import org .apache .fluss .server .replica .delay .DelayedFetchLog .FetchBucketStatus ;
3031
3132import org .junit .jupiter .api .Test ;
3233
3334import java .time .Duration ;
35+ import java .util .Arrays ;
3436import java .util .Collections ;
37+ import java .util .HashMap ;
3538import java .util .List ;
3639import java .util .Map ;
3740import java .util .concurrent .CompletableFuture ;
41+ import java .util .concurrent .atomic .AtomicInteger ;
3842import java .util .function .Consumer ;
3943
4044import static org .apache .fluss .record .TestData .DATA1 ;
@@ -83,7 +87,7 @@ void testCompleteDelayedFetchLog() throws Exception {
8387 assertThat (delayedFetchLogManager .numDelayed ()).isEqualTo (1 );
8488 assertThat (delayedFetchLogManager .watched ()).isEqualTo (1 );
8589
86- // write data.
90+ // Appending data completes the fetch without an additional completion trigger .
8791 assertThat (delayedResponse .isDone ()).isFalse ();
8892 CompletableFuture <List <ProduceLogResultForBucket >> future = new CompletableFuture <>();
8993 replicaManager .appendRecordsToLog (
@@ -93,10 +97,7 @@ void testCompleteDelayedFetchLog() throws Exception {
9397 null ,
9498 future ::complete );
9599 assertThat (future .get ()).containsOnly (new ProduceLogResultForBucket (tb , 0 , 10L ));
96-
97- // check and complete manually
98- numComplete = delayedFetchLogManager .checkAndComplete (delayedTableBucketKey );
99- assertThat (numComplete ).isEqualTo (1 );
100+ assertThat (delayedResponse .isDone ()).isTrue ();
100101 assertThat (delayedFetchLogManager .numDelayed ()).isEqualTo (0 );
101102 assertThat (delayedFetchLogManager .watched ()).isEqualTo (0 );
102103
@@ -107,6 +108,186 @@ void testCompleteDelayedFetchLog() throws Exception {
107108 assertLogRecordsEquals (DATA1_ROW_TYPE , resultForBucket .records (), DATA1 );
108109 }
109110
111+ @ Test
112+ void testSuccessfulBucketCompletesWhenAnotherBucketAppendFails () throws Exception {
113+ TableBucket successfulBucket = new TableBucket (DATA1_TABLE_ID , 1 );
114+ TableBucket failedBucket = new TableBucket (DATA1_TABLE_ID , 2 );
115+ makeLogTableAsLeader (successfulBucket .getBucket ());
116+ CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> delayedResponse =
117+ watchDelayedFetch (successfulBucket );
118+
119+ Map <TableBucket , MemoryLogRecords > entries = new HashMap <>();
120+ entries .put (successfulBucket , genMemoryLogRecordsByObject (DATA1 ));
121+ entries .put (failedBucket , genMemoryLogRecordsByObject (DATA1 ));
122+ CompletableFuture <List <ProduceLogResultForBucket >> produceResponse =
123+ new CompletableFuture <>();
124+
125+ replicaManager .appendRecordsToLog (20000 , 1 , entries , null , produceResponse ::complete );
126+
127+ List <ProduceLogResultForBucket > produceResults = produceResponse .get ();
128+ assertThat (produceResults ).hasSize (2 );
129+ assertThat (produceResults )
130+ .filteredOn (result -> result .getTableBucket ().equals (successfulBucket ))
131+ .hasSize (1 )
132+ .allSatisfy (result -> assertThat (result .succeeded ()).isTrue ());
133+ assertThat (produceResults )
134+ .filteredOn (result -> result .getTableBucket ().equals (failedBucket ))
135+ .hasSize (1 )
136+ .allSatisfy (result -> assertThat (result .failed ()).isTrue ());
137+ assertThat (delayedResponse ).isDone ();
138+ assertThat (replicaManager .getDelayedFetchLogManager ().numDelayed ()).isZero ();
139+ }
140+
141+ @ Test
142+ void testAppendCompletesDelayedFetchesForMultipleBuckets () throws Exception {
143+ TableBucket firstBucket = new TableBucket (DATA1_TABLE_ID , 1 );
144+ TableBucket secondBucket = new TableBucket (DATA1_TABLE_ID , 2 );
145+ makeLogTableAsLeader (firstBucket .getBucket ());
146+ makeLogTableAsLeader (secondBucket .getBucket ());
147+ CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> firstResponse =
148+ watchDelayedFetch (firstBucket );
149+ CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> secondResponse =
150+ watchDelayedFetch (secondBucket );
151+
152+ Map <TableBucket , MemoryLogRecords > entries = new HashMap <>();
153+ entries .put (firstBucket , genMemoryLogRecordsByObject (DATA1 ));
154+ entries .put (secondBucket , genMemoryLogRecordsByObject (DATA1 ));
155+ CompletableFuture <List <ProduceLogResultForBucket >> produceResponse =
156+ new CompletableFuture <>();
157+
158+ replicaManager .appendRecordsToLog (20000 , 1 , entries , null , produceResponse ::complete );
159+
160+ assertThat (produceResponse .get ())
161+ .hasSize (2 )
162+ .allSatisfy (result -> assertThat (result .succeeded ()).isTrue ());
163+ assertThat (firstResponse ).isDone ();
164+ assertThat (secondResponse ).isDone ();
165+ assertThat (replicaManager .getDelayedFetchLogManager ().numDelayed ()).isZero ();
166+ }
167+
168+ @ Test
169+ void testDelayedFollowerFetchesWithAllAcks () throws Exception {
170+ TableBucket tableBucket = new TableBucket (DATA1_TABLE_ID , 1 );
171+ int firstFollowerId = 2 ;
172+ int secondFollowerId = 3 ;
173+ List <Integer > replicas = Arrays .asList (TABLET_SERVER_ID , firstFollowerId , secondFollowerId );
174+ makeLogTableAsLeader (tableBucket , replicas , replicas , false );
175+ Replica leader = replicaManager .getReplicaOrException (tableBucket );
176+ long logEndOffset = DATA1 .size ();
177+
178+ CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> firstFollowerFetch =
179+ fetchLog (tableBucket , firstFollowerId , 0L , 1 );
180+ CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> secondFollowerFetch =
181+ fetchLog (tableBucket , secondFollowerId , 0L , 1 );
182+ CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> consumerFetch =
183+ fetchLog (tableBucket , -1 , 0L , 1 );
184+ assertThat (firstFollowerFetch ).isNotDone ();
185+ assertThat (secondFollowerFetch ).isNotDone ();
186+ assertThat (consumerFetch ).isNotDone ();
187+ assertThat (replicaManager .getDelayedFetchLogManager ().numDelayed ()).isEqualTo (3 );
188+
189+ CompletableFuture <List <ProduceLogResultForBucket >> produceResponse =
190+ new CompletableFuture <>();
191+ replicaManager .appendRecordsToLog (
192+ (int ) Duration .ofMinutes (3 ).toMillis (),
193+ -1 ,
194+ Collections .singletonMap (tableBucket , genMemoryLogRecordsByObject (DATA1 )),
195+ null ,
196+ produceResponse ::complete );
197+
198+ assertThat (leader .getLogTablet ().localLogEndOffset ()).isEqualTo (logEndOffset );
199+ assertThat (leader .getLogHighWatermark ()).isZero ();
200+ assertThat (produceResponse ).isNotDone ();
201+ assertThat (consumerFetch ).isNotDone ();
202+ assertThat (replicaManager .getDelayedWriteManager ().numDelayed ()).isOne ();
203+
204+ // Followers read up to LEO, while the consumer and acks=all produce must wait for HW.
205+ assertFetchedRecords (firstFollowerFetch , tableBucket , 0L );
206+ assertFetchedRecords (secondFollowerFetch , tableBucket , 0L );
207+ assertThat (leader .getLogHighWatermark ()).isZero ();
208+ assertThat (produceResponse ).isNotDone ();
209+ assertThat (consumerFetch ).isNotDone ();
210+ assertThat (replicaManager .getDelayedFetchLogManager ().numDelayed ()).isOne ();
211+
212+ // Fetching from the new LEO acknowledges the records returned by the previous fetch.
213+ acknowledgeFetchedRecords (tableBucket , firstFollowerId , logEndOffset );
214+ assertThat (leader .getLogHighWatermark ()).isZero ();
215+ assertThat (produceResponse ).isNotDone ();
216+ assertThat (consumerFetch ).isNotDone ();
217+
218+ acknowledgeFetchedRecords (tableBucket , secondFollowerId , logEndOffset );
219+ assertThat (leader .getLogHighWatermark ()).isEqualTo (logEndOffset );
220+ assertThat (produceResponse ).isDone ();
221+ assertThat (produceResponse .join ())
222+ .containsExactly (new ProduceLogResultForBucket (tableBucket , 0L , logEndOffset ));
223+ assertFetchedRecords (consumerFetch , tableBucket , logEndOffset );
224+ assertThat (replicaManager .getDelayedWriteManager ().numDelayed ()).isZero ();
225+ assertThat (replicaManager .getDelayedFetchLogManager ().numDelayed ()).isZero ();
226+ }
227+
228+ @ Test
229+ void testFetchCompletionFailureDoesNotFailProduceOrOtherBuckets () throws Exception {
230+ TableBucket firstBucket = new TableBucket (DATA1_TABLE_ID , 1 );
231+ TableBucket secondBucket = new TableBucket (DATA1_TABLE_ID , 2 );
232+ int followerId = 2 ;
233+ List <Integer > replicas = Arrays .asList (TABLET_SERVER_ID , followerId );
234+ Map <TableBucket , MemoryLogRecords > entries = new HashMap <>();
235+ Map <TableBucket , FetchLogResultForBucket > fetchResults = new HashMap <>();
236+ AtomicInteger completedFetches = new AtomicInteger ();
237+ AtomicInteger pendingWritesAtFirstFetch = new AtomicInteger (-1 );
238+ for (TableBucket tableBucket : Arrays .asList (firstBucket , secondBucket )) {
239+ makeLogTableAsLeader (tableBucket , replicas , replicas , false );
240+ entries .put (tableBucket , genMemoryLogRecordsByObject (DATA1 ));
241+ fetchLog (
242+ tableBucket ,
243+ followerId ,
244+ 0L ,
245+ 1 ,
246+ results -> {
247+ fetchResults .putAll (results );
248+ // Fail the first callback regardless of bucket iteration order.
249+ if (completedFetches .getAndIncrement () == 0 ) {
250+ pendingWritesAtFirstFetch .set (
251+ replicaManager .getDelayedWriteManager ().numDelayed ());
252+ throw new IllegalStateException ("Expected fetch callback failure" );
253+ }
254+ });
255+ }
256+ assertThat (completedFetches ).hasValue (0 );
257+ assertThat (replicaManager .getDelayedFetchLogManager ().numDelayed ()).isEqualTo (2 );
258+ CompletableFuture <List <ProduceLogResultForBucket >> produceResponse =
259+ new CompletableFuture <>();
260+
261+ replicaManager .appendRecordsToLog (
262+ (int ) Duration .ofMinutes (3 ).toMillis (),
263+ -1 ,
264+ entries ,
265+ null ,
266+ produceResponse ::complete );
267+
268+ assertThat (completedFetches ).hasValue (2 );
269+ assertThat (pendingWritesAtFirstFetch ).hasValue (1 );
270+ assertThat (fetchResults ).containsOnlyKeys (firstBucket , secondBucket );
271+ for (FetchLogResultForBucket result : fetchResults .values ()) {
272+ assertThat (result .succeeded ()).isTrue ();
273+ assertThat (result .getHighWatermark ()).isZero ();
274+ assertLogRecordsEquals (DATA1_ROW_TYPE , result .records (), DATA1 );
275+ }
276+ assertThat (produceResponse ).isNotDone ();
277+ assertThat (replicaManager .getDelayedWriteManager ().numDelayed ()).isOne ();
278+ assertThat (replicaManager .getDelayedFetchLogManager ().numDelayed ()).isZero ();
279+
280+ acknowledgeFetchedRecords (firstBucket , followerId , DATA1 .size ());
281+ assertThat (produceResponse ).isNotDone ();
282+ acknowledgeFetchedRecords (secondBucket , followerId , DATA1 .size ());
283+ assertThat (produceResponse ).isDone ();
284+ assertThat (produceResponse .join ())
285+ .containsExactlyInAnyOrder (
286+ new ProduceLogResultForBucket (firstBucket , 0L , DATA1 .size ()),
287+ new ProduceLogResultForBucket (secondBucket , 0L , DATA1 .size ()));
288+ assertThat (replicaManager .getDelayedWriteManager ().numDelayed ()).isZero ();
289+ }
290+
110291 @ Test
111292 void testDelayFetchLogTimeout () {
112293 TableBucket tb = new TableBucket (DATA1_TABLE_ID , 1 );
@@ -150,6 +331,55 @@ void testDelayFetchLogTimeout() {
150331 });
151332 }
152333
334+ private CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> fetchLog (
335+ TableBucket tableBucket , int replicaId , long fetchOffset , int minFetchBytes ) {
336+ CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> response =
337+ new CompletableFuture <>();
338+ fetchLog (tableBucket , replicaId , fetchOffset , minFetchBytes , response ::complete );
339+ return response ;
340+ }
341+
342+ private void fetchLog (
343+ TableBucket tableBucket ,
344+ int replicaId ,
345+ long fetchOffset ,
346+ int minFetchBytes ,
347+ Consumer <Map <TableBucket , FetchLogResultForBucket >> responseCallback ) {
348+ replicaManager .fetchLogRecords (
349+ new FetchParams (
350+ replicaId ,
351+ Integer .MAX_VALUE ,
352+ minFetchBytes ,
353+ Duration .ofMinutes (3 ).toMillis ()),
354+ Collections .singletonMap (
355+ tableBucket ,
356+ new FetchReqInfo (tableBucket .getTableId (), fetchOffset , Integer .MAX_VALUE )),
357+ null ,
358+ responseCallback );
359+ }
360+
361+ private void acknowledgeFetchedRecords (TableBucket tableBucket , int followerId , long offset ) {
362+ CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> response =
363+ fetchLog (tableBucket , followerId , offset , 0 );
364+ assertThat (response ).isDone ();
365+ FetchLogResultForBucket result = response .join ().get (tableBucket );
366+ assertThat (result .succeeded ()).isTrue ();
367+ assertThat (result .recordsOrEmpty ().sizeInBytes ()).isZero ();
368+ }
369+
370+ private void assertFetchedRecords (
371+ CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> response ,
372+ TableBucket tableBucket ,
373+ long expectedHighWatermark )
374+ throws Exception {
375+ assertThat (response ).isDone ();
376+ assertThat (response .join ()).containsOnlyKeys (tableBucket );
377+ FetchLogResultForBucket result = response .join ().get (tableBucket );
378+ assertThat (result .succeeded ()).isTrue ();
379+ assertThat (result .getHighWatermark ()).isEqualTo (expectedHighWatermark );
380+ assertLogRecordsEquals (DATA1_ROW_TYPE , result .records (), DATA1 );
381+ }
382+
153383 private DelayedFetchLog createDelayedFetchLogRequest (
154384 TableBucket tb ,
155385 int minFetchSize ,
@@ -165,4 +395,28 @@ private DelayedFetchLog createDelayedFetchLogRequest(
165395 TestingMetricGroups .TABLET_SERVER_METRICS ,
166396 null );
167397 }
398+
399+ private CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> watchDelayedFetch (
400+ TableBucket tableBucket ) {
401+ FetchLogResultForBucket previousResult =
402+ FetchLogResultForBucket .records (tableBucket , MemoryLogRecords .EMPTY , 0L , -1L , -1L );
403+ CompletableFuture <Map <TableBucket , FetchLogResultForBucket >> response =
404+ new CompletableFuture <>();
405+ DelayedFetchLog delayedFetchLog =
406+ createDelayedFetchLogRequest (
407+ tableBucket ,
408+ 1 ,
409+ Duration .ofMinutes (3 ).toMillis (),
410+ new FetchBucketStatus (
411+ new FetchReqInfo (150001L , 0L , Integer .MAX_VALUE ),
412+ new LogOffsetMetadata (0L , 0L , 0 ),
413+ previousResult ),
414+ response ::complete );
415+ replicaManager
416+ .getDelayedFetchLogManager ()
417+ .tryCompleteElseWatch (
418+ delayedFetchLog ,
419+ Collections .singletonList (new DelayedTableBucketKey (tableBucket )));
420+ return response ;
421+ }
168422}
0 commit comments