DanielLeens commented on PR #11557:
URL: https://github.com/apache/seatunnel/pull/11557#issuecomment-5289496990

   **[Continuing the full review from my previous comment above — this is Part 
2 of 2.]**
   
   
   ### Issue 33 — NEW. `checkpointAssignmentProofs` and 
`uncheckpointedAssignmentSplitIds` are unbounded
   
   **Location:** `ManagedSourceReaderRuntime.java:109-110`, against `:888-892, 
:901, :909-916`
   **Problem description:** `completedCheckpoints`/`abortedCheckpoints` are 
explicitly capped at `MAX_CHECKPOINT_HISTORY = 128`, but these two collections 
are pruned only when a `CHECKPOINT_COMPLETE`/`CHECKPOINT_ABORTED` command 
actually arrives.
   **Potential risk:** If the coordinator stops delivering completion 
notifications — which is exactly what happens when checkpointing is degraded, 
and exactly what Issue 1 causes — the map accumulates one `LinkedHashSet` of 
every split id per checkpoint, forever. OOM on a long-running high-split-count 
source, precisely when the job is already degraded.
   **Best improvement:** Bound `checkpointAssignmentProofs` to 
`MAX_CHECKPOINT_HISTORY` entries, evicting the lowest checkpoint id and folding 
its ids out of `uncheckpointedAssignmentSplitIds`.
   **Severity:** Medium
   **Raised by another reviewer:** No
   
   ---
   
   ### Issue 34 — NEW. `subscribeToAvailability()` accumulates handlers on a 
reused future
   
   **Location:** `ManagedSourceReaderRuntime.java:641-666`, against `:763`
   **Problem description:** Every `NOTHING_AVAILABLE` poll calls 
`available.whenComplete(...)`. Because `applySplits` sets 
`availabilitySignalled = true` (`:763`), a split assignment can re-open the 
gate while an earlier subscription is still outstanding, producing a second 
registration. Connectors commonly return the *same* `CompletableFuture` 
instance from `isAvailable()` until it completes.
   **Potential risk:** Unbounded retained-handler growth, each closure 
capturing `this`, in any source that receives splits faster than it drains 
them. A slow heap leak with no bound and no metric.
   **Best improvement:** Track an `availabilitySubscribed` flag and skip 
re-subscription while one is outstanding.
   **Severity:** Medium
   **Raised by another reviewer:** No
   
   ---
   
   ### Issue 35 — NEW. Async-task timeout cancellation leaks the interrupt flag 
onto shared worker-pool threads
   
   **Location:** `ManagedCoordinatorScheduler.java:337-395` (specifically 
`:352-367`, `:383`); `TaskExecutionService.java:1121-1127, 1148-1167`
   **Problem description:** On timeout, the scheduler calls 
`worker.cancel(true)` (`:383`), interrupting whichever 
`managedSourceIoExecutor`/`managedSourceCpuExecutor` thread is running the 
connector callable. `FutureTask.handlePossibleCancellationInterrupt` 
deliberately does **not** clear the interrupt status (the 
`Thread.interrupted()` call is commented out in the JDK), and the scheduler's 
own wrapper `finally` (`:362-366`) restores the context classloader but does 
not clear the flag either.
   **Potential risk:** These are node-wide shared pools. The next task 
submitted to that thread — belonging to a *different* managed Source, possibly 
a different job — begins with the interrupt flag set and fails spuriously. Same 
defect class as Issue 7, in a second location, with cross-job blast radius.
   **Best improvement:** Clear `Thread.interrupted()` in the wrapper's 
`finally` at `:362-366`.
   **Severity:** Medium
   **Raised by another reviewer:** No
   
   ---
   
   ### Issue 36 — NEW. Kafka's managed split-back handler calls `assignSplit()` 
eagerly, racing reader readiness
   
   **Location:** `KafkaSourceSplitEnumerator.java:635-650`, `:510-511`, against 
`ManagedSourceCoordinatorRuntime.java:1500-1507`
   **Problem description:** The managed split-back completion handler calls 
`assignSplit()` eagerly. The legacy path deliberately only parked splits and 
let `registerReader` drive re-assignment. Both dispatch routes now hard-require 
a ready reader via `requireReadyReader`. During reader-failure recovery a 
reader is registered but `ready == false`, so the 
`registeredReaders().isEmpty()` guard in `SeaTunnelSplitEnumeratorContext` does 
not save it.
   **Potential risk:** Two failure modes. First, `requireReadyReader` throws 
and fails the job — and because it is a race between the admin round trip and 
the epoch handshake, it presents as intermittent. In batch mode 
(`discoveryIntervalMillis <= 0`, the default) `signalNoMoreSplits` is invoked 
for every subtask, so any one not-yet-ready reader trips it. Second, and worse, 
if all readers are momentarily unregistered, `assignSplit()` still runs 
`assignedSplit.putAll(pendingSplit); pendingSplit.clear();` (`:510-511`), 
permanently excluding those partitions from future discovery snapshots — 
**silent partition loss with no error at all.**
   **Best improvement:** Drop the `assignSplit();` call from the split-back 
completion handler and let reader registration drive re-assignment, as the 
legacy path does. This is also a concrete instance of Issue 31, so the general 
deferral fix would mitigate the first failure mode but not the second.
   **Severity:** High
   **Raised by another reviewer:** No
   
   ---
   
   ### Issue 37 — NEW. `SourceReaderBase.isAvailable()` short-circuits on 
`noMoreSplitsAssignment`, defeating event-driven availability
   
   **Location:** `SourceReaderBase.java:170-182` (specifically `:172`)
   **Problem description:** `if (currentFetch != null || 
!elementsQueue.isEmpty() || noMoreSplitsAssignment) return 
CompletableFuture.completedFuture(null);` — the third term makes availability 
**permanently complete** once no-more-splits arrives.
   **Potential risk:** For an unbounded source in that state — Kafka with 
static assignment, or any streaming source whose enumerator signalled 
no-more-splits — the steady state becomes `pollNextManaged` -> 
`NOTHING_AVAILABLE` -> `subscribeToAvailability()` -> already-complete future 
fires `whenComplete` inline -> `availabilitySignalled.set(true)` -> poll again. 
This is not a CPU burn, because `SourceFlowLifeCycle:354-356` sleeps 
`idleWaitMillis` when `runOneTurn()` returns false — but it fully defeats the 
event-driven availability mechanism, degrading it to a fixed 100 Hz poll per 
subtask regardless of whether data exists.
   **Best improvement:** Drop `noMoreSplitsAssignment` from the shortcut, or 
qualify it with `Boundedness.BOUNDED`.
   **Severity:** Medium
   **Raised by another reviewer:** No
   
   ---
   
   ### Issue 38 — NEW. `FakeSourceReader.pollNextManaged` throws on a short 
read, producing a permanent crash loop
   
   **Location:** `FakeSourceReader.java:140-153`, with `:191-195`
   **Problem description:** The managed poll loop throws 
`IllegalStateException("Managed Fake source generator made no progress ...")` 
whenever `generateFakedRows` returns 0, which it does whenever `startInclusive 
>= effectiveRowCount(...)`.
   **Potential risk:** Configure an explicit `rows` list of 5; the job 
