[improve][broker] PIP-473 P5.3: metadata-store TC leader election + assignment watch - #25929
Conversation
…ssignment watch Motivation: the scalable-topics transaction coordinator (PIP-473) reused the transaction_coordinator_assign topic for both leader election and client discovery, tying TC coordination liveness to the topic/bundle/load-balancer machinery even though the TC keeps no state in that topic. This makes election depend only on the metadata store. Modifications: - Broker election: per-partition LeaderElection over /txn/tc/leader/<N> via the existing CoordinationService; a degree-of-parallelism config (transactionCoordinatorScalableTopicsParallelism, default 16) replaces the assign-topic partition count. TransactionCoordinatorV5.isLeaderFor(int) now gates both client-connect and the timeout/GC sweeps. - Discovery: new CommandWatchTcAssignments / ...Update / ...Close wire commands. The client opens one watch and the broker pushes the full partition->leader snapshot, re-pushing it on any leadership change (no point lookup, no diff/ hash — the map is small and changes rarely). Gated by a new supports_tc_metadata_discovery feature flag. - Client: TcDiscovery strategy selects WatchTcAssignmentsDiscovery (new path) when the broker advertises the flag, else the existing assign-topic flow (still the v4 path). Handlers connect directly to each coordinator's elected leader and retarget when a snapshot moves leadership. - Tests: TransactionCoordinatorV5Test election/leadership coverage; Commands round-trip; multi-broker docker integration test (TcMetadataDiscoveryTest) covering the transaction lifecycle over the watch and survival of a leader broker failure. The assign-topic remains in place as the v4 / fallback discovery surface during the deprecation window; defaults are unchanged (flip lands in P5.4).
startAsync() probed the broker's supports_tc_metadata_discovery flag via getConnectionToServiceUrl(), which throws InvalidServiceURL for http:// service URLs (it requires binary-proto lookup). That broke every transaction client configured with an HTTP service URL — the prior code used getPartitionedTopicMetadata(), which works over both HTTP and binary. selectDiscovery() now short-circuits to the assign-topic flow when the lookup service isn't binary-proto (HTTP URL), since the metadata-store assignment watch needs a binary connection anyway. Only probe the feature flag when binary lookup is available.
…n in tests Two follow-up fixes after the discovery refactor: - selectDiscovery() now falls back to the assign-topic flow if the feature-flag probe connection fails. Fixes testClientStartWithRetry, which uses a multi-host service URL with a deliberately-bad endpoint: the single probe connection has no cross-host retry, whereas the assign-topic lookup does. Falling back is always safe (the assign topic still works against v5 brokers during the deprecation window). - The handler array moved from TransactionCoordinatorClientImpl into the TcDiscovery strategies, which broke tests reflecting on the private "handlers" field. Add TcDiscovery.handlers() + a @VisibleForTesting TransactionCoordinatorClientImpl.getHandlers() accessor and switch TransactionClientConnectTest to it (no reflection into private state, per the repo testing guideline).
…REFIX_ The scalable-topics TC settings were passed as bare env vars, but apply-config-from-env.py only applies bare names to keys already present in broker.conf. transactionCoordinatorScalableTopicsEnabled / ...Parallelism aren't in broker.conf, so they were silently ignored: the v5 TC never started and the cluster ran 16 legacy assign-topic coordinators (assertion saw 16, expected 4). Prefix both new keys with PULSAR_PREFIX_ so they're appended as new config. transactionCoordinatorEnabled stays bare (it is in broker.conf). Also make the lifecycle assertion await-based: at startup a partition can be briefly mid-election and absent from the first assignment snapshot, so loop until every coordinator has minted a txn rather than assuming a fixed count.
Is is possible to modify |
|
These are the findings of a local Claude Code review. Please check whether these need to be addressed. 1. [BUG] Follower-broker snapshot push races the leader-value cache; no re-push → client stuck on a dead leader indefinitely
Trace for a 3+ broker cluster (A leads partition N; B and C follow; the client's watch is on C):
The PR description's claim that the client parks a mid-election partition "until a later snapshot fills it in" is therefore not upheld — the later snapshot may never come. The 2-broker integration test structurally masks this ( Suggested fixes: build the snapshot from 2. [BUG] Two paths where the client watch dies silently with no reconnect
Both leave all handlers pinned to stale leaders with no recovery. Combined with finding 1, eventual consistency of the assignment map is not guaranteed. 3. [BUG] Initial watch open is one-shot — a transient failure hard-fails transaction client startup
The probe ( 4. [QUALITY]
|
lhotari
left a comment
There was a problem hiding this comment.
LGTM, however please check the local Claude Code review findings about the possible bugs.
Fixes from lhotari's review on apache#25929: - Snapshot completeness (BUG): buildAssignmentsSnapshot now uses the async LeaderElection.getLeaderValue() (loads from the metadata store on a cache miss) instead of the cache-only getLeaderValueIfPresent(), so a follower that just transitioned for a partition doesn't omit it from the snapshot. ServerCnx schedules a short re-push when a snapshot is incomplete (a partition still mid-election) or fails to build, so the client converges without an external trigger. - Client watch recovery (BUG): WatchTcAssignmentsDiscovery now reconnects on every post-initial death path — a reconnected broker lacking the feature flag, and post-initial onError (ClientCnx has already removed the session, so nothing else recovers it). Previously these froze the watch on a stale snapshot. - Initial-open retry (BUG): the initial watch open retries retryable failures up to the lookup deadline instead of hard-failing transaction-client startup (the probe and the watch can land on different brokers). - TLS-only URL (NPE): HandlerState.setRedirectedClusterURI throws a clear URISyntaxException instead of new URI(null) NPE when no usable URL is selected; onSnapshot isolates per-partition failures so one bad leader URL can't tear down the watch. - Parallelism (config): minValue=1; the value is persisted cluster-wide at /txn/tc/parallelism on first start and every broker fails fast on mismatch. Documented the fixed-at-bring-up constraint (incl. aborted-txn retention). - Integration test now asserts the metadata-discovery path was actually selected, so a watch-path regression can't pass via the silent assign-topic fallback. - handlers() reads the field into a local to avoid a concurrent-close NPE.
|
Thanks for the very thorough review — went through every finding against the code; all were legitimate. Addressed in 6acca7f. On the design question (can Findings:
All affected unit tests + checkstyle pass locally; the integration test compiles and is wired into the TRANSACTION suite. |
…oker-paired connection The integration test's new assertion (isUsingMetadataDiscovery) caught that a client connecting through the proxy fell back to assign-topic discovery: - The proxy's handleBrokerConnected forwarded only supportsTopicWatchers and supportsScalableTopics from the broker's CONNECTED, dropping the new supportsTcMetadataDiscovery flag. Forward it too. - selectDiscovery() probed getConnectionToServiceUrl(), which through a proxy returns the proxy's lookup-handshake CONNECTED (no broker flags) rather than a broker's. Probe getAnyBrokerProxyConnection() instead — it pairs to an actual broker (the same connection the watch uses), so the forwarded feature flags reflect the broker. On failure it still falls back to assign-topic, so multi-host / transient cases are safe.
…proxied The watch-mode TransactionMetaStoreHandler connected directly to each leader's advertised broker URL, which is unreachable when the client talks through a proxy (the leader address isn't a proxiable target on its own) — causing newTransaction to time out with "connection: null". Mirror the scalable-topics client's proven pattern: the watch connection reports whether it's proxied (ClientCnx.isProxied()); when it is, dial the leader through the proxy (logical=leader, physical=proxy) via the new ConnectionHandler.grabCnx(uri, useProxy), otherwise connect directly. The handler now holds (leaderUri, useProxy), reconnects to the leader on connection loss (passing the leader URI rather than falling back to the service URL), and WatchTcAssignmentsDiscovery derives useProxy from the watch connection and selects the TLS/plaintext leader URL.
…ver IT robust Verified locally against a built docker image (2-broker + proxy cluster), two consecutive green runs of TcMetadataDiscoveryTest (lifecycle + failover). - ConnectionHandler.reconnectLater() called the no-arg grabCnx() on a failed connection attempt, falling back to the service URL instead of re-dialling the intended broker. For v5 TC metadata-store discovery the per-coordinator handler must keep targeting its elected leader, so remember the explicit target (explicitHostURI, set by grabCnx(uri,useProxy) and connectionClosed(...hostUrl)) and reuse it on the error-retry path. - The integration test's runTxnOnEveryCoordinator spun a tight no-wait retry loop and threw the moment a coordinator's handler was still connecting (handlers connect asynchronously; after a failover one is briefly mid-reconnect). Replace it with a bounded Awaitility wait that ignores transient MetaStoreHandlerNotReady/timeout — the assertion is "every coordinator becomes reachable", not "on the first attempt". The failover case now relies on this shared wait instead of a second nested await.
| // Re-dial the explicit leader target (v5 TC discovery) if set; otherwise the normal | ||
| // lookup path. Without this, a first-attempt failure during failover would fall back | ||
| // to the service URL and never reach the partition's new leader. | ||
| URI target = explicitHostURI; | ||
| if (target != null) { | ||
| grabCnx(Optional.of(target)); |
There was a problem hiding this comment.
just wondering what happens when the target broker goes down? does the retry loop exit?
Motivation
The scalable-topics transaction coordinator (PIP-473) so far reused the
transaction_coordinator_assignpartitioned topic for two jobs: per-partition leader election and client discovery. Both route through the topic / namespace / bundle / load-balancer machinery, so TC coordination liveness is entangled with that machinery — even though the TC keeps no state in the assign-topic (its ledgers are empty; it's purely an ownership token).This PR makes the scalable-topics TC's leader election depend on the metadata store only, and replaces the topic-lookup-based discovery with a dedicated assignment watch. The coordinator now rests directly on the metadata store it already hard-depends on for every header read/write, instead of on a layer above it.
Modifications
Broker election
LeaderElection<TcLeader>over/txn/tc/leader/<N>via the existingCoordinationService.transactionCoordinatorScalableTopicsParallelism(default 16) — the degree of coordinator parallelism; replaces the assign-topic partition count for the v5 path.TransactionCoordinatorV5.isLeaderFor(int)now gates both client-connect and the timeout/GC sweeps (was the assign-topic ownership check).Discovery (new wire commands)
CommandWatchTcAssignments/CommandWatchTcAssignmentsUpdate/CommandWatchTcAssignmentsClose+TcAssignment/TcAssignmentsSnapshot.partition → leadermap and re-pushes the full snapshot on every leadership change. No point lookup, no diff/hash — the map is bounded (parallelism, ~16) and changes rarely, so always sending the whole snapshot is simpler and removes a class of apply-ordering/drift bugs.FeatureFlags.supports_tc_metadata_discovery.Client
TcDiscoverystrategy:WatchTcAssignmentsDiscovery(new path) when the broker advertises the feature flag, elseAssignTopicTcDiscovery(the existing flow — still the v4 path).TransactionMetaStoreHandlercan connect directly to a coordinator's elected leader broker and retarget when a snapshot moves leadership.Backward compatibility
Purely additive on the wire. The assign-topic remains the v4 / fallback discovery surface during the deprecation window:
TC_CLIENT_CONNECTstill works);Defaults are unchanged — the scalable-topics TC stays off; the default flip lands in P5.4.
Tests
TransactionCoordinatorV5Test— election + leadership coverage (21 cases).CommandsTcAssignmentsTest— wire round-trip (5 cases).TcMetadataDiscoveryTest— multi-broker docker integration: transaction lifecycle over the assignment watch across all coordinator partitions, and survival of a leader-broker failure (re-election → watch refresh → handler retarget). Wired into theTRANSACTIONCI group.Scope note: the integration tests exercise the transaction lifecycle (newTransaction / commit / abort) over the discovered connections — the full surface the new client discovery path drives. A data-in-transaction e2e (produce/ack inside a txn) additionally needs the scalable-topic buffer + pending-ack providers and
segment://topics, which land with P5.4 / P6.