KAFKA-21060: Make StickyAssignor aware of stateless vs stateful tasks - #23482
Conversation
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The assignment changes are covered by focused tests and no unresolved correctness issues were found.
Review effort: Lite
Findings: None
What changed in this PR
Updates StickyTaskAssignor to balance stateful and stateless active tasks separately while preserving total task capacity and standby behavior.
Changes:
- Adds stateful/stateless task tracking and load comparison.
- Assigns stateful active tasks before stateless tasks.
- Adds focused balancing and stickiness tests.
| File | Description |
|---|---|
StickyTaskAssignor.java |
Implements task-type-aware assignment and quotas. |
ProcessState.java |
Tracks active task categories and stateless load. |
StickyTaskAssignorTest.java |
Adds mixed-task balancing tests. |
ProcessStateTest.java |
Updates tests for the expanded task API. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
lucasbru
left a comment
There was a problem hiding this comment.
Mostly looks good to me. I left two comments.
I would still benchmark and random-test this against the current sticky assignor to avoid regressions.
| final LocalState localState, | ||
| final ProcessState process, | ||
| final Member member | ||
| final Member member, |
There was a problem hiding this comment.
nit: This quota check only works because assignActive(stateful) always runs before assignActive(stateless)/assignStandby, so the member's task count still equals its stateful count at this point. Nothing enforces that ordering though - worth a comment calling out the invariant, so a future reorder doesn't silently corrupt the quota check?
There was a problem hiding this comment.
I have added the comment, thanks for the suggestion
| if (localState.numStandbyReplicas > 0) { | ||
| final LinkedList<TaskId> statefulTasks = taskIds(topologyDescriber, false); | ||
| assignStandby(localState, statefulTasks); | ||
| assignStandby(localState, new LinkedList<>(localState.statefulActiveTaskIds)); |
There was a problem hiding this comment.
Do we need to copy all three task lists here? assignActive consumes its input, but assignStandby only sorts and iterates its list. Could we preserve statefulActiveTaskIds for standby assignment and pass the stateless list directly, avoiding the extra LinkedList copy?
There was a problem hiding this comment.
I have updated my pr, thanks
|
JMH
"Significant" = the difference exceeds the sum of both 99.9% error bars. Rows where that holds:
|
|
JMH: no measurable regression.
|
|
Nice! Benchmarks look good. Since you are anyways at it, you should probably run the testbed on this assignor change before merging. But it looks good to me. |
Fuzz testbed comparison: trunk vs this PRRun with the shared assignor testbed from #23556 ( Headline
Stateful imbalance drops ~3× at scale and reaches the floor of 1 on fresh groups; stateless balance improves as a side effect. Cost: total-active spread +0.12 on LARGE (two buckets each rounding up), |
|
in the fuzzy test, fresh means it's the first time tasks being assigned, so no previous holder of any task, and events means we randomly choose event( member join, member leave, process join, process leave etc) |
|
Nice work, @gabriellefu ! |
The original sticky assignor in streams group didn't aware of
stateless/stateful assignor, which can cause some member assigned a lot
of stateful active task while some assigned a lot of stateless tasks,
cause the later member being stale
This pr change the assign order, first stateful active tasks are
assigned, then stateless active tasks, then standby tasks. Each type of
capacity are calculated to ensure each member can have a "balanced"
number of each type of tasks.
Reviewers: Lucas Brutschy lbrutschy@confluent.io