checkpoints mid-split at `(rowNum=3, totalRowNum=5)`; the config is then edited 
to 2 rows and the job restored. `addSplits:194` keeps `(3, 5)` because neither 
guard fires; `pollNextManaged` computes `start = 5 - 3 = 2`; 
`effectiveRowCount` now returns 2; `endExclusive = min(2, 3) = 2`, the loop 
body never executes, 0 is returned, the exception fires, the task fails, the 
same state restores — **permanent crash loop.**
   **Best improvement:** Treat a short read as end-of-split and poll the split 
off, rather than throwing.
   **Related Fake findings (Low):** `split_read_interval` now throttles only at 
split start on the managed lane, gated on `split.getRowNum() == 
split.getTotalRowNum()` (`:128`), where legacy throttled every poll (`:86-90`) 
— defensible, but an undocumented user-facing behaviour change. `addSplits` 
(`:191-195`) dereferences `fakeDataGeneratorMap.get(split.getTableId())` with 
no null check, moving a pre-existing NPE from poll time to assignment time. 
Explicitly **not** a defect: the `addSplits` rewrite of `rowNum` to 
`effectiveRowCount(...)` looks alarming but is inert on the legacy lane, 
because `FakeDataGenerator.generateFakedRows` (`:122-138`) ignores `rowNum` 
entirely when `fakeRows != null`.
   **Severity:** Medium
   **Raised by another reviewer:** No
   
   ---
   
   ### Issue 19 (rewritten) — Managed checkpoint-ack failure is propagated but 
never logged, and can be lost outright
   
   **Location:** `SeaTunnelTask.java:483-501, 505-520`; 
`SourceSeaTunnelTask.java:192-205`; `SourceFlowLifeCycle.java:846-856`; 
`ManagedSourceCoordinatorRuntime.java:936-951, 1597-1599`
   **Problem description (corrected):** My prior claim that the failure is 
thrown inside `whenComplete` and swallowed is **wrong**. 
`waitForCheckpointAcknowledge()` defaults to `true`, so legacy tasks keep exact 
`.join()` semantics, and the managed override routes the failure into 
`reportManagedRuntimeFailure` -> `reportAsynchronousFailure`, consumed by the 
owner loop at `:398`. That is a legitimate path. Three smaller real defects 
remain:
   **(19a)** `SourceSeaTunnelTask.onCheckpointAcknowledgeFailure` (`:198-205`) 
replaces the base body entirely, so the base class's `log.error(...)` 
(`SeaTunnelTask.java:516-520`) never runs on the managed lane. Combined with 
the zero-logging problem, a failed managed checkpoint ack produces no log line 
at all.
   **(19b)** `reportManagedRuntimeFailure` (`SourceFlowLifeCycle.java:846-852`) 
guards on `isManagedReaderRuntime()`, which reads only `runtimeSelection` and 
stays true for the task's whole lifetime, while `managedReaderRuntime` is 
assigned late in `setCollector` and cleared conceptually at close. A late ack 
failure arriving after `close()` sets `asynchronousFailure` on a runtime whose 
owner loop has exited — the failure is lost with no logging.
   **(19c)** The coordinator does the identical non-joined `whenComplete` at 
`:936-951`, and its `reportAsynchronousFailure` (`:1597-1599`) is a bare 
`compareAndSet` with no logging and no wakeup, unlike the reader's version.
   Ack *ordering* across barriers is also no longer serialized on the managed 
lane; acks are keyed by checkpoint id so `CheckpointCoordinator` likely 
tolerates it, but it is an undocumented behavioural change.
   **Best improvement:** Call `super.onCheckpointAcknowledgeFailure(...)` first 
in the override; log unconditionally in `reportManagedRuntimeFailure` before 
the guards; add logging and a wakeup to the coordinator's 
`reportAsynchronousFailure`; document the ordering change.
   **Severity:** Medium
   **Raised by another reviewer:** No
   
   ---
   
   ### Issue 16 (rewritten) — Three non-failure-atomic release paths leak the 
node-wide memory budget permanently
   
   **Location:** `ManagedSourceReaderRuntime.java:1246-1262` (16a), 
`ReaderCommandMailbox.java:278-294` (16b), 
`ManagedSourceReaderRuntime.java:506-537` (16c)
   **Problem description (corrected):** `ManagedSourceMemoryBudget` has no 
`close()` and its `release()` validates before mutating, so it *is* 
failure-atomic — my prior claim was wrong. The leak is real but lives 
elsewhere. **(16a)** `releaseOutbound` mutates all six accounting fields, then 
throws the underflow guard, leaving `outboundMemoryBudget.release(bytes)` at 
`:1261` unreachable. **(16b)** `ReaderCommandMailbox.close()` sets `closed = 
true` first, then runs a release loop that can throw mid-way, leaving the 
remaining bytes unreturned and `commands` uncleared. **(16c)** 
`ManagedSourceReaderRuntime.close()` has no `try/finally`, so a throw at `:522` 
skips `ControlEvent::discard` (which releases the `admitSerializedLegacySplits` 
reservation), `releaseAllOutbound()` and `lifecycle.closed()`.
   **Potential risk:** The budget is a `TaskExecutionService` singleton, so 
leaked bytes shrink the pool for **every other managed Source on the worker**, 
permanently, until node restart.
   **Best improvement:** Move `outboundMemoryBudget.release(bytes)` above the 
underflow guard; wrap the mailbox release loop and each stage of `close()` in 
`try/finally`.
   **Severity:** Medium-High
   **Raised by another reviewer:** No
   
   ---
   
   ### Issue 28 (rewritten) — narrow residual only
   
   **Location:** `ManagedSourceReaderRuntime.java:441, 465`; 
`SourceFlowLifeCycle.java:285-295, 438-442`
   **Problem description (corrected):** My prior claim that the managed lane 
never sets `SourceFlowLifeCycle.prepareClose` is **largely refuted** — 
`:439-442` sets it on the managed lane. The residual is narrow: the 
poll-status-driven path calls the *runtime's* `signalNoMoreElement()` 
(`ManagedSourceReaderRuntime:441` -> `:465`), not the lifecycle's, so on that 
path `prepareClose` stays `false` and the flush timer keeps admitting flush 
signals to a drained reader (`SourceFlowLifeCycle:285-295`), burning reserved 
mailbox capacity. The connector-driven path via 
`SourceReaderContext.signalNoMoreElement()` is correct.
   **Severity:** Low
   **Raised by another reviewer:** No
   
   ---
   
   ### Issues 10-15, 17, 18, 20-25, 27, 29, 30 — carried forward
   
   None of the underlying files changed since my last full read, so I am not 
re-deriving unchanged analysis a fifth time. Locations for traceability, with 
this round's verification status:
   
   | # | Issue | Location | Verified this round |
   |---|---|---|---|
   | 10 | `coordinatorEpoch`/`coordinatorAttemptId`/`closed` read cross-thread 
without `volatile`; a stale read returns `STALE_TARGET` and can kill the job 
right after the handshake | `ManagedSourceReaderRuntime.java:114-115, 136, 235, 
256-259, 510, 605, 747-748, 1429` | Yes, re-derived |
   | 11 | Deferred split request removed before the readiness test, so it is 
silently dropped | `ManagedSourceCoordinatorRuntime.java:1410-1419` | Yes, 
re-read |
   | 12 | `enumerator.run()` runs inline on the drain thread, blocking every 
queued barrier for its duration; strictly worse than legacy | 
`SourceSplitEnumeratorTask.java:445-449` | Yes, re-read |
   | 13 | Kafka managed discovery fails the job on transient errors; 
`scheduleNextManagedDiscovery()` (`:601`) sits **past** the `throw` with no 
`finally`, so any log-and-continue fix that does not also move the re-arm 
converts a job kill into silent permanent loss of partition discovery | 
`KafkaSourceSplitEnumerator.java:584-601` | Yes, re-derived and expanded — High 
|
   | 14 | **Refuted as stated.** `endOffsetsTimestamp` is set only in `case 
TIMESTAMP:` (`KafkaSourceConfig.java:206-232`), so it is null elsewhere. Real 
residual: the managed lane applies timestamp end-offsets on 
**dynamic-discovery** rounds too, where `fetchPendingPartitionSplit` never did, 
so a partition discovered mid-job now stops at the end timestamp instead of 
latest — arguably more correct, but unannounced | 
`KafkaSourceSplitEnumerator.java:749-767` | Corrected — Low |
   | 15 | `restoreMetadata()` has no owner check and no idempotence guard; a 
re-delivered restore forces a RUNNING reader back to RESTORING permanently | 
`ManagedSourceReaderRuntime.java:194`; `ManagedSourceLifecycle.java:187` | Yes, 
re-derived |
   | 17 | `markRestoredSplitsIncluded` uses `containsAll`, so a chunk in which 
the reader already finished some splits is never reconciled and is replayed 
whole on failover | `SourceAssignmentTracker.java:257-267`; callers `:283, 325, 
876` | Yes, re-derived; duplicates, not loss |
   | 18 | `transportRetries` / `transportFirstAttemptNanos` leak on every 
reader replacement | `ManagedSourceCoordinatorRuntime.java:1172-1196` | Carried 
forward |
   | 20 | Per-turn `updateMetrics()` holds `admissionLock` while taking three 
mailbox locks and scanning every buffered command | 
`ManagedSourceReaderRuntime.java:392-393, 1512-1515` | Yes, re-derived |
   | 21 | Epoch rebase unimplemented — a second epoch hard-throws; 
`resetForEpoch`/`awaitSignal` have zero callers; `restoredAppliedWatermark` is 
stored and never read; idleness is a fixed sleep | 
`ManagedSourceReaderRuntime.java:741-748, 679, 1290`; 
`ReaderCommandMailbox.java:215, 244`; 
`ManagedSourceCoordinatorRuntime.java:706, 1812` | Yes, re-derived and expanded 
|
   | 22 | **Partly refuted.** The connector's `close()` (`:285-301`) does 
discard both `callAsync` handles, but the engine closes the scheduler first, so 
in-flight discovery is interrupted before `adminClient.close()`. Residual 
window is small: `cancel(true)` interrupts without joining, so a late 
`listOffsets` can fire post-close, get a `TimeoutException`, and be discarded. 
Log noise, no corruption. Note the **legacy** branch of the same method closes 
the admin client before `executor.shutdownNow()` — a genuine ordering bug that 
pre-dates this PR | `KafkaSourceSplitEnumerator.java:285-301` | Corrected — Low 
|
   | 23 | **Premise refuted, real defect relocated.** No CDC connector can be 
selected into the managed lane (selection is allowlist plus explicit 
capability; CDC declares none). The real defect: 
`SourceReaderBase.pollNextManaged` is **concrete**, so the engine's safety 
check `if (isManagedReaderRuntime() && !(reader instanceof 
ManagedSourceReader))` (`SourceFlowLifeCycle.java:222`) is now vacuous for the 
entire `SourceReaderBase` family, including `IncrementalSourceReader` — whose 
`pollNext` (`:103-120`) sends the split request that `pollNextManaged` never 
sends, and which deliberately drops the `Boundedness.BOUNDED` check that 
`SourceReaderBase:143` re-imposes. A CDC connector allowlisted in future would 
hang with zero assignments, past a guard that reports it as safe. Fix: make 
`pollNextManaged` abstract, or have `activateManagedRuntime()` reject 
subclasses that override `pollNext` without overriding `pollNextManaged` | 
`SourceReaderBase.java:61,133-167`; `IncrementalSourceReade
 r.java:103-120`; `SourceFlowLifeCycle.java:222` | Corrected — Medium now, High 
on first CDC onboarding |
   | 24 | Metric names embed the per-attempt `executionId` under a 
"bounded-cardinality" Javadoc, and the code runs on the legacy lane | 
`SourceRuntimeMetrics.java:42-43, 151-160, 207-213`; 
`ManagedSourceRuntimeMetrics.java:313-322` | Yes, re-derived |
   | 25 | `SourceConfig` gains five fields with no `serialVersionUID`; no 
`incompatible-changes.md` entry | `SourceConfig.java:26-30, 41-42` | Yes, 
re-derived |
   | 27 | E2E lane assertion is order-dependent on an accumulating shared 
container log and passes vacuously | `ManagedSourceRuntimeIT.java:58-65, 72-78, 
102-104` | Yes, re-derived |
   | 29 | `requestSplit()` and `signalNoMoreElement()` throw before the epoch 
handshake, breaking the standard `sendSplitRequest()`-in-`open()` pattern; 
`checkOwner()` also rejects connector fetcher threads | 
`ManagedSourceReaderRuntime.java:448-458, 465-479, 1075-1078`; 
`SourceFlowLifeCycle.java:265, 271` | Yes, re-derived and expanded |
   | 30 | Grouped low-severity items — see below | — | Partly re-verified |
   
   **Issue 30, grouped Low items, with this round's additions:**
   
   *Dead code.* `ManagedSourceRuntimeMode.MANAGED_READER` can never be selected 
— `ManagedSourceCapability.validate()` rejects `supportsManagedReader && 
!supportsManagedCoordinator` (`:155-158`), which is the exact condition 
`ManagedSourceRuntimeSelector:81-82` uses to pick it. Verified this round. 
`admittedNanos` is written to the wire, always `0` at send time, and 
unconditionally overwritten on receipt — and `System.nanoTime()` is not 
comparable across JVMs anyway. The deprecated 4-arg `TaskExecutionContext` 
constructor silently yields `executionId = 0`, defeating attempt fencing; it 
now has zero callers.
   
   *Wire hygiene.* `SourceCommandCodec` writes magic plus `PAYLOAD_VERSION` for 
split assignment (`:64-65`) and barriers (`:104-105`) but not for 
`encodeCheckpointId`, `encodeCommandApplied`, `encodeReaderCheckpointReport`, 
`encodeRestoredSplits` or `encodeNoMoreSplits`. The envelope CRC32 covers the 
payload only, not the header. `SourceCommandEnvelope implements Serializable` 
with no `serialVersionUID` despite a Javadoc positioning it as explicitly 
non-Java-serialized. `SplitIdChunkAccumulator.splitIds()` silently 
de-duplicates across chunks via `LinkedHashSet`, absorbing what would be a 
coordinator bug.
   
   *Error handling.* `validateRestoredSelection` (`:1629-1641`) reports 
"capability or lane mismatch" without saying which of the four fields differed 
or what the expected and actual values were — the single most likely 
operator-facing failure in the whole feature, with the least useful message. 
Over-broad `catch (IllegalArgumentException)` in three serializers wraps an 
entire method body and mislabels any nested IAE. `handleSchemaSignals()` runs 
unconditionally after every poll and calls `lifecycle.beginSchemaChange`, which 
requires `RUNNING`, so a schema-change marker in a final poll turns a graceful 
drain into a failure. A non-snapshot barrier calls `finishBarrier` without a 
matching `beginCheckpointBarrier` (currently harmless, latent).
   
   *Config and style.* Two `ArrayBlockingQueue(4096)` plus a 
`ScheduledThreadPoolExecutor` are allocated on every worker regardless of the 
feature flag. Twelve YAML keys use bare `Long.parseLong` instead of the 
key-labelled helper, so a typo yields an unattributed `NumberFormatException`. 
An unknown key under `managed-source-runtime` is only warned about, so a 
misspelled `connector-allowlist` silently leaves the feature off. 
`seatunnel.yaml:46` describes the feature as "Source **serialization**" rather 
than Source runtime. Inline fully-qualified type names 
(`ManagedSourceCoordinatorRuntime.java:1630, 1664`, 
`ManagedCoordinatorScheduler.java:377`) instead of imports, against repo 
convention.
   
   *Selector hardening.* `runtime-protocol-version` is validated only as 
positive and only against the connector's declaration, never against 
`ManagedSourceCapability.CURRENT_RUNTIME_PROTOCOL_VERSION` — so an operator can 
configure `7`, a connector can declare `7`, and the engine will run v1 wire 
code under a v1-hardcoded error message. Allowlisting a connector that declares 
no managed capability hard-throws at `PhysicalPlanGenerator` for **every** job 
using that connector, new submissions and restores alike; falling back to 
legacy with a `WARN` would match operator intent better.
   
   ---
   
   ### Issue 26 — Test coverage does not reach the code that holds the risk, 
and the managed-lane E2E has now been cancelled at three consecutive heads
   
   **Location:** `seatunnel-engine-server/src/test/.../task/source/`; 
`ManagedSourceRuntimeIT.java`; fork CI run `31704069417` for head `2eb0e83782`
   **Problem description:** There is **no test class for 
`ManagedSourceCoordinatorRuntime` (1920 lines) and none for 
`ManagedSourceReaderRuntime` (1669 lines)**. The only contact with the 
coordinator runtime anywhere in the test tree is 
`ManagedSourceCheckpointStateTest:152-158` calling the static pure function 
`reconcileNoMoreSplitsSubtasks`. Nothing drives `runOneTurn()`, 
`applyBarrier()`, the restore path, or the `PREPARE_CLOSE` shutdown sequence. 
`SourceSplitEnumeratorTaskTest` gained 25 lines of config plumbing and **zero** 
managed-lane coverage — all four tests use the 3-arg legacy constructor. 
Roughly 820 lines of new unit test cover roughly 7,800 lines of new runtime 
code, and none of it covers the event loops.
   
   **CI status, checked live at review time.** The apache-side "Build" check is 
`FAILURE` (completed 2026-08-14T02:36:54Z). The real signal is the fork run 
`DanielLeens/seatunnel` `31704069417` for head `2eb0e83782`: 69 jobs succeeded, 
2 failed, 8 cancelled.
   
   - `unit-test (11, windows-latest)` **failed**, but the cause is 
`OptionRulesApiTest.before:73 » IllegalState Node failed to start!` — a 
Hazelcast node-startup failure in a test that is not in this PR's 104-file 
diff. 373 tests ran with 1 error. Not a regression from this branch.
   - `doris-connector-it (11, ubuntu-latest)` **failed** on `DorisErrorIT`, 
also unrelated to this diff.
   - Every managed-source unit test that ran **passed**: 
`ManagedSourceRuntimeConfigTest` (3), `ManagedSourceRuntimeSelectorTest` (7), 
`SourceConfigTest` (1), `SourceRuntimeMetricsTest` (4), 
`ManagedCoordinatorSchedulerTest` (2), `ManagedSourceCheckpointStateTest` (10), 
`ManagedSourceRuntimeMetricsTest` (2), `ReaderCommandMailboxTest` (2), 
`SourceAssignmentTrackerTest` (4), `SourceCommandCodecTest` (2), 
`SourceSplitEnumeratorTaskTest` (4), `JobCheckpointUtilsTest` (4) — all 
`Failures: 0, Errors: 0`. This confirms the protocol, codec and config layers 
are healthy, which was never in question.
   - **Both `engine-v2-it` legs (JDK 8 and 11) were cancelled**, as they were 
at `9c2ffde8` and at `2da2feed6`. `engine-v2-it` is the only job that builds 
and runs `ManagedSourceRuntimeIT`.
   
   **Why this matters more than usual here.** 
`batch_fake_to_inmemory_managed.conf` is a `job.mode = "BATCH"` FakeSource job 
with `checkpoint.interval = 5000`, parallelism 2 and 5 splits. That is 
precisely the shape that hits Issue 32 (silent hang on `NO_MORE_SPLITS`) and 
then Issue 1 (`PREPARE_CLOSE` deadlock). **My source-level analysis predicts 
`ManagedSourceRuntimeIT#testBatchJobCompletesOnManagedLane` times out at this 
head** — and the one job that would confirm or refute that has never been 
allowed to run to completion, at any head of this PR, for three different 
infrastructure reasons.
   **Best improvement:** Add direct owner-loop unit tests for both runtime 
classes that do not depend on slow, flaky, unrelated jobs finishing first; and 
get one `engine-v2-it` run to completion — pass or fail — on a head that also 
includes the Issue 1, 31 and 32 fixes.
   **Severity:** Medium, but treated as a merge gate
   **Raised by another reviewer:** No
   
   ---
   
   # 2. Code Quality Assessment
   
   ## 2.1 Coding Standards and Comment Completeness
   
   Formatting, ASF headers, import hygiene and naming are clean throughout; 
Spotless would be a no-op. Comment quality is **well above average for this 
repository** — most new classes carry a substantive class-level Javadoc 
explaining the invariant they maintain, not just what they are, and several 
comments explain *why* (for example 
`SourceAssignmentTracker.checkpointCompleted`'s explanation of why release must 
happen at completion and not before, `:269-279`).
   
   The problem is that three of the most load-bearing comments are false, and 
in two cases the code immediately below them does the opposite of what they say:
   
   - `ManagedSourceCoordinatorRuntime.java:575-583` states the restore path 
