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

  1. Guard shutdown with an idempotency check (AtomicBoolean) so double-shutdown is a no-op
  2. Track factory lifecycle ownership — only the component that created the factory should close it, exactly once
  3. If you need a working factory again, create a new ManagedLedgerFactory instance
  4. 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

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


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