Visitar URL original
KAFKA-21214: [8/8] Add a fuzzer for the uniform2 assignor by dajac · Pull Request #23724 · apache/kafka · GitHub
Skip to content

KAFKA-21214: [8/8] Add a fuzzer for the uniform2 assignor - #23724

Open
dajac wants to merge 11 commits into
apache:trunkfrom
dajac:uniform2-baseline-fuzzer
Open

dajac wants to merge 11 commits into
apache:trunkfrom
dajac:uniform2-baseline-fuzzer

Conversation

@dajac

@dajac dajac commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Depends on #23723: only the last commit belongs to this PR.

The unit tests of the uniform2 assignor check hand traced cases, but its
properties must hold for any group and any sequence of changes.

This patch adds a fuzzer checking them on random groups and random
sequences of events: members joining and leaving, one at a time or
several, topics gaining partitions, and subscriptions changing. Other
events make the input stale, as the coordinator can hand it to the
assignor: topics deleted or created again while members still hold their
partitions, and empty subscriptions.

Changes:

  • Uniform2FuzzScenario generates the groups and the events, and hands
    every group to the assignor as a group spec built with
    GroupSpecFixture. Uniform2FuzzScenarioTest checks that a seed
    determines the scenario and that every kind of event and of stale
    input happens.
  • Uniform2AssignorFuzzTest checks validity, spread and balance,
    determinism under shuffled inputs, the fixed point with the same map
    instances, the same map for every member whose partitions do not
    change, the movement bounds after a single leave or join, and the
    moves of tiny groups, at most one above the fewest found by a brute
    force oracle.
  • It also checks that the four ways of counting of the balance step give
    the same quotas, through AssignmentTestUtils.quotasPerWayOfCounting.
  • The default run takes seconds; environment variables scale it up, see
    the class Javadoc.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

The stateful balancing algorithm and extensive stacked changes warrant final human validation despite no confirmed blocker.

Review effort: Balanced
Findings: None

What changed in this PR

Adds randomized, property-based validation for the uniform2 assignor, alongside the stacked assignor implementation, utilities, unit tests, and benchmark support.

Changes:

  • Adds deterministic fuzz scenarios covering group changes and stale coordinator inputs.
  • Verifies assignment validity, balance, stickiness, determinism, and movement bounds.
  • Includes uniform2 implementation, focused unit tests, and JMH benchmarking support.
File Description
jmh-benchmarks/​.../​ConsumerAssignorBenchmark.java Benchmarks uniform2.
.../​assignor/​Uniform2FuzzScenario.java Generates randomized scenarios.
.../​assignor/​Uniform2FuzzScenarioTest.java Tests scenario generation.
.../​assignor/​Uniform2AssignorFuzzTest.java Runs property-based fuzz checks.
.../​assignor/​Uniform2AssignorTest.java Tests end-to-end assignments.
.../​uniform2/​util/​LongHeapTest.java Tests primitive heap behavior.
.../​uniform2/​util/​IntListTest.java Tests primitive list behavior.
.../​uniform2/​TestGroup.java Provides test fixtures.
.../​uniform2/​SharesTest.java Tests share calculation.
.../​uniform2/​PartitionPlacementTest.java Tests sticky partition placement.
.../​uniform2/​HandOutTest.java Tests extra-partition distribution.
.../​uniform2/​GroupModelTest.java Tests group modeling and cohorts.
.../​uniform2/​ExtraPartitionMovesTest.java Tests extra-partition transfers.
.../​uniform2/​CurrentAssignmentTest.java Tests current ownership parsing.
.../​uniform2/​AssignmentTestUtils.java Adds shared assignment assertions.
.../​uniform2/​AssignmentBuilderTest.java Tests result construction and reuse.
.../​uniform2/​ShareBalancerTest.java Tests balancing behavior.
.../​assignor/​Uniform2Assignor.java Exposes the uniform2 assignor.
.../​uniform2/​util/​LongHeap.java Implements a primitive min-heap.
.../​uniform2/​util/​IntList.java Implements a primitive integer list.
.../​uniform2/​Shares.java Computes member shares.
.../​uniform2/​ShareBalancer.java Balances extra partitions.
.../​uniform2/​PartitionPlacement.java Places partitions stickily.
.../​uniform2/​HandOut.java Distributes unclaimed extras.
.../​uniform2/​GroupModel.java Models members, topics, and cohorts.
.../​uniform2/​ExtraPartitionMoves.java Tracks efficient balance moves.
.../​uniform2/​CurrentAssignment.java Reads ownership and stale state.
.../​uniform2/​AssignmentBuilder.java Builds reusable assignment results.
.../​assignor/​RangeSet.java Makes range sets accessible to uniform2.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@dajac
dajac force-pushed the uniform2-baseline-fuzzer branch from b7e0c1f to dd29ad0 Compare October 6, 2026 14:23
@dajac dajac added the uniform2 label Oct 6, 2026
@dajac
dajac force-pushed the uniform2-baseline-fuzzer branch from dd29ad0 to 01b3a6c Compare October 6, 2026 17:07
@dajac dajac changed the title KAFKA-21214: [3/3] Add a fuzzer for the uniform2 assignor KAFKA-21214: [8/8] Add a fuzzer for the uniform2 assignor Oct 6, 2026
The uniform assignor balances the total number of partitions per member
but not the partitions of each topic: with homogeneous subscriptions, a
member can get all the partitions of a topic while the others get none
of them. uniform2, the next version of the uniform assignor, spreads
every topic over its subscribers. It reads the group, decides how many
partitions of every topic each member gets in three steps, keep, hand
out and balance, and then places the partitions. This stack adds it
one step at a time, and the assignor itself last.

