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 */
6675public 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}.
0 commit comments