{"record":{"id":"c915673cef394382","repo":"apache/druid","slug":"already-closed-c91567","errorCode":null,"errorMessage":"Already Closed!","messagePattern":"Already Closed!","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"processing/src/main/java/org/apache/druid/collections/StupidPool.java","lineNumber":286,"sourceCode":"        final ObjectId objectId,\n        final Cleaners.Cleanable cleanable,\n        final ObjectLeakNotifier notifier\n    )\n    {\n      this.objectRef = new AtomicReference<>(object);\n      this.objectId = objectId;\n      this.cleanable = cleanable;\n      this.notifier = notifier;\n    }\n\n    // WARNING: it is entirely possible for a caller to hold onto the object and call ObjectResourceHolder.close,\n    // Then still use that object even though it will be offered to someone else in StupidPool.take\n    @Override\n    public T get()\n    {\n      final T object = objectRef.get();\n      if (object == null) {\n        throw new ISE(\"Already Closed!\");\n      }\n\n      return object;\n    }\n\n    @Override\n    public void close()\n    {\n      final T object = objectRef.get();\n      if (object != null && objectRef.compareAndSet(object, null)) {\n        try {\n          tryReturnToPool(object, objectId, cleanable, notifier);\n        }\n        finally {\n          // Need to null reference to objectId because if ObjectResourceHolder is closed, but leaked, this reference\n          // will prevent reporting leaks of ResourceHandlers when this object and objectId are taken from the pool\n          // again.\n          objectId = null;","sourceCodeStart":268,"sourceCodeEnd":304,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/processing/src/main/java/org/apache/druid/collections/StupidPool.java#L268-L304","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","solutions":["Keep get() within the holder's open lifetime: close only after the last get()/use completes","Use try-with-resources around the holder to enforce ordering","If the object is needed beyond the lease, increment/copy ownership properly or take a new object from the pool","Check for double-close in cleanup paths (idempotent close or ownership flags)"],"exampleFix":"// before\nholder.close();\nT obj = holder.get(); // ISE: Already Closed!\n// after\nT obj;\ntry (ObjectResourceHolder<T> h = holder) {\n  obj = h.get();\n  use(obj);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { T obj = holder.get(); } catch (IllegalStateException e) { /* holder closed: take a new object from pool */ }","preventionTips":["Close holders only after the final get()/use","Use try-with-resources to enforce lease ordering","Do not cache StupidPool objects beyond their lease"],"tags":["java","pool","use-after-close"],"backgroundTag":"invalid-state-transition","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}