apache/seatunnel · critical · java.io.IOException

Invalid state: currentSplitId is null when finishing snapsho

Error message

Invalid state: currentSplitId is null when finishing snapshot split. emittedFinishedSplitId=%s, currentFetcher=%s, isFinished=%s

What it means

IncrementalSourceSplitReader throws this IOException from finishedSnapshotSplit when it is asked to finalize a snapshot split while its internal currentSplitId field is null, meaning no snapshot split is currently assigned to the reader. The reader tracks the active split id when a split is handed to it and uses it to report split completion; finalizing without an active split indicates the reader/split-enumerator state machine is out of sync, typically due to a checkpoint/restore or race in split assignment.

Source

Thrown at seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/main/java/org/apache/seatunnel/connectors/cdc/base/source/reader/IncrementalSourceSplitReader.java:207

    public boolean canAssignNextSplit() {
        return currentFetcher == null || currentFetcher.isFinished();
    }

    private boolean hasEmittedCurrentSplitFinished() {
        return currentSplitId != null && currentSplitId.equals(emittedFinishedSplitId);
    }

    private RecordsWithSplitIds<SourceRecords> finishedSnapshotSplit() throws IOException {
        final String splitId = currentSplitId;
        if (splitId == null) {
            log.warn(
                    "Invalid state: currentSplitId is null when finishing snapshot split. "
                            + "emittedFinishedSplitId={}, currentFetcher={}, isFinished={}",
                    emittedFinishedSplitId,
                    currentFetcher != null ? currentFetcher.getClass().getSimpleName() : "null",
                    currentFetcher != null && currentFetcher.isFinished());
            throw new IOException(
                    String.format(
                            "Invalid state: currentSplitId is null when finishing snapshot split. "
                                    + "emittedFinishedSplitId=%s, currentFetcher=%s, isFinished=%s",
                            emittedFinishedSplitId,
                            currentFetcher != null
                                    ? currentFetcher.getClass().getSimpleName()
                                    : "null",
                            currentFetcher != null && currentFetcher.isFinished()));
        }
        if (splitId.equals(emittedFinishedSplitId)) {
            return NoSplitRecords.INSTANCE;
        }
        emittedFinishedSplitId = splitId;
        return ChangeEventRecords.forFinishedSplit(splitId);
    }

    private static final class NoSplitRecords implements RecordsWithSplitIds<SourceRecords> {
        private static final NoSplitRecords INSTANCE = new NoSplitRecords();

View on GitHub (pinned to cf67b549a7)

Solutions

  1. Restart the job from the last consistent checkpoint so split assignment state and fetcher state are restored together.
  2. Check that all source parallelism instances share a consistent checkpoint state (SnapshotSplitState / incremental source state serialization is intact).
  3. Upgrade SeaTunnel to a version with fixes for CDC split-assignment races in IncrementalSourceSplitReader.
  4. If reproducible, capture logs of emittedFinishedSplitId and currentFetcher and file an issue; the message includes both to diagnose which reader/fetcher desynced.

Example fix

// defensive: skip finalization when no split is assigned
if (currentSplitId == null) {
    log.warn("Ignoring finished snapshot split signal with no assigned split");
    return;
}
// before (throws)
throw new IOException(String.format("Invalid state: currentSplitId is null ..."));
Defensive patterns

Strategy: retry

Validate before calling

// before relying on split completion, check assignment
if (reader.getCurrentSplitId() == null) {
    log.warn("No split assigned; request reassignment from enumerator");
}

Try / catch

try {
    reader.fetch();
} catch (IOException e) {
    if (e.getMessage().contains("currentSplitId is null")) {
        // trigger checkpoint restore / reassignment instead of failing job
    }
}

Prevention

When it happens

Trigger: fetch() detects the current fetcher has finished the snapshot split and calls finishedSnapshotSplit(), but currentSplitId is null because no split was assigned (or was already cleared after restore/failover) before completion was detected.

Common situations: Job recovery from a checkpoint where the split assignment was lost but the underlying Debezium fetcher still reports isFinished=true; a race where the enumerator reassigns splits while the old fetcher is finishing; bugs in custom split-assignment logic for CDC sources (MySQL/PostgreSQL CDC).

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of apache/seatunnel@cf67b549a7 (2026-09-10). Data as JSON: /api/errors/d94b9d424c7bf69b. Report an issue: GitHub.