apache/druid · critical · RuntimeException

Leaks happened, each suppressed exception represents one…

Error message

Leaks happened, each suppressed exception represents one code path that checked out an object and didn't return it.

What it means

StupidPool.take throws this when the pool has been 'poisoned' by a previously detected leak (a checked-out object's holder was garbage-collected without close/return) and the recorded leak list is present. Each suppressed LeakedException carries the stack trace of a code path that checked out an object (typically an off-heap ByteBuffer) and never returned it. This is a resource-leak detection mechanism, not a transient failure.

Solutions

  1. Inspect the suppressed LeakedException stack traces to identify the code path that leaked the object.
  2. Ensure every ResourceHolder from take() is used in try-with-resources or explicitly closed in a finally block.
  3. Fix the leaky code path (Druid or extension code) that dropped the holder without returning it.
  4. Restart/retry after the leak fix — the pool stays poisoned, so the error repeats until the process is restarted.

Example fix

// before
ResourceHolder<ByteBuffer> holder = pool.take();
ByteBuffer buf = holder.get();
process(buf); // may throw; holder never closed
// after
try (ResourceHolder<ByteBuffer> holder = pool.take()) {
  process(holder.get());
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (pool.poolSize() < 0 || pool.leakedObjectsCount() > 0) { log.warn("pool has prior leaks; holders may not be closed somewhere"); }

Try / catch

try (ResourceHolder<T> holder = pool.take()) {
  use(holder.get());
} catch (RuntimeException e) {
  if (e.getMessage() != null && e.getMessage().startsWith("Leaks happened")) {
    for (Throwable suppressed : e.getSuppressed()) { logLeakSite(suppressed); }
  }
  throw e;
}

Prevention

When it happens

Trigger: Pool is poisoned by a leak notification (Cleaner detected an unclosed ObjectResourceHolder); a later take() finds no pooled object and capturedException is non-null, so makeExceptionForLeaks throws with all leak stack traces as suppressed exceptions.

Common situations: Query processing code paths that allocate merge buffers/column caches and exit early on exceptions without closing holders; forgotten close() in custom operator or extension code; holder dropped without try-with-resources.

Related errors


AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/6f592e2d17cf2f35. Report an issue: GitHub.

Appendix: source

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

  }

  @Override
  public String toString()
  {
    return "StupidPool{" +
           "name=" + name +
           ", objectsCacheMaxCount=" + objectsCacheMaxCount +
           ", poolSize=" + poolSize() +
           "}";
  }

  @Override
  public ResourceHolder<T> take()
  {
    ObjectResourceHolder resourceHolder = objects.poll();
    if (resourceHolder == null) {
      if (POISONED.get() && capturedException.get() != null) {
        throw makeExceptionForLeaks(capturedException.get());
      }
      return makeObjectWithHandler();
    } else {
      poolSize.decrementAndGet();
      if (POISONED.get()) {
        final CopyOnWriteArrayList<LeakedException> exceptionList = capturedException.get();
        if (exceptionList == null) {
          resourceHolder.notifier.except = new LeakedException(Thread.currentThread().getName());
        } else {
          throw makeExceptionForLeaks(exceptionList);
        }
      }
      return resourceHolder;
    }
  }

  private RuntimeException makeExceptionForLeaks(CopyOnWriteArrayList<LeakedException> exceptionList)
  {

View on GitHub (pinned to 9b90983fd2)