ryanmeowy commented on issue #10203:
URL: https://github.com/apache/seatunnel/issues/10203#issuecomment-6357660652

   @DanielLeens @SEZ9 the transition table for the four cuts. One question per 
row: after the crash, how many times does T's snapshot run, and what happens to 
the rows it already produced. T is the table admitted while the job was running.
   
   Terms mapped to your wording from the 16th: admitted = `admittedTables`, 
in-flight = `pendingSnapshotTables`, watermark = `tableWatermarks[T] = W_high`.
   
   Two rules it depends on:
   
   - The enumerator is the only writer of the registry. A reader reports a 
finished split and never admits a table, so a reader crash or reassignment 
cannot produce a second admission; a poller tick that sees T already in 
`pendingSnapshotTables` is a no-op, which covers the timer-tick duplicate.
   - T joins the incremental split's `tableIds` only in a checkpoint that also 
carries `admittedTables[T]` and `tableWatermarks[T]`. Anything before a 
completed checkpoint is lost on recovery, so a cut before one re-runs instead 
of resuming.
   
   | # | cut | durable when it dies | recovery | T's snapshot | T's rows |
   | - | --- | --- | --- | --- | --- |
   | 1 | admitted, split assigned, nothing read yet | registry has no T, or 
`pendingSnapshotTables[T]` if a checkpoint landed after the decision | the 
poller re-discovers T on its next tick, or point 6's re-drive recreates the 
split from `pendingSnapshotTables` | once, from a fresh `[low', high']` | none 
produced |
   | 2 | `low`/`high` captured and the backfill done, but the checkpoint 
carrying `W_high` never completed | `pendingSnapshotTables[T]`; the reader 
state holds the split only if a checkpoint landed after the assignment | the 
split is handed back (2a / 2b below), or point 6's re-drive recreates it if the 
checkpoint predates the assignment | 2a re-runs, 2b does not | 2a replays the 
split |
   | 3 | checkpointed: `admittedTables[T]` + `W_high` + the rebuilt incremental 
split | all three, in the same checkpoint | nothing to drive: restore subtracts 
the registry, and the incremental branch has no pending entry | does not run | 
`shouldEmit()` drops `< W_high` |
   | 4 | the reader holding the split is reassigned mid-read | unchanged; 
whatever the last checkpoint carried | the split goes back through 
`addSplitsBack` and is dispatched to the reader that holds the streaming split 
| as row 1 or row 2 | as row 1 or row 2 |
   
   **Death inside the unit.** @SEZ9 asked on the 11th what is persisted if the 
job dies after the snapshot read but before the backfill finishes. Nothing 
about `[low, high]`, and that is deliberate: `MySqlSnapshotFetchTask.execute` 
runs the snapshot read and the bounded backfill as one task, and the split only 
counts as finished once that task has returned and drained its queue 
(`IncrementalSourceScanFetcher.isFinished()`), so a death between the two is 2a 
— the pair re-runs with a fresh range. The range never needs persisting because 
it is never half-applied; what must not happen is a second admission, and that 
is the registry's job.
   
   **2a / 2b** differ in one bit: whether the last completed checkpoint already 
contains the finished split. If it does not (2a), the split comes back 
unfinished and re-runs from a fresh `[low', high']`. If it does (2b), the 
watermark is recovered without replaying: `SnapshotSplitAssigner.addSplits` 
calls `restoreCompletedSnapshotSplit`, which — only when the split is 
`isSnapshotReadFinished()` and still present in `assignedSplits` — puts its 
`SnapshotSplitWatermark` back into `splitCompletedOffsets` and drops the split 
instead of queueing it. The reader side cooperates: since #12163 
`finishedUnackedSplits` is cleared only on the enumerator's 
`CompletedSnapshotSplitsAckEvent`, and `snapshotState()` writes those splits 
into the checkpoint, so a reader preempted before the ack restores them as 
finished and re-reports on `addSplits`. That precondition is the one to respect 
when we add T: the admission split must be registered in `assignedSplits` for 
the whole read, or 2b silently de
 grades to 2a.
   
   **Correction to my last note.** I wrote there that the re-read happens "with 