must not bind ownership, then binds it ten lines later through 
`assignmentTracker.restore()` (Issue 2).
   - `PhysicalPlanGenerator.java:624-626` says the lane "is decided once, here, 
and then persisted with the physical plan." There is no persisted physical plan 
(Issue 3).
   - `SourceRuntimeMetrics.java:151-157` and 
`ManagedSourceRuntimeMetrics.java:313-319` describe the suffix as 
"bounded-cardinality" while embedding a fresh flake id per deployment (Issue 
24).
   
   A comment that asserts an invariant the adjacent code violates is worse than 
no comment, because it stops the next reader from checking. These three should 
be treated as defects in their own right.
   
   New-class Javadoc coverage is otherwise complete; new public methods and 
non-trivial fields are documented. `ManagedSourceCoordinatorRuntime`'s complete 
absence of logging (Issue 5) is the one place where the class falls below what 
an operator will need.
   
   ## 2.2 Test Coverage and Test Stability
   
   **Stability rating: Risk present.**
   
   Not "Unstable": the new unit tests are deterministic, self-contained, fast, 
and assert real invariants rather than happy-path round-trips. 
`ManagedSourceCheckpointStateTest:115-118` and `:137-140` flip a bit in the 
serialized bytes and assert `IOException` — genuine corruption-detection 
coverage, and exactly the right instinct. No shared mutable statics, no 
hard-coded ports, no unseeded randomness in the unit tests.
   
   Not "Stable" either, for three independent reasons:
   
   1. **The code that will actually break is untested** (Issue 26). Roughly 
3,600 lines of event-loop, ownership, lifecycle and barrier-interleaving logic 
have no direct unit test at all.
   2. **`ManagedSourceRuntimeIT`'s lane assertion is order-dependent and 
vacuous in one direction** (Issue 27). `server` is one container started in 
`@BeforeAll` and `server.getLogs()` is cumulative for the whole class. Both 
`testBatchJobCompletesOnManagedLane` (`:72-78`) and the streaming test 
(`:102-104`) assert the same `MANAGED_LANE_LOG` string against that shared log, 
so if the batch test runs first the streaming assertion passes without the 
streaming job ever entering the managed lane. The class's own Javadoc 
(`:42-45`) says that without a real lane assertion the suite "would be worse 
than no coverage at all" — which is the state it is in for the second test. 
Both `jobFuture.get()` (`:110`) and `restoreFuture.get()` (`:134`) are untimed, 
so a hung job blocks the CI worker rather than failing it — directly relevant 
given Issues 1 and 32.
   3. **`SourceSplitEnumeratorTaskTest`, now load-bearing for this feature's 
config plumbing, carries pre-existing flake sources**: a `Thread.sleep(1000)` 
plus a strict `<` comparison on two `System.currentTimeMillis()` stamps 
(`:114`, `:129`) that fails if both land in the same millisecond; an unbounded 
busy-wait `while (openTime.get() == 0 || ...) { enumeratorTask.call(); }` with 
no timeout (`:125-127`); and a negative timing assertion 
`assertFalse(latch.await(200, MILLISECONDS))` (`:242`) that a GC pause inverts.
   
   Suggested minimum before merge: a `ManagedSourceCoordinatorRuntimeTest` 
driving `stateProcess()` through `RUNNING -> PREPARE_CLOSE -> CLOSED` with a 
queued barrier (Issue 1); a restore test asserting the tracker is usable from 
the owner loop after `restoreRuntimeState` on a different thread (Issue 2); a 
`ManagedSourceReaderRuntimeTest` asserting a bounded reader reaches 
`END_OF_INPUT` after `NOTHING_AVAILABLE` followed by `NO_MORE_SPLITS` (Issue 
32); and a dispatch test asserting a `!ready` reader defers rather than throws 
(Issue 31). Each is a few dozen lines and each pins a current High.
   
   ## 2.3 Documentation Updates
   
   `docs/en` and `docs/zh` are both present, structurally parallel, and 
correctly wired into `docs/sidebars.js:284`. The configuration table in 
`managed-source-runtime.md:100-126` was checked field-by-field against 
`ManagedSourceRuntimeConfig.java:42-70` — **all 24 defaults match exactly**, 
which is genuinely good discipline.
   
   Four mismatches, in severity order:
   
   1. **The lane-persistence claim is wrong** (`en:82-83`, `en:214-215`, 
`zh:74`, `zh:201`) — Issue 3. This is the most consequential, because the 
rollback procedure built on it will brick production jobs.
   2. **The metric name format is wrong** (`realtime-observability.md:311` en 
and zh: "All names above use the same `#{sourceId}` suffix"). Code emits 
`#{sourceId}#attempt-{executionId}`. A dashboard query written from this doc 
matches nothing. The same omission applies to the metric list at 
`managed-source-runtime.md:170-180`.
   3. **"Managed metrics use a stable Source action suffix"** (`en:165`, 
`zh:154`) is the opposite of what the code does — Issue 24. The adjacent 
sentence about command ids, split ids and table names never being metric 
dimensions *is* accurate.
   4. **No `incompatible-changes.md` entry**, and — more importantly — no 
documentation anywhere of the three operator-facing hazards: config flip makes 
managed jobs unrestorable, connector-jar upgrade invalidates every managed 
checkpoint via the digest, and an allowlist typo hard-fails every job using 
that connector at submit *and* at restore.
   
   # 3. Architectural Soundness
   
   ## 3.1 Elegance of the Solution
   
   **This is a long-term solution, correctly shaped, incompletely executed.** 
It is not a workaround and not a point fix. Moving Source concurrency ownership 
from every connector author into the engine is the right call, and the specific 
mechanisms chosen are the ones a mature system would choose: an explicit 
versioned command envelope rather than ad-hoc messages; CRC-checksummed, 
magic-prefixed, length-bounded, explicitly non-Java serializers rather than 
`ObjectOutputStream`; an engine-owned assignment ledger with an explicit state 
machine rather than implicit ownership; attempt fencing via a monotonic 
deployment id; bounded mailboxes with reserved control capacity so control 
traffic cannot be starved by data traffic; a scheduler with explicit overlap 
and failure policies rather than raw threads.
   
   The steady-state protocol is well developed. What is missing is the 
transition machinery, and it is missing consistently: shutdown (Issue 1), 
restore (Issue 2), cancel (Issue 8), coordinator epoch change (Issue 21), 
reader failover (Issue 31) and bounded completion (Issue 32) are all broken or 
unimplemented. That is a coherent gap rather than scattered sloppiness, which 
is encouraging — it suggests one focused pass rather than a rewrite.
   
   ## 3.2 Maintainability
   
   Two 1,700-to-1,900-line classes with hand-rolled event loops, mailboxes, 
retry and deadline machines and thread-ownership assertions, carrying **zero 
direct unit tests** and, in the coordinator's case, **zero log statements**. 
That combination is the main maintainability concern, ahead of any individual 
bug.
   
   The `checkOwner()` lazy first-caller-wins pattern is the main structural 
concern and is the direct cause of two Highs (Issues 2 and 9) plus part of 
Issue 29. Replacing it with an explicit `attachToOwnerLoop()` called once from 
the first `runOneTurn()` — with a clear "coordinator loop has not started" 
error for any earlier caller — would make both bug classes impossible by 
construction rather than by convention, and would make the 
`ThreadShareMode.ALL` incompatibility a startup-time rejection instead of a 
turn-two crash.
   
   ## 3.3 Extensibility
   
   Good. `runtimeProtocolVersion`, `connectorStateVersion` and 
`capabilityDigest` give three independent evolution axes; wire codes are 
explicit enums with integer codes; serializers are versioned with magic and 
checksum. Two gaps: a capability-flag change is currently an unrecoverable 
restore break with no migration story (Issue 5b), and 
`runtime-protocol-version` is never bounded against the engine's actual 
supported version, so a v2-labelled deployment would silently run v1 wire code.
   
   ## 3.4 Historical-Version Compatibility
   
   Covered in 1.2. The SPI surface is fully compatible and the serializer IDs 
are append-only — both done properly. The restore and deployment paths are not: 
Issue 3 (lane re-derivation), Issue 6 (`FakeSourceSplit` identity, legacy lane, 
ungated), Issue 25 (`SourceConfig` serialization), and the undocumented digest 
gate. None of it appears in `incompatible-changes.md`.
   
   # 4. Issue Summary
   
   | No. | Issue | Location | Sev | Status |
   |---|---|---|---|---|
   | 1 | Enumerator deadlocks in `PREPARE_CLOSE`; control events never drained 
| `SourceSplitEnumeratorTask.java:466` | High | Open, re-derived |
   | 2 | Restore binds tracker owner to the restore thread; all managed 
restores fail | `ManagedSourceCoordinatorRuntime.java:593`, 
`SourceAssignmentTracker.java:345` | High | Open, re-derived |
   | 3 | Lane re-derived from live config on every init; docs claim the 
opposite; rollback bricks jobs | `PhysicalPlanGenerator.java:170,612`, 
`JobMaster.java:290` | High | Open, re-derived |
   | 4 | Iceberg declares `supportsAsyncEnumerator` but the BOUNDED path blocks 
| `IcebergSource.java:136,149,166` | High | Open, re-derived |
   | 5 | Managed ops collapse all exceptions to `STALE_TARGET`; coordinator has 
zero logging | `ManagedSourceCommandOperation.java:70` +2 | High | Open, scope 
corrected |
   | 6 | `FakeSourceSplit` identity widened -> enumerator dedup set misses -> 
**duplicate data on upgrade restore**, legacy lane, ungated | 
`FakeSourceSplit.java:22-43`, `FakeSourceSplitEnumerator.java:135` | High | 
Open, mechanism now concrete |
   | 7 | Watchdog `interrupt()` outside the lock; no re-validation; flag never 
cleared | `ManagedSourceReaderRuntime.java:601` | High | Open, re-derived |
   | 8 | CANCEL cannot preempt a pending barrier; clean cancel reported FAILED 
| `ManagedSourceReaderRuntime.java:1487,1546` | High | Open, mechanism 
corrected |
   | 9 | Thread-identity guards break under `ThreadShareMode.ALL` | 
`TaskExecutionService.java:654,1387` +4 | High | Open, re-derived |
   | **31** | **Reader mid-replacement turns `assignSplit` into a whole-job 
failure** | `ManagedSourceCoordinatorRuntime.java:1500`, 
`SeaTunnelSplitEnumeratorContext.java:70` | **High** | **NEW — carryover miss** 
|
   | **32** | **`NO_MORE_SPLITS` never re-opens the poll gate; bounded reader 
hangs forever** | `ManagedSourceReaderRuntime.java:702` | **High** | **NEW — 
carryover miss** |
   | 10 | Epoch/attempt/`closed` read cross-thread without `volatile` | 
`ManagedSourceReaderRuntime.java:114,136,256` | Med | Open |
   | 11 | Deferred split request dropped when the reader is not ready | 
`ManagedSourceCoordinatorRuntime.java:1410` | Med | Open |
   | 12 | `enumerator.run()` inline on the drain thread blocks all barriers | 
`SourceSplitEnumeratorTask.java:448` | Med | Open |
   | 13 | Kafka managed discovery kills the job on transient errors; re-arm 
sits past the `throw` with no `finally` | 
`KafkaSourceSplitEnumerator.java:584-601` | **High** | Open, severity raised |
   | 14 | Kafka `end_offsets_timestamp` — refuted as stated; residual is 
dynamic-discovery rounds only | `KafkaSourceSplitEnumerator.java:749` | Low | 
Corrected |
   | 15 | `restoreMetadata()` lacks owner check and idempotence guard | 
`ManagedSourceReaderRuntime.java:194` | Med | Open |
   | 16 | Three non-failure-atomic release paths leak the node-wide budget | 
`ManagedSourceReaderRuntime.java:1246,506`, `ReaderCommandMailbox.java:278` | 
Med-High | Open, relocated |
   | 17 | `containsAll` reconciliation replays partially finished chunks on 
failover | `SourceAssignmentTracker.java:257` | Med | Open |
   | 18 | Transport retry maps leak on reader replacement | 
`ManagedSourceCoordinatorRuntime.java:1172` | Med | Open |
   | 19 | Managed ack failure never logged; can be lost after close; 
coordinator variant worse | `SourceSeaTunnelTask.java:198`, 
`SourceFlowLifeCycle.java:846` | Med | Open, scope corrected |
   | 20 | Per-turn metrics update double-locks and scans the mailbox | 
`ManagedSourceReaderRuntime.java:392` | Med | Open |
   | 21 | Epoch rebase unimplemented; `resetForEpoch`/`awaitSignal` dead; 
watermark never read | `ManagedSourceReaderRuntime.java:741` | Med | Open |
   | 22 | Enumerator client close vs in-flight discovery — engine closes the 
scheduler first; small residual window | `KafkaSourceSplitEnumerator.java:285` 
| Low | Corrected |
   | 23 | Concrete `pollNextManaged` makes the `instanceof ManagedSourceReader` 
guard vacuous for the whole `SourceReaderBase`/CDC family | 
`SourceReaderBase.java:61,133`; `SourceFlowLifeCycle.java:222` | Med | 
Corrected, relocated |
   | 24 | Metric names embed the task attempt; contradicts docs and own 
Javadoc; hits legacy lane | `SourceRuntimeMetrics.java:158,207` | Med | Open |
   | 25 | `SourceConfig` +5 fields, no `serialVersionUID`; no 
`incompatible-changes.md` entry | `SourceConfig.java:26-30` | Med | Open |
   | 26 | Owner-loop classes untested; managed-lane E2E cancelled at three 
consecutive heads | new UT dir, `ManagedSourceRuntimeIT.java` | Med (gate) | 
Open, CI re-checked |
   | 27 | E2E lane assertion order-dependent on an accumulating shared 
container log | `ManagedSourceRuntimeIT.java:102` | Med | Open |
   | 28 | Poll-status drain path leaves `prepareClose` false, so flush signals 
continue | `ManagedSourceReaderRuntime.java:441` | Low | Open, largely refuted |
   | 29 | `requestSplit()`/`signalNoMoreElement()` throw before the epoch 