This patch adds the model of the group that every step reads:
- `GroupModel` numbers the members in member id order and the topics in
  topic id order, so that every decision depends on the content of the
  input only. It drops the topics that do not exist, and groups the
  members with the same subscription into cohorts.
- `CurrentAssignment` reads the owner of every partition. What the
  members hold beyond that is stale: partitions of topics they do not
  subscribe to or which no longer exist, partitions beyond the
  partition count, and empty sets.
- `GroupSpecFixture` and `TopicsFixture` build the inputs of the tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dajac
dajac force-pushed the uniform2-baseline-fuzzer branch 3 times, most recently from 51a7daf to bddcd2d Compare October 8, 2026 09:09
dajac and others added 2 commits October 12, 2026 15:19
- Rename GroupModel to IndexedGroup.
- Replace CohortsByTopics with CohortsBySubscription: a map from a
  hash to the cohorts with that hash, a map-like getOrCreateCohort
  and an internal lookup counter instead of the member.
- Read the homogeneous and heterogeneous cohorts in two methods.
- Stop handling partitions beyond the partition count and empty sets
  of partitions, which the coordinator never sends.
- Document the null owners array, every field of IndexedGroup and the
  handling of hash collisions.
- Rename readSubscribedTopics, subscribedTopicDescriber, topicIndices
  and topicIds; use "seen", "index" and {@code true} in the Javadoc.
- Reorganize IndexedGroupTest into member numbering, topic numbering
  and cohort scenarios, and rename the tests and helpers as suggested.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dajac
dajac force-pushed the uniform2-baseline-fuzzer branch 2 times, most recently from f8c94b1 to 66d50c3 Compare October 12, 2026 14:52
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dajac
dajac force-pushed the uniform2-baseline-fuzzer branch from 66d50c3 to cc27a82 Compare October 12, 2026 15:27
dajac and others added 7 commits October 12, 2026 17:56
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…orm2 assignor

uniform2 decides how many partitions of every topic each member gets,
its quotas, in three steps, and then places the partitions. A topic
with P partitions and N subscribers has a minimum quota of P / N
partitions, and its P % N extra partitions go to as many distinct
subscribers.

This patch adds the quotas and their first two steps:
- `Quotas` holds the minimum quota and the extra partitions of every
  topic, the members getting the extra partitions, and the assignment
  sizes of the members. The steps give the extra partitions and move
  them.
- `KeepStickyExtraPartitions`: a member owning more than the minimum
  quota of a topic keeps one of its extra partitions, the members with
  the smallest assignments first when the owners outnumber the extra
  partitions.
- `AssignRemainingExtraPartitions` gives the extra partitions that
  nobody kept, one at a time, to subscribers without one of the topic,
  among the ones with the smallest assignments: the topics with the
  fewest subscribers first, and round robin within a cohort. It returns
  without allocating anything when the owners kept them all.
- `IntPair` packs two ints in a long, so that pairs sort in a `long[]`
  or a `LongHeap` without allocating.
- A step is created with what it works on and run once; the assignor
  runs the steps in order in a later patch.
- `QuotasFixture` holds the indexed group, the current assignment and
  the quotas of a group for the tests of the steps.

The balance step comes in a later patch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
uniform2 decides how many partitions of every topic each member gets,
its quotas, in three steps: keep the sticky extra partitions, assign the
remaining ones, and balance them. The balance step moves extra
partitions from members with large assignments to members with small
ones.

This patch adds `ExtraPartitionMoves`, which the balance step uses to
choose and make these moves:
- It keeps the extra partitions of every member in two lists, those of
  topics it owns more than the minimum quota of, and the free ones.
  A transfer gives the cheapest extra partition that the receiver can
  take: one that saves a move, then one that costs none, then one that
  costs a move.
- It tells whether a receiver can take an extra partition from a giver,
  and counts the extra partitions of a giver per cohort, those of
  topics that the cohort subscribes to, to find receivers fast.
- The counts are kept for every member when they fit, and for one
  member at a time otherwise; the subscriptions are compared as bit
  sets when they fit.

