Skip to content

fix(hive-sync): close the SessionState HiveQL sync starts - #19812

Open
skywalker0618 wants to merge 4 commits into
apache:masterfrom
skywalker0618:fix-hive-sync-close-session-state
Open

fix(hive-sync): close the SessionState HiveQL sync starts#19812
skywalker0618 wants to merge 4 commits into
apache:masterfrom
skywalker0618:fix-hive-sync-close-session-state

Conversation

@skywalker0618

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

HiveQueryDDLExecutor starts a SessionState in its constructor and never closes it. Hive derives
a session's four scratch directory roots from hive.session.id and reclaims them only in
SessionState.close(), so every sync leaves a directory set behind, along with the session's
function registry and the class loaders it created. A HiveSyncTool, and therefore an executor and
its session, is built per sync, so on a long-running streaming job this accumulates for the life of
the JVM: close() released the metastore client and the Driver, never the session.

Observed on a Flink job syncing a 5,000-partition table in HiveQL mode: one unclosed session per
sync cycle, 63 of 63 and 76 of 76 across two runs, each leaving its scratch directory set on local
disk.

Summary and Changelog

Users running hive_sync.mode=hiveql on a long-lived job stop accumulating scratch directories and
per-session objects; the session is now released with the rest of the executor's resources.

  • close() closes the SessionState. It runs after the Driver teardown, because
    SessionState.close() detaches the session from the calling thread and Driver.destroy() can
    reach SessionState.get() while releasing locks, and it runs in a finally so a Driver close()
    that throws cannot skip it.
  • The constructor's error path now uses the same closeQuietly helper instead of its own inline
    block. Besides removing the duplication, it widens the caught type from IOException to
    Exception, so a RuntimeException during teardown can no longer mask the construction failure
    that is about to be thrown.
  • updateHiveSQLs re-asserts its own session before running statements. This is required by the
    change above: SessionState.close() calls detachSession(), which clears the thread local for
    whichever session is attached, so an executor can no longer assume the session it started in its
    constructor is still current when a second executor on the same thread is constructed or closed.
    Driver.compile() dereferences SessionState.get() unconditionally, and Hive documents that a
    thread running several sessions must set the current session when switching between them.
  • New TestHiveQueryDDLExecutorSession covers the session lifecycle: closed after the Driver and in
    that order, closed even when the Driver close() throws, a failing session close swallowed so it
    does not become the caller's problem, and SQL running under the session the executor started.

Impact

No public API or config change. Behaviour change is that the HiveQL sync path now releases its Hive
session when the sync tool is closed, so its scratch directories are reclaimed instead of being left
until the JVM exits.

Risk Level

low

Verification: TestHiveSyncTool passes in full, 275 tests, which exercises the HiveQL path
end-to-end against the embedded metastore, plus TestHiveDriverPool,
TestHiveQueryDDLExecutorFailures, TestHoodieHiveSyncClientClose and the new tests. The session
re-assert was checked by removing it and confirming the new test fails with SessionState.get()
returning null, which is the same NPE in Driver.compile() that the full suite surfaced in teardown
before the re-assert was added.

Documentation Update

none

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable

HiveQueryDDLExecutor starts a SessionState in its constructor and never
closed it. Hive derives a session's four scratch directory roots from
hive.session.id and reclaims them only in SessionState.close(), so every
sync left a directory set behind, along with the session's registry and
class loaders. A HiveSyncTool is built per sync, so on a long-running
streaming job this accumulates for the life of the JVM.

close() now closes the session, after the Driver teardown because
Driver.destroy() can reach SessionState.get() while releasing locks, and
in a finally so a Driver close that throws cannot skip it. The
constructor's error path uses the same helper, which also stops a
RuntimeException from a failing teardown masking the construction error.

Closing the session detaches it from the calling thread, which Hive does
unconditionally for whichever session is attached. The single-session SQL
path therefore re-asserts its own session before running statements, as
Hive documents a thread running several sessions must, instead of relying
on a thread local another executor may have replaced or cleared.

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR closes the SessionState that HiveQueryDDLExecutor starts per sync, fixing a scratch-directory / per-session-object leak on long-lived HiveQL-mode jobs. It moves the session close into a finally after the Driver teardown, reuses a closeQuietly helper on the constructor error path, and re-asserts the executor's own thread-local session before running statements. No correctness issues found. A few style/readability suggestions in the inline comments. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here. Code looks clean overall — one minor naming nit below.