handshake | `ManagedSourceReaderRuntime.java:1075` | Med | Open |
   | 30 | Grouped low-severity items (dead code, wire hygiene, error messages, 
config, style) | see Issue 30 | Low | Open |
   | **33** | **`checkpointAssignmentProofs`/`uncheckpointedAssignmentSplitIds` 
unbounded** | `ManagedSourceReaderRuntime.java:109` | **Med** | **NEW** |
   | **34** | **`subscribeToAvailability()` accumulates handlers on a reused 
future** | `ManagedSourceReaderRuntime.java:641` | **Med** | **NEW** |
   | **35** | **Scheduler timeout `cancel(true)` leaks the interrupt flag onto 
shared pools** | `ManagedCoordinatorScheduler.java:352,383` | **Med** | **NEW** 
|
   | **36** | **Kafka split-back handler calls `assignSplit()` eagerly, racing 
reader readiness; silent partition loss variant** | 
`KafkaSourceSplitEnumerator.java:635,510` | **High** | **NEW** |
   | **37** | **`SourceReaderBase.isAvailable()` short-circuits on 
`noMoreSplitsAssignment`, degrading to a fixed 100 Hz poll** | 
`SourceReaderBase.java:172` | **Med** | **NEW** |
   | **38** | **`FakeSourceReader.pollNextManaged` throws on a short read -> 
permanent crash loop after a config edit plus restore** | 
`FakeSourceReader.java:140-153` | **Med** | **NEW** |
   
   **Refuted or substantially narrowed this round:** codec unchecked length 
reads (not present — every read is bounded by `in.available()` before 
allocation); unbounded silent retry loop on `STALE_TARGET` (deadline-bounded in 
both directions); swallowed checkpoint ack on the legacy lane (legacy `.join()` 
byte-for-byte preserved); `ManagedSourceMemoryBudget` not failure-atomic (it 
is); managed lane never sets `prepareClose` (it does); Kafka 
`end_offsets_timestamp` outside TIMESTAMP mode (config only ever sets it in 
TIMESTAMP mode); Kafka client close vs in-flight discovery (engine closes the 
scheduler first); CDC readers broken by inherited `pollNextManaged` (CDC cannot 
be selected into the lane at all); silent cross-lane checkpoint restore 
(fail-closed in all four directions); config-table drift (all 24 defaults 
match); `SourceReaderBase` exhausted-batch infinite loop, `FetchTask` lost 
wakeup, `SplitFetcher.wakeUp()` deadlock, and dropping the checkpoint lock in 
`pollNextManaged` (
 all verified correct by design).
   
   **Reach classification, which should drive the verdict.** Exactly **one** 
finding reaches production unconditionally: Issue 6, on the legacy lane, with 
the feature flag off. Everything else is opt-in behind `enabled: true` plus an 
explicit allowlist entry, or latent until a connector is onboarded.
   
   # 5. Merge Recommendation
   
   ### Conclusion: Not recommended for merge
   
   Two process notes before the substance. First, **I am the author of this 
PR.** GitHub does not permit an author to approve or request changes on their 
own pull request, so my account can only leave a comment-type review regardless 
of the conclusion below. Please read this with that bias disclosed. The final 
call has to come from an independent maintainer with write access. Second, 
**this PR is still a Draft** (`isDraft: true`), and `mergeStateStatus` is 
`BLOCKED` with `reviewDecision: REVIEW_REQUIRED` — both expected for a draft 
awaiting a write-capable reviewer, not new problems. `mergeable` is 
`MERGEABLE`; there are no conflicts with `dev`.
   
   **There is no existing maintainer APPROVED or CHANGES_REQUESTED review on 
this PR to respond to.** The review history is six comment-type reviews from me 
and one comment-type review from @nzw921rx carrying a single inline comment. So 
no merge intent from an independent maintainer has been expressed, and nothing 
in this review contradicts a maintainer's stated position.
   
   The technical picture did not change with this commit, because the commit is 
a pure `dev` sync. What changed is my own confidence and, unfortunately, the 
issue count: a cold re-read surfaced **two new High-severity findings I missed 
in five prior rounds** (Issues 31 and 32), **three new Medium findings** 
(33-35), and **five places where my own earlier reports were wrong or 
overstated** (Issues 5, 8, 16, 19, 28), which I have corrected above and 
apologised for. Issue 32 in particular means a bounded managed job hangs 
silently *before* it can even reach the `PREPARE_CLOSE` deadlock I have been 
reporting since round one.
   
   ### 1. Blockers — must be fixed, sorted by severity
   
   1. **Issue 32 (High, NEW)** — `NO_MORE_SPLITS` never re-opens the poll gate. 
A bounded managed reader hangs silently forever. One-line fix; highest 
severity-to-effort ratio in this list.
   2. **Issue 1 (High)** — the managed enumerator deadlocks in `PREPARE_CLOSE`. 
No managed bounded job can complete and no savepoint can finish.
   3. **Issue 2 (High)** — coordinator restore poisons the assignment tracker's 
owner thread. No managed job recovers from any checkpoint or savepoint, 
non-deterministically.
   4. **Issue 31 (High, NEW)** — a routine reader failover turns 
enumerator-initiated `assignSplit` into a whole-job failure. The deferral 
mechanism already exists for split requests; it just needs extending.
   5. **Issue 8 (High)** — CANCEL cannot preempt a pending barrier, so every 
cancel racing an in-flight checkpoint reports FAILED instead of CANCELED, and a 
post-cancel barrier still snapshots a cancelled reader.
   6. **Issue 3 (High)** — lane selection is re-derived from live cluster 
config on every init. The documented rollback procedure permanently bricks 
running managed jobs. Either persist the selection or delete the four false 
claims and document the real hazard.
   7. **Issue 9 (High)** — the four thread-identity guards are incompatible 
with the supported `ThreadShareMode.ALL`. Reject the combination at selection 
time at minimum.
   8. **Issue 7 (High)** — the poll watchdog can deliver an interrupt to 
unrelated work and never clears the flag, so cleanup itself fails and connector 
resources leak.
   9. **Issue 4 (High)** — Iceberg's capability declaration does not match its 
bounded enumerator.
   10. **Issue 5 (High)** — all three managed operations erase the root cause, 
and `ManagedSourceCoordinatorRuntime` has zero logging in 1,920 lines. This one 
is not a correctness bug, but shipping an engine feature with no diagnostics is 
not something I would sign off on.
   11. **Issue 36 (High, NEW)** — Kafka's split-back handler calls 
`assignSplit()` eagerly, which both races reader readiness into an intermittent 
job failure and, in the all-readers-unregistered case, silently drops 
partitions from every future discovery snapshot. Deleting one line fixes it.
   12. **Issue 13 (High, severity raised)** — Kafka's managed discovery kills 
the job on a transient broker error where legacy retried forever, and the 
re-arm call sits past the `throw` with no `finally`, so the obvious fix 
silently disables discovery instead. Both halves must be fixed together.
   13. **Issue 6 (High, and the only unconditional defect in the PR)** — 
