Skip to content

[improve][broker] PIP-473 P5.4: v4/v5 transaction coordinator coexistence + enable v5 by default#25945

Merged
merlimat merged 4 commits into
apache:masterfrom
merlimat:mmerli/pip-473-tc-coexist
Jun 7, 2026
Merged

[improve][broker] PIP-473 P5.4: v4/v5 transaction coordinator coexistence + enable v5 by default#25945
merlimat merged 4 commits into
apache:masterfrom
merlimat:mmerli/pip-473-tc-coexist

Conversation

@merlimat

@merlimat merlimat commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Motivation

The metadata-driven (v5, PIP-473) transaction coordinator and the legacy (v4) coordinator must serve their own clients on the same cluster, and the v5 coordinator should be enabled by default. Before this change, the broker chose the coordinator from a global config flag, so enabling the v5 TC routed every client — including v4 SDK clients on persistent:// topics — to the v5 coordinator. That breaks v4 transactions: the v5 TC notifies participants via metadata-store events, which the legacy transaction buffer / pending-ack store don't consume.

This PR routes coordination by client/SDK kind instead of broker capability, then flips the defaults so v5 is on out of the box while v4 keeps working.

Modifications

Routing by SDK kind

  • New internal ClientConfigurationData.scalableTransactions flag — not on the public ClientBuilder. PulsarClientBuilderV5 sets it true; the v4 builder never does.
  • TransactionCoordinatorClientImpl.selectDiscovery() routes by that flag: v5 SDK → metadata-store coordinator (assignment watch), v4 SDK → legacy assign-topic coordinator. A v5 SDK on an http:// service URL now fails clearly rather than silently downgrading.
  • Wire: an optional scalable flag on CommandTcClientConnectRequest, CommandNewTxn, CommandEndTxn, CommandAddPartitionToTxn, CommandAddSubscriptionToTxn (additive, defaults false). ServerCnx routes each command to the v5 TC when scalable=true (rejecting it if the v5 TC isn't enabled), else to the legacy TC; verifyTxnOwnership is parameterized by the same flag.

No shared TxnID namespace is needed: the two coordinators keep separate state, and a client only ever talks to one kind, so a bare TxnID is never looked up across both.

Default flip (P5.4)

  • transactionCoordinatorScalableTopicsEnabledtrue
  • transactionBufferProviderClassNameDispatchingTransactionBufferProvider
  • transactionPendingAckStoreProviderClassNameDispatchingTransactionPendingAckStoreProvider

The dispatching providers route segment:// topics to the metadata-driven implementations and persistent:// / topic:// to the legacy ones by TopicName.isSegment(), so v4 topics get the unchanged legacy buffer/ack-store.

Verification

  • Unit: CommandsScalableTxnFlagTest round-trips the scalable flag on all five commands (present + default-false).
  • Integration (TcMetadataDiscoveryTest, verified locally against a freshly built docker image — all pass): on a v5-enabled cluster, a v5 SDK client drives the transaction lifecycle and survives a leader-broker kill via the metadata coordinator, while a v4 SDK client runs a transaction on a persistent:// topic via the legacy coordinator — concurrently. Asserts the v4 client does not use metadata-store discovery.

Notes for reviewers

  • This adds client-broker wire-protocol surface (the additive scalable flag); flagging for protocol review.
  • The enable-by-default flip means transaction-enabled broker tests now also start the v5 TC; CI exercises that breadth.

merlimat added 2 commits June 5, 2026 08:39
…d v5 coexist

Enabling the scalable-topics (v5) transaction coordinator previously sent ALL
clients' NEW_TXN/END_TXN/etc. to it via a global config check, which would break
v4 transactions on persistent:// topics (the v5 TC notifies participants via
metadata-store events, not the legacy RPC commit a v4 TopicTransactionBuffer
waits for). Route per client instead so both coordinators serve their own clients
on the same cluster.

- Add an optional `scalable` flag to CommandTcClientConnectRequest, CommandNewTxn,
  CommandEndTxn, CommandAddPartitionToTxn, CommandAddSubscriptionToTxn (additive,
  defaults false) + Commands builder overloads.
