Skip to content

[core] Restore the interrupt status in the two JDBC catalog paths that drop it - #9159

Merged
JingsongLi merged 2 commits into
apache:masterfrom
PDGGK:fix-jdbc-catalog-interrupt-status
Aug 12, 2026
Merged

[core] Restore the interrupt status in the two JDBC catalog paths that drop it#9159
JingsongLi merged 2 commits into
apache:masterfrom
PDGGK:fix-jdbc-catalog-interrupt-status

Conversation

@PDGGK

@PDGGK PDGGK commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Purpose

The jdbc package converts an InterruptedException into an unchecked exception in nine places. Seven re-assert the interrupt before rethrowing; two do not. This PR closes those two, so the package stops contradicting itself.

site
JdbcCatalog:131 constructor → initializeCatalogTablesIfNeed() drops the flag
JdbcUtils:677 insertTable drops the flag
JdbcCatalog 640, 832, 1018, 1047, 1314 · JdbcUtils 627, 655 already restore it

Why it matters: LinkedBlockingDeque.pollFirst, which is what ClientPoolImpl.run blocks on, clears the flag when it throws. Whoever catches the resulting RuntimeException — a retry loop, an executor's task wrapper, a catalog-loader that falls back to another catalog — sees a thread that looks like it was never cancelled, and every subsequent blocking call on it behaves accordingly. The two fixed sites are on the catalog-open and table-create paths, so they run on exactly the threads a shutdown is trying to stop.

I deliberately kept this to the two deviating sites, matching the shape already used beside each one rather than introducing a new one:

  • JdbcCatalog:131 is a single catch (InterruptedException e) → mirrors JdbcCatalog:1018
  • JdbcUtils:677 is a catch (SQLException | InterruptedException e) → mirrors JdbcCatalog:1047, keeping the instanceof guard so a SQLException is unaffected

No exception type or message changes, so existing assertions such as JdbcCatalogTest#testInsertTableUtility's hasMessageContaining("Failed to insert table") still hold.

Tests

New JdbcInterruptStatusTest, one case per fixed site.

The constructor case needs no mocking. ClientPoolImpl.run waits on LinkedBlockingDeque.pollFirst(10, SECONDS), and lockInterruptibly() throws immediately when the calling thread already carries the flag — so setting the flag and then calling the real constructor drives the real code down its real interrupt path. insertTable is a plain static call, so it takes a mocked JdbcClientPool whose run throws. Both cases clear the flag in @AfterEach so it cannot leak into later tests on the same thread.

Checked against master, both fail — and they fail at the interrupt-status assertion, with the exception type and message assertions already passing, which is what confirms they are exercising the intended path rather than erroring out early:

JdbcInterruptStatusTest.catalogConstructorKeepsTheInterruptStatus:87
JdbcInterruptStatusTest.insertTableKeepsTheInterruptStatus:102
Tests run: 2, Failures: 2

With the fix:

JdbcClientPoolTest        Tests run: 4,  Failures: 0
JdbcInterruptStatusTest   Tests run: 2,  Failures: 0
JdbcCatalogTest           Tests run: 77, Failures: 0

spotless:check, checkstyle:check and apache-rat:check are clean. Java 8 syntax only, per AGENTS.md.

No overlap with the open #7475 — its hunks are elsewhere in both files, and it only calls insertTable.

PDGGK added 2 commits August 11, 2026 11:29
…t drop it

The jdbc package turns an InterruptedException into an unchecked exception in
nine places. Seven re-assert the flag before rethrowing; two do not, so a
thread that gets cancelled inside them comes back out looking un-cancelled and
every later blocking call on it behaves as if nothing happened.

  JdbcCatalog:131   the constructor's initializeCatalogTablesIfNeed() call
  JdbcUtils:677     insertTable

Both now follow the shape already used next to them: the single-catch site
mirrors JdbcCatalog:1018, and the multi-catch site mirrors JdbcCatalog:1047,
which keeps its instanceof guard. No exception type or message changes.

JdbcInterruptStatusTest covers both. The catalog constructor needs no mocking:
ClientPoolImpl.run waits on LinkedBlockingDeque.pollFirst, whose
lockInterruptibly() throws as soon as it sees a thread that already carries the
flag, so setting it first drives the real code down its real interrupt path.
Both cases fail on master at the interrupt-status assertion.
…ment

CI caught this: catalogConstructorKeepsTheInterruptStatus passed locally
but failed on Linux with "Expecting code to raise a throwable".

The first version set the thread's interrupt flag and relied on the real
pool reaching LinkedBlockingDeque.pollFirst, whose lockInterruptibly()
throws when the flag is already set. That assumed nothing between the
flag and the wait consumes it -- but the constructor opens a real JDBC
connection first, and driver initialisation apparently swallows the
interrupt on Linux. So no exception, so no assertion.

Both cases now stub JdbcClientPool.run to throw InterruptedException
outright. For the constructor that means seeding CachedJdbcClientPool's
shared cache through its existing @VisibleForTesting clientPools()
accessor, the same seam CachedJdbcClientPoolTest already uses, so no
real connection is opened at all and there is nothing left to be
environment-dependent about.

Both still fail against the unfixed catch blocks.
@PDGGK

PDGGK commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

CI caught a real defect in my test — pushed 394346d for it.

catalogConstructorKeepsTheInterruptStatus passed locally but failed on Linux with Expecting code to raise a throwable. The first version set the thread's interrupt flag and then relied on the real pool reaching LinkedBlockingDeque.pollFirst, whose lockInterruptibly() throws when the calling thread already carries the flag. That quietly assumed nothing between setting the flag and that wait consumes it — but the constructor opens a real JDBC connection first, and driver initialisation evidently swallows the interrupt on Linux. No exception, so no assertion. My mistake, and the seam was environment-dependent in a way that would have stayed fragile even if it had happened to pass.

Both cases now stub JdbcClientPool.run to throw InterruptedException outright. For the constructor that means seeding CachedJdbcClientPool's shared cache via its existing @VisibleForTesting clientPools() accessor — the same seam CachedJdbcClientPoolTest already uses — so no real connection is opened and there is nothing environment-dependent left. Both still fail against the unfixed catch blocks, and Jdbc*Test + CachedJdbcClientPoolTest are green locally (93 tests).

Separately, the Test Flink job failed on PrimaryKeyFileStoreTableITCase.testFullCompactionChangelogProducerStreamingRandom. That one I don't believe is mine: the test file contains no reference to jdbc, this change is confined to paimon-core's jdbc package, and that class has a long history of instability — #4634, #4933, #4940, #7036, #6913 and most recently #8940 (30 Jul) are all test-stability fixes for it, and #5563 tracked its sibling testNoChangelogProducerStreamingRandom being unstable. I can't re-run jobs myself; happy to rebase if you'd like a fresh run.

@JingsongLi

Copy link
Copy Markdown
Contributor

+1

@JingsongLi
JingsongLi merged commit a5ed6ad into apache:master Aug 12, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants