{"record":{"id":"ea0f984f5d871f72","repo":"apache/iceberg","slug":"http-client-reference-count-went-negative-key","errorCode":null,"errorMessage":"HTTP client reference count went negative key={}, refCount={}","messagePattern":"HTTP client reference count went negative key=(.+?), refCount=(.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"aws/src/main/java/org/apache/iceberg/aws/HttpClientCache.java","lineNumber":147,"sourceCode":"\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;\n    }\n\n    /**\n     * Close the HTTP client if not already closed.\n     *\n     * @return true if the client was closed by this call, false if already closed\n     */\n    private boolean closeHttpClient() {\n      if (!closed) {","sourceCodeStart":129,"sourceCodeEnd":165,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/aws/src/main/java/org/apache/iceberg/aws/HttpClientCache.java#L129-L165","documentation":"A warning logged by HttpClientCache.release() when the reference count drops below zero, meaning more releases were issued than acquires for this client key. The cache defensively resets refCount to 0 to keep the entry usable and returns false instead of throwing.","triggerScenarios":"An unbalanced release with no matching acquire, or a double release racing past the refCount==0 close check. Because release() is synchronized, the negative value is only observable when the closed-flag guard didn't fire first (e.g. entry recreated between calls).","commonSituations":"Utility wrappers that release a client they never acquired; refactoring removed an acquire but left the release; concurrent callers sharing a key with hand-rolled counting that drifts.","solutions":["Match every release() 1:1 with an acquire(); never release a client you didn't acquire for the same key","Check whether an exception path releases both where it failed and in finally, and remove the duplicate","If you see this repeatedly, log the calling stack at your call site to find the unbalanced release","Rely on the cache's self-heal (refCount reset to 0) only as a safety net, not as expected behavior"],"exampleFix":"// before\nif (condition) {\n  cache.releaseClient(key); // released without ever acquiring\n}\n// after\nS3Client client = cache.acquire(key);\ntry {\n  use(client);\n} finally {\n  cache.releaseClient(key);\n}","handlingStrategy":"validation","validationCode":"// ensure acquire happened before release\nif (!acquiredKeys.contains(key)) {\n  throw new IllegalStateException(\"release without acquire for \" + key);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pair every acquire with exactly one release","Centralize client borrowing in a wrapper that owns the count","Audit refactors that remove acquire calls but leave releases","Watch for this warning — it means your counting drifted"],"tags":["aws","http-client","reference-counting","resource-lifecycle"],"backgroundTag":"internal-invariant-violation","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"}