{"record":{"id":"47e075c4ccd831e6","repo":"apache/iceberg","slug":"attempted-to-release-already-closed-http-client-k","errorCode":null,"errorMessage":"Attempted to release already closed HTTP client: key={}","messagePattern":"Attempted to release already closed HTTP client: key=(.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"aws/src/main/java/org/apache/iceberg/aws/HttpClientCache.java","lineNumber":138,"sourceCode":"     */\n    synchronized ManagedHttpClient acquire() {\n      if (closed) {\n        throw new IllegalStateException(\"Cannot acquire closed HTTP client: \" + clientKey);\n      }\n      refCount++;\n      LOG.debug(\"Acquired HTTP client: key={}, refCount={}\", clientKey, refCount);\n      return this;\n    }\n\n    /**\n     * Release a reference to the HTTP client, decrementing the reference count. If the count\n     * reaches zero, the client is closed.\n     *\n     * @return true if the client was closed, false otherwise\n     */\n    synchronized boolean release() {\n      if (closed) {\n        LOG.warn(\"Attempted to release already closed HTTP client: key={}\", clientKey);\n        return false;\n      }\n\n      refCount--;\n      LOG.debug(\"Released HTTP client: key={}, refCount={}\", clientKey, refCount);\n      if (refCount == 0) {\n        return closeHttpClient();\n      } else if (refCount < 0) {\n        LOG.warn(\n            \"HTTP client reference count went negative key={}, refCount={}\", clientKey, refCount);\n        refCount = 0;\n      }\n      return false;\n    }\n\n    @VisibleForTesting\n    SdkHttpClient httpClient() {\n      return httpClient;","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/aws/src/main/java/org/apache/iceberg/aws/HttpClientCache.java#L120-L156","documentation":"This is a warning log (not an exception) from the internal reference-counted HTTP client cache in HttpClientCache. Each acquire() increments a ref count per client key and each release() decrements it; when the count reaches zero the client is closed. Seeing this means release() was called on a client whose entry was already closed, so the release is ignored and returns false.","triggerScenarios":"Calling HttpClientCache.releaseClient (or the internal release()) more times than acquire() for the same client key, or after the entry was closed by another thread due to the ref count hitting zero. The tests closed, closedAgain, multipleReleasesAfterClose exercise exactly this double-release path.","commonSituations":"A caller wraps client usage in a finally block that releases even when acquire failed or an earlier release already ran; a race where two threads both observe refCount==1 and both call release; a retry wrapper that releases the client on both the failure and success paths.","solutions":["Audit call sites so release() is called exactly once per acquire(), typically in a single finally block","Guard with your own boolean flag so a double release can't happen on retry/failure paths","Treat the warning as benign if it comes from tests intentionally probing double-release; the call returns false and takes no action","Ensure no code path closes the cache entry externally before all users finish with the client"],"exampleFix":"// before\nS3Client client = cache.acquire(key);\ntry {\n  use(client);\n} finally {\n  cache.releaseClient(key);\n  cache.releaseClient(key); // second release logs the warning\n}\n// after\nS3Client client = cache.acquire(key);\ntry {\n  use(client);\n} finally {\n  cache.releaseClient(key); // exactly one release per acquire\n}","handlingStrategy":"try-catch","validationCode":"// track balance yourself\nint acquired = 0;\nacquired++; client = cache.acquire(key);\n// assert before releasing\nassert acquired > 0;","typeGuard":"boolean canRelease = acquiredCount.get(key) != null && acquiredCount.get(key) > 0;","tryCatchPattern":"// release() never throws; just don't double-release\ntry {\n  use(client);\n} finally {\n  if (released.compareAndSet(false, true)) {\n    cache.releaseClient(key);\n  }\n}","preventionTips":["Release exactly once per acquire in one finally block","Use an AtomicBoolean/compareAndSet guard for idempotent release","Don't release clients you didn't acquire for the same key","Treat the warning log as a signal of an unbalanced lifecycle bug"],"tags":["aws","http-client","reference-counting","resource-lifecycle"],"backgroundTag":"invalid-state-transition","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}