2323#include < cstdint>
2424#include < iterator>
2525#include < memory>
26+ #include < random>
2627#include < ranges>
2728#include < tuple>
2829#include < vector>
@@ -98,6 +99,7 @@ make_strategy_from_partitions(
9899 nodes.size (),
99100 std::move (index),
100101 std::move (group_to_topic),
102+ absl::flat_hash_set<cluster::leader_balancer_types::topic_id_t >{},
101103 cluster::leader_balancer_types::muted_index{muted_nodes, {}},
102104 std::move (preference_idx));
103105
@@ -125,18 +127,38 @@ make_greedy_strategy(
125127 }
126128 }
127129
130+ // Each node independently assigns partitions to shards in a
131+ // balanced-but-shuffled order: a permutation of [0..shards-1]
132+ // that repeats, so every shard gets the same number of replicas.
133+ // Different nodes use different permutations (seeded by node id
134+ // and topic) so replicas of the same partition land on different
135+ // shards across nodes, matching real node-local shard placement.
136+ auto make_shard_sequence = [&](int node, int topic) {
137+ std::vector<uint32_t > perm (static_cast <size_t >(shards_per_broker));
138+ std::iota (perm.begin (), perm.end (), uint32_t {0 });
139+ std::mt19937 rng (static_cast <uint32_t >(node * 137 + topic * 31 ));
140+ std::ranges::shuffle (perm, rng);
141+ return perm;
142+ };
143+
128144 for (int topic : std::views::iota (0 , topic_count)) {
145+ // Build per-node shard permutations and counters.
146+ std::vector<std::vector<uint32_t >> node_perms;
147+ std::vector<size_t > node_counters (static_cast <size_t >(broker_count), 0 );
148+ for (int n = 0 ; n < broker_count; ++n) {
149+ node_perms.push_back (make_shard_sequence (n, topic));
150+ }
151+
129152 for (int partition : std::views::iota (0 , partitions_per_topic)) {
130153 std::vector<model::broker_shard> replicas;
131- uint32_t topic_shard_offset = static_cast <uint32_t >(
132- std::max (1 , shards_per_broker / 2 ));
133154 int broker_offset = partition + topic;
134155 for (int replica : std::views::iota (0 , replication_factor)) {
135- replicas.push_back (bs (
136- (broker_offset + replica) % broker_count,
137- static_cast <uint32_t >(
138- (partition + topic * topic_shard_offset)
139- % shards_per_broker)));
156+ int node = (broker_offset + replica) % broker_count;
157+ auto & ctr = node_counters[static_cast <size_t >(node)];
158+ auto & perm = node_perms[static_cast <size_t >(node)];
159+ auto shard = perm[ctr % perm.size ()];
160+ ++ctr;
161+ replicas.push_back (bs (node, shard));
140162 }
141163 partitions.push_back (
142164 partition_spec{
@@ -419,28 +441,27 @@ TEST(GreedyLeaderBalancerTest, TwoTopicsFourBrokersThreeShards) {
419441 balanced,
420442 0 ,
421443 std::array<std::array<int , 3 >, 4 >{{
422- {{2 , 2 , 1 }},
444+ {{2 , 1 , 1 }},
423445 {{1 , 2 , 2 }},
446+ {{2 , 2 , 1 }},
424447 {{1 , 1 , 2 }},
425- {{2 , 1 , 1 }},
426448 }});
427449 expect_shard_counts_per_broker (
428450 balanced,
429451 1 ,
430452 std::array<std::array<int , 3 >, 4 >{{
453+ {{1 , 1 , 2 }},
454+ {{1 , 2 , 1 }},
455+ {{2 , 2 , 1 }},
431456 {{2 , 2 , 1 }},
432- {{1 , 1 , 1 }},
433- {{1 , 2 , 2 }},
434- {{2 , 1 , 2 }},
435457 }});
436- // Not perfect
437458 expect_combined_shard_counts (
438459 balanced,
439460 std::array<std::array<int , 3 >, 4 >{{
461+ {{3 , 2 , 3 }},
462+ {{2 , 4 , 3 }},
440463 {{4 , 4 , 2 }},
441- {{2 , 3 , 3 }},
442- {{2 , 3 , 4 }},
443- {{4 , 2 , 3 }},
464+ {{3 , 3 , 3 }},
444465 }});
445466}
446467
@@ -468,10 +489,10 @@ TEST(GreedyLeaderBalancerTest, RemainderPartitions) {
468489 expect_shard_counts_per_broker (
469490 balanced_leaders,
470491 1 ,
471- std::array<std::array<int , 2 >, 3 >{{{{1 , 1 }}, {{2 , 1 }}, {{1 , 2 }}}});
492+ std::array<std::array<int , 2 >, 3 >{{{{1 , 2 }}, {{1 , 1 }}, {{1 , 2 }}}});
472493 expect_combined_shard_counts (
473494 balanced_leaders,
474- std::array<std::array<int , 2 >, 3 >{{{{3 , 2 }}, {{3 , 3 }}, {{2 , 3 }}}});
495+ std::array<std::array<int , 2 >, 3 >{{{{3 , 3 }}, {{2 , 3 }}, {{2 , 3 }}}});
475496}
476497
477498TEST (GreedyLeaderBalancerTest, RemainderPartitionsThreeTopics) {
@@ -486,11 +507,11 @@ TEST(GreedyLeaderBalancerTest, RemainderPartitionsThreeTopics) {
486507 expect_shard_counts_per_broker (
487508 balanced_leaders,
488509 1 ,
489- std::array<std::array<int , 2 >, 3 >{{{{1 , 1 }}, {{2 , 1 }}, {{1 , 2 }}}});
510+ std::array<std::array<int , 2 >, 3 >{{{{1 , 2 }}, {{1 , 1 }}, {{1 , 2 }}}});
490511 expect_shard_counts_per_broker (
491512 balanced_leaders,
492513 2 ,
493- std::array<std::array<int , 2 >, 3 >{{{{1 , 2 }}, {{1 , 1 }}, {{2 , 1 }}}});
514+ std::array<std::array<int , 2 >, 3 >{{{{1 , 1 }}, {{2 , 1 }}, {{2 , 1 }}}});
494515 expect_combined_shard_counts (
495516 balanced_leaders,
496517 std::array<std::array<int , 2 >, 3 >{{{{4 , 4 }}, {{4 , 4 }}, {{4 , 4 }}}});
@@ -506,23 +527,23 @@ TEST(GreedyLeaderBalancerTest, BalancesShardsWithinBroker) {
506527 0 ,
507528 std::array<std::array<int , 4 >, 3 >{{
508529 {{2 , 1 , 1 , 2 }},
509- {{2 , 2 , 1 , 1 }},
510- {{1 , 2 , 2 , 1 }},
530+ {{1 , 1 , 2 , 2 }},
531+ {{1 , 2 , 1 , 2 }},
511532 }});
512533 expect_shard_counts_per_broker (
513534 balanced_leaders,
514535 1 ,
515536 std::array<std::array<int , 4 >, 3 >{{
516537 {{1 , 2 , 2 , 1 }},
517- {{1 , 1 , 2 , 2 }},
518538 {{2 , 1 , 1 , 2 }},
539+ {{2 , 2 , 1 , 1 }},
519540 }});
520541 expect_combined_shard_counts (
521542 balanced_leaders,
522543 std::array<std::array<int , 4 >, 3 >{{
523544 {{3 , 3 , 3 , 3 }},
524- {{3 , 3 , 3 , 3 }},
525- {{3 , 3 , 3 , 3 }},
545+ {{3 , 2 , 3 , 4 }},
546+ {{3 , 4 , 2 , 3 }},
526547 }});
527548}
528549
@@ -536,17 +557,16 @@ TEST(GreedyLeaderBalancerTest, TwoTopicsSixBrokersTwoShardsRfThree) {
536557 balanced_leaders,
537558 0 ,
538559 std::array<std::array<int , 2 >, 6 >{
539- {{{2 , 1 }}, {{1 , 2 }}, {{2 , 1 }}, {{1 , 2 }}, {{2 , 1 }}, {{1 , 2 }}}});
560+ {{{2 , 1 }}, {{1 , 2 }}, {{2 , 1 }}, {{2 , 1 }}, {{1 , 2 }}, {{1 , 2 }}}});
540561 expect_shard_counts_per_broker (
541562 balanced_leaders,
542563 1 ,
543564 std::array<std::array<int , 2 >, 6 >{
544- {{{2 , 2 }}, {{2 , 1 }}, {{1 , 2 }}, {{1 , 1 }}, {{1 , 2 }}, {{2 , 1 }}}});
565+ {{{2 , 2 }}, {{2 , 1 }}, {{1 , 2 }}, {{1 , 1 }}, {{1 , 2 }}, {{1 , 2 }}}});
545566 expect_combined_shard_counts (
546- // Not perfect
547567 balanced_leaders,
548568 std::array<std::array<int , 2 >, 6 >{
549- {{{4 , 3 }}, {{3 , 3 }}, {{3 , 3 }}, {{2 , 3 }}, {{3 , 3 }}, {{3 , 3 }}}});
569+ {{{4 , 3 }}, {{3 , 3 }}, {{3 , 3 }}, {{3 , 2 }}, {{2 , 4 }}, {{2 , 4 }}}});
550570}
551571
552572TEST (GreedyLeaderBalancerTest, SkippedMoveDoesNotDesyncIndex) {
0 commit comments