Skip to content

fix(lock): stop guarding the filesystem lock with an interned string literal - #19486

Open
rangareddy wants to merge 2 commits into
apache:masterfrom
rangareddy:fix-16943-lock-monitor
Open

fix(lock): stop guarding the filesystem lock with an interned string literal#19486
rangareddy wants to merge 2 commits into
apache:masterfrom
rangareddy:fix-16943-lock-monitor

Conversation

@rangareddy

Copy link
Copy Markdown
Collaborator

Describe the issue this Pull Request addresses

Part of #16943 (HUDI-9254), "Ensure lock providers are thread safe". That issue covers five providers and
the close-during-unlock semantics; this PR fixes one concrete, self-contained defect in
FileSystemBasedLockProvider and deliberately leaves the rest.
What still needs doing is listed at the
bottom.

tryLock, unlock and close all guarded their lock-file operations with:

private static final String LOCK_FILE_NAME = "lock";
...
synchronized (LOCK_FILE_NAME) { ... }

LOCK_FILE_NAME is a compile-time String constant, so it is interned. The monitor is therefore the
JVM-wide canonical "lock" instance, shared with every other "lock" literal in the process — Hudi's,
Spark's, or the user's. Any code that happens to synchronize on that literal blocks Hudi from acquiring or
releasing its lock, and Hudi blocks it in turn.

This is not hypothetical. FileSystemBasedLockProviderTestClass, in this repo, declares:

private static final String LOCK = "lock";   // same interned object as LOCK_FILE_NAME

and synchronizes on it. Two classes with no relationship share one monitor purely because they chose the
same string.

The correct form already exists in the codebase — KafkaConnectControlAgent uses
private static final Object LOCK = new Object().

Summary and Changelog

  • Guard on a private LOCK_FILE_MONITOR object. It is kept static, so the mutual-exclusion scope is
    exactly what it was; the only thing that changes is which object is used, and that it can no longer be
    aliased from outside the class.
  • currentOwnerLockInfo becomes volatile. It is written while holding the monitor but read through the
    Lombok @Getter without it — LockManager reads it to report the current lock holder — so the value was
    not guaranteed visible to the reading thread.

Verification

New testAcquisitionIsNotBlockedByTheInternedLockLiteral starts a thread that does nothing but
synchronized ("lock") { … }, standing in for any unrelated code in the JVM, then acquires the lock from
another thread. With the fix it acquires immediately. With synchronized (LOCK_FILE_NAME) restored it
blocks for the full budget and the test fails:

[ERROR] testAcquisitionIsNotBlockedByTheInternedLockLiteral -- Time elapsed: 10.02 s <<< ERROR!
java.util.concurrent.TimeoutException

That 10-second block is the defect, reproduced.

TestFileSystemBasedLockProvider 10 tests green, and the whole org.apache.hudi.client.transaction.**
package 256 tests green — worth running in full here because FileSystemBasedLockProviderTestClass aliased
this monitor, so anything that had come to depend on the accidental coupling would show up there.
checkstyle:check and apache-rat:check clean.

Still open under HUDI-9254, not in this PR

  • The monitor is static, so all FileSystemBasedLockProvider instances in a JVM serialise even for
    different tables and different lock files. Narrowing it to per-instance would be correct only if
    acquireLock is genuinely atomic for every supported scheme — the constructor does reject schemes
    without atomic creation, so it looks safe, but that is a behaviour change in a lock provider and wants a
    maintainer's call rather than mine.
  • The other providers. BaseZookeeperBasedLockProvider and HiveMetastoreBasedLockProvider have since
    had their mutable fields made volatile, which fixes visibility but not the check-then-act sequences
    around them (for example ZK's ValidationUtils.checkArgument(this.lock == null, …) followed by creating
    and assigning a new mutex). DynamoDBBasedLockProvider still holds its lock item as instance state.
  • The close-during-unlock semantics the Jira calls out, which is the part that actually decides whether
    sharing one provider across threads is supported at all.

I would rather land the unambiguous fix than bundle it with changes that need a design decision. Happy to
follow up on any of the above.

Impact

No API, config or table format change. For a JVM where nothing else synchronizes on "lock", behaviour is
identical. Where something does, Hudi's lock operations stop being blocked by it.

Risk Level

low — one monitor object swapped for a private one at the same scope, plus a volatile. The scope is
deliberately unchanged, and the regression surface is covered by the full transaction/lock package.

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
  • CI passes on my PR

…literal

