Skip to content

[fix][broker] Fix TableViewLoadDataStoreImpl close deadlock that stalls broker shutdown - #26243

Merged
lhotari merged 1 commit into
apache:masterfrom
lhotari:lh-fix-loaddatastore-deadlock
Jul 25, 2026
Merged

lhotari merged 1 commit into
apache:masterfrom
lhotari:lh-fix-loaddatastore-deadlock

Conversation

@lhotari

@lhotari lhotari commented Jul 25, 2026

Copy link
Copy Markdown
Member

Fixes #24628

Motivation

ExtensibleLoadManagerCloseTest.testCloseAfterLoadingBundles has been flaky in CI for a long time (#24628), with several failure signatures. Analysis of a recent failure (test-report artifact of this run) shows the reported deferGetOwner TimeoutException is only fallout: one invocation earlier, broker.close() hung for 300 seconds until the TestNG method timeout killed the test thread, leaving half-closed zombie brokers that poisoned the subsequent invocation's lookups.

The hang is a deadlock in TableViewLoadDataStoreImpl that can stall broker shutdown in production:

  • All methods of TableViewLoadDataStoreImpl are synchronized, and shutdown()/close() performed a blocking tableView.close() while holding the store monitor (via ExtensibleLoadManagerImpl.disableBroker → stopLoadDataReportTasks).
  • The table view reader's close future can only complete via a task on the Pulsar client's pinned internal executor thread (ConsumerImpl.cleanupAtClose → failPendingReceive).
  • That same executor thread can concurrently be inside a ServiceUnitStateChannel StateChangeListener calling back into the store — BrokerLoadDataReporter.handleEvent → tombstone() → removeAsync(), also synchronized — where it blocks on the store monitor.

The closing thread holds the monitor and waits for the close future; the only thread that can complete that future waits for the monitor: a permanent deadlock. The trigger window is exactly what the test creates: a broker closing while Owned events from a previously closed broker's bundle handoff are still being processed. The captured logs confirm the mechanism: the reader's Closed consumer wire-level log appears immediately, and the blocked executor thread's Restarted producer ... restartReason=object is null fires at the exact moment the 300s interrupt releases the monitor.

The same lock/executor contention also explains the other #24628 signatures (bounded 5s startTableView/startProducer restart failures and cascade failures in later invocations).

Modifications

  • TableViewLoadDataStoreImpl.closeTableView()/closeProducer(): close the table view and producer with non-blocking closeAsync() and log failures, instead of blocking close() under the store monitor. No client close is awaited while holding the monitor anymore, which removes the deadlock. PulsarService closes the shared internal client later during shutdown, so in-flight asynchronous closes cannot leak.
  • Added a deterministic regression test LoadDataStoreTest.testShutdownDoesNotDeadlockWithConcurrentStoreAccess that reproduces the interleaving with a stubbed table view whose close future only completes after another thread has entered a synchronized store method. It deadlocks (30s test timeout) on the previous code and passes with the fix.
  • Added a package-private @VisibleForTesting setTableView accessor used by the test.

Verifying this change

  • Make sure that the change passes the CI checks.

This change added tests and can be verified as follows:

  • LoadDataStoreTest.testShutdownDoesNotDeadlockWithConcurrentStoreAccess deterministically reproduces the deadlock: it fails with a 30s timeout on the previous code (test thread parked in the blocking close holding the store monitor) and passes with the fix.
  • Full LoadDataStoreTest passes (7/7).
  • ExtensibleLoadManagerCloseTest passes locally with all 10 invocationCount runs of testCloseAfterLoadingBundles plus testLookup.

Does this pull request potentially affect one of the following parts:

If the box was checked, please highlight the changes

  • Dependencies (add or upgrade a dependency)
  • The public API
  • The schema
  • The default values of configurations
  • The threading model
  • The binary protocol
  • The REST endpoints
  • The admin CLI options
  • The metrics
  • Anything that affects deployment

…ls broker shutdown

Fixes apache#24628

TableViewLoadDataStoreImpl closed its table view and producer with
blocking close() calls while holding the store monitor (all methods are
synchronized). The table view reader's close future can only complete
via a task on the Pulsar client's internal pinned executor thread
(ConsumerImpl.cleanupAtClose -> failPendingReceive). When that same
thread is concurrently handling a ServiceUnitStateChannel event whose
StateChangeListener calls into the store (BrokerLoadDataReporter
tombstone -> removeAsync, also synchronized), the two threads deadlock:
the closing thread holds the monitor and waits for the close future,
while the executor thread that must complete it waits for the monitor.

In CI this permanently hung PulsarService.close() inside
ExtensibleLoadManagerImpl.disableBroker -> stopLoadDataReportTasks,
making ExtensibleLoadManagerCloseTest.testCloseAfterLoadingBundles time
out after 300s and poisoning subsequent invocations (deferGetOwner
timeouts from half-closed zombie brokers).

Close the producer and table view with non-blocking closeAsync() and
log failures instead, so no client close is awaited while holding the
store monitor. PulsarService closes the shared internal client later in
shutdown, so in-flight closes cannot leak.

Add a deterministic regression test that reproduces the interleaving
with a stubbed table view whose close future only completes after
another thread enters a synchronized store method; it deadlocks on the
old code and passes with the fix.

Assisted-by: Claude Code (claude-fable-5)
@lhotari
lhotari merged commit aa3ec21 into apache:master Jul 25, 2026
44 checks passed
lhotari added a commit that referenced this pull request Jul 26, 2026
lhotari added a commit that referenced this pull request Jul 26, 2026
sandeep-ctds pushed a commit to datastax/pulsar that referenced this pull request Jul 31, 2026
sandeep-ctds pushed a commit to datastax/pulsar that referenced this pull request Jul 31, 2026
sandeep-ctds pushed a commit to datastax/pulsar that referenced this pull request Jul 31, 2026
nodece pushed a commit to ascentstream/pulsar that referenced this pull request Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flaky-test: ExtensibleLoadManagerCloseTest.testCloseAfterLoadingBundles

2 participants