apache/druid · error · IllegalStateException

Already Closed!

Error message

Already Closed!

What it means

StupidPool's ObjectResourceHolder.get() reads from an AtomicReference that is cleared to null when the holder is closed. If the reference is null, the holder was already closed and using the underlying object would be unsafe, so an IllegalStateException is thrown.

Solutions

  1. Keep get() within the holder's open lifetime: close only after the last get()/use completes
  2. Use try-with-resources around the holder to enforce ordering
  3. If the object is needed beyond the lease, increment/copy ownership properly or take a new object from the pool
  4. Check for double-close in cleanup paths (idempotent close or ownership flags)

Example fix

// before
holder.close();
T obj = holder.get(); // ISE: Already Closed!
// after
T obj;
try (ObjectResourceHolder<T> h = holder) {
  obj = h.get();
  use(obj);
}
Defensive patterns

Strategy: try-catch

Try / catch

try { T obj = holder.get(); } catch (IllegalStateException e) { /* holder closed: take a new object from pool */ }

Prevention

When it happens

Trigger: Calling get() on a ObjectResourceHolder after close() was called on it, or on a holder obtained from a pool object whose lease was already terminated; also double-close followed by get().

Common situations: Async code retaining pool objects past their lease; closing the holder in one callback and reading the object in another; misuse of StupidPool objects as long-lived cached objects.

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 apache/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/c915673cef394382. Report an issue: GitHub.

Appendix: source

Thrown at processing/src/main/java/org/apache/druid/collections/StupidPool.java:286

        final ObjectId objectId,
        final Cleaners.Cleanable cleanable,
        final ObjectLeakNotifier notifier
    )
    {
      this.objectRef = new AtomicReference<>(object);
      this.objectId = objectId;
      this.cleanable = cleanable;
      this.notifier = notifier;
    }

    // WARNING: it is entirely possible for a caller to hold onto the object and call ObjectResourceHolder.close,
    // Then still use that object even though it will be offered to someone else in StupidPool.take
    @Override
    public T get()
    {
      final T object = objectRef.get();
      if (object == null) {
        throw new ISE("Already Closed!");
      }

      return object;
    }

    @Override
    public void close()
    {
      final T object = objectRef.get();
      if (object != null && objectRef.compareAndSet(object, null)) {
        try {
          tryReturnToPool(object, objectId, cleanable, notifier);
        }
        finally {
          // Need to null reference to objectId because if ObjectResourceHolder is closed, but leaked, this reference
          // will prevent reporting leaks of ResourceHandlers when this object and objectId are taken from the pool
          // again.
          objectId = null;

View on GitHub (pinned to 9b90983fd2)