`FakeSourceSplit`'s widened identity makes the enumerator's dedup set miss on 
any restore across the upgrade boundary, re-emitting already-consumed splits, 
on the **legacy** lane with the feature flag off. One-annotation fix, plus a 
test that actually serializes.
   14. **Issue 26 (Medium, treated as a gate)** — `engine-v2-it` has now been 
cancelled at three consecutive heads, so there is still **zero CI evidence at 
any head of this PR** that the managed lane's shutdown, restore or cancel paths 
behave as designed. I would not consider this gate satisfied until one 
`ManagedSourceRuntimeIT` run completes — pass or fail — on a head that also 
includes the Issue 1, 31 and 32 fixes.
   
   **If only one thing is fixed before anything else, make it Issue 6** — it is 
the sole defect that affects users who never enable this feature.
   
   ### 2. Recommended fixes — non-blocking
   
   Issues 10-25, 27-30, 33-35 and 37-38 should be addressed before the feature 
graduates from experimental. Issue 23 deserves early attention despite its 
Medium rating: making `SourceReaderBase.pollNextManaged` abstract is a small 
change that restores the engine's `instanceof ManagedSourceReader` guard to 
actually meaning something, and it is the trap the next connector onboarding 
will otherwise fall into. Several are latent correctness or leak issues that 
will be far harder to diagnose once the lane is enabled anywhere: Issue 10 
(missing `volatile` producing spurious `STALE_TARGET` right after the 
handshake), Issue 16 (permanent node-wide budget leak), Issue 33 (unbounded 
growth exactly when checkpointing is already stuck), Issue 35 (interrupt 
leaking across jobs on shared pools), Issues 11, 17, 18 (silent split-handling 
anomalies). Issue 30 is cleanup, though the `validateRestoredSelection` error 
message is worth fixing early — it is the message an operator is most likely to 
hit 
 and the least useful one in the PR.
   
   ### 3. Response to @nzw921rx
   
   On the connector allowlist 
(`docs/zh/engines/zeta/managed-source-runtime.md:31`): thank you for raising 
this, and I want to engage with it properly rather than just restate my earlier 
position, because part of your argument is now better supported by evidence 
than my counter-argument was.
   
   **Where you are right, with new evidence.** You argued that this should be a 
feature switch and that an unsupported connector should fail fast rather than 
be governed by a separate list. The code already does the fail-fast half of 
what you describe: `ManagedSourceRuntimeSelector.java:49-55` hard-throws when 
an allowlisted connector declares no managed capability. So the allowlist is 
not preventing silent weakening — the capability check is. Your point that its 
effect "will gradually weaken" as connectors adopt the capability is also 
correct: once a connector declares a capability, the allowlist is the only 
thing standing between it and the managed lane, and maintaining two switches 
for one decision is real long-term cost.
   
   **Where I would push back, with counter-evidence.** The allowlist's value 
today is not semantic, it is blast-radius control during rollout, and this 
round turned up a concrete reason that matters more than I previously argued. 
Because of Issue 3, the lane is recomputed from live cluster config on 
**every** `JobMaster#init`, including restore and master failover. With a 
per-connector allowlist, an operator enabling the feature affects only the 
connectors they named. Without it — a single global `enabled: true` — flipping 
the switch changes the lane for *every* capability-declaring connector in the 
cluster at once, and flipping it back makes every job that took a managed 
checkpoint in the interim permanently unrestorable. That is a cluster-wide, 
unrecoverable outage from one config edit.
   
   There is also a hazard in the current allowlist that argues for tightening 
it rather than removing it: because the check throws rather than falling back, 
adding a non-capable connector name — a plausible typo, or an over-broad 
rollout entry like `jdbc` — hard-fails **every** job using that connector, both 
new submissions and restores of already-running jobs. That blast radius is 
undocumented.
   
   **Concretely, what I think should happen.** Keep the allowlist for phase 0, 
but (a) fix Issue 3 so the selection travels with the job, which is what makes 
any switch safe to flip; (b) change the allowlisted-but-incapable case to 
`WARN` and fall back to legacy, since the operator's intent is unambiguously 
"use managed if capable" — that directly implements the fail-safe behaviour you 
are asking for without the outage risk; and (c) document the removal path, so 
the allowlist can be retired once Issue 3 is fixed and adoption is broad. If a 
maintainer prefers your simpler model, I would accept it as long as (a) lands 
first — without persisted selection I think a single global switch is genuinely 
dangerous.
   
   ### Overall assessment
   
   The right architecture, roughly 80% built, with the transition machinery 
still the missing 20% — and this round that gap got larger, not smaller, 
because two of the three ways a bounded managed job fails to finish were 
previously unreported. The steady-state protocol, the fail-closed compatibility 
gating, the versioned checksummed serializers and the append-only serializer 
IDs remain genuinely good work, and they are why the remaining problems are 
localized rather than systemic. I want to be clear that my conclusion is about 
executable correctness on the primary transition paths, not about the design, 
which I still think is the right one for this project.
   
   **Option A (recommended): finish phase 0 as scoped.** Fix the twelve 
blockers, add the four small owner-loop unit tests named in section 2.2, and 
get one `engine-v2-it` run to completion. That is a focused, bounded amount of 
work — most of the blockers are localized, and Issues 32, 6 and 4 are close to 
one-liners. Then this is mergeable as an experimental, disabled-by-default 
feature.
   
   **Option B: narrow phase 0 to the coordinator lane only.** Ship 
`MANAGED_COORDINATOR` and defer `MANAGED_READER_AND_COORDINATOR` to a 
follow-up. This removes `ManagedSourceReaderRuntime` — 1,669 lines carrying 
Issues 7, 8, 10, 15, 20, 21, 29, 32, 33, 34 — plus Issues 23, 37 and 38 from 
the merge surface entirely, cutting the blocker list roughly in half while 
still delivering the engine-owned enumerator, the scheduler and the assignment 
ledger, which are the parts most connectors would benefit from first. Issues 1, 
2, 3, 4, 5, 6, 9, 13, 26, 31 and 36 would still need fixing. Note that Issue 6 
must be fixed under **either** option, since it does not depend on the lane at 
all. If the goal is to get value into `dev` sooner and reduce review surface, I 
think Option B is genuinely worth considering.
   
   I do not think there is a materially better Option C for the overall design. 
The one structural change I would still push for in either option: replace the 
lazy `checkOwner()` first-caller-wins bind with an explicit 
`attachToOwnerLoop()` invoked once from the first `runOneTurn()`. That closes 
Issue 2 by construction, turns Issue 9 into a clear startup-time rejection 
instead of a turn-two crash, and removes a whole class of future bug.
   
   Thank you again for the care that went into the documentation, the 
compatibility gating and the serializers — the fail-closed posture, the 
explicit capability handshake and the checksummed versioned wire format are the 
right instincts, and the 24-of-24 config-table match is the kind of discipline 
that is rare in this repo. The thing I would most like to see before the next 
round is still the same: one completed `ManagedSourceRuntimeIT` run. My 
analysis now predicts the batch test hangs for two independent reasons; a real 
run is the single piece of evidence most likely to move my assessment in either 
direction.
   


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