nothing yet committed". That only holds if no checkpoint completed while the 
snapshot was being read. If one did, the rows it carried are committed and the 
2a re-read replays them, because a split read is not resumable: 
`SnapshotSplitState` holds only the two watermarks, and both are set when the 
read ends. That is the exposure the initial snapshot phase already has for 
every table today, not something admission introduces, and it is not what the 
exactly-once precondition in point 1 is for. It does mean the cut-2 test 
asserts "no loss and no second admission", not "no replay at all", unless the 
sink keys on the primary key.
   
   **Cut 4** needs one rule the note left implicit. `assignSplits()` offers 
`getNext()` to whichever reader is waiting, and after #11677 the loop re-runs 
when splits come back, so the returned snapshot split can be picked up by a 
reader that does not hold T's streaming split. If that happens the split sits 
in that reader's queue forever: `checkSplitOrStartNext()` returns early while a 
stream fetcher is alive (`IncrementalSourceSplitReader.java:153-156`), and an 
unbounded stream fetcher never finishes. So the enumerator must hand T's 
admission split only to the reader that holds S, and the incremental-phase 
assigner needs a snapshot branch for it: `IncrementalSplitAssigner.addSplits` 
still casts every returned split to `IncrementalSplit`, and `getNext()` keeps 
returning empty once `splitAssigned` is set. With that, cut 4 is row 1 or row 2 
on another subtask, and the returned split keeps its `splitId`, so dedup does 
not depend on which reader picks it up.
   
   **The hand-off, for the state-machine test.** Point 2's stop/restart shape, 
one client at a time: `STREAMING → CLOSING → SNAPSHOT(T) → BACKFILL(T) → 
STREAMING(S ∪ {T})`. `CLOSING` is bounded by `READER_CLOSE_TIMEOUT_SECONDS = 
30` then `shutdownNow()` (`IncrementalSourceStreamFetcher.java:89`, 
`:246-251`); `BACKFILL` starts only after it returns, on the same 
`database.server.id`. So the test asserts one thing, never two clients with the 
same server id connected (sampled server-side from the replication user's `SHOW 
PROCESSLIST`), plus one crash per state that has to land in row 1 or row 2. 
There is no reserved-id branch, hence one path, not two.
   
   **Tests.** Rows 1 and 2a end in the same place, so one case covers both: 
give T a row the sink rejects (a sink trigger as in #11677, or a value the sink 
column cannot hold), and the crash is caused by the unit itself, so it always 
lands inside it. Whether any of T's rows were emitted before that only decides 
between row 1 and 2a, not what recovery does. The assertion is T's full history 
plus every change after admission, exactly once. Row 3 needs the checkpoint to 
be in first, and the harness already has the two pieces: 
`awaitCompletedCheckpointCountAtLeast()` for the barrier and the `BEFORE INSERT 
... SIGNAL SQLSTATE '45000'` trigger on the sink table used by #11677's 
`testMysqlCdcContinuesAfterSinkFailure` to force the failure exactly once, 
confirmed through the engine log rather than a sleep. Row 2b cannot be produced 
deterministically from outside and is a reader unit test instead: `addSplits` 
with a finished split must re-report it and the enumerator must admit without a 
seco
 nd read. Row 4 is row 1 or 2 on another subtask, so it is the same cases run 
with parallelism above 1.
   
   One constraint that comes out of that: 
`mysqlcdc_to_mysql_with_checkpoint_restore.conf` is `exactly_once = false` on 
both sides and this feature requires exactly-once, so the new cases need their 
own conf (the module already has exactly-once variants to copy from). With 
discovery defaulting off, the existing CDC suite is the `table-pattern` 
backward-compatibility regression; I will still add one case that asserts a 
discovery-off job's restore path is unchanged.
   


-- 
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