The balance step comes in the next patch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
uniform2 decides how many partitions of every topic each member gets,
its quotas, in three steps. The first two give every extra partition to
a distinct subscriber of its topic, but they can leave a member with an
assignment much larger than another subscriber of one of its topics.

This patch adds the balance step, `BalanceExtraPartitions`, a step like
the other two, which the assignor runs after them in a later patch. It
applies a single rule until it no longer applies: the member with the
largest assignment among those having an extra partition that can move
gives its cheapest one to the smallest subscriber of the topics of its
extra partitions that has no extra partition of the topic and is at
least two partitions smaller. Every move lowers the sum of the squares
of the assignment sizes, so the step ends, and it ends exactly when no
extra partition can move to a subscriber of its topic at least two
partitions smaller.

Changes:
- The givers wait in a heap, the largest first, and a giver without a
  receiver is parked until a move may give it one. A cohort sharing no
  topic with another is done at its first giver without a receiver.
- On equal assignment sizes, a giver with a free extra partition gives
  first, and a receiver owning more than the minimum quota of a topic
  with extra partitions receives first, since only it can save a move.
- When the quotas are balanced for sure, the step returns without
  building any state.
- A constructor takes the ways of counting of `ExtraPartitionMoves`, and
  a test checks that they give the same quotas on random groups.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Once uniform2 has decided how many partitions of every topic each member
gets, its quotas, it places the partitions.

This patch adds `PartitionPlacement`, which places the partitions of a
topic on its subscribers, moving as few partitions as possible: every
owner keeps its partitions up to its quota, the lowest partition ids
first, and the other partitions go to the subscribers below their quota,
in member order. A partition thus moves only when its owner's quota is
below what it owns. The placement tells when nothing moves, so that the
topic can keep its sets of partitions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The uniform assignor balances the total number of partitions per member
but not the partitions of each topic: with homogeneous subscriptions, a
member can get all the partitions of a topic while the others get none
of them. The throughput is usually about the same across the partitions
of a topic but not across topics, so adding members does not spread the
load.

This patch adds uniform2, the next version of the uniform assignor, from
the steps of the previous patches. It lives next to the uniform assignor
while it is developed and is not registered as a built-in
assignor. In priority order:
- every partition of every subscribed topic goes to exactly one of its
  subscribers;
- every subscriber of a topic gets the same number of its partitions,
  the minimum quota, or one more, an extra partition;
- no extra partition could move to another subscriber of its topic whose
  assignment is at least two partitions smaller;
- partitions stay with their owners where the above allows: the
  placement moves the fewest partitions for the quotas, and an
  assignment that already has these properties is returned unchanged;
- the result only depends on the content of the input.

Changes:
- `Uniform2Assignor` and `AssignmentBuilder`, which reads the group,
  runs the `KeepStickyExtraPartitions`, `AssignRemainingExtraPartitions`
  and `BalanceExtraPartitions` steps on the quotas, places every topic
  and builds the result. Partitions of topics that a member does not
  subscribe to or which no longer exist are dropped. The class Javadoc
  of `Uniform2Assignor` describes the steps with a worked example.
- The result reuses the maps and sets of the members that do not change.
  `RangeSet` becomes public to return consecutive partitions.
  `Quotas.maxTopicsWithPartitions` bounds the size of the new maps.
- The assignor does not use racks; rack awareness comes in a later
  patch.
- `ConsumerAssignorBenchmark` gets `UNIFORM2`.
- End to end tests with hand traced expectations, written with the group
  spec and topics fixtures, and a shared checker of the properties.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The unit tests of the uniform2 assignor check hand traced cases, but its
properties must hold for any group and any sequence of changes.

This patch adds a fuzzer checking them on random groups and random
sequences of events: members joining and leaving, one at a time or
several, topics gaining partitions, and subscriptions changing. Other
events make the input stale, as the coordinator can hand it to the
assignor: topics deleted or created again while members still hold their
partitions, and empty subscriptions.

Changes:
- `Uniform2FuzzScenario` generates the groups and the events, and hands
  every group to the assignor as a group spec built with
  `GroupSpecFixture`. `Uniform2FuzzScenarioTest` checks that a seed
  determines the scenario and that every kind of event and of stale
  input happens.
- `Uniform2AssignorFuzzTest` checks validity, spread and balance,
  determinism under shuffled inputs, the fixed point with the same map
  instances, the same map for every member whose partitions do not
  change, the movement bounds after a single leave or join, and the
  moves of tiny groups, at most one above the fewest found by a brute
  force oracle.
- It also checks that the four ways of counting of the balance step give
  the same quotas, through `AssignmentTestUtils.quotasPerWayOfCounting`.
- The default run takes seconds; environment variables scale it up, see
  the class Javadoc.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dajac
dajac force-pushed the uniform2-baseline-fuzzer branch from cc27a82 to a3d6fe3 Compare October 12, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants