Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3834,6 +3834,22 @@ public double getLoadBalancerBandwidthOutResourceWeight() {
)
private int transactionCoordinatorScalableTopicsGcRetentionSeconds = 900;

@FieldContext(
category = CATEGORY_TRANSACTION,
minValue = 1,
doc = "Degree of parallelism for the scalable-topics transaction coordinator: how many"
+ " independent coordinator instances run across the cluster. Each is"
+ " leader-elected independently in the metadata store and coordinates the"
+ " transactions whose id maps to it. Fixed at cluster bring-up — changing it"
+ " later would strand the coordinator id encoded in existing transaction ids"
+ " (and, because an aborted transaction's records are retained as long as its"
+ " messages are, the value can only be reduced once all transactions created"
+ " under the previous value have been fully cleaned up). All brokers must agree"
+ " on this value; a mismatch is rejected at startup. Only relevant when"
+ " transactionCoordinatorScalableTopicsEnabled = true."
)
private int transactionCoordinatorScalableTopicsParallelism = 16;

@FieldContext(
category = CATEGORY_TRANSACTION,
doc = "Class name for transaction metadata store provider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
import org.apache.pulsar.broker.service.schema.exceptions.InvalidSchemaDataException;
import org.apache.pulsar.broker.topiclistlimit.TopicListMemoryLimiter;
import org.apache.pulsar.broker.topiclistlimit.TopicListSizeResultCache;
import org.apache.pulsar.broker.transaction.coordinator.v5.TransactionCoordinatorV5;
import org.apache.pulsar.broker.web.RestException;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.transaction.TxnID;
Expand Down Expand Up @@ -505,6 +506,10 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
});
scalableTopicsWatchers.clear();

// Same for transaction-coordinator assignment watchers.
tcAssignmentWatchers.values().forEach(this::closeQuietly);
tcAssignmentWatchers.clear();

// Notify the scalable-topic controller that this connection's scalable consumers
// have dropped. The controller marks them disconnected and starts the grace-period
// timer; if they reconnect in time, their assignment is preserved.
Expand Down Expand Up @@ -865,6 +870,13 @@ protected void handleCommandScalableTopicLookup(
ScalableTopicsWatcherSession>
scalableTopicsWatchers = new ConcurrentHashMap<>();

// --- Transaction-coordinator assignment watchers ---
// watchId -> deregistration handle for the listener registered on TransactionCoordinatorV5.
private final ConcurrentHashMap<Long, AutoCloseable> tcAssignmentWatchers = new ConcurrentHashMap<>();
// Delay before re-pushing a TC-assignment snapshot that was incomplete (a partition mid-election)
// or that failed to build, so the client converges without waiting for an external trigger.
private static final long TC_ASSIGNMENTS_REPUSH_DELAY_MS = 1000L;

@Override
protected void handleCommandWatchScalableTopics(
CommandWatchScalableTopics cmd) {
Expand Down Expand Up @@ -965,6 +977,85 @@ protected void handleCommandWatchScalableTopicsClose(
}
}

// --- Transaction-coordinator assignment watch ---

@Override
protected void handleCommandWatchTcAssignments(
org.apache.pulsar.common.api.proto.CommandWatchTcAssignments cmd) {
checkArgument(state == State.Connected);
final long watchId = cmd.getWatchId();
log.debug().attr("watchId", watchId).log("Received WatchTcAssignments");

if (!service.getPulsar().getConfig().isTransactionCoordinatorScalableTopicsEnabled()) {
ctx.writeAndFlush(Commands.newWatchTcAssignmentsError(watchId, ServerError.NotAllowedError,
"Scalable-topics transaction coordinator is disabled on this broker"));
return;
}
TransactionCoordinatorV5 tc = service.getPulsar().getTransactionCoordinatorV5();
if (tc == null) {
ctx.writeAndFlush(Commands.newWatchTcAssignmentsError(watchId, ServerError.ServiceNotReady,
"Transaction coordinator not ready"));
return;
}
// Register a listener that re-pushes the full snapshot on any leadership change, then send
// the initial snapshot. Authz: this is broker-internal coordination, not a per-topic op, so
// an authenticated connection is sufficient (same trust model as TC_CLIENT_CONNECT).
AutoCloseable handle = tc.registerAssignmentChangeListener(
() -> ctx.executor().execute(() -> sendTcAssignmentsSnapshot(watchId, tc)));
AutoCloseable prev = tcAssignmentWatchers.put(watchId, handle);
closeQuietly(prev);
sendTcAssignmentsSnapshot(watchId, tc);
}

private void sendTcAssignmentsSnapshot(long watchId, TransactionCoordinatorV5 tc) {
if (!tcAssignmentWatchers.containsKey(watchId)) {
return;
}
tc.buildAssignmentsSnapshot().thenAccept(snapshot -> ctx.executor().execute(() -> {
if (!tcAssignmentWatchers.containsKey(watchId)) {
return;
}
java.util.Map<Integer, String[]> leaders = new java.util.HashMap<>();
snapshot.assignments().forEach((partition, leader) -> leaders.put(partition,
new String[] {leader.brokerServiceUrl(), leader.brokerServiceUrlTls()}));
ctx.writeAndFlush(Commands.newWatchTcAssignmentsSnapshot(
watchId, snapshot.partitionCount(), leaders));
// If some partition is still mid-election, the snapshot is incomplete. Schedule a single
// delayed re-push so the client doesn't stay parked on a missing partition waiting for a
// leadership change that may never come (the cache repopulating fires no TC listener).
if (!snapshot.isComplete()) {
ctx.executor().schedule(() -> sendTcAssignmentsSnapshot(watchId, tc),
TC_ASSIGNMENTS_REPUSH_DELAY_MS, TimeUnit.MILLISECONDS);
}
})).exceptionally(ex -> {
log.warn().attr("watchId", watchId).exception(ex)
.log("Failed to build TC-assignments snapshot; retrying shortly");
ctx.executor().schedule(() -> sendTcAssignmentsSnapshot(watchId, tc),
TC_ASSIGNMENTS_REPUSH_DELAY_MS, TimeUnit.MILLISECONDS);
return null;
});
}

@Override
protected void handleCommandWatchTcAssignmentsClose(
org.apache.pulsar.common.api.proto.CommandWatchTcAssignmentsClose cmd) {
checkArgument(state == State.Connected);
long watchId = cmd.getWatchId();
log.debug().attr("watchId", watchId).log("Received WatchTcAssignmentsClose");
closeQuietly(tcAssignmentWatchers.remove(watchId));
}

private void closeQuietly(AutoCloseable handle) {
if (handle == null) {
return;
}
try {
handle.close();
} catch (Exception e) {
log.warn().exceptionMessage(e).log("Error closing TC-assignment watcher");
}
}

@Override
protected void handleCommandScalableTopicClose(
CommandScalableTopicClose commandScalableTopicClose) {
Expand Down Expand Up @@ -1316,7 +1407,8 @@ private void completeConnect(int clientProtoVersion, String clientVersion) {
maybeScheduleAuthenticationCredentialsRefresh();
}
writeAndFlush(Commands.newConnected(clientProtoVersion, maxMessageSize, enableTopicListWatcher,
scalableTopicsEnabled));
scalableTopicsEnabled,
service.getPulsar().getConfig().isTransactionCoordinatorScalableTopicsEnabled()));
state = State.Connected;
service.getPulsarStats().recordConnectionCreateSuccess();
log.debug()
Expand Down
Loading
Loading