apache/cassandra · error · RuntimeException

Stream is finished with state

Error message

Stream %s is finished with state %s

What it means

StreamSession guards every mutation (attach, transfer, message handling) with failIfFinished(), which throws if the session has already reached a final state (COMPLETED/FAILED). This prevents operating on a session whose streaming plan is over, e.g. messages or ranges arriving after completion.

Solutions

  1. Check session state via session.state().isFinalState() before attaching ranges/streams or sending messages.
  2. Create a new StreamSession (new planId) for retries instead of reusing a finished session.
  3. Inspect earlier logs for the session's planId to see why it reached COMPLETED/FAILED before this call.
  4. Fix network delays/duplicates or messaging retry settings that deliver messages after session completion.

Example fix

// before
session.messageReceived(msg); // throws if finished
// after
if (!session.state().isFinalState()) {
    session.messageReceived(msg);
} else {
    logger.warn("Dropping message for finished stream session {}", session.planId());
}
Defensive patterns

Strategy: validation

Validate before calling

if (session.state().isFinalState()) { /* drop or start new session */ }

Try / catch

try { session.messageReceived(msg); } catch (RuntimeException e) { if (e.getMessage().startsWith("Stream ") && e.getMessage().contains("is finished")) { logger.warn("Message after stream end, ignoring"); } else { throw e; } }

Prevention

When it happens

Trigger: Calling attachInbound/attachOutbound, addTransferRanges, addTransferStreams, messageReceived, or prepareReceiving on a StreamSession whose state is in a final state — typically a control or data message arriving after the session already completed or failed.

Common situations: Slow/late messages delivered after session success; a session that failed earlier (node down, timeout) receiving follow-up traffic; coordinator retrying a stream request against a session that already ran.

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/cassandra@88fd0f6a0e (2026-09-10). Data as JSON: /api/errors/97492b740a270d1c. Report an issue: GitHub.

Appendix: source

Thrown at src/java/org/apache/cassandra/streaming/StreamSession.java:481

        //Do we need to unwrap here also or is that just making it worse?
        //Range and if it's transient
        RangesAtEndpoint unwrappedRanges = replicas.unwrap();
        List<OutgoingStream> streams = getOutgoingStreamsForRanges(unwrappedRanges, stores, pendingRepair, previewKind);

        addTransferStreams(streams);
        Set<Range<Token>> toBeUpdated = transferredRangesPerKeyspace.get(keyspace);
        if (toBeUpdated == null)
        {
            toBeUpdated = new HashSet<>();
        }
        toBeUpdated.addAll(replicas.ranges());
        transferredRangesPerKeyspace.put(keyspace, toBeUpdated);
    }

    private void failIfFinished()
    {
        if (state().isFinalState())
            throw new RuntimeException(String.format("Stream %s is finished with state %s", planId(), state().name()));
    }

    private Collection<ColumnFamilyStore> getColumnFamilyStores(String keyspace, Collection<String> columnFamilies)
    {
        Collection<ColumnFamilyStore> stores = new HashSet<>();
        // if columnfamilies are not specified, we add all cf under the keyspace
        if (columnFamilies.isEmpty())
        {
            stores.addAll(Keyspace.open(keyspace).getColumnFamilyStores());
        }
        else
        {
            for (String cf : columnFamilies)
                stores.add(Keyspace.open(keyspace).getColumnFamilyStore(cf));
        }
        return stores;
    }

View on GitHub (pinned to 88fd0f6a0e)