Part of HUDI-9254. FileSystemBasedLockProvider guarded tryLock, unlock and close with
synchronized (LOCK_FILE_NAME), where LOCK_FILE_NAME is the compile-time constant
"lock". String constants are interned, so that monitor is shared JVM-wide with every
other "lock" literal, and any unrelated code holding it blocks lock acquisition
outright. This is not hypothetical: FileSystemBasedLockProviderTestClass in this repo
declares its own static final String LOCK = "lock" and therefore contends on exactly
the same object. KafkaConnectControlAgent already uses the correct form,
private static final Object LOCK = new Object().

Guard on a private object instead. It is kept static, so the mutual-exclusion scope
is unchanged and this is purely about which object is used.

currentOwnerLockInfo is also made volatile: it is written while holding the monitor
but read through the Lombok-generated getter without it, so the value was not
guaranteed visible to the thread reading it.

Not addressed here, and still open under HUDI-9254: the monitor being static
serialises unrelated tables in one JVM, the other providers' instance state, and the
close-during-unlock semantics the Jira describes.

@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 replaces the interned "lock" string literal used as a synchronization monitor in FileSystemBasedLockProvider with a private static Object, and makes currentOwnerLockInfo volatile so its value is visible to the (unsynchronized) getter readers. I traced all three converted synchronized blocks, the write site (reloadCurrentOwnerLockInfo), and both external readers (LockManager, TimeGeneratorBase) — the static scope is preserved so mutual exclusion is unchanged, and the volatile read is sufficient for these diagnostic single-reads. 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. One small nit on the production Javadoc; otherwise the fix is clean and well-documented.

cc @yihua

@codecov-commenter

codecov-commenter commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.96%. Comparing base (637996c) to head (1556e0d).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19486      +/-   ##
============================================
- Coverage     76.97%   76.96%   -0.01%     
  Complexity    33855    33855              
============================================
  Files          2575     2575              
  Lines        143378   143379       +1     
  Branches      17573    17573              
============================================
- Hits         110362   110355       -7     
- Misses        24755    24762       +7     
- Partials       8261     8262       +1     
Components Coverage Δ
hudi-common 82.27% <ø> (+<0.01%) ⬆️
hudi-client 81.83% <100.00%> (+<0.01%) ⬆️
hudi-flink 83.96% <ø> (ø)
hudi-spark-datasource 75.09% <ø> (-0.01%) ⬇️
hudi-utilities 73.65% <ø> (ø)
hudi-cli 15.32% <ø> (ø)
hudi-hadoop 63.49% <ø> (ø)
hudi-sync 70.87% <ø> (ø)
hudi-io 79.60% <ø> (ø)
hudi-timeline-service 83.44% <ø> (-0.79%) ⬇️
hudi-cloud 64.00% <ø> (ø)
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 49.53% <100.00%> (-0.01%) ⬇️
flink-integration-tests 48.80% <100.00%> (-0.01%) ⬇️
hadoop-mr-java-client 43.71% <100.00%> (-0.07%) ⬇️
integration-tests 13.58% <0.00%> (-0.01%) ⬇️
spark-client-hadoop-common 48.67% <0.00%> (+<0.01%) ⬆️
spark-java-tests 51.34% <100.00%> (+<0.01%) ⬆️
spark-scala-tests 47.40% <100.00%> (+<0.01%) ⬆️
utilities 36.57% <100.00%> (-0.01%) ⬇️

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

Files with missing lines Coverage Δ
.../transaction/lock/FileSystemBasedLockProvider.java 77.17% <100.00%> (+0.25%) ⬆️

... and 11 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.

@github-actions github-actions Bot added the size:S PR with lines of changes in (10, 100] label Aug 3, 2026
Review nit: a production javadoc referencing a test class by name goes stale silently
if the test is renamed. The durable point is the mechanism - any class interning the
same literal shares the monitor - so state that instead. The concrete instance in this
repo stays in the commit message and the PR.

@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 fixes a real concurrency defect in FileSystemBasedLockProvider by replacing the interned "lock" String-literal monitor with a private static Object, and makes currentOwnerLockInfo volatile so its value is visibly published to the unsynchronized getter readers. I traced the monitor usage across close()/tryLock()/unlock() and the write/read paths for currentOwnerLockInfo (writers in reloadCurrentOwnerLockInfo() under the monitor, readers in LockManager and TimeGeneratorBase without it) — the change is well-scoped and consistent, preserves the prior mutual-exclusion scope, and introduces no new lock-ordering or serialization concerns. No issues flagged from this automated pass — a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

@hudi-bot

hudi-bot commented Aug 4, 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

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

Labels

size:S PR with lines of changes in (10, 100]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants