aeron-io/aeron · error · ArchiveException

cannot live merge without active source recording

Error message

cannot live merge without active source recording

What it means

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.

Solutions

  1. If you only want to copy a completed recording, call replicate without a liveMerge destination (liveDestination == null).
  2. Verify the source recording is still active (Archive.listRecording / recording position not NULL) before requesting a live merge.
  3. Ensure the source publisher keeps sending so the source recording stays open for the duration of the merge.
  4. If the source may end, handle the failure error code and fall back to a non-live replication of the closed recording.

Example fix

// before: live merge on a possibly-finished recording
archive.replicate(srcRecordingId, dstRecordingId, liveMergeChannel, streamId, srcArchiveCtl, srcArchiveRep);
// after: only merge live when the source is still recording
if (archive.getRecordingPosition(srcRecordingId) != Aeron.NULL_POSITION) {
    archive.replicate(srcRecordingId, dstRecordingId, liveMergeChannel, streamId, srcArchiveCtl, srcArchiveRep);
} else {
    archive.replicate(srcRecordingId); // no live merge
}
Defensive patterns

Strategy: validation

Validate before calling

// only request a live merge if the source recording is still active
long srcPos = srcArchive.getRecordingPosition(srcRecordingId);
if (srcPos == Aeron.NULL_POSITION) {
    // source recording finished: replicate without live merge
    dstArchive.replicate(srcRecordingId);
} else {
    dstArchive.replicate(srcRecordingId, dstRecordingId, liveChannel, streamId, srcCtl, srcRep);
}

Type guard

boolean canLiveMerge(long sourceRecordingPosition) {
    return sourceRecordingPosition != Aeron.NULL_POSITION;
}

Try / catch

try {
    startReplicationWithLiveMerge();
} catch (ArchiveException e) {
    if (e.getMessage().contains("cannot live merge without active source recording")) {
        log.info("source already ended; falling back to plain replication");
        replicateWithoutLiveMerge();
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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/32bc9a6220cb0af4. Report an issue: GitHub.

Appendix: source

Thrown at aeron-archive/src/main/java/io/aeron/archive/ReplicationSession.java:511

            {
                workCount += trackAction(correlationId);
            }
            else if (epochClock.time() >= (timeOfLastActionMs + actionTimeoutMs))
            {
                throw new TimeoutException("failed to send recording position request");
            }
        }
        else
        {
            final ControlResponsePoller poller = srcArchive.controlResponsePoller();
            workCount += poller.poll();

            if (hasResponse(poller))
            {
                srcRecordingPosition = poller.relevantId();
                if (NULL_POSITION == srcRecordingPosition && null != liveDestination)
                {
                    throw new ArchiveException("cannot live merge without active source recording");
                }

                state(State.EXTEND, "");
            }
            else if (epochClock.time() >= (timeOfLastActionMs + actionTimeoutMs))
            {
                throw new TimeoutException("failed to get recording position");
            }
        }

        return workCount;
    }

    private int extend()
    {
        final boolean isMds = isTagged || null != liveDestination;
        final ChannelUri channelUri = ChannelUri.parse(replicationChannel);
        final String endpoint = channelUri.get(CommonContext.ENDPOINT_PARAM_NAME);

View on GitHub (pinned to 6d60124e15)