apache/pulsar · error · ManagedLedgerException

ManagedLedger ${name} has already been closed

Error message

ManagedLedger ${name} has already been closed

What it means

checkManagedLedgerIsOpen() throws this ManagedLedgerException when any synchronous operation is invoked on a managed ledger whose state is Closed. Once closed, a ManagedLedger instance is terminal — it cannot be reopened; a new instance must be obtained from the factory.

Source

Thrown at managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/ManagedLedgerImpl.java:4526

        return mlInfo;
    }

    /**
     * Throws an exception if the managed ledger has been previously fenced.
     *
     * @throws ManagedLedgerException
     */
    private void checkFenced() throws ManagedLedgerException {
        if (STATE_UPDATER.get(this).isFenced()) {
            log.error("Attempted to use a fenced managed ledger");
            throw new ManagedLedgerFencedException();
        }
    }

    private void checkManagedLedgerIsOpen() throws ManagedLedgerException {
        if (STATE_UPDATER.get(this) == State.Closed) {
            throw new ManagedLedgerException("ManagedLedger " + name + " has already been closed");
        }
    }

    @VisibleForTesting
    public synchronized void setFenced() {
        log.info().log("Moving to Fenced state");
        State prev = STATE_UPDATER.getAndSet(this, State.Fenced);
        if (prev != State.Fenced) {
            clearPendingAddEntries(new ManagedLedgerFencedException("ManagedLedger "
                + name + " is fenced"));
        }
    }

    synchronized void setFencedForDeletion() {
        log.info().log("Moving to FencedForDeletion state");
        State prev = STATE_UPDATER.getAndSet(this, State.FencedForDeletion);
        if (prev != State.FencedForDeletion) {
            clearPendingAddEntries(new ManagedLedgerFencedException("ManagedLedger "

View on GitHub (pinned to 820761864e)

Solutions

  1. Re-acquire the managed ledger via ManagedLedgerFactory.open()/openLedger after a close — closed instances are not reusable
  2. Check ledger state before operations (isClosed()) in long-lived holders
  3. Fix races by closing/invalidating references to the ledger before calling close()
  4. At the Pulsar-client level, expect this as a topic re-creation signal: reconnect/re-subscribe to get the new ledger

Example fix

// before
ml.addEntry(data); // throws if closed
// after
synchronized (this) {
    if (ml.getState() == State.Closed) {
        ml = factory.open(name);
    }
    ml.addEntry(data);
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (ml.getState() == State.Closed) {
    ml = factory.open(ledgerName); // re-acquire
}

Try / catch

try {
    ml.addEntry(data);
} catch (ManagedLedgerException e) {
    if (String.valueOf(e.getMessage()).contains("has already been closed")) {
        reopenLedgerAndRetry();
    } else { throw e; }
}

Prevention

When it happens

Trigger: Calling addEntry/getEntries/close/delete/etc. after close() completed; a topic whose managed ledger was closed during broker unload but is still referenced by a producer/consumer path; concurrent close racing an in-flight operation.

Common situations: Broker unload/partition reassignment closing the ledger while a client operation is mid-flight; topic deletion racing producers; application holding a stale ManagedLedger reference after close.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/50eebcb3ec062fa2. Report an issue: GitHub.