c/scheduling: fix allocation underflow on no-op reallocate#31071
Draft
joe-redpanda wants to merge 1 commit into
Draft
c/scheduling: fix allocation underflow on no-op reallocate#31071joe-redpanda wants to merge 1 commit into
joe-redpanda wants to merge 1 commit into
Conversation
partition_allocator::allocate() for a partition that needs no new
replicas (replication_factor == existing replica count) never calls
allocated_partition::add_replica(), so _original_node2shard is left
unset. release_new_partition() then treated every existing replica as
newly-allocated and ~allocation_units deallocated them -- decrements
with no matching increment. The per-node _allocated_partitions counter
dropped below the true value, and a later remove_allocation() (e.g. a
decommission finish) aborted the node on the
'_allocated_partitions > allocation_capacity{0}' vassert.
This is exercised heavily by the event-driven health_manager, which
requests RF=target for all internal-topic (ct_l1_domain) partitions on
every membership change, including partitions already at target.
Only treat a replica as new when _original_node2shard has a value,
matching has_changes() and ~allocated_partition which already treat a
nullopt map as "no changes".
Adds a deterministic unit test for the no-op reallocate case and a
minimal ducktape reproducer for the integration scenario.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a partition-allocator accounting bug that aborts a node during
decommission on:
The bug is latent in
dev; it is surfaced frequently by the event-drivenhealth_manager introduced in #30779, which is why it showed up as a
NodeCrashinShadowLinkingRandomOpsTest.test_node_operationson that PR'sCI.
The bug
partition_allocator::allocate()builds anallocated_partitionperrequested partition and only calls
allocated_partition::add_replica()forpartitions that need new replicas (
do_allocate'sin_progressloopgates on
replication_factor > current replica count).add_replica()is what lazily initializes_original_node2shard. So when arequest contains a partition that needs no new replicas
(
replication_factor == existing replica count),add_replica()is nevercalled and
_original_node2shardstaysnullopt.release_new_partition()then decides which replicas are "new" (and thereforeown an allocation that must be released when the
allocation_unitsRAII objectis destroyed):
With
_original_node2shard == nullopt, the!_original_node2shard ||branchclassifies every existing replica as new.
~allocation_unitsthen callsremove_allocation()on each of them — decrements that were never matched by acorresponding
allocate()/increment. The per-node_allocated_partitionscounter drops below the true replica count, and the next legitimate
remove_allocation()(a decommissionfinish_moving_partition_replicas_cmd)underflows the assert.
Why #30779 triggers it
health_manager::increase_replication_factor→do_increase_replication_factorbuilds an allocation request for allpartitions of an internal topic at
rf = target. Forct_l1_domain(bornRF=1, then reconciled to RF=3), once some partitions are already at target,
each health_manager pass re-requests them with
num_new_replicas == 0and hitsthe path above. #30779 makes health_manager run on every membership-change
notification, so this fires repeatedly during decommission churn. Observed in a
minimal repro to underflow even for a node in
state: active, confirming thedefect is the no-op reallocate, independent of which node is being
decommissioned.
The fix
release_new_partitionshould only treat a replica as new whenadd_replica()actually allocated it, i.e. when_original_node2shardhas avalue:
A
nulloptmap means nothing was allocated, so nothing should be released.This matches
has_changes()and~allocated_partition, which already treat anullopt_original_node2shardas "no changes / nothing to undo".How the tests reproduce it
Deterministic unit test —
topic_updates_dispatcher_test.cc: rf_noop_allocate_spurious_dealloc. Creates an RF=3 single-partition topic (3allocations), then calls
allocate()again for the same partition at the sameRF (
num_new_replicas == 0) and lets the returnedallocation_unitsgo out ofscope. Without the fix the per-node total drops
3 -> 0(
BOOST_CHECK_EQUAL(total_allocated(), 3)fails); with the fix it stays3.No concurrency, sub-second.
Integration reproducer —
tests/rptest/tests/l1_decom_repro_test.py.Single cluster (no shadow linking) with the ingredients that open the race in
the real system: cloud topics enabled (so
ct_l1_domainis born RF=1 andhealth-manager-reconciled), a rate-limited background producer keeping the
metastore raft groups busy, an
AdminOperationsFuzzerissuing concurrenttopic/RF-change commands, and a decommission/recommission/add node-op loop.
Against the #30779 build this reproduced the underflow within the first repeat
iteration; with this fix applied it ran the full node-op loop with zero
underflows.
Validation
rf_noop_allocate_spurious_dealloctotal3 → 0, FAIL🤖 Generated with Claude Code