Skip to content

Commit c0add3d

Browse files
committed
Add CDC+BST catch-up dedup hook in SourceBasedDeduper
1. SourceBasedDeduper: add protected isCdcBstStreamCaughtUp(Datastream) hook (returns false by default) so subclasses can short-circuit the 6-hour grace window when consumer-group lag has already drained. Update findDedupCandidateForNewCdcOnlyStream to allow dedup if either the window has expired OR the hook returns true. 2. TestSourceBasedDeduper: add two tests covering the caught-up and not-caught-up hook paths.
1 parent 4de3633 commit c0add3d

2 files changed

Lines changed: 81 additions & 6 deletions

File tree

datastream-server/src/main/java/com/linkedin/datastream/server/SourceBasedDeduper.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@
6262
* a new independent destination must be created. Once this window has expired, the CDC-only
6363
* stream is old enough that deduping the new CDC+BST stream against it is safe.</li>
6464
* </ul>
65+
*
66+
* <h3>Early catch-up detection (subclass hook)</h3>
67+
* <p>Subclasses may override {@link #isCdcBstStreamCaughtUp(Datastream)} to allow a CDC-only
68+
* stream to dedup against a CDC+BST stream <em>before</em> the
69+
* {@link #CONFIG_NEW_STREAM_GRACE_PERIOD_MS} window expires, when the CDC+BST stream's consumer
70+
* group lag has already dropped to an acceptable level. The base implementation returns
71+
* {@code false} (time-window only). Subclasses with access to a Kafka client should override
72+
* this method to query consumer group lag using {@code group.id} from the stream's metadata and
73+
* the broker addresses from the stream's source connection string.
6574
*/
6675
public class SourceBasedDeduper extends AbstractDatastreamDeduper {
6776
private static final Logger LOG = LoggerFactory.getLogger(SourceBasedDeduper.class);
@@ -247,10 +256,11 @@ private Optional<Datastream> findDedupCandidateForNewCdcBootstrapStream(Datastre
247256
* effective offset positions are compatible.
248257
*
249258
* <ol>
250-
* <li><b>Existing CDC+BST candidate whose grace window has expired</b>
251-
* ({@code creationTime + newStreamGracePeriodMs < now}) — dedup against it.
259+
* <li><b>Existing CDC+BST candidate whose grace window has expired OR that has caught up</b>
260+
* ({@code creationTime + newStreamGracePeriodMs < now}, or
261+
* {@link #isCdcBstStreamCaughtUp(Datastream)} returns {@code true}) — dedup against it.
252262
* The CDC+BST stream started from an earlier offset to catch up on historical lag, but
253-
* its grace window expiring means it has finished catching up and its consumer position
263+
* once its grace window expires or its lag drops to the threshold, its consumer position
254264
* has reached the live tail. At that point the two streams' effective offsets converge
255265
* and sharing a destination is safe.</li>
256266
* <li><b>Existing CDC-only candidate</b> — dedup immediately, no timing check needed.
@@ -269,12 +279,12 @@ private Optional<Datastream> findDedupCandidateForNewCdcBootstrapStream(Datastre
269279
private Optional<Datastream> findDedupCandidateForNewCdcOnlyStream(Datastream newStream,
270280
List<Datastream> cdcOnlyCandidates, List<Datastream> cdcBstCandidates) {
271281

272-
// Step 1: prefer a CDC+BST stream whose grace window has expired (bootstrap phase is done).
282+
// Step 1: prefer a CDC+BST stream whose grace window has expired, or that has already caught up.
273283
Optional<Datastream> eligibleCdcBstStream = cdcBstCandidates.stream()
274-
.filter(d -> hasWindowExpired(d, _newStreamGracePeriodMs))
284+
.filter(d -> hasWindowExpired(d, _newStreamGracePeriodMs) || isCdcBstStreamCaughtUp(d))
275285
.findFirst();
276286
if (eligibleCdcBstStream.isPresent()) {
277-
LOG.info("Deduping new CDC stream {} against CDC+BST stream {} whose grace window has expired",
287+
LOG.info("Deduping new CDC stream {} against CDC+BST stream {} (grace window expired or lag within threshold)",
278288
newStream.getName(), eligibleCdcBstStream.get().getName());
279289
return eligibleCdcBstStream;
280290
}
@@ -307,6 +317,26 @@ private boolean isCdcBootstrapRequiredStream(Datastream stream) {
307317
return Boolean.parseBoolean(stream.getMetadata().getOrDefault(_cdcBootstrapRequiredMetadataKey, "false"));
308318
}
309319

320+
/**
321+
* Returns {@code true} if the existing CDC+BST stream has caught up to the live tail, allowing
322+
* a new CDC-only stream to dedup against it before the {@link #CONFIG_NEW_STREAM_GRACE_PERIOD_MS}
323+
* window expires.
324+
*
325+
* <p>The base implementation always returns {@code false} (time-window only). Subclasses with
326+
* access to a Kafka client should override this to query consumer group lag in real time using
327+
* {@code DatastreamMetadataConstants.GROUP_ID} from the stream's metadata and the broker
328+
* addresses from the stream's source connection string.
329+
*
330+
* <p>Implementations must be defensive: return {@code false} on any error so that dedup is
331+
* not incorrectly granted.
332+
*
333+
* @param cdcBstStream existing CDC+BST stream to check
334+
* @return {@code true} if the stream has caught up to the live tail
335+
*/
336+
protected boolean isCdcBstStreamCaughtUp(Datastream cdcBstStream) {
337+
return false;
338+
}
339+
310340
/**
311341
* Returns {@code true} when the given stream's time window has expired, i.e.
312342
* {@code System.currentTimeMillis() >= creationTime + windowMs}.

datastream-server/src/test/java/com/linkedin/datastream/server/TestSourceBasedDeduper.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,51 @@ public void testDefaultDedupeConfigWindowsDisabled() throws DatastreamValidation
260260
Assert.assertTrue(result.isPresent());
261261
}
262262

263+
// ---- isCdcBstStreamCaughtUp hook: grace window still active but hook says caught up → dedup ----
264+
@Test
265+
public void testCdcOnlyNewVsExistingCdcBstCaughtUpEarly() throws DatastreamValidationException {
266+
Datastream newStream = generateCdcDatastream(0, false, false, -1);
267+
// creationMs = now → grace window (1 hour) has NOT expired
268+
Datastream candidate = generateCdcDatastream(0, true, true, System.currentTimeMillis());
269+
270+
// Subclass overrides hook to report stream as caught up
271+
Properties props = new Properties();
272+
props.setProperty(SourceBasedDeduper.CONFIG_NEW_STREAM_GRACE_PERIOD_MS, "3600000");
273+
props.setProperty(SourceBasedDeduper.CONFIG_SNAPSHOT_WORST_REFRESH_DAYS, "4");
274+
SourceBasedDeduper deduper = new SourceBasedDeduper(props) {
275+
@Override
276+
protected boolean isCdcBstStreamCaughtUp(Datastream cdcBstStream) {
277+
return true;
278+
}
279+
};
280+
281+
Optional<Datastream> result = deduper.findExistingDatastream(newStream, Collections.singletonList(candidate));
282+
Assert.assertTrue(result.isPresent());
283+
Assert.assertEquals(result.get(), candidate);
284+
}
285+
286+
// ---- isCdcBstStreamCaughtUp hook: grace window active and hook returns false → new destination ----
287+
@Test
288+
public void testCdcOnlyNewVsExistingCdcBstNotCaughtUp() throws DatastreamValidationException {
289+
Datastream newStream = generateCdcDatastream(0, false, false, -1);
290+
Datastream candidate = generateCdcDatastream(0, true, true, System.currentTimeMillis());
291+
292+
// Hook reports not caught up (same as base behaviour)
293+
SourceBasedDeduper deduper;
294+
Properties props = new Properties();
295+
props.setProperty(SourceBasedDeduper.CONFIG_NEW_STREAM_GRACE_PERIOD_MS, "3600000");
296+
props.setProperty(SourceBasedDeduper.CONFIG_SNAPSHOT_WORST_REFRESH_DAYS, "4");
297+
deduper = new SourceBasedDeduper(props) {
298+
@Override
299+
protected boolean isCdcBstStreamCaughtUp(Datastream cdcBstStream) {
300+
return false;
301+
}
302+
};
303+
304+
Optional<Datastream> result = deduper.findExistingDatastream(newStream, Collections.singletonList(candidate));
305+
Assert.assertFalse(result.isPresent());
306+
}
307+
263308
// ---- Different source in CDC-aware path → no match ----
264309
@Test
265310
public void testCdcAwarePathDifferentSourceNoMatch() throws DatastreamValidationException {

0 commit comments

Comments
 (0)