{"record":{"id":"32bc9a6220cb0af4","repo":"aeron-io/aeron","slug":"cannot-live-merge-without-active-source-recording","errorCode":null,"errorMessage":"cannot live merge without active source recording","messagePattern":"cannot live merge without active source recording","errorType":"exception","errorClass":"ArchiveException","httpStatus":null,"severity":"error","filePath":"aeron-archive/src/main/java/io/aeron/archive/ReplicationSession.java","lineNumber":511,"sourceCode":"            {\n                workCount += trackAction(correlationId);\n            }\n            else if (epochClock.time() >= (timeOfLastActionMs + actionTimeoutMs))\n            {\n                throw new TimeoutException(\"failed to send recording position request\");\n            }\n        }\n        else\n        {\n            final ControlResponsePoller poller = srcArchive.controlResponsePoller();\n            workCount += poller.poll();\n\n            if (hasResponse(poller))\n            {\n                srcRecordingPosition = poller.relevantId();\n                if (NULL_POSITION == srcRecordingPosition && null != liveDestination)\n                {\n                    throw new ArchiveException(\"cannot live merge without active source recording\");\n                }\n\n                state(State.EXTEND, \"\");\n            }\n            else if (epochClock.time() >= (timeOfLastActionMs + actionTimeoutMs))\n            {\n                throw new TimeoutException(\"failed to get recording position\");\n            }\n        }\n\n        return workCount;\n    }\n\n    private int extend()\n    {\n        final boolean isMds = isTagged || null != liveDestination;\n        final ChannelUri channelUri = ChannelUri.parse(replicationChannel);\n        final String endpoint = channelUri.get(CommonContext.ENDPOINT_PARAM_NAME);","sourceCodeStart":493,"sourceCodeEnd":529,"githubUrl":"https://github.com/aeron-io/aeron/blob/6d60124e15e35c11b49ba2e3c2c2858a09a18803/aeron-archive/src/main/java/io/aeron/archive/ReplicationSession.java#L493-L529","documentation":"Thrown by ReplicationSession.srcRecordingPosition when the source archive answers getRecordingPosition with NULL_POSITION, meaning the source recording is no longer active (recorder closed), while a liveDestination was configured for the merge. A live merge requires the source recording to still be appending; without an active source there is nothing to follow, so Aeron fails the replication with ArchiveException instead of silently merging into a dead source.","triggerScenarios":"AeronArchive.replicate(...) called with a non-null liveMerge destination while the source recording has already stopped (NULL_POSITION returned), e.g. replicating a finished recording as a 'live' merge, or the source recording ends between starting replication and the first position query.","commonSituations":"Mistakenly setting a live destination (or MDS control-mode=response) when you actually want a historical replay/replication of a completed recording; racing a short-lived source recording that ends before the merge attaches; source application crash ending the recording mid-merge.","solutions":["If you only want to copy a completed recording, call replicate without a liveMerge destination (liveDestination == null).","Verify the source recording is still active (Archive.listRecording / recording position not NULL) before requesting a live merge.","Ensure the source publisher keeps sending so the source recording stays open for the duration of the merge.","If the source may end, handle the failure error code and fall back to a non-live replication of the closed recording."],"exampleFix":"// before: live merge on a possibly-finished recording\narchive.replicate(srcRecordingId, dstRecordingId, liveMergeChannel, streamId, srcArchiveCtl, srcArchiveRep);\n// after: only merge live when the source is still recording\nif (archive.getRecordingPosition(srcRecordingId) != Aeron.NULL_POSITION) {\n    archive.replicate(srcRecordingId, dstRecordingId, liveMergeChannel, streamId, srcArchiveCtl, srcArchiveRep);\n} else {\n    archive.replicate(srcRecordingId); // no live merge\n}","handlingStrategy":"validation","validationCode":"// only request a live merge if the source recording is still active\nlong srcPos = srcArchive.getRecordingPosition(srcRecordingId);\nif (srcPos == Aeron.NULL_POSITION) {\n    // source recording finished: replicate without live merge\n    dstArchive.replicate(srcRecordingId);\n} else {\n    dstArchive.replicate(srcRecordingId, dstRecordingId, liveChannel, streamId, srcCtl, srcRep);\n}","typeGuard":"boolean canLiveMerge(long sourceRecordingPosition) {\n    return sourceRecordingPosition != Aeron.NULL_POSITION;\n}","tryCatchPattern":"try {\n    startReplicationWithLiveMerge();\n} catch (ArchiveException e) {\n    if (e.getMessage().contains(\"cannot live merge without active source recording\")) {\n        log.info(\"source already ended; falling back to plain replication\");\n        replicateWithoutLiveMerge();\n    }\n}","preventionTips":["Only pass a liveMerge destination when the source recording is known to be open.","Handle short-lived sources: end-of-recording races are expected, fall back to replay.","Check recording position != NULL_POSITION before configuring merge destinations.","Keep the source publisher running until the merge completes."],"tags":["aeron","archive","live-merge","replication","invalid-state"],"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-15T23:17:13.987Z"}