@@ -279,6 +279,9 @@ private void initializeFlushScheduler() {
279279 */
280280 private void checkAndFlushIfNeeded () throws StarrocksRetryableException {
281281 synchronized (writeLock ) {
282+ // 驱逐队首连续的、已全部落库(不在 pendingFlushTables)的陈旧 offset 锚点,避免长期无变更的冷表占据队首,导致断点被永久钉死在其旧位点上
283+ evictStaleFirstOffsets ();
284+
282285 if (pendingFlushTables .isEmpty ()) {
283286 return ; // 没有数据需要刷新
284287 }
@@ -317,6 +320,35 @@ private void checkAndFlushIfNeeded() throws StarrocksRetryableException {
317320 }
318321 }
319322
323+ /**
324+ * 驱逐 firstOffsetByTable 队首连续的、已全部落库的陈旧 offset 锚点。
325+ * <p>
326+ * flushTable 回传断点时取的是队首表的 offset。若一张长期无变更的冷表在全量/增量早期
327+ * 进入队首后再无新数据,它就不会再进入 pendingFlushTables,也就永远不会被 flush,
328+ * 从而永远不会被移出队首(见 flushTable 中 tableName.equals(firstTableName) 分支),
329+ * 导致所有其它表 flush 回传的都是这张冷表的旧位点,断点被永久钉死。
330+ * <p>
331+ * 判据:队首表若不在 pendingFlushTables,说明它此刻没有待落库数据、之前的数据均已成功
332+ * 导入,丢弃其 offset 锚点不会丢数,可直接驱逐;一旦遇到队首表在 pendingFlushTables 中,
333+ * 则停止驱逐——该表会被定时/大小阈值 flush 后按正常流程移出队首并推进断点。
334+ */
335+ private void evictStaleFirstOffsets () {
336+ synchronized (firstOffsetByTable ) {
337+ Iterator <Map .Entry <String , TapCallbackOffset >> iterator = firstOffsetByTable .entrySet ().iterator ();
338+ while (iterator .hasNext ()) {
339+ Map .Entry <String , TapCallbackOffset > firstEntry = iterator .next ();
340+ String firstTableName = firstEntry .getKey ();
341+ if (pendingFlushTables .contains (firstTableName )) {
342+ // 队首表仍有待落库数据,等它被正常 flush 后自然推进,停止驱逐
343+ break ;
344+ }
345+ iterator .remove ();
346+ taplogger .info ("Evicted stale offset anchor of table {} from queue head " +
347+ "(already flushed, no pending data), allowing breakpoint to advance" , firstTableName );
348+ }
349+ }
350+ }
351+
320352 /**
321353 * 刷新指定的表
322354 */
0 commit comments