This is an automated email from the ASF dual-hosted git repository.
merlimat pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git
The following commit(s) were added to refs/heads/master by this push:
new 1f45e889087 [improve][broker] PIP-486 (PR2): controller entry-bucket
budget per segment (#26118)
1f45e889087 is described below
commit 1f45e889087f33215ab15aeac9e284e931bd44a7
Author: Matteo Merli <[email protected]>
AuthorDate: Tue Jun 30 10:56:23 2026 -0700
[improve][broker] PIP-486 (PR2): controller entry-bucket budget per segment
(#26118)
---
.../apache/pulsar/broker/ServiceConfiguration.java | 12 ++
.../pulsar/broker/admin/v2/ScalableTopics.java | 7 +-
.../broker/service/scalable/EntryBucketSplits.java | 64 +++++++++++
.../service/scalable/ScalableTopicController.java | 19 ++-
.../service/scalable/ScalableTopicService.java | 4 +-
.../broker/service/scalable/SegmentLayout.java | 15 ++-
.../scalable/AutoScalePolicyEvaluatorTest.java | 2 +-
.../service/scalable/DagWatchSessionTest.java | 2 +-
.../service/scalable/EntryBucketSplitsTest.java | 128 +++++++++++++++++++++
.../ScalableTopicControllerAutoScaleTest.java | 2 +-
.../scalable/ScalableTopicControllerTest.java | 10 +-
.../broker/service/scalable/SegmentLayoutTest.java | 38 +++---
.../scalable/SubscriptionCoordinatorTest.java | 2 +-
13 files changed, 268 insertions(+), 37 deletions(-)
diff --git
a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/ServiceConfiguration.java
b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/ServiceConfiguration.java
index 987a7ab8385..67112f19a77 100644
---
a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/ServiceConfiguration.java
+++
b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/ServiceConfiguration.java
@@ -1428,6 +1428,18 @@ public class ServiceConfiguration implements
PulsarConfiguration {
)
private int scalableTopicMinSegments = 1;
+ @FieldContext(
+ dynamic = true,
+ category = CATEGORY_POLICIES,
+ doc = "Total entry-bucket budget per scalable topic. Entry-buckets
are the unit of key-shared "
+ + "consumption parallelism within a segment, so this
budget is how many consumers can "
+ + "share a single segment's keys. It is distributed across
the topic's segments (each "
+ + "gets floor(budget / segmentCount), at least 1): a
single-segment topic starts with "
+ + "the whole budget, and as the topic splits into more
segments each segment settles "
+ + "toward 1 bucket (full batching)."
+ )
+ private int scalableTopicEntryBucketBudget = 4;
+
@FieldContext(
dynamic = true,
category = CATEGORY_POLICIES,
diff --git
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ScalableTopics.java
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ScalableTopics.java
index 5a365ffa523..ff9b16ccf20 100644
---
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ScalableTopics.java
+++
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/ScalableTopics.java
@@ -188,7 +188,9 @@ public class ScalableTopics extends AdminResource {
}
Map<String, String> props = properties != null ?
properties : Map.of();
ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(
- numInitialSegments, props);
+ numInitialSegments,
+
pulsar().getConfiguration().getScalableTopicEntryBucketBudget(),
+ props);
return resources().createScalableTopicAsync(tn, metadata)
.thenCompose(ignored ->
createInitialSegmentTopicsAsync(tn, metadata));
})
@@ -331,7 +333,8 @@ public class ScalableTopics extends AdminResource {
return precheck.thenApply(__ -> partitions);
}).thenCompose(partitions -> {
ScalableTopicMetadata metadata =
-
ScalableTopicController.createMigratedMetadata(persistentBase, partitions);
+
ScalableTopicController.createMigratedMetadata(persistentBase, partitions,
+
pulsar().getConfiguration().getScalableTopicEntryBucketBudget());
return createMigratedChildTopicsAsync(scalableName, metadata)
.thenCompose(__ ->
resources().createScalableTopicAsync(scalableName, metadata))
.thenCompose(__ ->
terminateLegacyTopicsAsync(persistentBase, partitions));
diff --git
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/EntryBucketSplits.java
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/EntryBucketSplits.java
new file mode 100644
index 00000000000..1cfd013982e
--- /dev/null
+++
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/EntryBucketSplits.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.service.scalable;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.pulsar.common.scalable.HashRange;
+
+/**
+ * PIP-486 controller boundary-selection policy: turns a segment's
entry-bucket count {@code N} into the
+ * equal-width split points stored on the segment — the ascending, inclusive
start hashes of buckets
+ * {@code 1..N-1} (bucket 0 implicitly starts at {@code 0x0000}). {@code N =
1} is a single bucket
+ * spanning the whole ring, i.e. no splits.
+ *
+ * <p>The entry-bucket ring is always the full {@code [0x0000, 0xFFFF]} (the
low 16 bits of the key's
+ * {@code Murmur3_32} hash), independent of the segment's segment-routing hash
range. The split values
+ * must match the producer's bucketing (a key falls in the bucket equal to the
number of splits {@code <=}
+ * its entry-bucket hash).
+ *
+ * <p>Equal-width is the initial policy; the wire format is range-based, so a
future controller could
+ * place arbitrary (traffic-balanced) boundaries with no wire or dispatch
change.
+ */
+final class EntryBucketSplits {
+
+ private EntryBucketSplits() {
+ }
+
+ /**
+ * The number of entry-buckets a segment gets from a topic budget shared
across its segments:
+ * {@code floor(budget / segmentCount)}, but at least 1.
+ */
+ static int bucketsForBudget(int budget, int segmentCount) {
+ return Math.max(1, budget / segmentCount);
+ }
+
+ /** Equal-width split points for {@code bucketCount} buckets; empty when
{@code bucketCount <= 1}. */
+ static List<Integer> equalWidth(int bucketCount) {
+ if (bucketCount <= 1) {
+ return List.of();
+ }
+ int ringSize = HashRange.MAX_HASH + 1;
+ List<Integer> splits = new ArrayList<>(bucketCount - 1);
+ for (int i = 1; i < bucketCount; i++) {
+ splits.add((int) ((long) i * ringSize / bucketCount));
+ }
+ return splits;
+ }
+}
diff --git
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/ScalableTopicController.java
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/ScalableTopicController.java
index a85187df28c..aebeca97c6a 100644
---
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/ScalableTopicController.java
+++
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/ScalableTopicController.java
@@ -1436,6 +1436,7 @@ public class ScalableTopicController {
* Create initial segment layout for a new scalable topic.
*/
public static ScalableTopicMetadata createInitialMetadata(int
numInitialSegments,
+ int entryBucketBudget,
Map<String, String>
properties) {
if (numInitialSegments < 1) {
throw new IllegalArgumentException("Must have at least 1 segment");
@@ -1444,12 +1445,17 @@ public class ScalableTopicController {
int rangeSize = (HashRange.MAX_HASH + 1) / numInitialSegments;
Map<Long, SegmentInfo> segments = new LinkedHashMap<>();
+ // PIP-486: share the topic's entry-bucket budget equally across the
initial segments.
+ List<Integer> entryBucketSplits = EntryBucketSplits.equalWidth(
+ EntryBucketSplits.bucketsForBudget(entryBucketBudget,
numInitialSegments));
+
long nowMs = System.currentTimeMillis();
for (int i = 0; i < numInitialSegments; i++) {
int start = i * rangeSize;
int end = (i == numInitialSegments - 1) ? HashRange.MAX_HASH :
(start + rangeSize - 1);
HashRange range = HashRange.of(start, end);
- SegmentInfo segment = SegmentInfo.active(i, range, 0, nowMs);
+ SegmentInfo segment = SegmentInfo.active(i, range, 0, nowMs)
+ .withEntryBucketSplits(entryBucketSplits);
segments.put((long) i, segment);
}
@@ -1484,11 +1490,17 @@ public class ScalableTopicController {
* @param partitions the source partition count; {@code <= 0} means
non-partitioned
*/
public static ScalableTopicMetadata createMigratedMetadata(TopicName
persistentBase,
- int partitions)
{
+ int partitions,
+ int
entryBucketBudget) {
int n = Math.max(partitions, 1);
long nowMs = System.currentTimeMillis();
Map<Long, SegmentInfo> segments = new LinkedHashMap<>();
+ // PIP-486: the active children share the topic's entry-bucket budget.
The sealed legacy parents
+ // take no new writes, so they keep a single bucket (no splits).
+ List<Integer> childEntryBucketSplits = EntryBucketSplits.equalWidth(
+ EntryBucketSplits.bucketsForBudget(entryBucketBudget, n));
+
// Child IDs are N..2N-1; every child lists every parent (full fan-in).
List<Long> childIds = new ArrayList<>(n);
for (int j = 0; j < n; j++) {
@@ -1516,7 +1528,8 @@ public class ScalableTopicController {
long segId = n + j;
int start = j * rangeSize;
int end = (j == n - 1) ? HashRange.MAX_HASH : (start + rangeSize -
1);
- SegmentInfo child = SegmentInfo.active(segId, HashRange.of(start,
end), parentIds, 0, nowMs);
+ SegmentInfo child = SegmentInfo.active(segId, HashRange.of(start,
end), parentIds, 0, nowMs)
+ .withEntryBucketSplits(childEntryBucketSplits);
segments.put(segId, child);
}
diff --git
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/ScalableTopicService.java
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/ScalableTopicService.java
index 2489840a2f0..3e5081e3985 100644
---
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/ScalableTopicService.java
+++
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/ScalableTopicService.java
@@ -144,7 +144,9 @@ public class ScalableTopicService {
}
ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(
- numInitialSegments, properties);
+ numInitialSegments,
+
brokerService.getPulsar().getConfiguration().getScalableTopicEntryBucketBudget(),
+ properties);
// Write the scalable metadata FIRST, then materialize the underlying
segment topics.
// The metadata is the source of truth: its presence is what defines
whether the topic
diff --git
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/SegmentLayout.java
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/SegmentLayout.java
index c823fb75e91..9618ac5df4e 100644
---
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/SegmentLayout.java
+++
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/scalable/SegmentLayout.java
@@ -176,11 +176,15 @@ public class SegmentLayout {
long childId1 = nextSegmentId;
long childId2 = nextSegmentId + 1;
+ // PIP-486: a split divides the parent's entry-buckets between its
children — N/2 each (at least
+ // 1) — so the topic's total stays ≈ the budget as it fans out into
more, narrower segments.
+ List<Integer> childEntryBucketSplits =
+ EntryBucketSplits.equalWidth(Math.max(1, segment.bucketCount()
/ 2));
SegmentInfo sealedParent = segment.sealed(newEpoch, nowMs,
List.of(childId1, childId2));
SegmentInfo child1 = SegmentInfo.active(childId1, splitRanges[0],
- List.of(segmentId), newEpoch, nowMs);
+ List.of(segmentId), newEpoch,
nowMs).withEntryBucketSplits(childEntryBucketSplits);
SegmentInfo child2 = SegmentInfo.active(childId2, splitRanges[1],
- List.of(segmentId), newEpoch, nowMs);
+ List.of(segmentId), newEpoch,
nowMs).withEntryBucketSplits(childEntryBucketSplits);
Map<Long, SegmentInfo> newSegments = new LinkedHashMap<>(allSegments);
newSegments.put(segmentId, sealedParent);
@@ -217,10 +221,15 @@ public class SegmentLayout {
long mergedId = nextSegmentId;
HashRange mergedRange = seg1.hashRange().merge(seg2.hashRange());
+ // PIP-486: a merge is the inverse of a split — the merged segment
recovers both parents' buckets
+ // (N1 + N2), so the topic's total entry-bucket count stays ≈ the
budget as segments coalesce.
+ List<Integer> mergedEntryBucketSplits =
+ EntryBucketSplits.equalWidth(seg1.bucketCount() +
seg2.bucketCount());
SegmentInfo sealed1 = seg1.sealed(newEpoch, nowMs, List.of(mergedId));
SegmentInfo sealed2 = seg2.sealed(newEpoch, nowMs, List.of(mergedId));
SegmentInfo merged = SegmentInfo.active(mergedId, mergedRange,
- List.of(segmentId1, segmentId2), newEpoch, nowMs);
+ List.of(segmentId1, segmentId2), newEpoch, nowMs)
+ .withEntryBucketSplits(mergedEntryBucketSplits);
Map<Long, SegmentInfo> newSegments = new LinkedHashMap<>(allSegments);
newSegments.put(segmentId1, sealed1);
diff --git
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/AutoScalePolicyEvaluatorTest.java
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/AutoScalePolicyEvaluatorTest.java
index b85845de647..e4420411607 100644
---
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/AutoScalePolicyEvaluatorTest.java
+++
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/AutoScalePolicyEvaluatorTest.java
@@ -68,7 +68,7 @@ public class AutoScalePolicyEvaluatorTest {
private static SegmentLayout initialLayout(int segments) {
return SegmentLayout.fromMetadata(
- ScalableTopicController.createInitialMetadata(segments,
Map.of()));
+ ScalableTopicController.createInitialMetadata(segments, 4,
Map.of()));
}
/** A load sample with the given rates, last modified {@code ageMs} ago. */
diff --git
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/DagWatchSessionTest.java
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/DagWatchSessionTest.java
index f1967c7e959..f513f7ae45d 100644
---
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/DagWatchSessionTest.java
+++
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/DagWatchSessionTest.java
@@ -441,7 +441,7 @@ public class DagWatchSessionTest {
public void testOnMetadataChangedAfterCloseIsNoop() {
session.close();
// Build a minimal metadata object; close should short-circuit before
any work runs.
- ScalableTopicMetadata md =
ScalableTopicController.createInitialMetadata(1, Map.of());
+ ScalableTopicMetadata md =
ScalableTopicController.createInitialMetadata(1, 4, Map.of());
session.onMetadataChanged(md);
verify(ctx, never()).writeAndFlush(any());
diff --git
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/EntryBucketSplitsTest.java
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/EntryBucketSplitsTest.java
new file mode 100644
index 00000000000..040d4495f99
--- /dev/null
+++
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/EntryBucketSplitsTest.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.broker.service.scalable;
+
+import static org.testng.Assert.assertEquals;
+import java.util.List;
+import java.util.Map;
+import org.apache.pulsar.broker.resources.ScalableTopicMetadata;
+import org.apache.pulsar.common.naming.TopicName;
+import org.testng.annotations.Test;
+
+/**
+ * PIP-486: the controller's entry-bucket budget — equal-width split
computation and how it is shared
+ * across a topic's segments on create / migrate / split / merge.
+ */
+public class EntryBucketSplitsTest {
+
+ // --- equalWidth ---
+
+ @Test
+ public void testEqualWidthSingleBucketHasNoSplits() {
+ assertEquals(EntryBucketSplits.equalWidth(1), List.of());
+ assertEquals(EntryBucketSplits.equalWidth(0), List.of());
+ }
+
+ @Test
+ public void testEqualWidthPowersOfTwoAreExact() {
+ assertEquals(EntryBucketSplits.equalWidth(2), List.of(0x8000));
+ assertEquals(EntryBucketSplits.equalWidth(4), List.of(0x4000, 0x8000,
0xC000));
+ }
+
+ @Test
+ public void testEqualWidthNonPowerOfTwoRoundsDown() {
+ // 65536/3 = 21845.33, 2*65536/3 = 43690.67
+ assertEquals(EntryBucketSplits.equalWidth(3), List.of(21845, 43690));
+ }
+
+ // --- bucketsForBudget ---
+
+ @Test
+ public void testBucketsForBudgetFloorsAtLeastOne() {
+ assertEquals(EntryBucketSplits.bucketsForBudget(4, 1), 4);
+ assertEquals(EntryBucketSplits.bucketsForBudget(4, 2), 2);
+ assertEquals(EntryBucketSplits.bucketsForBudget(4, 4), 1);
+ assertEquals(EntryBucketSplits.bucketsForBudget(4, 8), 1); // floor
never drops below 1
+ assertEquals(EntryBucketSplits.bucketsForBudget(4, 3), 1); // 4/3 -> 1
+ }
+
+ // --- createInitialMetadata shares the budget across segments ---
+
+ @Test
+ public void testInitialSingleSegmentTakesWholeBudget() {
+ ScalableTopicMetadata md =
ScalableTopicController.createInitialMetadata(1, 4, Map.of());
+ assertEquals(md.getSegments().get(0L).bucketCount(), 4);
+ }
+
+ @Test
+ public void testInitialBudgetSplitAcrossSegments() {
+ ScalableTopicMetadata md =
ScalableTopicController.createInitialMetadata(2, 4, Map.of());
+ assertEquals(md.getSegments().get(0L).bucketCount(), 2);
+ assertEquals(md.getSegments().get(1L).bucketCount(), 2);
+ }
+
+ @Test
+ public void testInitialSegmentsAtOrAboveBudgetSettleAtOneBucket() {
+ ScalableTopicMetadata md =
ScalableTopicController.createInitialMetadata(4, 4, Map.of());
+ for (long id = 0; id < 4; id++) {
+ assertEquals(md.getSegments().get(id).bucketCount(), 1);
+ }
+ }
+
+ // --- createMigratedMetadata: children share the budget, sealed legacy
parents keep one bucket ---
+
+ @Test
+ public void testMigratedChildrenShareBudgetParentsHaveOneBucket() {
+ TopicName base = TopicName.get("persistent://tenant/ns/topic");
+ ScalableTopicMetadata md =
ScalableTopicController.createMigratedMetadata(base, 2, 4);
+ // parents 0..1 (sealed legacy), children 2..3 (active).
+ assertEquals(md.getSegments().get(0L).bucketCount(), 1);
+ assertEquals(md.getSegments().get(1L).bucketCount(), 1);
+ assertEquals(md.getSegments().get(2L).bucketCount(), 2);
+ assertEquals(md.getSegments().get(3L).bucketCount(), 2);
+ }
+
+ // --- split halves buckets, merge sums them ---
+
+ @Test
+ public void testSplitHalvesParentBuckets() {
+ ScalableTopicMetadata md =
ScalableTopicController.createInitialMetadata(1, 4, Map.of());
+ SegmentLayout afterSplit =
SegmentLayout.fromMetadata(md).splitSegment(0, 0L);
+ // parent 0 had N=4; the two children (ids 1, 2) get N=2 each.
+ assertEquals(afterSplit.getAllSegments().get(1L).bucketCount(), 2);
+ assertEquals(afterSplit.getAllSegments().get(2L).bucketCount(), 2);
+ }
+
+ @Test
+ public void testSplitOfSingleBucketStaysSingleBucket() {
+ ScalableTopicMetadata md =
ScalableTopicController.createInitialMetadata(4, 4, Map.of());
+ SegmentLayout afterSplit =
SegmentLayout.fromMetadata(md).splitSegment(0, 0L);
+ long firstChild =
afterSplit.getAllSegments().get(0L).childIds().get(0);
+
assertEquals(afterSplit.getAllSegments().get(firstChild).bucketCount(), 1);
+ }
+
+ @Test
+ public void testMergeSumsChildBuckets() {
+ // 4 segments, each N=1; merging two adjacent ones recovers N = 1 + 1
= 2.
+ ScalableTopicMetadata md =
ScalableTopicController.createInitialMetadata(4, 4, Map.of());
+ SegmentLayout merged = SegmentLayout.fromMetadata(md).mergeSegments(0,
1, 0L);
+ long mergedId = merged.getAllSegments().get(0L).childIds().get(0);
+ assertEquals(merged.getAllSegments().get(mergedId).bucketCount(), 2);
+ }
+}
diff --git
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/ScalableTopicControllerAutoScaleTest.java
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/ScalableTopicControllerAutoScaleTest.java
index 6b1fc907696..c7a5ac04762 100644
---
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/ScalableTopicControllerAutoScaleTest.java
+++
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/ScalableTopicControllerAutoScaleTest.java
@@ -153,7 +153,7 @@ public class ScalableTopicControllerAutoScaleTest {
private void startController(int initialSegments) throws Exception {
resources.createScalableTopicAsync(topicName,
- ScalableTopicController.createInitialMetadata(initialSegments,
Map.of())).get();
+ ScalableTopicController.createInitialMetadata(initialSegments,
4, Map.of())).get();
controller = new ScalableTopicController(topicName, resources,
brokerService,
coordinationService);
controller.initialize().get();
diff --git
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/ScalableTopicControllerTest.java
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/ScalableTopicControllerTest.java
index 959a6a11064..52d7a25adb9 100644
---
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/ScalableTopicControllerTest.java
+++
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/ScalableTopicControllerTest.java
@@ -99,7 +99,7 @@ public class ScalableTopicControllerTest {
// Seed the topic's initial metadata so initialize() has something to
load.
ScalableTopicMetadata metadata =
-
ScalableTopicController.createInitialMetadata(INITIAL_SEGMENTS, Map.of());
+
ScalableTopicController.createInitialMetadata(INITIAL_SEGMENTS, 4, Map.of());
resources.createScalableTopicAsync(topicName, metadata).get();
// --- Mock the BrokerService / PulsarService / PulsarAdmin chain ---
@@ -478,7 +478,7 @@ public class ScalableTopicControllerTest {
@Test
public void testCreateInitialMetadataDefaults() {
- ScalableTopicMetadata md =
ScalableTopicController.createInitialMetadata(4, Map.of());
+ ScalableTopicMetadata md =
ScalableTopicController.createInitialMetadata(4, 4, Map.of());
assertEquals(md.getEpoch(), 0);
assertEquals(md.getNextSegmentId(), 4);
assertEquals(md.getSegments().size(), 4);
@@ -487,7 +487,7 @@ public class ScalableTopicControllerTest {
@Test
public void testCreateInitialMetadataRejectsZeroSegments() {
assertThrows(IllegalArgumentException.class,
- () -> ScalableTopicController.createInitialMetadata(0,
Map.of()));
+ () -> ScalableTopicController.createInitialMetadata(0, 4,
Map.of()));
}
// --- close / lifecycle ---
@@ -827,7 +827,7 @@ public class ScalableTopicControllerTest {
// 3-partition source → 3 sealed legacy parents (ids 0..2, full range,
wrapping
// each -partition-K) + 3 active range-based children (ids 3..5, full
fan-in).
TopicName base = TopicName.get("persistent://tenant/ns/my-topic");
- ScalableTopicMetadata md =
ScalableTopicController.createMigratedMetadata(base, 3);
+ ScalableTopicMetadata md =
ScalableTopicController.createMigratedMetadata(base, 3, 4);
assertEquals(md.getEpoch(), 0L);
assertEquals(md.getNextSegmentId(), 6L);
@@ -869,7 +869,7 @@ public class ScalableTopicControllerTest {
// Non-partitioned source (partitions <= 0) → 1 sealed legacy parent
wrapping the
// base persistent:// topic + 1 active child covering the full range.
TopicName base = TopicName.get("persistent://tenant/ns/np-topic");
- ScalableTopicMetadata md =
ScalableTopicController.createMigratedMetadata(base, 0);
+ ScalableTopicMetadata md =
ScalableTopicController.createMigratedMetadata(base, 0, 4);
assertEquals(md.getNextSegmentId(), 2L);
assertEquals(md.getSegments().size(), 2);
diff --git
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/SegmentLayoutTest.java
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/SegmentLayoutTest.java
index afcab4c8b98..fae5afaa100 100644
---
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/SegmentLayoutTest.java
+++
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/SegmentLayoutTest.java
@@ -34,7 +34,7 @@ public class SegmentLayoutTest {
@Test
public void testInitialLayout() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
assertEquals(layout.getEpoch(), 0);
@@ -54,7 +54,7 @@ public class SegmentLayoutTest {
@Test
public void testSingleSegmentInitialLayout() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(1, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(1, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
assertEquals(layout.getActiveSegments().size(), 1);
@@ -64,7 +64,7 @@ public class SegmentLayoutTest {
@Test
public void testFourSegmentInitialLayout() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(4, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(4, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
assertEquals(layout.getActiveSegments().size(), 4);
@@ -81,7 +81,7 @@ public class SegmentLayoutTest {
@Test
public void testFindActiveSegment() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
SegmentInfo found = layout.findActiveSegment(0x1000);
@@ -93,7 +93,7 @@ public class SegmentLayoutTest {
@Test
public void testSplitSegment() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
SegmentLayout afterSplit = layout.splitSegment(0, 0L);
@@ -122,7 +122,7 @@ public class SegmentLayoutTest {
@Test
public void testSplitNonActiveSegment() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
SegmentLayout afterSplit = layout.splitSegment(0, 0L);
@@ -133,7 +133,7 @@ public class SegmentLayoutTest {
@Test
public void testMergeSegments() {
// Start with 2 segments, split seg-0, then merge the children back
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
SegmentLayout afterSplit = layout.splitSegment(0, 0L); // seg-2
[0000-3fff], seg-3 [4000-7fff]
@@ -155,7 +155,7 @@ public class SegmentLayoutTest {
@Test
public void testSplitRecordsWallClockTimestamps() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(1, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(1, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
long splitAt = 1_700_000_000_000L;
@@ -179,7 +179,7 @@ public class SegmentLayoutTest {
@Test
public void testMergeRecordsWallClockTimestamps() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
long splitAt = 1_700_000_000_000L;
@@ -199,7 +199,7 @@ public class SegmentLayoutTest {
@Test
public void testMergeNonAdjacentSegments() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(4, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(4, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
// Segments 0 and 2 are not adjacent
@@ -208,7 +208,7 @@ public class SegmentLayoutTest {
@Test
public void testPruneSegment() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
SegmentLayout afterSplit = layout.splitSegment(0, 0L);
@@ -223,7 +223,7 @@ public class SegmentLayoutTest {
@Test
public void testCannotPruneActiveSegment() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
assertThrows(IllegalArgumentException.class, () ->
layout.pruneSegment(0));
@@ -231,7 +231,7 @@ public class SegmentLayoutTest {
@Test
public void testGetChildren() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(1, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(1, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
SegmentLayout afterSplit = layout.splitSegment(0, 0L);
@@ -243,7 +243,7 @@ public class SegmentLayoutTest {
@Test
public void testGetParents() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(1, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(1, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
SegmentLayout afterSplit = layout.splitSegment(0, 0L);
@@ -257,7 +257,7 @@ public class SegmentLayoutTest {
@Test
public void testGetLineage() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(1, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(1, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
SegmentLayout afterSplit = layout.splitSegment(0, 0L);
@@ -268,7 +268,7 @@ public class SegmentLayoutTest {
@Test
public void testToMetadata() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, Map.of("key", "value"));
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, 4, Map.of("key", "value"));
// A layout mutation must round-trip every non-layout field, not just
properties.
metadata.setAutoScalePolicy(org.apache.pulsar.common.policies.data.AutoScalePolicyOverride
.builder().enabled(false).maxSegments(8).build());
@@ -286,7 +286,7 @@ public class SegmentLayoutTest {
@Test
public void testNextSegmentIdAdvances() {
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(2, 4, Map.of());
SegmentLayout layout = SegmentLayout.fromMetadata(metadata);
assertEquals(layout.getNextSegmentId(), 2);
@@ -300,7 +300,7 @@ public class SegmentLayoutTest {
@Test
public void testMergeDepthZeroForNeverMergedSegments() {
SegmentLayout layout = SegmentLayout.fromMetadata(
- ScalableTopicController.createInitialMetadata(2, Map.of()));
+ ScalableTopicController.createInitialMetadata(2, 4, Map.of()));
assertEquals(layout.mergeDepth(0), 0);
assertEquals(layout.mergeDepth(1), 0);
@@ -314,7 +314,7 @@ public class SegmentLayoutTest {
public void testMergeDepthCountsMergesInLineage() {
// split(0) → {1,2}; merge(1,2) → {3}; split(3) → {4,5}.
SegmentLayout layout = SegmentLayout
- .fromMetadata(ScalableTopicController.createInitialMetadata(1,
Map.of()))
+ .fromMetadata(ScalableTopicController.createInitialMetadata(1,
4, Map.of()))
.splitSegment(0, 0L)
.mergeSegments(1, 2, 0L)
.splitSegment(3, 0L);
diff --git
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/SubscriptionCoordinatorTest.java
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/SubscriptionCoordinatorTest.java
index 3fa16caec49..57c797639c3 100644
---
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/SubscriptionCoordinatorTest.java
+++
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/scalable/SubscriptionCoordinatorTest.java
@@ -56,7 +56,7 @@ public class SubscriptionCoordinatorTest {
@BeforeMethod
public void setup() {
topicName = TopicName.get("topic://tenant/ns/my-topic");
- ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(4, Map.of());
+ ScalableTopicMetadata metadata =
ScalableTopicController.createInitialMetadata(4, 4, Map.of());
initialLayout = SegmentLayout.fromMetadata(metadata);
resources = mock(ScalableTopicResources.class);
// All persistence ops succeed