Skip to content

DELETE calls leak pooled HTTP connections, exhausting the pool (Apache connector / proxy) #1328

Description

@buehlmann

AbstractApi.delete(...) returns the JAX-RS Response without reading its entity or closing it. The vast majority of delete callers are void (e.g. GroupApi.deleteGroup, ProjectApi.deleteProject, *.deleteHook, *.deleteVariable, …) and discard the returned Response. When GitLabApi uses the Apache pooling connector (i.e. when configured with a proxy via ProxyClientConfig/ApacheConnectorProvider), a Response whose entity is never read or closed keeps its connection leased and never returns it to the pool. GitLab's delete endpoints answer 202 Accepted / 200 OK with a body, so each such delete leaks one pooled connection.

With the default pool of 2 connections per route, a handful of deletes exhausts the pool, after which every subsequent request blocks indefinitely in org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking.

Affected versions

6.3.0 and later. 6.2.0 is not affected - see "Why it regressed" below.

Environment

  • gitlab4j-api 6.3.0
  • Client configured with a proxy (so the Jersey Apache connector + PoolingHttpClientConnectionManager is used)
  • Default connection pool (2 per route), no request timeouts

Steps to reproduce

  1. Create a GitLabApi that goes through a proxy (forces the Apache pooling connector):
    GitLabApi api = new GitLabApi(url, token, ProxyClientConfig.createProxyClientConfig(proxyUri));
  2. Issue more than maxPerRoute (default 2) concurrent deleteGroup/deleteProject calls to the same host.

Expected

All deletes complete; connections are returned to the pool and reused.

Actual

Exactly 2 deletes succeed (and hold both pooled connections without releasing them); every further delete blocks forever waiting to lease a connection. The successful deletes really happen on the GitLab side, but their connections are never returned, so the pool stays exhausted.

Root cause

AbstractApi.delete(...)validate(...) returns the open Response; validate only reads response.getStatus(). Nothing reads the entity or calls Response.close(). With the Apache connector, the connection backing a Response is only returned to the pool when the entity is fully read/closed, so the discarded delete responses leak their connections.

GET/POST/PUT do not leak because their callers consume the entity via response.readEntity(...), which releases the connection.

Why it regressed in 6.3.0

PR #1312 ("moved 'property' calls on Jersey APIs") moved the per-request .property() calls onto the ClientConfig. That was a correct performance fix, but as a side effect it changed the lifetime of the connection pool: before, mutating the client per request kept the ClientConfig RequestScoped, so Jersey rebuilt the runtime (and its connection manager) per request - leaked connections were
discarded along with the per-request client and never accumulated. Since #1312 the ClientConfig is a singleton and the ApacheConnector + PoolingHttpClientConnectionManager are created once and shared.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions