Skip to content

c/scheduling: fix allocation underflow on no-op reallocate#31071

Merged
joe-redpanda merged 5 commits into
redpanda-data:devfrom
joe-redpanda:joe/fix-alloc-reserve-underflow
Jul 15, 2026
Merged

c/scheduling: fix allocation underflow on no-op reallocate#31071
joe-redpanda merged 5 commits into
redpanda-data:devfrom
joe-redpanda:joe/fix-alloc-reserve-underflow

Conversation

@joe-redpanda

@joe-redpanda joe-redpanda commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Attempts to deflake the nodewise recovery tests led us to more agressively change partition replication factors. This surfaced a bug on reconfigurations where if the reconfiguration was a no-op, we would actually accidentally delete all of that partitions allocations.

This pr does a few things.

  1. adds tests on allocated partition because there were previously none
  2. adds a regression test for the above double free
  3. refactors allocated partition via a helper type which makes the error immediately obvious
  4. fixes that issue
  5. a little cleanup on the side because its nice to have

The actual bug:

  1. allocated_partition is meant to yield the newly added replicas on a given partition
  2. those go in an raii guard which should make sure that should anything go wrong, those allocations are re-decremented
  3. When there were no actually allocated replicas, the logic to yield the added replicas accidentally yielded all of the replicas.
  4. No actually replicas get added so nothing gets incremented.
  5. The guard though, destructs decrementing allocations on all nodes where the partition had replicas.
  6. Whenever that node gets drained, the replica count goes negative

Backports Required

  • none - not a bug fix
  • none - this is a backport
  • none - issue does not exist in previous branches
  • none - papercut/not impactful enough to backport
  • v26.1.x
  • v25.3.x
  • v25.2.x

Release Notes

Bug Fixes

  • fix allocation double free on no-op rf changes

@joe-redpanda joe-redpanda force-pushed the joe/fix-alloc-reserve-underflow branch from 9b68942 to 8e3449c Compare July 14, 2026 16:46
Comment thread src/v/cluster/scheduling/types.h Outdated
Comment thread src/v/cluster/scheduling/types.h Outdated
Comment thread src/v/cluster/scheduling/tests/allocated_partition_test.cc Outdated
Comment thread src/v/cluster/scheduling/tests/allocated_partition_test.cc Outdated
Comment thread src/v/cluster/scheduling/tests/allocated_partition_test.cc Outdated
Comment thread src/v/cluster/scheduling/tests/allocated_partition_test.cc Outdated
Comment thread src/v/cluster/scheduling/tests/allocated_partition_test.cc Outdated
Comment thread src/v/cluster/scheduling/types.h Outdated
Comment thread tests/rptest/tests/l1_decom_repro_test.py Outdated
@joe-redpanda joe-redpanda force-pushed the joe/fix-alloc-reserve-underflow branch 2 times, most recently from 2409a0e to e61c38a Compare July 14, 2026 18:03
Comment thread src/v/cluster/scheduling/tests/allocated_partition_test.cc Outdated
Comment thread src/v/cluster/scheduling/tests/allocated_partition_test.cc Outdated
Comment thread src/v/cluster/tests/topic_updates_dispatcher_test.cc Outdated
@joe-redpanda joe-redpanda force-pushed the joe/fix-alloc-reserve-underflow branch from 01db72c to 93eb7f7 Compare July 14, 2026 18:24
@joe-redpanda joe-redpanda marked this pull request as ready for review July 14, 2026 18:30
Copilot AI review requested due to automatic review settings July 14, 2026 18:30
Adds unit tests for allocated partition.
Why?
We found a bug in it that could reasonably have been caught with unit
test coverage.
Adds a convenience wrapper on allocated partition's _original_node2shard
map.

There is a nuance in the logic where nullopt is not the same as empty
which is not terribly apparent to a code reader.

This change allows for the mistake to be self evident.
Regression test which simulates what happens when a reallocate is called
with the same replication factor that the partition had prior.

The long and short: allocated partition would mistakenly consider all
replicas on the partition to be new replicas, and delete all of the
allocations.

This would eventually bubble up as a node crash on double freed
allocations.

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.

Pull request overview

Fixes an allocation-state underflow/double-free triggered by “no-op” reallocations (requested RF equals current replica count), where existing replicas were incorrectly treated as newly allocated and then released.

Changes:

  • Add a regression test covering allocator behavior when allocating with RF equal to the existing replica count.
  • Refactor allocated_partition to track an “original placement snapshot” via a helper type, and adjust release/destructor logic to only release truly new replicas.
  • Add a focused allocated_partition unit test suite and Bazel target to exercise allocation counter behavior (no-op, new partition, move, revert, destructor reconciliation).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/v/cluster/tests/topic_updates_dispatcher_test.cc Adds an end-to-end regression test ensuring no-op reallocate does not deallocate existing replicas.
src/v/cluster/scheduling/types.h Introduces allocated_partition::original_node2shard helper and test friendship to make original-placement handling explicit.
src/v/cluster/scheduling/types.cc Implements snapshot helper, fixes “added replicas” reporting for no-op reallocations, and adjusts destructor reconciliation behavior.
src/v/cluster/scheduling/tests/BUILD Adds Bazel redpanda_cc_gtest target for the new allocated_partition unit tests.
src/v/cluster/scheduling/tests/allocated_partition_test.cc Adds unit tests that validate allocation/final-count accounting across key allocated_partition scenarios.

Comment on lines 14 to 17
#include "base/format_to.h"
#include "base/vassert.h"
#include "cluster/scheduling/allocation_state.h"
#include "utils/exceptions.h"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

will fix if this gets another pass

@joe-redpanda joe-redpanda force-pushed the joe/fix-alloc-reserve-underflow branch from 93eb7f7 to c13eb6f Compare July 14, 2026 18:37
Fixes the logic for yielding new replicas, if there were no changes,
there are very clearly no new replicas.
Use is_original() + one pass instead of find/erase scratch; get() is now
only used for full iteration.
@joe-redpanda joe-redpanda force-pushed the joe/fix-alloc-reserve-underflow branch from c13eb6f to 7d32db4 Compare July 14, 2026 19:05
@vbotbuildovich

Copy link
Copy Markdown
Collaborator

CI test results

test results on build#87085
test_status test_class test_method test_arguments test_kind job_url passed reason test_history
FLAKY(PASS) ConsumerGroupBalancingTest test_coordinator_nodes_balance null integration https://buildkite.com/redpanda/redpanda/builds/87085#019f620b-925c-453f-b505-623ab2c45ccb 10/11 Test PASSES after retries.No significant increase in flaky rate(baseline=0.0058, p0=1.0000, reject_threshold=0.0100. adj_baseline=0.1000, p1=0.3487, trust_threshold=0.5000) https://redpanda.metabaseapp.com/dashboard/87-tests?tab=142-dt-individual-test-history&test_class=ConsumerGroupBalancingTest&test_method=test_coordinator_nodes_balance
FLAKY(PASS) NodeWiseRecoveryTest test_recovery_local_data_missing {"wait_for_final_manifest_uploads": false} integration https://buildkite.com/redpanda/redpanda/builds/87085#019f6210-1955-49a2-bec7-5baf8a974df7 9/11 Test PASSES after retries.No significant increase in flaky rate(baseline=0.0836, p0=0.5823, reject_threshold=0.0100. adj_baseline=0.2304, p1=0.2910, trust_threshold=0.5000) https://redpanda.metabaseapp.com/dashboard/87-tests?tab=142-dt-individual-test-history&test_class=NodeWiseRecoveryTest&test_method=test_recovery_local_data_missing
FLAKY(PASS) ShadowLinkingRandomOpsTest test_node_operations {"failures": false, "workload_set": "cloud_combos"} integration https://buildkite.com/redpanda/redpanda/builds/87085#019f6210-1956-4c5b-96bb-234930314b03 10/11 Test PASSES after retries.No significant increase in flaky rate(baseline=0.0263, p0=1.0000, reject_threshold=0.0100. adj_baseline=0.1000, p1=0.3487, trust_threshold=0.5000) https://redpanda.metabaseapp.com/dashboard/87-tests?tab=142-dt-individual-test-history&test_class=ShadowLinkingRandomOpsTest&test_method=test_node_operations

Comment on lines +312 to +321
auto find(this Self&& self, model::node_id node) {
vassert(self._snapshot, "original placement snapshot not captured");
return std::forward<Self>(self)._snapshot->find(node);
}

template<typename Self>
auto end(this Self&& self) {
vassert(self._snapshot, "original placement snapshot not captured");
return std::forward<Self>(self)._snapshot->end();
}

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.

can we club these both into contains(node).. end() API is a little odd.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

are you thinking like find -> optional where nullopt implies not found?

the deducing this nonsense is for const correctness on the returned iterator (cend & normal end for mutable)

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.

all the callers of find() are essentially doing foo.find() != foo.end() .. which is effectively equivalent to contains(), so I was wondering if we even need find() and and end() as separate methods.

@joe-redpanda joe-redpanda Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

theres a few
find
found == end
erase

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

actually, no I think you're right, the find end erase gets folded away in the destructor commit. I'll add another cleanup round and remove the unused apis

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

add has one legitimate usage of find with the it

@joe-redpanda joe-redpanda merged commit 7656fac into redpanda-data:dev Jul 15, 2026
18 checks passed
@vbotbuildovich

Copy link
Copy Markdown
Collaborator

/backport v26.1.x

@vbotbuildovich

Copy link
Copy Markdown
Collaborator

/backport v25.3.x

@vbotbuildovich

Copy link
Copy Markdown
Collaborator

/backport v25.2.x

@vbotbuildovich

Copy link
Copy Markdown
Collaborator

Failed to create a backport PR to v25.3.x branch. I tried:

git remote add upstream https://github.com/redpanda-data/redpanda.git
git fetch --all
git checkout -b backport-pr-31071-v25.3.x-178 remotes/upstream/v25.3.x
git cherry-pick -x f2694e9e12 939fb1bfd1 4024162ca5 3b8d9c0f19 7d32db4beb

Workflow run logs.

@vbotbuildovich

Copy link
Copy Markdown
Collaborator

Failed to create a backport PR to v25.2.x branch. I tried:

git remote add upstream https://github.com/redpanda-data/redpanda.git
git fetch --all
git checkout -b backport-pr-31071-v25.2.x-73 remotes/upstream/v25.2.x
git cherry-pick -x f2694e9e12 939fb1bfd1 4024162ca5 3b8d9c0f19 7d32db4beb

Workflow run logs.

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.

4 participants