[fix][broker] Fix tableview divergence in ServiceUnitStateTableViewSyncer causing flaky tests#25946
Merged
lhotari merged 3 commits intoJun 7, 2026
Conversation
…ncer causing flaky tests
### Motivation
ExtensibleLoadManagerImplTest.testLoadBalancerServiceUnitTableViewSyncer hangs for
the full 300s TestNG suite-default timeout in CI (both attempts of run 27011985664)
and then cascades into a later test's @BeforeMethod initializeState() failing with
HTTP 500 ("The producer ... can not send message to the topic
persistent://pulsar/system/loadbalancer-service-unit-state within given timeout").
Investigation found two production bugs in ServiceUnitStateTableViewSyncer that make
waitUntilSynced() spin forever on entrySet-size divergence:
1. Tombstone asymmetry: a deletion is delivered to the syncer's put-based tail
listeners as a null value. ServiceUnitStateMetadataStoreTableViewImpl.put() is
@nonnull on the value, so the resulting NPE is silently swallowed by the table
view dispatchers and the deletion never propagates to the metadata-store view.
2. Existing-vs-tail listener gap: channel updates that land between the
existing-items copy and the registration of the tail listeners in syncTailItems()
are replayed to the freshly started views as existing items, which are wired to a
dummy listener, so they never propagate. This is near-deterministic in the
MetadataStoreToSystemTopicSyncer direction because closing the previous reader
triggers a re-assignment of the channel-topic bundle itself into that gap
(reproduced locally with the same sizes-off-by-one signature as CI).
Additionally, the role-churning tests (testRoleChange, testRoleChangeIdempotency,
testHandleNoChannelOwner) can leave the channel-topic bundle unserved ("Namespace
bundle (pulsar/system/0x00000000_0xffffffff) ... not served by this instance"), so
the next test's namespace unload cannot publish its Releasing event and fails with
HTTP 500 or hangs, until the background monitor task (120s interval) reconciles the
brokers' roles with the channel ownership (reproduced locally and in personal CI).
### Modifications
Production (ServiceUnitStateTableViewSyncer):
- Route null tail items to delete() instead of put(), and log failed sync futures.
- Make waitUntilSynced() reconcile periodically while diverged: copy source-only
items to the destination (direction-aware via dst.isMetadataStoreBased(); raw
metadata-store puts to avoid conflict-out) and remove destination-only items that
predate the sync phase and are re-confirmed absent from the source. Batched at
MAX_CONCURRENT_SYNC_COUNT; failures are logged and retried on the next pass;
InterruptedException propagates.
- Make the sync-wait budget an overridable field with a @VisibleForTesting setter
and add throttled debug progress logging.
Tests:
- testLoadBalancerServiceUnitTableViewSyncer: cap at 240s (below the 300s suite
default), shrink the sync-wait budget to 30s before the first start(), retry
monitor() inside the activation await, and tear the syncer down in a finally
block so a failure cannot poison later tests.
- awaitChannelOwnerStable(): after the leader-election-churning tests, drive
monitor() to reconcile roles with channel ownership (the eager equivalent of the
120s background monitor task) and probe that the channel topic is looked up and
served, before yielding to the next test (method timeouts raised to 60s).
- initializeState(): drive monitor() and retry a bounded unloadAsync (15s per
attempt) for up to 120s before failing, healing a transiently unserved
channel-topic bundle instead of failing the whole suite.
Assisted-by: Claude Code (Opus 4.8)
void-ptr974
approved these changes
Jun 7, 2026
Contributor
11 tasks
lhotari
added a commit
that referenced
this pull request
Jun 10, 2026
…ncer causing flaky tests (#25946)
lhotari
added a commit
that referenced
this pull request
Jun 10, 2026
…ncer causing flaky tests (#25946)
priyanshu-ctds
pushed a commit
to datastax/pulsar
that referenced
this pull request
Jun 15, 2026
…ncer causing flaky tests (apache#25946) (cherry picked from commit 2cf0aa7)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #24357
Motivation
ExtensibleLoadManagerImplTest.testLoadBalancerServiceUnitTableViewSyncerhung for the full 300s TestNG suite-default timeout in both attempts of CI run 27011985664, then cascaded into a later test's@BeforeMethod initializeState()failing with HTTP 500 (The producer ... can not send message to the topic persistent://pulsar/system/loadbalancer-service-unit-state within given timeout), skippingtestHandleNoChannelOwner.Investigation (details in this comment on #24357) found two production bugs in
ServiceUnitStateTableViewSyncerthat makewaitUntilSynced()spin forever on entrySet-size divergence:Tombstone asymmetry — a deletion is delivered to the syncer's put-based tail listeners as a
nullvalue.ServiceUnitStateMetadataStoreTableViewImpl.put()is@NonNullon the value, so the resulting NPE is silently swallowed by the table-view dispatchers and the deletion never propagates to the metadata-store view. (The reverse direction works only by accident: the system-topic view'sdelete(key)isput(key, null).)Existing-vs-tail listener gap — channel updates that land between the existing-items copy and the registration of the tail listeners in
syncTailItems()are replayed to the freshly started views as existing items, which are wired to a no-op listener, so they never propagate. This is near-deterministic in theMetadataStoreToSystemTopicSyncerdirection because closing the previous reader triggers a re-assignment of the channel-topic bundle itself into that gap (reproduced locally with the same sizes-off-by-one signature as CI, e.g.MetadataStoreTableView.size: 44, SystemTopicTableView.size: 43).Additionally, the leader-election-churning tests (
testRoleChange,testRoleChangeIdempotency,testHandleNoChannelOwner) can leave thepulsar/system/0x00000000_0xffffffffbundle unserved (Namespace bundle ... not served by this instance), so the next test's namespace unload cannot publish itsReleasingevent and fails with HTTP 500 or hangs server-side. Nothing heals that state until the background monitor task (120s interval) reconciles the brokers' roles with the channel ownership — passive waiting/retrying alone cannot recover (reproduced locally and in personal CI).Modifications
Production —
ServiceUnitStateTableViewSyncer(a migration feature; no interface, call-site, or config changes):nulltail items todelete()instead ofput(), and log failed sync futures at WARN (previously silently discarded by the dispatchers).waitUntilSynced()reconcile periodically while diverged: copy source-only items to the destination (direction-aware viadst.isMetadataStoreBased(); raw metadata-store writes to avoid items being conflicted out, matchingsyncExistingItemsToMetadataStore) and remove destination-only items that predate the sync phase and are re-confirmed absent from the source (so a concurrent fresh write to the destination is never discarded). Batched atMAX_CONCURRENT_SYNC_COUNT; failures are logged and retried on the next pass;InterruptedExceptionpropagates.@VisibleForTestingsetter (default unchanged at 300s) and add throttled DEBUG progress logging.NOOP_CONSUMER,maybeWaitCompletion()andwriteToMetadataStore()helpers.Tests:
testLoadBalancerServiceUnitTableViewSyncer: cap at 240s (below the 300s suite default so a hang fails fast and is retried instead of consuming the whole slot), shrink the sync-wait budget to 30s before the firststart(), drivemonitor()inside the activation await, and tear the syncer down in afinallyblock so a failure cannot poison later tests.awaitChannelOwnerStable()helper: after the leader-election-churning tests, drivemonitor()to reconcile roles with channel ownership (the eager equivalent of the 120s background monitor task) and probe that the channel topic is looked up and served before yielding to the next test.testHandleNoChannelOwneradditionally forces a deterministic leader in its cleanup, since a body failure mid-churn can otherwise leave leadership flapping between the brokers. Method timeouts for the churning tests raised from 30s to 60s.initializeState(): drivemonitor()and retry a boundedunloadAsync(15s per attempt) for up to 120s before failing loudly, healing a transiently unserved channel-topic bundle instead of failing the whole suite.Verifying this change
This change is already covered by existing tests, such as
ExtensibleLoadManagerImplTest(both@Factoryparametrizations exercise both syncer directions intestLoadBalancerServiceUnitTableViewSyncer).Verification performed:
MetadataStoreToSystemTopicSyncerdirection locally (twice, same signature as both CI attempts). After the fix, the syncer test completes in seconds and repeated full-class runs pass (102 tests, 0 failures), including runs where the residual pre-existing first-attempt flakes intestRoleChange/testHandleNoChannelOwnerfired and were contained by the retry without cascading.Pulsar CI Flaky(BROKER_FLAKY group) passed twice consecutively ([fix][broker] Fix tableview divergence in ServiceUnitStateTableViewSyncer causing flaky tests lhotari/pulsar#225); the only full-pipeline failure was an unrelatedGeoReplicationTestintegration flake.Does this pull request potentially affect one of the following parts:
If the box was checked, please highlight the changes