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-560 — HBaseReader.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-776 — HBaseWriterFn.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-940 — HBaseRowMutationWriterFn.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
What happened?
HBaseIOreleases its resources as consecutive, unguarded statements in three places. If an earlierclose()throws, everything after it is skipped. All line numbers are againstmastertoday.1.
HBaseIO.java:550-560—HBaseReader.close()A throwing
scanner.close()leaks theConnection.2.
HBaseIO.java:766-776—HBaseWriterFn.tearDown()This is the sharpest of the three, because
mutator.close()is expected to be able to throw.BufferedMutator.close()is documented as:So any bundle whose final buffered flush fails — a down region server, a network blip — skips
connection.close()and leaks the wholeConnection. The connection here is created per-@SetupviaConnectionFactory.createConnection(:744), so this is a real per-worker leak, not a shared handle.3.
HBaseIO.java:931-940—HBaseRowMutationWriterFn.tearDown()— worst consequenceHere the skipped call is not an ordinary
close(), it is a reference-count decrement.HBaseSharedConnectionkeeps astatic HashMap<String, Pair<Connection, Integer>> connectionPool(HBaseSharedConnection.java:46);getOrCreateincrements the count (:80) andclose(Configuration)decrements it, closing the underlyingConnectiononly once it reaches zero (:119,:128-129).So a throwing
table.close()means the count is never decremented. Because the map isstatic, that entry — and its ZooKeeper session and RPC threads — then survives for the lifetime of the JVM, and every latergetOrCreatehands 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/finallywould 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