cc @yihua

} catch (Exception e) {
log.error("Error while closing SessionState", e);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: could you rename the parameter to avoid the same name as the instance field? Something like state would make it immediately clear this is a static helper and not accidentally referencing the field.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@github-actions github-actions Bot added the size:M PR with lines of changes in (100, 300] label Sep 1, 2026
@codecov-commenter

codecov-commenter commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.07692% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.32%. Comparing base (2315459) to head (a106b79).
⚠️ Report is 53 commits behind head on master.

Files with missing lines Patch % Lines
...org/apache/hudi/hive/ddl/HiveQueryDDLExecutor.java 73.07% 4 Missing and 3 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19812      +/-   ##
============================================
+ Coverage     77.88%   78.32%   +0.43%     
- Complexity    33265    33904     +639     
============================================
  Files          2533     2542       +9     
  Lines        140348   141787    +1439     
  Branches      16913    17586     +673     
============================================
+ Hits         109304   111048    +1744     
+ Misses        23406    23028     -378     
- Partials       7638     7711      +73     
Components Coverage Δ
hudi-common 83.64% <96.52%> (+0.28%) ⬆️
hudi-client 83.20% <95.18%> (+0.24%) ⬆️
hudi-flink 85.66% <ø> (+0.08%) ⬆️
hudi-spark-datasource 73.24% <75.71%> (+0.87%) ⬆️
hudi-utilities 74.55% <ø> (+0.21%) ⬆️
hudi-cli 15.13% <ø> (+0.06%) ⬆️
hudi-hadoop 70.73% <78.57%> (+1.64%) ⬆️
hudi-sync 75.75% <76.66%> (+0.23%) ⬆️
hudi-io 80.02% <ø> (+0.26%) ⬆️
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 65.81% <ø> (+1.48%) ⬆️
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 51.45% <73.07%> (+0.51%) ⬆️
flink-integration-tests 48.81% <0.00%> (-0.30%) ⬇️
hadoop-mr-java-client 44.01% <ø> (+0.11%) ⬆️
integration-tests 13.51% <0.00%> (-0.09%) ⬇️
spark-client-hadoop-common 50.55% <ø> (-0.06%) ⬇️
spark-java-tests 52.23% <0.00%> (+0.25%) ⬆️
spark-scala-tests 46.96% <0.00%> (+0.46%) ⬆️
utilities 36.56% <0.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...org/apache/hudi/hive/ddl/HiveQueryDDLExecutor.java 82.00% <73.07%> (+5.18%) ⬆️

... and 152 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@skywalker0618

Copy link
Copy Markdown
Contributor Author

Hi @danny0405 do you mind taking a look at this fix? Found this issue during the stress testing in Uber and fixed it by this change. Thanks!

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR closes the SessionState that HiveQueryDDLExecutor starts per sync, releasing its scratch directories and per-session objects that previously accumulated for the life of a long-running JVM. The session-close ordering (after Driver teardown, in a finally), the widened catch in the constructor error path, and the SessionState re-assertion in updateHiveSQLs all trace through correctly, and the new tests cover the ordering and failure paths. No issues flagged from this automated pass — a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

hiveDriver.close();
} finally {
destroyQuietly(hiveDriver);
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Bind the owned session during Driver teardown

Driver.close() and destroy() consult SessionState.get()—the former clears the current session’s lineage state, and the latter can obtain its transaction manager when releasing locks. If executor B was constructed or run after executor A, calling A.close() here tears down A’s Driver while B’s session is current, potentially clearing or operating on B’s state; A.sessionState.close() then also detaches B. Capture the previous session, bind sessionState before Driver/session teardown, and restore the previous session afterward.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, thanks!

// thread writes: the one constructed most recently wins, and one that is closed clears it.
// Re-assert ours, as Hive documents a thread running several sessions must, so these
// statements run under the session that owns hiveDriver.
SessionState.setCurrentSessionState(sessionState);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Restore the previous thread-local session

This assignment remains in the thread local after updateHiveSQLs returns. With interleaved executors—or an embedding application that already owns a Hive session—subsequent Hive work silently runs under this executor’s database, configuration, and transaction state. Make the binding scoped: save SessionState.get(), bind this executor for the Driver calls, then restore the saved session (or detach when it was null) in a finally block.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as well

Driver.close() and Driver.destroy() both act on whichever SessionState the
thread currently holds, so an executor closed while a later-constructed
executor's session is attached would clear that session's lineage state,
release locks through its transaction manager, and then detach it.

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! The PR closes the SessionState that HiveQueryDDLExecutor starts, adding a closeDriverAndSession() helper that binds the owned session during Driver teardown and restores the previously-attached session afterward, plus a session re-assertion in updateHiveSQLs. I traced the teardown ordering and the thread-local session save/restore path; the remaining concerns (binding the owned session during Driver teardown, and restoring the previous thread-local in updateHiveSQLs) are already captured in the prior-round comments from @danny0405, so nothing new to add from this automated pass. Please take a look at any inline comments, and this should be ready for a Hudi committer or PMC member to take it from here.

Leaving this executor's session in the thread local made the caller's later
Hive work run under our database, configuration and transaction state, which
is wrong for an interleaved executor and for an application that embeds the
sync and owns its own session.
@github-actions github-actions Bot added size:L PR with lines of changes in (300, 1000] and removed size:M PR with lines of changes in (100, 300] labels Sep 2, 2026
@hudi-bot

hudi-bot commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build

if (previousSession != null) {
SessionState.setCurrentSessionState(previousSession);
} else {
SessionState.detachSession();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Restore the caller context class loader too

Hive SessionState.setCurrentSessionState changes Thread.currentThread().getContextClassLoader() to the session conf loader, while detachSession() only removes the Hive thread-local. Thus, when previousSession is null, this finally block leaves the caller thread using this executor class loader; closeDriverAndSession has the same problem and can leave the thread pointing at a loader that SessionState.close() just closed. That both leaks the session loader and can break later class loading on long-running worker threads. Please capture the original context class loader before binding and restore it in the finally path, including the null-session case, with an assertion in the lifecycle tests.

}
destroyQuietly(this.hiveDriver);
try {
closeDriverAndSession();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve the session displaced by construction

SessionState.start(this.sessionState) overwrites any session already attached to the caller thread, but the constructor never saves or restores that session. In the real lifecycle, updateHiveSQLs therefore observes this executor session as previousSession and restores it, and this teardown observes the same session, closes/detaches it, and leaves the caller with no session; the original embedding session is never recovered. The new tests miss this because executorWith bypasses the constructor and manually attaches otherSession afterward. Now that each SQL/teardown operation explicitly binds the owned session, could the constructor capture the pre-existing session before start, restore it after initialization and on failure, and add a constructor-path regression test?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L PR with lines of changes in (300, 1000]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants