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
- Re-acquire the managed ledger via ManagedLedgerFactory.open()/openLedger after a close — closed instances are not reusable
- Check ledger state before operations (isClosed()) in long-lived holders
- Fix races by closing/invalidating references to the ledger before calling close()
- 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
- Drop references to a ledger once close() is called
- Serialize close vs in-flight operations with proper synchronization
- Handle broker unload by reopening the ledger/topic
- Treat closed-ledger as terminal: never operate on the old instance
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
- ManagedLedgerFactory is already closed.
- Timeout during managed ledger close
- Timeout during managed ledger delete operation
- Timeout during managed ledger offload operation
- Positions must not be null
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/50eebcb3ec062fa2.
Report an issue: GitHub.