Skip to content

[Bug]: HBaseIO skips connection cleanup when an earlier close() throws, permanently corrupting the shared-connection refcount #39710

Description

@PDGGK

What happened?

HBaseIO releases its resources as consecutive, unguarded statements in three places. If an earlier close() throws, everything after it is skipped. All line numbers are against master today.

1. HBaseIO.java:550-560HBaseReader.close()

if (scanner != null) { scanner.close(); scanner = null; }
if (connection != null) { connection.close(); connection = null; }

A throwing scanner.close() leaks the Connection.

2. HBaseIO.java:766-776HBaseWriterFn.tearDown()

if (mutator != null) { mutator.close(); mutator = null; }
if (connection != null) { connection.close(); connection = null; }

This is the sharpest of the three, because mutator.close() is expected to be able to throw. BufferedMutator.close() is documented as:

Performs a flush() and releases any resources held.
@throws IOException if a remote or network exception occurs.

So any bundle whose final buffered flush fails — a down region server, a network blip — skips connection.close() and leaks the whole Connection. The connection here is created per-@Setup via ConnectionFactory.createConnection (:744), so this is a real per-worker leak, not a shared handle.

3. HBaseIO.java:931-940HBaseRowMutationWriterFn.tearDown() — worst consequence

if (table != null) { table.close(); table = null; }
HBaseSharedConnection.close(configuration);

Here the skipped call is not an ordinary close(), it is a reference-count decrement. HBaseSharedConnection keeps a static HashMap<String, Pair<Connection, Integer>> connectionPool (HBaseSharedConnection.java:46); getOrCreate increments the count (:80) and close(Configuration) decrements it, closing the underlying Connection only once it reaches zero (:119, :128-129).

So a throwing table.close() means the count is never decremented. Because the map is static, that entry — and its ZooKeeper session and RPC threads — then survives for the lifetime of the JVM, and every later getOrCreate hands back the same permanently unreleasable connection. A leak here is not bounded by the bundle or even the worker's use of the sink.

What I'd propose

Make each teardown run every step unconditionally while preserving the first failure, attaching later ones as suppressed, so the caller still sees the error that actually broke the job rather than a teardown symptom. A plain nested try/finally would guarantee the calls happen but would silently swap which exception propagates, so it is only half a fix.

I'm happy to submit the PR if this looks right.

Issue Priority

Priority: 2 (default / most bugs should be filed as P2)

Issue Components

  • Component: Java SDK
  • Component: IO connector

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions