{"record":{"id":"c21c0815b9963c92","repo":"aeron-io/aeron","slug":"generic","errorCode":"GENERIC","errorMessage":"cannot extend recording ${recordingId} due to an outstanding delete operation: streamId=${streamId} channel=${originalChannel}","messagePattern":"cannot extend recording (.+?) due to an outstanding delete operation: streamId=(.+?) channel=(.+?)","errorType":"error_code","errorClass":"ArchiveEvent","httpStatus":null,"severity":"error","filePath":"aeron-archive/src/main/java/io/aeron/archive/ArchiveConductor.java","lineNumber":2089,"sourceCode":"            if (recordingSessionByIdMap.containsKey(recordingId))\n            {\n                final String msg = \"cannot extend active recording \" + recordingId +\n                    \" streamId=\" + image.subscription().streamId() + \" channel=\" + originalChannel;\n                controlSession.sendErrorResponse(correlationId, ACTIVE_RECORDING, msg);\n                throw new ArchiveEvent(msg);\n            }\n\n            catalog.recordingSummary(recordingId, recordingSummary);\n\n            final DeleteSegmentsSession deleteSegmentsSession = deleteSegmentsSessionByIdMap.get(recordingId);\n            if (null != deleteSegmentsSession &&\n                deleteSegmentsSession.maxDeletePosition() >= recordingSummary.stopPosition)\n            {\n                final String msg = \"cannot extend recording \" + recordingId +\n                    \" due to an outstanding delete operation: streamId=\" +\n                    image.subscription().streamId() + \" channel=\" + originalChannel;\n                controlSession.sendErrorResponse(correlationId, GENERIC, msg);\n                throw new ArchiveEvent(msg);\n            }\n\n            validateImageForExtendRecording(correlationId, controlSession, image, recordingSummary);\n\n            final Counter position = RecordingPos.allocate(\n                aeron,\n                counterMetadataBuffer,\n                ctx.archiveId(),\n                recordingId,\n                image.sessionId(),\n                image.subscription().streamId(),\n                strippedChannel,\n                image.sourceIdentity());\n\n            position.setRelease(image.joinPosition());\n\n            final RecordingSession session = new RecordingSession(\n                correlationId,","sourceCodeStart":2071,"sourceCodeEnd":2107,"githubUrl":"https://github.com/aeron-io/aeron/blob/6d60124e15e35c11b49ba2e3c2c2858a09a18803/aeron-archive/src/main/java/io/aeron/archive/ArchiveConductor.java#L2071-L2107","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\narchive.purgeRecording(recId);\narchive.extendRecording(recId, channel, streamId, SourceLocation.LOCAL); // deletion outstanding\n\n// after\narchive.purgeRecording(recId);\nawaitPurgeComplete(archive, recId); // wait for deletion session to finish\narchive.extendRecording(recId, channel, streamId, SourceLocation.LOCAL);","handlingStrategy":"validation","validationCode":"// ensure no purge/delete is pending for this recording before extending\nif (recentlyPurged(recordingId)) {\n    awaitPurgeComplete(archive, recordingId); // blocks until delete session done\n}","typeGuard":null,"tryCatchPattern":"try {\n    archive.extendRecording(recId, channel, streamId, SourceLocation.LOCAL);\n} catch (ArchiveException e) {\n    if (e.errorCode() == ArchiveErrorCode.GENERIC && e.getMessage().contains(\"outstanding delete\")) {\n        // wait for deletion session completion, then retry once\n    }\n}","preventionTips":["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."],"tags":["aeron-archive","extend-recording","purge","conflict"],"backgroundTag":"invalid-state-transition","analyzedSha":"6d60124e15e35c11b49ba2e3c2c2858a09a18803","analyzedAt":"2026-09-12T11:17:07.683Z","contentChangedAt":"2026-09-12T11:17:07.683Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}