apache/pulsar · error · ManagedLedgerException.ManagedLedgerFactoryClosedException
ManagedLedgerFactory is already closed.
Error message
ManagedLedgerFactory is already closed.
What it means
ManagedLedgerFactoryImpl.shutdownAsync() throws ManagedLedgerFactoryClosedException when the factory's `closed` flag is already set. Once a ManagedLedgerFactory is shut down it cannot be reused; any further shutdown or open attempt on it is rejected. This is a lifecycle/state guard, not an infra failure.
Source
Thrown at managed-ledger/src/main/java/org/apache/bookkeeper/mledger/impl/ManagedLedgerFactoryImpl.java:661
void close(ManagedLedger ledger) {
// If the future in map is not done or has exceptionally complete, it means that @param-ledger is not in the
// map.
CompletableFuture<ManagedLedgerImpl> ledgerFuture = ledgers.get(ledger.getName());
if (ledgerFuture == null || !ledgerFuture.isDone() || ledgerFuture.isCompletedExceptionally()){
return;
}
if (ledgerFuture.join() != ledger){
return;
}
// Remove the ledger from the internal factory cache.
if (ledgers.remove(ledger.getName(), ledgerFuture)) {
entryCacheManager.removeEntryCache(ledger.getName());
}
}
public CompletableFuture<Void> shutdownAsync() throws ManagedLedgerException {
if (closed) {
throw new ManagedLedgerException.ManagedLedgerFactoryClosedException();
}
closed = true;
statsTask.cancel(true);
flushCursorsTask.cancel(true);
cacheEvictionExecutor.shutdownNow();
List<String> ledgerNames = new ArrayList<>(this.ledgers.keySet());
List<CompletableFuture<Void>> futures = new ArrayList<>(ledgerNames.size());
int numLedgers = ledgerNames.size();
log.info().attr("numLedgers", numLedgers).log("Closing ledgers");
for (String ledgerName : ledgerNames) {
CompletableFuture<ManagedLedgerImpl> ledgerFuture = ledgers.remove(ledgerName);
if (ledgerFuture == null) {
continue;
}
CompletableFuture<Void> future = new CompletableFuture<>();
futures.add(future);View on GitHub (pinned to 820761864e)
Solutions
- Guard shutdown with an idempotency check (AtomicBoolean) so double-shutdown is a no-op
- Track factory lifecycle ownership — only the component that created the factory should close it, exactly once
- If you need a working factory again, create a new ManagedLedgerFactory instance
- If this comes from Pulsar shutdown ordering, ensure shutdown hooks are registered once, not per-module
Example fix
// before
factory.shutdown(); // may run twice
// after
private final AtomicBoolean shutDown = new AtomicBoolean(false);
if (shutDown.compareAndSet(false, true)) {
factory.shutdown();
} Defensive patterns
Strategy: validation
Validate before calling
private final AtomicBoolean shutdownOnce = new AtomicBoolean(false); if (!shutdownOnce.compareAndSet(false, true)) return; // skip second shutdown
Try / catch
try {
factory.shutdownAsync();
} catch (ManagedLedgerException.ManagedLedgerFactoryClosedException e) {
// already shut down — safe to ignore
} Prevention
- Make shutdown idempotent with compareAndSet
- Give exactly one component ownership of factory lifecycle
- Never reuse a factory after close; create a new one
- Register shutdown hooks once
When it happens
Trigger: Calling shutdown()/shutdownAsync() twice on the same ManagedLedgerFactory; calling them after close() was already invoked; a shared factory instance shut down by one component (e.g. PulsarClient close triggering broker shutdown) then touched again by another.
Common situations: Double shutdown in cleanup paths; Pulsar broker restart hooks running twice; tests sharing a factory across suites and closing it in one suite's teardown.
Related errors
- ManagedLedger ${name} has already been 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/870b427b7425cb60.
Report an issue: GitHub.