yzeng1618 commented on code in PR #10951:
URL: https://github.com/apache/seatunnel/pull/10951#discussion_r3338179218


##########
seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/main/java/org/apache/seatunnel/translation/flink/schema/SchemaOperator.java:
##########
@@ -266,6 +290,155 @@ public void notifyCheckpointComplete(long checkpointId) 
throws Exception {
                 tableId,
                 eventTime);
 
+        applyNextPendingSchemaChange();
+    }
+
+    /**
+     * Handles fallback timer firing on the task thread (Flink 1.13 checkpoint 
stall workaround).
+     *
+     * <p>IMPORTANT: This method still respects the checkpoint-completion 
safety fence. The DDL is
+     * only applied if at least one checkpoint has completed after the schema 
event was detected
+     * ({@code firstSeenCheckpointId >= 0}). This ensures XA transactions from 
the earlier
+     * checkpoint cycle have finished before ALTER TABLE runs.
+     *
+     * <p>If no checkpoint has completed yet, we reschedule the fallback timer 
and wait. The
+     * fallback only kicks in when checkpoints stall AFTER the first post-DDL 
checkpoint has
+     * completed, which is the scenario in Flink 1.13 high-parallelism CDC 
jobs where some source
+     * subtasks finish before others.
+     */
+    private void handleFallbackTimerOnTaskThread() throws InterruptedException 
{
+        fallbackTimerFired = false;
+
+        if (!schemaChangePending || pendingQueue.isEmpty()) {
+            return;
+        }
+
+        if (lastCheckpointCompletedMs > 0
+                && System.currentTimeMillis() - lastCheckpointCompletedMs
+                        < CHECKPOINT_STALL_TIMEOUT_MS) {
+            scheduleFallbackTimer();
+            return;
+        }
+
+        BufferedRecord head = advancePastDataRecords();
+        if (head == null) {
+            return;
+        }
+
+        if (firstSeenCheckpointId < 0) {
+            log.info(
+                    "Fallback timer fired but no checkpoint has completed 
after schema event "
+                            + "for table {} (epoch {}). Rescheduling fallback 
to preserve "
+                            + "checkpoint-completion safety fence.",
+                    head.schemaEvent.tableIdentifier(),
+                    head.schemaEvent.getCreatedTime());
+            scheduleFallbackTimer();
+            return;
+        }
+
+        log.warn(
+                "Checkpoint stall detected after first post-DDL checkpoint {}. 
"
+                        + "Applying deferred DDL for table {} (epoch {}) via 
fallback timer. "
+                        + "Note: data committed via normal Flink checkpoint 
lifecycle may be "
+                        + "delayed until checkpoints resume.",
+                firstSeenCheckpointId,
+                head.schemaEvent.tableIdentifier(),
+                head.schemaEvent.getCreatedTime());
+
+        applyNextPendingSchemaChange();
+    }
+
+    private void scheduleFallbackTimer() {
+        if (fallbackScheduler == null || fallbackScheduler.isShutdown()) {
+            return;
+        }
+        ScheduledFuture<?> existing = pendingFallbackFuture;
+        if (existing != null && !existing.isDone()) {
+            return;
+        }
+        pendingFallbackFuture =
+                fallbackScheduler.schedule(
+                        () -> {
+                            fallbackTimerFired = true;
+                            registerProcessingTimeCallback();
+                        },
+                        CHECKPOINT_STALL_TIMEOUT_MS,
+                        TimeUnit.MILLISECONDS);
+    }
+
+    private void registerProcessingTimeCallback() {

Review Comment:
   registerProcessingTimeCallback() uses reflection throughout to infer the 
method names of Flink’s internal APIs. Does this introduce potential risks? For 
instance, any change to method names or parameters would break the logic. Could 
we instead register the ProcessingTimeCallback via the strongly-typed public 
API exposed by getProcessingTimeService()?



##########
seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/main/java/org/apache/seatunnel/translation/flink/schema/SchemaOperator.java:
##########
@@ -116,20 +140,25 @@ public void open() throws Exception {
     }
 
     @Override
-    public void processElement(StreamRecord<SeaTunnelRow> streamRecord) {
+    public void processElement(StreamRecord<SeaTunnelRow> streamRecord)
+            throws InterruptedException {
         SeaTunnelRow element = streamRecord.getValue();
 
         if (!isSchemaEvolutionEnabled(pluginConfig)) {
             output.collect(streamRecord);
             return;
         }
 
+        if (fallbackTimerFired) {
+            handleFallbackTimerOnTaskThread();
+        }

Review Comment:
   One entry of the fallback logic in SchemaOperator relies on continuous 
invocations of processElement. However, this PR addresses cases where no more 
data flows from the source and processElement is no longer triggered, rendering 
this path invalid. Consequently, fallback processing entirely depends on the 
timer registered via reflection.
   If reflective registration fails, the logic falls back to a flag-based 
implementation that also requires processElement to run. When there is no 
incoming data, neither fallback path can be executed, the fallback mechanism 
fails to work, and the task hanging issue remains unresolved under reflection 
failure scenarios.



##########
seatunnel-translation/seatunnel-translation-flink/seatunnel-translation-flink-common/src/main/java/org/apache/seatunnel/translation/flink/schema/coordinator/LocalSchemaCoordinator.java:
##########
@@ -218,20 +227,26 @@ public enum SchemaProcessingStatus {
     public boolean requestSchemaChange(TableIdentifier tableId, long epoch, 
long timeoutMs)
             throws InterruptedException, SchemaCoordinationException {
         String key = tableId.toString() + "#" + epoch;
-        int expectedAcks = activeSinkSubtasks.size();
-        if (expectedAcks == 0) {
+        int totalSubtasks = activeSinkSubtasks.size();
+        if (totalSubtasks == 0) {
             log.warn(
                     "No active sink subtasks. Cannot coordinate schema change 
for table {} (epoch {}). "
                             + "Assuming success to avoid deadlock.",
                     tableId,
                     epoch);
             return true;
         }
+        // Schema changes (DDL) are database-level operations that only need 
to execute once.
+        // Due to Flink's partitioning, only one subtask receives the schema 
change event,
+        // so we only need 1 ACK to confirm the DDL was applied successfully.
+        int expectedAcks = 1;

Review Comment:
   Completing the request on a single ACK is correct for "the DDL runs once on 
the DB", but it implicitly assumes every sink subtask's local schema view is 
refreshed by the broadcast/State path rather than by receiving the event. Could 
we add a comment stating this precondition, and a multi-table (table-names with 
≥2 tables) regression test so the "view refresh covers all subtasks" invariant 
is guarded? Current E2E only covers single-table.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to