- ServerCnx routes each command to the v5 TC when scalable=true (rejecting it if
  the v5 TC isn't enabled), else to the legacy TC — replacing the global-config
  branch. verifyTxnOwnership is parameterized by the same flag.
- The v5 client (metadata-store discovery: handlers built with a leader URI) sets
  scalable=true on every txn command; the v4/assign-topic path leaves it unset.
  No shared txnId namespace needed — each TC keeps its own state, and a client
  only ever talks to one TC kind, so a bare TxnID is never looked up across both.

Tests: Commands round-trip of the flag on all five commands; integration test
legacyTransactionStillWorksWhenScalableTcEnabled runs a v4 transaction on a
persistent topic against a v5-enabled cluster. Verified locally against a built
docker image — all three TcMetadataDiscoveryTest methods pass.
…actions by default

Routes transaction coordination by client/SDK kind rather than broker capability,
then flips the defaults so the scalable-topics (v5) coordinator is on out of the box
while v4 transactions keep working — both SDKs coexist on one cluster.

- ClientConfigurationData.scalableTransactions: internal flag (not on the public
  ClientBuilder). PulsarClientBuilderV5 sets it true; the v4 builder never does.
- selectDiscovery() routes by that flag: v5 SDK -> metadata-store coordinator
  (assignment watch), v4 SDK -> legacy assign-topic coordinator. Previously it keyed
  off the broker's supports_tc_metadata_discovery feature flag, which once the default
  was enabled would re-route v4 clients to the v5 TC and break their transactions
  (the v5 TC notifies participants via metadata-store events the legacy buffer /
  pending-ack store don't consume). A v5 SDK on an http:// URL now fails clearly
  instead of silently downgrading.
- Defaults flipped: transactionCoordinatorScalableTopicsEnabled=true,
  transactionBufferProviderClassName=DispatchingTransactionBufferProvider,
  transactionPendingAckStoreProviderClassName=DispatchingTransactionPendingAckStoreProvider.
  The dispatching providers route segment:// topics to the metadata-driven
  implementations and persistent:// to the legacy ones, so v4 topics are unaffected.

Integration test (verified locally on a built docker image, all PASS): a v5 SDK
client drives the lifecycle + survives a leader-broker kill via the metadata
coordinator, while a v4 SDK client runs a transaction on a persistent:// topic via
the legacy coordinator — concurrently, on the same v5-enabled cluster. Asserts the
v4 client does NOT use metadata discovery.
@merlimat merlimat requested a review from lhotari June 5, 2026 17:17

@lhotari lhotari left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

merlimat added 2 commits June 6, 2026 20:21
… broker

Motivation:
The P5.4 default flip activates MetadataTransactionBuffer and
MetadataPendingAckStore in a live broker for the first time (previously
only exercised against mocks), exposing two latent bugs that broke v5
transactions on scalable topics, plus two perf-tool gaps on that path.

Modifications:
- MetadataPendingAckStore.replayAsync: on recovery, flip the handle to
  Ready, complete the pending-ack handle future, and drain queued ack
  requests (mirroring the legacy MLPendingAckStore). Without completing
  that future, PersistentSubscription.addConsumer blocked forever and
  every segment-topic subscribe timed out.
- MetadataTransactionBuffer.recomputeMaxReadPositionLocked: once no open
  txns remain, advance the max read position to the last confirmed entry
  (it was pinned at the last non-transactional publish, so committed
  transactional data never became visible). Hold while an open txn's
  positions aren't tracked yet (in-flight append); drop the now-unused
  lastDispatchable field.
- testclient: open the initial transaction with a bounded retry while the
  transaction-coordinator handler connects (avoids
  MetaStoreHandlerNotReadyException), and let PerformanceConsumer terminate
  on idle receives so an async commit landing after the last consumed
  message no longer hangs it.
- Convert PerformanceTransactionTest and Oauth2PerformanceTransactionTest
  to scalable topics + the v5 SDK verification client; Performance
  Transaction gains --scalable.

Assisted-by: Claude Code
…connecting

Motivation:
With the scalable-topics (v5) coordinator, the transaction-meta-store
handler resolves its partition leader asynchronously via the assignment
watch, so it sits in the Uninitialized state briefly after the client is
built. A first newTransaction in that window failed fast with
MetaStoreHandlerNotReadyException ("not connected"), which broke
V5ClientLifecycleTest.testNewTransactionAsyncReturnsOpenTransaction and
forced perf tools to retry.

Modifications:
- TransactionMetaStoreHandler.checkStateAndSendRequest: treat Uninitialized
  like Connecting — leave the op queued in pendingRequests so it is retried
  from connectionOpened once the handler is Ready, instead of failing fast.
  The operation-timeout sweep still fails the op if the connection never
  comes. Failed/Closing/Closed keep their existing fail-fast behavior, so
  TransactionClientConnectTest's Closed-state assertion is unaffected.

Assisted-by: Claude Code
@merlimat merlimat merged commit c670571 into apache:master Jun 7, 2026
43 checks passed
@lhotari lhotari added this to the 5.0.0-M1 milestone Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants