aeron-io/aeron · error · ArchiveException

replication image closed unexpectedly

Error message

replication image closed unexpectedly

What it means

Thrown by ReplicationSession.catchup() when, during the catch-up phase of a live replication (a liveDestination is configured and the replay image is still behind srcRecordingPosition), the replay Image becomes closed before it reaches the source recording position. This means the source replay stream terminated prematurely — the replicated recording's catch-up data path died mid-replication instead of completing the merge to the live stream.

Solutions

  1. Check the source archive logs for the replay session termination reason (recording deleted, archive shutdown, replay cancelled) at the failure position.
  2. Verify the source recording still exists and its recorded data covers the catch-up range (use ArchiveTool to inspect the catalog) before re-replicating.
  3. Re-issue the replication from the last successfully replicated position (or use a recording token / truncate destination and restart).
  4. If loss caused the closure, tune reliability (retransmission settings, term-buffer sizes, aeron.rcv.initial.window.length) so the replay stream survives bursts.
  5. Ensure no competing replication or ArchiveTool operation stops/deletes the source recording during catch-up.

Example fix

// before: purging old recordings while replication is in flight
archiveControl.purgeRecording(srcRecordingId); // kills the replay mid-catchup
// after: verify the recording is no longer the target of an active replication first
if (!activeReplications.contains(srcRecordingId))
{
    archiveControl.purgeRecording(srcRecordingId);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before starting a live replication, confirm the source recording exists and is extendable
RecordingSignal signal;
try (Subscription sub = archive.controlResponsePoller().subscription())
{
    // listRecording throws ArchiveException with RECORDING_NOT_FOUND if purged
    archive.listRecording(srcRecordingId, (ctrl, lbl, d) -> {});
}

Try / catch

try
{
    long id = archive.replicate(srcRecordingId, dstRecordingId, liveChannel, liveStreamId,
        replicationChannel, replayStreamId);
}
catch (ArchiveException e)
{
    if (e.getMessage().contains("replication image closed unexpectedly"))
    {
        LOGGER.warn("source replay died during catch-up (recording purged or archive restarted); " +
            "re-replicating from last position", e);
        restartReplicationFromLastAckedPosition();
    }
    else
    {
        throw e;
    }
}

Prevention

When it happens

Trigger: During state CATCHUP, image.position() < srcRecordingPosition and image.isClosed() becomes true; the source archive stopped or failed the replay (source recording deleted, source archive shutdown, replay session cancelled, or flow-control/loss causing the publication to give up), or the destination removed the subscription due to an unblock/no-progress condition.

Common situations: Source recording was purged/truncated (archive catalog cleanup) while being replicated; source archive process restarted mid-catchup; network partition long enough to close the replay connection; source stop-position reached while a live merge was still expected; Aeron driver termination on either side; TTL/linger expiry killing the replay publication.

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/4aa392ed90c3dd7c. Report an issue: GitHub.

Appendix: source

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

            workCount += 1;
        }

        return workCount;
    }

    private int catchup()
    {
        int workCount = 0;

        if (image.position() >= srcRecordingPosition)
        {
            state(State.ATTEMPT_LIVE_JOIN, "image position (" + image.position() +
                ") >= srcRecordingPosition (" + srcRecordingPosition + ")");
            workCount += 1;
        }
        else if (image.isClosed())
        {
            throw new ArchiveException("replication image closed unexpectedly");
        }

        return workCount;
    }

    private int attemptLiveJoin()
    {
        int workCount = 0;

        if (NULL_VALUE == activeCorrelationId)
        {
            final long correlationId = aeron.nextCorrelationId();
            if (srcArchive.archiveProxy().getRecordingPosition(
                srcRecordingId, correlationId, srcArchive.controlSessionId()))
            {
                workCount += trackAction(correlationId);
            }
            else if (epochClock.time() >= (timeOfLastActionMs + actionTimeoutMs))

View on GitHub (pinned to 6d60124e15)