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
- Wait for the delete segments session to complete before extending (poll catalog or RecordingSignal/purge completion).
- Do not extend a recording that was recently purged; start a fresh recording instead.
- Sequence operations: purge -> confirm completion -> then decide extend vs new recording.
- Reduce purge scope (truncate to a position instead of full purge) if the recording will be extended.
- 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
- Avoid purgeRecording on recordings you plan to extend; use truncate to a position instead.
- Wait for purge completion (recording signals) before any extend.
- Serialize archive mutations per recordingId in your application.
- Implement bounded retry with backoff for extend after purge.
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
- ACTIVE_RECORDING
- unknown recording id: " + recordingId
- catalogFileSyncLevel
- invalid fileIoMaxLength=
- Archive.Context.controlChannel must be set
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)