aeron-io/aeron · error · ArchiveEvent

GENERIC

GENERIC

Error message

cannot extend recording ${recordingId} due to an outstanding delete operation: streamId=${streamId} channel=${originalChannel}

What it means

An extendRecording request targeted a recording whose recorded segments are still queued for deletion by an outstanding DeleteSegmentsSession (e.g. from a purgeRecording/truncate). Extending now would race with segment deletion, so the archive rejects the request with a GENERIC error response and throws ArchiveEvent.

Solutions

  1. Wait for the delete segments session to complete before extending (poll catalog or RecordingSignal/purge completion).
  2. Do not extend a recording that was recently purged; start a fresh recording instead.
  3. Sequence operations: purge -> confirm completion -> then decide extend vs new recording.
  4. Reduce purge scope (truncate to a position instead of full purge) if the recording will be extended.
  5. Add retry with backoff around extendRecording when a purge was issued recently.

Example fix

// before
archive.purgeRecording(recId);
archive.extendRecording(recId, channel, streamId, SourceLocation.LOCAL); // deletion outstanding

// after
archive.purgeRecording(recId);
awaitPurgeComplete(archive, recId); // wait for deletion session to finish
archive.extendRecording(recId, channel, streamId, SourceLocation.LOCAL);
Defensive patterns

Strategy: validation

Validate before calling

// ensure no purge/delete is pending for this recording before extending
if (recentlyPurged(recordingId)) {
    awaitPurgeComplete(archive, recordingId); // blocks until delete session done
}

Try / catch

try {
    archive.extendRecording(recId, channel, streamId, SourceLocation.LOCAL);
} catch (ArchiveException e) {
    if (e.errorCode() == ArchiveErrorCode.GENERIC && e.getMessage().contains("outstanding delete")) {
        // wait for deletion session completion, then retry once
    }
}

Prevention

When it happens

Trigger: A purgeRecording (or similar) on recordingId left a DeleteSegmentsSession in deleteSegmentsSessionByIdMap whose maxDeletePosition() >= the recording's stopPosition, and the client then attempts to extend that recording.

Common situations: Application purges/truncates a recording then immediately extends it before the deletion session finishes; long deletion of many segments still in progress; reusing a recordingId recently purged; automation scripts that purge-then-extend without waiting for completion.

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 aeron-io/aeron@6d60124e15 (2026-09-12). Data as JSON: /api/errors/c21c0815b9963c92. Report an issue: GitHub.

Appendix: source

Thrown at aeron-archive/src/main/java/io/aeron/archive/ArchiveConductor.java:2089

            if (recordingSessionByIdMap.containsKey(recordingId))
            {
                final String msg = "cannot extend active recording " + recordingId +
                    " streamId=" + image.subscription().streamId() + " channel=" + originalChannel;
                controlSession.sendErrorResponse(correlationId, ACTIVE_RECORDING, msg);
                throw new ArchiveEvent(msg);
            }

            catalog.recordingSummary(recordingId, recordingSummary);

            final DeleteSegmentsSession deleteSegmentsSession = deleteSegmentsSessionByIdMap.get(recordingId);
            if (null != deleteSegmentsSession &&
                deleteSegmentsSession.maxDeletePosition() >= recordingSummary.stopPosition)
            {
                final String msg = "cannot extend recording " + recordingId +
                    " due to an outstanding delete operation: streamId=" +
                    image.subscription().streamId() + " channel=" + originalChannel;
                controlSession.sendErrorResponse(correlationId, GENERIC, msg);
                throw new ArchiveEvent(msg);
            }

            validateImageForExtendRecording(correlationId, controlSession, image, recordingSummary);

            final Counter position = RecordingPos.allocate(
                aeron,
                counterMetadataBuffer,
                ctx.archiveId(),
                recordingId,
                image.sessionId(),
                image.subscription().streamId(),
                strippedChannel,
                image.sourceIdentity());

            position.setRelease(image.joinPosition());

            final RecordingSession session = new RecordingSession(
                correlationId,

View on GitHub (pinned to 6d60124e15)