This is an automated email from the ASF dual-hosted git repository.

krishvishal pushed a commit to branch part-mat-barrier
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/part-mat-barrier by this push:
     new 2e78511bb fix: address review comments
2e78511bb is described below

commit 2e78511bb68ed2769dc528515f77609c7054da74
Author: Krishna Vishal <[email protected]>
AuthorDate: Mon Aug 3 23:15:07 2026 +0530

    fix: address review comments
---
 .../cluster/multi_shard_partition_convergence.rs   |  38 +-
 core/server-ng/Cargo.toml                          |  14 +-
 core/server-ng/src/partition_reconciler.rs         | 504 ++++++++++-------
 core/shard/src/lib.rs                              | 602 ++++++++++++---------
 core/shard/src/metrics.rs                          |  34 +-
 5 files changed, 727 insertions(+), 465 deletions(-)

diff --git 
a/core/integration/tests/cluster/multi_shard_partition_convergence.rs 
b/core/integration/tests/cluster/multi_shard_partition_convergence.rs
index 82376abf2..a26fbd293 100644
--- a/core/integration/tests/cluster/multi_shard_partition_convergence.rs
+++ b/core/integration/tests/cluster/multi_shard_partition_convergence.rs
@@ -51,6 +51,7 @@
 #![cfg(feature = "vsr")]
 
 use iggy::prelude::*;
+use integration::harness::TestHarness;
 use integration::iggy_harness;
 
 const STREAM: &str = "convergence-stream";
@@ -119,6 +120,25 @@ async fn poll_payloads(
         .collect()
 }
 
+/// Assert no park path degraded. All three modes log at `warn`, which the
+/// harness captures by default, so counting them pins the mechanism 
positively:
+/// the round trips stay green whether a produce parked and re-dispatched or 
was
+/// denied and replayed, and only these markers tell the two apart.
+fn assert_no_degraded_park_paths(harness: &TestHarness) {
+    for marker in [
+        "park buffer at capacity",
+        "outlived their admission window",
+        "rejecting parked partition frame",
+    ] {
+        assert_eq!(
+            harness.server().stdout_occurrences(marker),
+            0,
+            "server log must not contain {marker:?}: the park buffer degraded 
instead of \
+             converging"
+        );
+    }
+}
+
 /// Topics are created in a batch first, so several materialisations are in
 /// flight at once when the produces start.
 #[iggy_harness(cluster_nodes = 1, server(system.sharding.cpu_allocation = 
"2"))]
@@ -148,6 +168,7 @@ async fn 
given_two_shards_when_producing_right_after_create_topic_should_round_t
             "topic {index} must return the message produced right after its 
creation"
         );
     }
+    assert_no_degraded_park_paths(harness);
 }
 
 /// Delete + recreate reuses the freed slab keys, so the namespace is
@@ -156,14 +177,14 @@ async fn 
given_two_shards_when_producing_right_after_create_topic_should_round_t
 /// multi-shard node: the recreated topic serves its own data and none of the
 /// dead incarnation's.
 ///
-/// It does NOT exercise the incarnation fence. Each step here is a completed
-/// round trip, so the reconciler converges before the next one starts and
-/// `serves_committed_incarnation` never denies (verified: zero "unverified
-/// incarnation" denials in the server log across a full run). Driving the 
fence
-/// needs a produce concurrent with the delete, from a second connection --
-/// worth adding, but it is a different test. What this one catches is the
-/// steady-state failure modes: a rebuild that wedges, or stale segments
-/// surviving the delete and being served under the recycled identity.
+/// Does NOT exercise the incarnation fence. Every step is a completed round
+/// trip, so the reconciler converges before the next starts and
+/// `serves_committed_incarnation` has nothing to deny. That is an argument 
from
+/// sequencing, not an observation: the deny logs at `debug` and the harness 
runs
+/// the server at `info`, so its absence from the log proves nothing. Driving 
the
+/// fence needs a produce concurrent with the delete from a second connection,
+/// which is a different test. This one catches the steady-state failures: a
+/// rebuild that wedges, or stale segments served under the recycled identity.
 #[iggy_harness(cluster_nodes = 1, server(system.sharding.cpu_allocation = 
"2"))]
 async fn 
given_two_shards_when_recreating_a_topic_should_serve_only_the_new_incarnation(
     harness: &TestHarness,
@@ -198,4 +219,5 @@ async fn 
given_two_shards_when_recreating_a_topic_should_serve_only_the_new_inca
             "topic {index} must serve only the recreated incarnation"
         );
     }
+    assert_no_degraded_park_paths(harness);
 }
diff --git a/core/server-ng/Cargo.toml b/core/server-ng/Cargo.toml
index 695f1ecae..ce6a86d68 100644
--- a/core/server-ng/Cargo.toml
+++ b/core/server-ng/Cargo.toml
@@ -184,13 +184,13 @@ vergen-git2 = { workspace = true }
 assert_cmd = { workspace = true }
 bytemuck = { workspace = true }
 iggy = { workspace = true }
-# The reconciler's unit tests assert on `ShardMetrics` snapshots and
-# `IggyShard::parked_frame_count`, which are gated to test/simulator builds so
-# they cannot grow production callers. `shard`'s own `cfg(test)` is false when 
it
-# is compiled as our dependency, so the feature is how those accessors become
-# visible here. Dev-only: a production `cargo build -p iggy-server-ng` does not
-# resolve dev-dependencies, so nothing extra is compiled in.
-shard = { path = "../shard", features = ["simulator"] }
+# Reconciler unit tests assert on `ShardMetrics` snapshots and
+# `IggyShard::parked_frame_count`, gated to test/simulator so they cannot grow
+# production callers. `shard`'s own `cfg(test)` is false when compiled as our
+# dependency, so the feature is how those accessors become visible. The 
resolver
+# keeps a dev-dependency's features out of non-test targets, so a production
+# build still links `shard` without `simulator`.
+shard = { workspace = true, features = ["simulator"] }
 tokio = { workspace = true, features = ["full", "test-util"] }
 
 [lints.clippy]
diff --git a/core/server-ng/src/partition_reconciler.rs 
b/core/server-ng/src/partition_reconciler.rs
index dafd190cf..b150e9ecb 100644
--- a/core/server-ng/src/partition_reconciler.rs
+++ b/core/server-ng/src/partition_reconciler.rs
@@ -46,14 +46,14 @@
 //!   drain whose epoch disagrees with that stamp answers the client instead of
 //!   serving it: recycled slab keys make the namespace byte-identical, so 
such a
 //!   frame would otherwise land a dead topic's write inside the topic that
-//!   replaced it. A frame parked with NO stamp is served -- see
-//!   `redispatch_parked_frames` for why absence of a committed revision is not
-//!   evidence of a prior incarnation. Re-queuing appends, so a parked frame is
-//!   ordered behind whatever is already in the inbox rather than restored to 
its
-//!   original arrival position; a frame the inbox refuses is re-parked rather
-//!   than answered, since the deny would ride the same full sender, and the 
pump
-//!   re-drives it on its next iteration (`retry_reparked_frames`) once a slot
-//!   has freed.
+//!   replaced it. One gap, recorded below: the stamp is re-derived if the 
frame
+//!   re-enters the park path from the inbox. A frame parked with NO stamp is
+//!   served; see `redispatch_parked_frames` for why a missing committed 
revision
+//!   is not evidence of a prior incarnation. Re-queuing appends, so a parked
+//!   frame is ordered behind whatever is already in the inbox. A frame the 
inbox
+//!   refuses is re-parked rather than answered, since the deny would ride the
+//!   same full sender, and the pump re-drives it (`retry_reparked_frames`) 
once
+//!   a slot frees.
 //! - `IggyShard::serves_committed_incarnation` refuses a namespace whose
 //!   committed `created_revision` disagrees with the epoch on the local row, 
so
 //!   a request arriving mid-teardown cannot be acked against the incarnation
@@ -82,17 +82,28 @@
 //! that level-triggered repair for cross-core delta propagation that has to be
 //! ordered, retried, and repaired to stay correct.
 //!
-//! Park residency is bounded on three axes, because the frame count alone 
bounds
-//! nothing useful (`Message::into_generic` is a retag, so each entry retains 
its
-//! whole buffer, up to 64 MiB): a per-namespace frame cap, a shard-wide byte
-//! budget, and an age in reconciler passes. The per-namespace byte cap is 
waived
-//! for the first frame of an empty entry, or a frame larger than it could 
never
-//! park at all and a replicated prepare would be lost outright. Every frame 
that
-//! leaves the buffer unserved is counted under
-//! `frame_drops_total{variant=partition}` -- `park_overflow` when it was shed 
on
-//! arrival, `park_dropped` when it parked and then aged out or lost its
-//! namespace. A client request is additionally answered with a retriable 
status;
-//! a prepare has nobody to answer, which is why the counter is the record.
+//! Park residency is bounded on three axes, since the frame count alone bounds
+//! nothing useful (`Message::into_generic` is a retag, so each entry keeps its
+//! whole buffer, up to 64 MiB): a per-namespace frame cap, byte budgets per
+//! namespace and per shard, and an age in reconciler passes.
+//!
+//! They apply asymmetrically, because the two frame classes fail differently. 
A
+//! shed request costs a retry: answered with a retriable status, re-issued by
+//! the SDK. A shed prepare is permanent loss on this replica, with no client 
to
+//! answer and `consensus::retransmit_targets` skipping any op that already
+//! reached quorum. So a request is refused the moment admitting it would 
cross a
+//! budget, and answered once it outlives `MAX_PARKED_PASSES`; a prepare never
+//! expires by age and is refused only once a budget is already spent. That 
caps
+//! prepare residency at one frame of overshoot per budget instead of at the
+//! budget, and is what makes an oversize frame parkable at all: measured 
against
+//! an empty entry, a 5 MiB append would fail the per-namespace check on every
+//! attempt. A parked prepare is destroyed only for a namespace this shard 
cannot
+//! serve at all, tombstoned or not hashing here.
+//!
+//! Everything leaving the buffer unserved is counted under
+//! `frame_drops_total{variant=partition}`: `park_overflow` when shed on 
arrival,
+//! `park_dropped` when it parked and then lost its namespace. A prepare has
+//! nobody to answer, so the counter is the only record it existed.
 //!
 //! # Known gaps
 //!
@@ -100,12 +111,24 @@
 //! materialization barrier this module used to promise, and the barrier is 
gone
 //! (see above) while these are not:
 //!
-//! TODO(krishna): a shed or refused *prepare* has no recovery once its op has
+//! TODO(krishna): a shed or discarded *prepare* has no recovery once its op 
has
 //! reached quorum. `consensus::retransmit_targets` skips entries with
 //! `ok_quorum_received`, and the partition plane creates a repair session only
 //! in `on_start_view` -- `tick_partitions` re-drives an existing session but
 //! cannot open one -- so the backup stays behind `commit_max` until an 
unrelated
-//! view change. It needs a normal-status repair driver.
+//! view change. It needs a normal-status repair driver. The park policy above
+//! shrinks the exposure to two cases, a genuinely exhausted budget and a
+//! namespace this shard cannot serve, but only the repair driver removes it.
+//!
+//! TODO(krishna): the park stamp is not stable across re-entry. A 
re-dispatched
+//! frame still in the inbox when a delete + recreate completes 
(`ConfirmRemove`
+//! removes and untombstones in one arm, then the rebuild lands) re-enters
+//! `park_if_unmaterialised` and is re-stamped with the NEW revision and
+//! `passes: 0`, then served against the replacement: the write the stamp 
exists
+//! to block. Narrow (a full delete + recreate has to finish while one frame
+//! waits), but the guarantee is not absolute the way the bullet above reads.
+//! Closing it needs the frame to carry provenance through the inbox instead of
+//! re-deriving it on arrival.
 //!
 //! TODO(krishna): re-dispatch APPENDS to the inbox, so a parked prepare loses 
its
 //! arrival position. `router.rs`'s `select_biased!` puts the consensus tick 
(which
@@ -344,8 +367,11 @@ struct PassCounters {
     /// blocked by a consumer barrier or by a rejoin whose offsets land via
     /// journal repair, and neither unblocking bumps `Streams::revision`.
     trims_pending: usize,
-    /// Namespaces whose parked frames were answered because this shard is not
-    /// going to materialise them (see [`reconcile_parked_frames`]).
+    /// Namespaces the sweep acted on (see [`reconcile_parked_frames`]):
+    /// discarded because this shard cannot serve them, or aged past
+    /// `MAX_PARKED_PASSES` while it still might. Namespaces, not frames, and
+    /// acted on is not answered: aging answers requests, discarding also
+    /// destroys prepares.
     parked_reclaimed: usize,
     /// Rebuilds deferred until an in-flight `ConfirmRemove` drains. Counted
     /// so the pass does not arm the fast-skip: the pump's drop clears the
@@ -457,10 +483,10 @@ async fn reconcile_once(ctx: &ReconcilerCtx) -> bool {
     true
 }
 
-/// Returns the namespaces whose `ReconcileOp::InsertOwned` this pass staged.
-/// They are not in `IggyPartitions` yet -- the pump applies the op on its own
-/// task -- so [`reconcile_parked_frames`] would otherwise read them as
-/// un-materialised and age their frames on the very pass that built them.
+/// Returns the namespaces whose `ReconcileOp::InsertOwned` this pass staged. 
The
+/// pump applies the op on its own task, so they are not in `IggyPartitions` 
yet
+/// and [`reconcile_parked_frames`] would read them as un-materialised, aging
+/// their frames on the pass that built them.
 async fn reconcile_additions(
     ctx: &ReconcilerCtx,
     target: Vec<(IggyNamespace, u64)>,
@@ -603,46 +629,50 @@ async fn reconcile_additions(
     staged
 }
 
-/// Answer parked frames for namespaces this shard is not going to materialise.
+/// Retire parked frames the shard cannot serve, age the ones it might.
+///
+/// `park_if_unmaterialised` holds a frame until `ReconcileOp::InsertOwned` 
lands;
+/// the only other drains are `ConfirmRemove` and `RemoveRouted`. Neither can 
name
+/// a namespace that was never built, since it is in neither `IggyPartitions` 
(no
+/// owned ghost for `reconcile_removals`) nor `shards_table` (the owner seeds a
+/// row only via `InsertOwned`, and emits `InsertRouted` only for namespaces it
+/// does NOT own). Without this sweep the frames are held for the process 
lifetime
+/// and every waiting client burns its full read-timeout.
 ///
-/// `park_if_unmaterialised` holds a frame until `ReconcileOp::InsertOwned` 
lands
-/// for its namespace, and the only other things that drain the entry are
-/// `ConfirmRemove` and `RemoveRouted`. Neither can name a namespace that was
-/// never built: it is absent from `IggyPartitions` (so `reconcile_removals`
-/// sees no owned ghost) and absent from `shards_table` (the owner seeds a row
-/// only via `InsertOwned`, and emits `InsertRouted` only for namespaces it 
does
-/// NOT own). So without this sweep the frames are held for the process
-/// lifetime and every waiting client burns its full response read-timeout.
+/// Immediate discard needs positive evidence this shard can never serve the
+/// namespace, because it destroys prepares outright. Two signals carry it: the
+/// namespace is tombstoned (a wedged disk delete can postpone the
+/// `ConfirmRemove` that would otherwise answer them indefinitely), or it does 
not
+/// hash here, so no `InsertOwned` for it ever lands.
 ///
-/// Immediate reclaim needs positive evidence that the build will not finish. 
Two
-/// signals carry it: `build_partition_fresh` failed (ENOSPC, EPERM) and is 
backed
-/// off -- the backoff clamps at 60s, well past the client's 30s read timeout, 
so
-/// holding the frames cannot help -- or the namespace does not hash to this 
shard
-/// at all, so no `InsertOwned` for it will ever land here.
+/// A failed `build_partition_fresh` is not that evidence, which is why the
+/// backoff no longer discards. `next_backoff(1)` is one second, so the first
+/// transient ENOSPC destroyed every parked frame for a namespace that 
rebuilds a
+/// second later; the 60s clamp the old reasoning leaned on takes seven
+/// consecutive failures.
 ///
-/// Absence from the target set is NOT that evidence, which is why this no 
longer
-/// consults it. "Not in the target" covers a namespace that left committed
-/// metadata AND one this replica has simply not applied yet, and those are
-/// indistinguishable from local state: `snapshot_target_namespaces` reads this
-/// node's committed metadata, so a metadata-lagging backup reports a 
namespace it
-/// is milliseconds from committing exactly as it reports a deleted one. 
Reclaiming
-/// on that reading destroys the in-flight traffic the park buffer exists to 
hold
-/// (silently, for a replicated prepare, which has no client to answer). The 
stale
-/// reading was doubly wrong: `target_set` is snapshotted before
-/// `reconcile_additions` awaits `build_partition_fresh`, so a topic committing
-/// during those awaits was judged against a set that predates it.
+/// Absence from the target set is not evidence either. It covers both a 
namespace
+/// that left committed metadata and one this replica has not applied yet, and
+/// local state cannot tell them apart: `snapshot_target_namespaces` reads this
+/// node's committed metadata, so a lagging backup reports a namespace it is
+/// milliseconds from committing exactly as it reports a deleted one. It is 
also
+/// snapshotted before `reconcile_additions` awaits `build_partition_fresh`, 
so a
+/// topic committing during those awaits is judged against a stale set.
 ///
-/// Everything without that evidence -- building, still committing, or 
genuinely
-/// deleted -- is aged instead. 
[`shard::IggyShard::age_parked_partition_frames`]
-/// answers frames past `MAX_PARKED_PASSES`, so residency stays bounded and no
-/// client waits out its read timeout; the deleted case simply takes a few 
passes
-/// rather than one. The bound is residency only -- the SDK replays the 
identical
-/// request, so answering a late frame does not stop its operation from being
-/// applied late (see `ParkedFrame::passes`).
+/// Everything else is aged: building, backed off, still committing, genuinely
+/// deleted, or materialised with frames the inbox refused.
+/// [`shard::IggyShard::age_parked_partition_frames`] answers CLIENT REQUESTS 
past
+/// `MAX_PARKED_PASSES` and leaves prepares alone, so no client waits out its 
read
+/// timeout and no committed op dies on a local-convergence signal. Residency
+/// only; see `ParkedFrame::passes`.
 ///
-/// A namespace in `staged_this_pass` is exempt: its `ReconcileOp::InsertOwned`
-/// is queued for the pump but not applied, so it reads as un-materialised here
-/// and would burn one of its three passes on the pass that built it.
+/// `staged_this_pass` is exempt: its `InsertOwned` is queued but not applied, 
so
+/// it reads as un-materialised here. Not a one-pass concession.
+/// `reconcile_additions` has no cross-pass guard against a 
queued-but-unapplied
+/// op (it tests `partitions.contains`, false the whole time it sits in the
+/// queue), so it re-stages every pass until the pump drains. The exemption
+/// therefore covers arbitrary pump lag; dropping it ages frames on every
+/// commit-driven pass the pump falls behind.
 fn reconcile_parked_frames(
     ctx: &ReconcilerCtx,
     staged_this_pass: &AHashSet<IggyNamespace>,
@@ -654,57 +684,34 @@ fn reconcile_parked_frames(
     }
     let partitions = ctx.shard.plane.partitions();
     let total_shards = u32::from(ctx.total_shards);
-    let now = Instant::now();
     for ns in parked {
         if staged_this_pass.contains(&ns) {
             continue;
         }
-        // Tombstoned but still in the map, so `contains` is true: the fence
-        // forbids serving these frames and only `ConfirmRemove` would 
otherwise
-        // reach them, which a wedged disk delete postpones indefinitely (it
-        // retries under `FailureCause::Delete` backoff, clamped at 60s, past 
the
-        // client's read timeout). Nothing will ever serve them, so reclaim now
-        // rather than aging towards the same outcome.
-        if partitions.is_tombstoned(&ns) {
-            ctx.shard.reclaim_parked_partition_frames(ns);
+        // Tombstoned namespaces are still in the map, so `contains` below 
reads
+        // them as materialised.
+        let not_ours = calculate_shard_assignment(&ns, total_shards) != 
ctx.shard.id;
+        let tombstoned = partitions.is_tombstoned(&ns);
+        if tombstoned || not_ours {
+            debug!(
+                shard = ctx.shard.id,
+                ns_raw = ns.inner(),
+                tombstoned,
+                not_ours,
+                "discarding parked frames for a namespace this shard cannot 
serve"
+            );
+            ctx.shard.discard_parked_partition_frames(ns);
             counters.parked_reclaimed += 1;
             continue;
         }
-        if partitions.contains(&ns) {
-            // Materialised, yet frames remain: the re-dispatch found the inbox
-            // full and re-parked them. The pump retries on every iteration, so
-            // this is only the residency backstop for an inbox that never
-            // drains. Aging, not reclaiming: a refusal is transient, and
-            // `MAX_PARKED_PASSES` is the same bound the un-materialised case
-            // gets. Without it the frames have no exit at all -- the branch
-            // below never runs for a namespace already in `IggyPartitions`, 
and
-            // `reconcile_additions` stages no second `InsertOwned` for one.
-            if ctx.shard.age_parked_partition_frames(ns) > 0 {
-                counters.parked_reclaimed += 1;
-            }
-            continue;
-        }
-        // This shard will never materialise a namespace it does not own. The
-        // frame got here through a stale `shards_table` row (the table is a 
hash
-        // cache, never a readiness proof), so no `InsertOwned` will ever 
drain it
-        // and aging is otherwise its only exit.
-        let not_ours = calculate_shard_assignment(&ns, total_shards) != 
ctx.shard.id;
-        let backed_off = ctx.is_backed_off(ns, FailureCause::Add, now);
-        if !not_ours && !backed_off {
-            if ctx.shard.age_parked_partition_frames(ns) > 0 {
-                counters.parked_reclaimed += 1;
-            }
-            continue;
+        // Materialised with frames still parked means the re-dispatch hit a 
full
+        // inbox and re-parked them. The pump retries every iteration, so 
aging is
+        // only the backstop for an inbox that never drains. Without it they 
have
+        // no exit: `reconcile_additions` stages no second `InsertOwned` for a
+        // namespace already in `IggyPartitions`.
+        if ctx.shard.age_parked_partition_frames(ns) > 0 {
+            counters.parked_reclaimed += 1;
         }
-        debug!(
-            shard = ctx.shard.id,
-            ns_raw = ns.inner(),
-            not_ours,
-            backed_off,
-            "reclaiming parked frames for a namespace this shard will not 
materialise"
-        );
-        ctx.shard.reclaim_parked_partition_frames(ns);
-        counters.parked_reclaimed += 1;
     }
 }
 
@@ -1199,22 +1206,7 @@ mod tests {
     /// receives it off the wire. Only the routing fields matter: parking reads
     /// `operation` + `namespace` and never touches the body.
     fn build_partition_request(namespace: IggyNamespace) -> 
Message<GenericHeader> {
-        let header_size = size_of::<RequestHeader>();
-        let mut msg = Message::<RequestHeader>::new(header_size);
-        let header = bytemuck::checked::try_from_bytes_mut::<RequestHeader>(
-            &mut msg.as_mut_slice()[..header_size],
-        )
-        .expect("zeroed bytes form a valid RequestHeader");
-        header.command = Command2::Request;
-        header.size = u32::try_from(header_size).expect("request size fits 
u32");
-        header.operation = Operation::SendMessages;
-        header.namespace = namespace.inner();
-        // Header validation rejects a zero session / request on a non-register
-        // op, and the park path runs after that validation.
-        header.session = 1;
-        header.request = TEST_REQUEST_ID;
-        header.client = TEST_CLIENT_ID;
-        msg.into_generic()
+        build_partition_request_sized(namespace, 0)
     }
 
     /// Park one client request for `namespace` through the real pump entry
@@ -1242,6 +1234,8 @@ mod tests {
         header.size = u32::try_from(total_size).expect("request size fits 
u32");
         header.operation = Operation::SendMessages;
         header.namespace = namespace.inner();
+        // Header validation rejects a zero session / request on a non-register
+        // op, and the park path runs after that validation.
         header.session = 1;
         header.request = TEST_REQUEST_ID;
         header.client = TEST_CLIENT_ID;
@@ -1417,21 +1411,36 @@ mod tests {
         Rc::new(shard)
     }
 
-    /// [`build_test_shard`] with this shard's own inbox wired up, for the 
tests
-    /// that assert on work handed back to the pump (transient denies, 
parked-frame
-    /// re-dispatch). The receiver comes back so the caller keeps it alive and 
can
-    /// drain it; without a live receiver every `try_send` reports 
`Disconnected`.
+    /// [`build_test_shard`] with a sender mesh, for tests asserting on work
+    /// handed back to the pump (transient denies, parked-frame re-dispatch).
+    /// Caller must keep the returned receiver alive; dropping it turns every
+    /// `try_send` into `Disconnected`.
+    ///
+    /// Mesh covers `0..=shard_id` since consumers index `senders[shard_id]`.
+    /// Peer receivers are dropped, so a misroute fails loudly instead of 
landing
+    /// in this shard's inbox and reading as success.
     fn build_test_shard_with_inbox(
         shard_id: u16,
         config: &ServerNgConfig,
         mux: TestMux,
         capacity: usize,
     ) -> (Rc<TestShard>, shard::Receiver<shard::ShardFrame>) {
-        let (tx, rx) = shard::shard_channel(shard_id, capacity);
+        let mut senders = Vec::with_capacity(usize::from(shard_id) + 1);
+        let mut own_rx = None;
+        for peer in 0..=shard_id {
+            let (tx, rx) = shard::shard_channel(peer, capacity);
+            senders.push(tx);
+            if peer == shard_id {
+                own_rx = Some(rx);
+            }
+        }
         let mut shard = Rc::into_inner(build_test_shard(shard_id, config, mux))
             .expect("freshly built shard is uniquely owned");
-        shard.attach_self_sender(tx);
-        (Rc::new(shard), rx)
+        shard.attach_senders(senders);
+        (
+            Rc::new(shard),
+            own_rx.expect("the loop covers shard_id itself"),
+        )
     }
 
     /// Drain a test shard's inbox into `(re-dispatched frames, staged client
@@ -2592,7 +2601,7 @@ mod tests {
         // Drain path 3: an explicit reclaim.
         park_one_request(&shard, never).await;
         assert!(shard.has_parked_partition_frames());
-        shard.reclaim_parked_partition_frames(never);
+        shard.discard_parked_partition_frames(never);
         assert!(
             !shard.has_parked_partition_frames(),
             "reclaim must debit the parked-byte total"
@@ -2740,23 +2749,24 @@ mod tests {
         );
     }
 
-    /// Same hole, reached the other way: the namespace stays committed but
-    /// `build_partition_fresh` keeps failing. The `FailureCause::Add` backoff
-    /// clamps at 60s, twice the client's read timeout, so holding the frames
-    /// cannot help - answer them and let the client re-issue.
+    /// Namespace stays committed, `build_partition_fresh` keeps failing. One
+    /// failure must destroy nothing: `next_backoff(1)` is a second, the 
rebuild
+    /// usually lands on the next pass, and the sweep cannot tell a transient
+    /// ENOSPC from a permanent one. Request gets the age bound, prepare is 
kept.
     #[compio::test]
-    async fn parked_frames_are_reclaimed_while_the_build_is_backed_off() {
+    async fn a_backed_off_build_ages_requests_and_retains_prepares() {
         let tmp = TempDir::new().expect("tempdir for system path");
         let config = test_config(&tmp);
         let mux = TestMux::default();
         seed_stream(&mux, 1, "stream-backoff");
         seed_topic(&mux, 2, 0, "topic-backoff", vec![assignment(0, 1)]);
 
-        let shard = build_test_shard(0, &config, mux);
+        let (shard, inbox) = build_test_shard_with_inbox(0, &config, mux, 16);
         let ctx = make_ctx(Rc::clone(&shard), 1, Rc::new(config));
         let ns = IggyNamespace::new(0, 0, 0);
 
         park_one_request(&shard, ns).await;
+        park_one_prepare(&shard, ns, 7).await;
         assert_eq!(shard.parked_namespaces(), vec![ns]);
 
         // Stand in for a failed build (ENOSPC / EPERM): the additions pass 
skips
@@ -2768,9 +2778,40 @@ mod tests {
             !ctx.shard.plane.partitions().contains(&ns),
             "a backed-off namespace must not have been built"
         );
-        assert!(
-            shard.parked_namespaces().is_empty(),
-            "frames waiting on a backed-off build must be answered, not held"
+        assert_eq!(
+            shard.parked_frame_count(ns),
+            2,
+            "one failed build must not destroy anything; the backoff is a 
second"
+        );
+
+        // Held only until the request outlives the admission window.
+        for _ in 0..PARK_MAX_PASSES {
+            reconcile_pass(&ctx).await;
+        }
+        assert_eq!(
+            shard.parked_frame_count(ns),
+            1,
+            "the request must age out, so no client waits out its read timeout"
+        );
+        assert_eq!(
+            drain_staged_client_sends(&inbox),
+            1,
+            "and it must be answered, not dropped"
+        );
+        assert_eq!(
+            park_dropped_count(&shard),
+            0,
+            "the prepare must be retained: destroying it is unrecoverable"
+        );
+
+        // Retained across an unbounded number of further passes.
+        for _ in 0..(PARK_MAX_PASSES * 4) {
+            reconcile_pass(&ctx).await;
+        }
+        assert_eq!(
+            shard.parked_frame_count(ns),
+            1,
+            "prepares are bounded by the byte budget, never by the age bound"
         );
     }
 
@@ -2836,11 +2877,10 @@ mod tests {
     /// lockstep, so a silent shed leaves the connection waiting out its full
     /// response read-timeout.
     ///
-    /// Driven against a registered in-process client, so the assertion is 
that a
-    /// reply reached a waiter -- not that a counter moved. With no client on 
the
-    /// bus every send fails as `ClientNotFound` and a counter bumped before 
the
-    /// send reports an answer nobody received, which is the failure this test
-    /// exists to catch.
+    /// Registers an in-process client, so the assertion is that a reply 
reached
+    /// a waiter, not that a counter moved. With no client every send fails
+    /// `ClientNotFound`, and a counter bumped before the send reports an 
answer
+    /// nobody received.
     #[compio::test]
     async fn park_overflow_answers_the_client_instead_of_shedding_silently() {
         let tmp = TempDir::new().expect("tempdir for system path");
@@ -2898,11 +2938,9 @@ mod tests {
         );
     }
 
-    /// The counter must credit only denies the bus delivered. It previously
-    /// incremented before `send_to_client`, so a shard with no client 
registered
-    /// still reported the request answered - which blinded
-    /// `park_overflow_answers_the_client_instead_of_shedding_silently`, the 
one
-    /// test that asserts the client hears back.
+    /// Credit only denies the bus delivered. Incrementing before
+    /// `send_to_client` reported an answer with no client registered, blinding
+    /// `park_overflow_answers_the_client_instead_of_shedding_silently`.
     #[compio::test]
     async fn overflow_deny_is_not_counted_when_the_client_is_gone() {
         let tmp = TempDir::new().expect("tempdir for system path");
@@ -2939,12 +2977,12 @@ mod tests {
         );
     }
 
-    /// A parked prepare that ages out has no client to answer, so nothing is
-    /// staged and `partition_requests_denied_transient_total` stays put. The 
op
-    /// is destroyed all the same - the primary retransmits only what has not
-    /// reached quorum - so `park_dropped` is the only record it existed.
+    /// The age bound answers requests and steps over prepares. Expiring a
+    /// prepare is permanent loss (no client, and `retransmit_targets` skips 
an op
+    /// already at quorum), and passes are commit-driven, so a create burst
+    /// elapses four in milliseconds across every parked namespace at once.
     #[compio::test]
-    async fn aged_out_prepare_is_counted_even_though_nobody_can_be_answered() {
+    async fn aging_answers_requests_and_never_expires_a_prepare() {
         let tmp = TempDir::new().expect("tempdir for system path");
         let config = test_config(&tmp);
         let mux = TestMux::default();
@@ -2955,11 +2993,50 @@ mod tests {
         let ns = IggyNamespace::new(0, 0, 0);
 
         park_one_prepare(&shard, ns, 7).await;
+        park_one_request(&shard, ns).await;
         for _ in 0..=PARK_MAX_PASSES {
             shard.age_parked_partition_frames(ns);
         }
 
-        assert_eq!(shard.parked_frame_count(ns), 0, "the prepare aged out");
+        assert_eq!(
+            shard.parked_frame_count(ns),
+            1,
+            "the request ages out; the prepare stays"
+        );
+        assert_eq!(
+            drain_staged_client_sends(&inbox),
+            1,
+            "the request must be answered rather than dropped"
+        );
+        assert_eq!(
+            shard.metrics().partition_requests_denied_transient_value(),
+            1
+        );
+        assert_eq!(
+            park_dropped_count(&shard),
+            0,
+            "nothing may be destroyed on an age bound"
+        );
+    }
+
+    /// The one path that still destroys a prepare: namespace gone from this
+    /// shard, so holding it buys nothing. No client, so `park_dropped` is the
+    /// only record the op existed.
+    #[compio::test]
+    async fn 
a_discarded_prepare_is_counted_even_though_nobody_can_be_answered() {
+        let tmp = TempDir::new().expect("tempdir for system path");
+        let config = test_config(&tmp);
+        let mux = TestMux::default();
+        seed_stream(&mux, 1, "stream-prepare-discard");
+        seed_topic(&mux, 2, 0, "topic-prepare-discard", vec![assignment(0, 
1)]);
+
+        let (shard, inbox) = build_test_shard_with_inbox(0, &config, mux, 8);
+        let ns = IggyNamespace::new(0, 0, 0);
+
+        park_one_prepare(&shard, ns, 7).await;
+        shard.discard_parked_partition_frames(ns);
+
+        assert_eq!(shard.parked_frame_count(ns), 0, "the prepare is gone");
         assert_eq!(
             drain_staged_client_sends(&inbox),
             0,
@@ -2977,10 +3054,9 @@ mod tests {
         );
     }
 
-    /// A frame larger than the per-namespace byte cap used to fail the check
-    /// even against an empty entry, so it could never park on any attempt. 
For a
-    /// replicated prepare that is unrecoverable: `retransmit_targets` skips 
an op
-    /// that already reached quorum, so the backup stays permanently short of 
it.
+    /// A frame larger than the per-namespace cap failed the check even against
+    /// an empty entry, so it could never park. Unrecoverable for a prepare:
+    /// `retransmit_targets` skips an op already at quorum.
     #[compio::test]
     async fn 
a_frame_over_the_namespace_byte_cap_still_parks_into_an_empty_entry() {
         const OVER_NAMESPACE_CAP: usize = 5 * 1024 * 1024;
@@ -3105,14 +3181,16 @@ mod tests {
     }
 
     /// The frame cap bounds count, not residency: `Message::into_generic` is a
-    /// retag, so each parked entry keeps its whole buffer -- up to 64 MiB. 
With
-    /// only a frame cap, one namespace could pin 128 x 64 MiB and nothing 
capped
-    /// the namespace count. The shard-wide byte budget is what actually bounds
-    /// it, so large frames must shed well before the frame cap.
+    /// retag, so each entry keeps its whole buffer, up to 64 MiB. With only a
+    /// frame cap one namespace could pin 128 x 64 MiB, so bytes must bite 
first.
+    ///
+    /// Single namespace, so this proves the PER-NAMESPACE cap: 1 MiB bodies
+    /// charge 1052672 each, so the 4th crosses 4 MiB, nowhere near the 
shard-wide
+    /// 16 MiB that
+    /// [`park_shard_wide_byte_budget_sheds_a_namespace_that_would_cross_it`]
+    /// covers.
     #[compio::test]
-    async fn park_byte_budget_sheds_large_frames_before_the_frame_cap() {
-        const BODY: usize = 1024 * 1024;
-
+    async fn 
park_namespace_byte_budget_sheds_large_frames_before_the_frame_cap() {
         let tmp = TempDir::new().expect("tempdir for system path");
         let config = test_config(&tmp);
         let mux = TestMux::default();
@@ -3124,30 +3202,70 @@ mod tests {
 
         for _ in 0..PARK_CAP {
             shard
-                .on_message(build_partition_request_sized(ns, BODY))
+                .on_message(build_partition_request_sized(ns, MIB_BODY))
                 .await;
             if park_overflow_count(&shard) > 0 {
                 break;
             }
         }
 
-        assert!(
-            park_overflow_count(&shard) > 0,
-            "1 MiB frames must reach the shard-wide byte budget"
+        assert_eq!(
+            shard.parked_frame_count(ns),
+            PER_NAMESPACE_MIB_FRAMES,
+            "the per-namespace budget admits {PER_NAMESPACE_MIB_FRAMES} x 
{MIB_FOOTPRINT} \
+             and sheds the next"
         );
-        assert!(
-            shard.parked_frame_count(ns) < PARK_CAP,
-            "the byte budget must bite before the frame cap; parked {} of 
{PARK_CAP}",
-            shard.parked_frame_count(ns)
+        assert_eq!(park_overflow_count(&shard), 1);
+    }
+
+    /// Shard-wide budget on its own terms: fill several namespaces to just 
under
+    /// it, then one frame into a fresh namespace. The per-namespace check 
waives
+    /// the first frame of an empty entry, so only the shard-wide check can 
shed.
+    ///
+    /// Needs its own test because nothing else reaches it: a single namespace
+    /// hits its quarter-sized cap first, leaving `MAX_PARKED_BYTES` 
unexercised.
+    #[compio::test]
+    async fn 
park_shard_wide_byte_budget_sheds_a_namespace_that_would_cross_it() {
+        let tmp = TempDir::new().expect("tempdir for system path");
+        let config = test_config(&tmp);
+        let mux = TestMux::default();
+        seed_stream(&mux, 1, "stream-shard-bytes");
+        seed_topic(&mux, 2, 0, "topic-shard-bytes", vec![assignment(0, 1)]);
+
+        let shard = build_test_shard(0, &config, mux);
+        let filled = SHARD_BUDGET / (PER_NAMESPACE_MIB_FRAMES * MIB_FOOTPRINT);
+        for topic_id in 0..filled {
+            let ns = IggyNamespace::new(0, topic_id, 0);
+            for _ in 0..PER_NAMESPACE_MIB_FRAMES {
+                shard
+                    .on_message(build_partition_request_sized(ns, MIB_BODY))
+                    .await;
+            }
+        }
+        assert_eq!(
+            park_overflow_count(&shard),
+            0,
+            "{filled} namespaces x {PER_NAMESPACE_MIB_FRAMES} frames must all 
fit"
         );
+
+        // One frame into an empty entry: per-namespace waived, shard-wide not.
+        let crossing = IggyNamespace::new(0, filled, 0);
+        shard
+            .on_message(build_partition_request_sized(crossing, MIB_BODY))
+            .await;
+        assert_eq!(
+            shard.parked_frame_count(crossing),
+            0,
+            "the frame that would cross the shard-wide budget must be shed"
+        );
+        assert_eq!(park_overflow_count(&shard), 1);
     }
 
-    /// A re-dispatch the inbox refuses re-parks the frame, and by then the
-    /// namespace is materialised - which closes every other exit. The sweep
-    /// skips a namespace in `IggyPartitions`, and `reconcile_additions` stages
-    /// no second `InsertOwned` for one, so before the pump-side retry the 
frame
-    /// sat there until a topic delete: the client never answered, the 
shard-wide
-    /// byte budget never returned, and the revision fast-skip never re-armed.
+    /// A refused re-dispatch re-parks the frame, and by then the namespace is
+    /// materialised, closing every other exit: the sweep skips a namespace in
+    /// `IggyPartitions` and `reconcile_additions` stages no second
+    /// `InsertOwned`. Before the pump retry the frame sat until a topic 
delete,
+    /// unanswered, with its bytes charged and the fast-skip never re-arming.
     #[compio::test]
     async fn a_re_parked_frame_is_re_dispatched_once_the_inbox_drains() {
         let tmp = TempDir::new().expect("tempdir for system path");
@@ -3196,9 +3314,9 @@ mod tests {
     }
 
     /// A namespace mid-teardown is still in `IggyPartitions`, so it reads as
-    /// materialised while the fence forbids serving anything. `ConfirmRemove`
-    /// would answer its frames, but a disk delete that keeps failing never
-    /// enqueues one, so the sweep must reclaim them itself.
+    /// materialised while the fence forbids serving it. `ConfirmRemove` would
+    /// answer its frames, but a disk delete that keeps failing never enqueues
+    /// one, so the sweep has to.
     #[compio::test]
     async fn 
parked_frames_of_a_tombstoned_namespace_are_reclaimed_without_confirm_remove() {
         let tmp = TempDir::new().expect("tempdir for system path");
@@ -3236,9 +3354,8 @@ mod tests {
         );
     }
 
-    /// Residency backstop for the same path: an inbox that never drains must 
not
-    /// hold a re-parked frame forever. `MAX_PARKED_PASSES` applies to a
-    /// materialised namespace too, so the sweep eventually answers it.
+    /// Residency backstop: an inbox that never drains must not hold a 
re-parked
+    /// frame forever, so `MAX_PARKED_PASSES` covers a materialised namespace 
too.
     #[compio::test]
     async fn a_re_parked_frame_ages_out_when_the_inbox_never_drains() {
         let tmp = TempDir::new().expect("tempdir for system path");
@@ -3268,6 +3385,17 @@ mod tests {
 
     /// Mirrors `MAX_PARKED_PER_NAMESPACE` in `shard::park_if_unmaterialised`.
     const PARK_CAP: usize = 128;
+    /// Mirrors `MAX_PARKED_BYTES`.
+    const SHARD_BUDGET: usize = 16 * 1024 * 1024;
+    /// Mirrors `MAX_PARKED_BYTES_PER_NAMESPACE`.
+    const NAMESPACE_BUDGET: usize = SHARD_BUDGET / 4;
+    /// Body the byte-budget tests park, and its charged footprint: a parked
+    /// frame keeps its whole `MESSAGE_ALIGN`-granular buffer, so the header
+    /// pushes a 1 MiB body into the next page.
+    const MIB_BODY: usize = 1024 * 1024;
+    const MIB_FOOTPRINT: usize = MIB_BODY + 4096;
+    /// [`MIB_BODY`] frames one namespace admits before its budget refuses 
more.
+    const PER_NAMESPACE_MIB_FRAMES: usize = NAMESPACE_BUDGET / MIB_FOOTPRINT;
     /// Mirrors `MAX_PARKED_PASSES`.
     const PARK_MAX_PASSES: u32 = 3;
     /// Top 16 bits are the owning shard, so this resolves to shard 0's 
registry
@@ -3309,9 +3437,9 @@ mod tests {
         )
     }
 
-    /// Register the client id [`build_partition_request`] stamps, with a 
waiter
-    /// for its request id, so a deny that reaches the bus resolves a oneshot 
the
-    /// test can await. The guard keeps the slot installed.
+    /// Register the client id [`build_partition_request`] stamps plus a waiter
+    /// for its request id, so a deny reaching the bus resolves a oneshot. The
+    /// guard keeps the slot installed.
     fn register_waiting_client(
         shard: &TestShard,
     ) -> (
diff --git a/core/shard/src/lib.rs b/core/shard/src/lib.rs
index 5445f6395..6bca33a19 100644
--- a/core/shard/src/lib.rs
+++ b/core/shard/src/lib.rs
@@ -1012,33 +1012,26 @@ where
     /// order would vary run to run for identical committed state, making the
     /// simulator's deny ordering unreproducible for a fixed seed -- the same
     /// hazard `router.rs` documents as its reason for `select_biased!`.
-    pending_partition_frames: RefCell<BTreeMap<IggyNamespace, 
Vec<ParkedFrame>>>,
+    pending_partition_frames: RefCell<BTreeMap<IggyNamespace, ParkEntry>>,
 
-    /// Running sum of [`parked_footprint`] over every frame in
-    /// [`Self::pending_partition_frames`], maintained at each mutation site.
+    /// Running sum of [`ParkEntry::bytes`], maintained at each mutation site.
     ///
-    /// Recomputing it per arriving frame is O(all parked frames): the budget
-    /// admits ~262k entries at 256 bytes each, so filling the buffer would 
cost
-    /// ~10^10 element visits on the reactor thread, inside the map's
-    /// `borrow_mut`, collapsing a saturated shard to a few hundred frames/sec
-    /// while starving the reconciler pass that would drain it.
+    /// Recomputing per arriving frame is O(all parked frames). Footprints 
floor
+    /// at [`MESSAGE_ALIGN`], so the budget admits 4096 entries: ~8.4M visits 
per
+    /// admission, on the reactor thread inside the map's `borrow_mut`.
     parked_partition_bytes: Cell<usize>,
 
-    /// Namespaces whose park entry still holds frames the inbox refused during
-    /// [`Self::redispatch_parked_frames`], so the pump re-drives them on its
-    /// next iteration.
+    /// Namespaces holding frames [`Self::redispatch_parked_frames`] could not
+    /// re-queue, for the pump to retry.
     ///
-    /// Without this set a re-parked frame has no exit at all. Its namespace is
-    /// materialised by then, so the reconciler sweep skips it, and
-    /// `reconcile_additions` skips a namespace already in `IggyPartitions`, so
-    /// no second `InsertOwned` ever stages another re-dispatch. Only a topic
-    /// delete would reach it. The retry runs on the pump, which is the task
-    /// that drains the inbox, so a refusal normally clears on the very next
-    /// iteration.
+    /// Without it a re-parked frame has no exit: its namespace is materialised
+    /// by then, so the sweep skips it and `reconcile_additions` stages no 
second
+    /// `InsertOwned`. Only a topic delete would reach it. The pump drains the
+    /// inbox, so a refusal usually clears on its next iteration.
     ///
-    /// [`BTreeSet`] for the same reason [`Self::pending_partition_frames`] is 
a
-    /// [`BTreeMap`]: the simulator replays a fixed seed, so iteration order 
has
-    /// to be a function of the namespaces alone.
+    /// [`BTreeSet`] for the reason [`Self::pending_partition_frames`] is a
+    /// [`BTreeMap`]: fixed-seed simulator replay needs iteration order to be a
+    /// function of the namespaces alone.
     reparked_partition_namespaces: RefCell<BTreeSet<IggyNamespace>>,
 
     /// Live ceiling on prepares served per `RequestPrepares` round. Defaults
@@ -1429,27 +1422,34 @@ where
         &self.metrics
     }
 
-    /// Attach this shard's own inbox sender to a shard built by
-    /// [`Self::without_inbox`], which leaves the mesh empty.
+    /// Attach the sender mesh to a shard built by [`Self::without_inbox`], 
which
+    /// leaves it empty.
     ///
     /// Exists for out-of-crate tests: the paths that hand work back to the 
pump
     /// (`stage_transient_deny`, the parked-frame re-dispatch) index
-    /// `senders[self.id]`, so without it they silently no-op and a test 
asserting
-    /// on them proves nothing. The caller must keep the paired receiver alive;
-    /// dropping it turns every `try_send` into `Disconnected`.
+    /// `senders[self.id]`, so without a mesh they silently no-op and a test
+    /// asserting on them proves nothing. The caller must keep the paired
+    /// receivers alive; dropping one turns every `try_send` into 
`Disconnected`.
+    ///
+    /// Whole mesh, not one sender: consumers index by shard id and
+    /// `forward_metadata_submit` indexes `senders[0]` unconditionally, so a
+    /// one-element vec is correct only for shard 0. `shard_count` tracks it, 
as
+    /// in both constructors.
     ///
     /// # Panics
-    /// If `sender` is not tagged for this shard, which would route this 
shard's
-    /// own frames to a peer.
-    pub fn attach_self_sender(&mut self, sender: TaggedSender) {
-        assert_eq!(
-            sender.shard_id(),
-            self.id,
-            "attach_self_sender: sender is tagged for shard {} but this is 
shard {}",
-            sender.shard_id(),
+    /// If the mesh is not ordered `senders[i].shard_id() == i` or does not 
cover
+    /// this shard. Either routes frames to the wrong pump.
+    #[cfg(any(test, feature = "simulator"))]
+    pub fn attach_senders(&mut self, senders: Vec<TaggedSender>) {
+        assert!(
+            (self.id as usize) < senders.len(),
+            "attach_senders: mesh of {} does not cover shard {}",
+            senders.len(),
             self.id
         );
-        self.senders = vec![sender];
+        validate_sender_ordering(&senders).expect("attach_senders: mesh must 
be ordered by shard");
+        self.shard_count = u32::try_from(senders.len()).expect("shard count 
fits u32");
+        self.senders = senders;
     }
 
     /// `None` removes the handler; subsequent ticks drop with a metric bump.
@@ -1543,10 +1543,9 @@ where
     /// fires right after a frame was consumed and a slot freed. Only another
     /// refusal puts a namespace back, so the set empties itself.
     ///
-    /// The epoch comes from the routing row: `InsertOwned` writes it alongside
-    /// the partition, so a materialised namespace always has one. Skipped when
-    /// the row is gone or the namespace is fenced -- teardown did both, and 
the
-    /// reconciler sweep answers the frames.
+    /// Epoch comes from the routing row, which `InsertOwned` writes alongside
+    /// the partition. Skipped when the row is gone or the namespace is fenced;
+    /// teardown does both, and the reconciler sweep retires the frames.
     fn retry_reparked_frames(&self) {
         let pending: Vec<IggyNamespace> = {
             let mut reparked = self.reparked_partition_namespaces.borrow_mut();
@@ -1575,9 +1574,8 @@ where
     where
         B: MessageBus + 'static,
     {
-        // Before the staged ops, and outside the empty-queue early return
-        // below: a re-parked frame is waiting on inbox capacity, not on 
another
-        // reconcile op, and a quiet shard stages none.
+        // Ahead of the staged ops and outside their empty-queue early return: 
a
+        // re-parked frame waits on inbox capacity, not on a reconcile op.
         self.retry_reparked_frames();
         let staged: Vec<ReconcileOp<B>> = {
             let mut q = self.reconcile_queue.borrow_mut();
@@ -1704,22 +1702,66 @@ enum ParkOutcome<H> {
 /// incarnation - and never the frame's provenance.
 struct ParkedFrame {
     epoch: Option<u64>,
-    /// Reconciler passes this frame has survived. The sweep in
-    /// `partition_reconciler::reconcile_parked_frames` increments it and
-    /// reclaims past [`MAX_PARKED_PASSES`], bounding how long a frame can sit
-    /// here in units the simulator's virtual clock already controls.
+    /// Reconciler passes survived. `reconcile_parked_frames` increments it and
+    /// answers CLIENT REQUESTS past [`MAX_PARKED_PASSES`], in units the
+    /// simulator's virtual clock controls.
     ///
-    /// This bounds RESIDENCY, not staleness. Answering a late frame does not 
stop
-    /// its operation from being applied late: the SDK replays the identical
-    /// request, same payload, for the rest of its response timeout, so an
-    /// absolute-offset `StoreConsumerOffset` that would have rewound the 
group on
-    /// admission rewinds it on the replay too. What the bound buys is a park
-    /// buffer that cannot accumulate without limit, and a client that learns 
the
-    /// outcome from a reply rather than a timeout.
+    /// Never expires a replicated prepare: no client to answer, and
+    /// `consensus::retransmit_targets` skips an op that already reached 
quorum,
+    /// so expiry is silent permanent loss. Byte budgets bound those instead.
+    ///
+    /// Bounds RESIDENCY, not staleness. The SDK replays the identical request
+    /// for the rest of its response timeout, so an absolute-offset
+    /// `StoreConsumerOffset` rewinds the group on the replay anyway. What it
+    /// buys: a buffer that cannot grow without limit, and a client that learns
+    /// the outcome from a reply rather than a timeout.
     passes: u32,
     message: Message<GenericHeader>,
 }
 
+impl ParkedFrame {
+    fn footprint(&self) -> usize {
+        parked_footprint(self.message.as_slice().len())
+    }
+
+    /// No client on this node: nothing to answer, nothing recovers it.
+    fn is_replicated(&self) -> bool {
+        self.message.header().command != Command2::Request
+    }
+}
+
+/// One namespace's parked frames plus their running footprint.
+///
+/// Carried, not re-summed: `park_if_unmaterialised` reads it per arriving 
frame
+/// over an entry up to [`MAX_PARKED_PER_NAMESPACE`] deep, so a rescan makes
+/// admission quadratic in the depth it exists to bound.
+#[derive(Default)]
+struct ParkEntry {
+    frames: Vec<ParkedFrame>,
+    bytes: usize,
+    /// Frames shed since the entry was created. Only the first warns.
+    shed: u64,
+}
+
+impl ParkEntry {
+    fn push(&mut self, frame: ParkedFrame) {
+        self.bytes = self.bytes.saturating_add(frame.footprint());
+        self.frames.push(frame);
+    }
+
+    /// Remove the selected frames, returning them and the footprint freed so 
the
+    /// caller can debit the shard-wide total.
+    fn extract(
+        &mut self,
+        predicate: impl FnMut(&mut ParkedFrame) -> bool,
+    ) -> (Vec<ParkedFrame>, usize) {
+        let taken: Vec<ParkedFrame> = self.frames.extract_if(.., 
predicate).collect();
+        let freed: usize = taken.iter().map(ParkedFrame::footprint).sum();
+        self.bytes = self.bytes.saturating_sub(freed);
+        (taken, freed)
+    }
+}
+
 /// Per-namespace ceiling on parked frames.
 const MAX_PARKED_PER_NAMESPACE: usize = 128;
 
@@ -1742,20 +1784,12 @@ const MAX_PARKED_BYTES: usize = 16 * 1024 * 1024;
 /// Per-namespace ceiling on parked bytes, so one un-materialised namespace
 /// cannot spend the whole shard's budget and shed everyone else's frames.
 ///
-/// Applied only to a namespace that already holds something. A frame larger
-/// than this on its own would otherwise never park at all -- the check fails
-/// even against an empty entry -- and for a replicated prepare that is silent,
+/// Applied only to an entry that already holds something. Sized against an
+/// empty entry a larger frame could never park at all, and for a prepare that 
is
 /// unrecoverable loss: `consensus::retransmit_targets` skips an op that 
already
-/// reached quorum, so the backup stays permanently short of it. Shipped
-/// `message_bus.max_message_size` is 64 MiB, well past this, so an ordinary
-/// batched append hits it. Admitting the first frame regardless costs the 
other
-/// namespaces up to the whole shard budget for one convergence window; losing 
a
-/// committed op costs them the replica.
-///
-/// TODO(krishna): [`MAX_PARKED_BYTES`] is still a hard ceiling, so a frame 
above
-/// 16 MiB remains unparkable. Closing that needs the budget derived from the
-/// configured `message_bus.max_message_size` rather than a const here, which
-/// puts worst-case park residency at one max-size frame per shard.
+/// reached quorum. Shipped `message_bus.max_message_size` is 64 MiB, so an
+/// ordinary batched append exceeds this. Cost of the waiver is one convergence
+/// window of shard budget; cost of the loss is the replica.
 const MAX_PARKED_BYTES_PER_NAMESPACE: usize = MAX_PARKED_BYTES / 4;
 
 /// Resident cost of parking a frame of `len` bytes.
@@ -1951,13 +1985,15 @@ where
         false
     }
 
-    /// Drop parked frames for a namespace that will never materialise (it was
-    /// removed before its `ReconcileOp::InsertOwned`), so the pending entry is
-    /// reclaimed instead of leaking until process exit. Parked client requests
-    /// are denied with a transient status rather than dropped: the transports
-    /// decode replies in lockstep, so silence wedges the connection until the
-    /// SDK's response read-timeout.
-    fn discard_parked_partition_frames(&self, namespace: IggyNamespace) {
+    /// Discard every frame parked under a namespace this shard can never 
serve:
+    /// gone from committed metadata, mid-teardown, or not hashing here. Client
+    /// requests get a transient deny, not silence; transports decode replies 
in
+    /// lockstep, so silence wedges the connection until the SDK read-timeout.
+    ///
+    /// The one retirement path a prepare still travels. It is retained
+    /// everywhere else (see [`ParkedFrame::passes`]); here the namespace 
itself
+    /// is unreachable, so holding it buys nothing.
+    pub fn discard_parked_partition_frames(&self, namespace: IggyNamespace) {
         // Bound the borrow to this statement: the guard in an `if let`
         // scrutinee otherwise lives to the end of the then-block, holding a
         // shard-global map locked across the outbound sends below.
@@ -1965,55 +2001,66 @@ where
         if let Some(frames) = parked
             && !frames.is_empty()
         {
-            let total = frames.len();
-            let mut answered = 0;
-            for frame in frames {
-                if self.deny_parked_client_request(frame) {
-                    answered += 1;
-                } else {
-                    self.metrics.record_frame_drop(
-                        crate::metrics::frame_drop_variant::PARTITION,
-                        crate::metrics::frame_drop_reason::PARK_DROPPED,
-                    );
-                }
-            }
+            let (answered, dropped) = self.retire_parked_frames(frames);
             tracing::debug!(
                 shard = self.id,
                 namespace_raw = namespace.inner(),
                 answered,
-                dropped = total - answered,
-                "discarding parked partition frames for removed namespace"
+                dropped,
+                "discarding parked partition frames for an unreachable 
namespace"
             );
         }
     }
 
-    /// Remove a namespace's park entry, debiting its bytes from
-    /// [`Self::parked_partition_bytes`]. The single place entries leave the 
map,
-    /// so the running total and the pending-retry set cannot drift out of step
-    /// with it.
+    /// Remove a namespace's entry, debiting [`Self::parked_partition_bytes`] 
and
+    /// disarming the pump retry. Single place an entry leaves the map, so
+    /// neither can drift out of step with it.
     fn take_parked_partition_frames(&self, namespace: IggyNamespace) -> 
Option<Vec<ParkedFrame>> {
         self.reparked_partition_namespaces
             .borrow_mut()
             .remove(&namespace);
-        let frames = self
+        let entry = self
             .pending_partition_frames
             .borrow_mut()
             .remove(&namespace)?;
-        let freed: usize = frames
-            .iter()
-            .map(|frame| parked_footprint(frame.message.as_slice().len()))
-            .sum();
-        self.parked_partition_bytes
-            .set(self.parked_partition_bytes.get().saturating_sub(freed));
-        Some(frames)
+        self.parked_partition_bytes.set(
+            self.parked_partition_bytes
+                .get()
+                .saturating_sub(entry.bytes),
+        );
+        Some(entry.frames)
+    }
+
+    /// Answer client requests, destroy the rest, report `(answered, dropped)`.
+    /// A destroyed frame has nobody to reply to, so
+    /// `frame_drops_total{variant=partition,reason=park_dropped}` is the only
+    /// record it existed.
+    fn retire_parked_frames(&self, frames: Vec<ParkedFrame>) -> (usize, usize) 
{
+        let mut answered = 0;
+        let mut dropped = 0;
+        for frame in frames {
+            if self.deny_parked_client_request(frame) {
+                answered += 1;
+            } else {
+                dropped += 1;
+                self.metrics.record_frame_drop(
+                    crate::metrics::frame_drop_variant::PARTITION,
+                    crate::metrics::frame_drop_reason::PARK_DROPPED,
+                );
+            }
+        }
+        (answered, dropped)
     }
 
     /// Whether any frame is parked. Cheap enough for the reconciler's per-tick
     /// fast-skip guard: a non-empty buffer means the shard is by definition 
not
     /// converged, so the skip must not fire.
+    ///
+    /// Read from the map, not the byte cell: an empty entry is never left
+    /// behind, but keying convergence off bytes makes that a silent invariant.
     #[must_use]
-    pub const fn has_parked_partition_frames(&self) -> bool {
-        self.parked_partition_bytes.get() > 0
+    pub fn has_parked_partition_frames(&self) -> bool {
+        !self.pending_partition_frames.borrow().is_empty()
     }
 
     /// Namespaces currently holding parked frames. The reconciler pairs this
@@ -2054,17 +2101,18 @@ where
     /// discriminator (see the `TODO(krishna)` in
     /// `partition_reconciler`'s module docs), not a `None`-means-stale rule.
     ///
-    /// A frame the inbox refuses is re-parked, not answered. Re-queuing 
appends,
-    /// so a pass that materialises many namespaces at once can overrun the 
inbox;
-    /// staging a deny there is futile because the deny rides that same sender
-    /// with no await in between, so nothing can have drained a slot.
+    /// A frame the inbox refuses is re-parked: retained, so not counted as a
+    /// drop. Re-queuing appends, so a pass materialising many namespaces can
+    /// overrun the inbox; staging a deny is futile because it rides the same
+    /// sender with no await in between. The first `Full` ends the loop, since
+    /// the sole consumer of `senders[self.id]` is the pump task running this
+    /// call and no later frame can find a slot the first one could not.
     ///
-    /// [`MAX_PARKED_PASSES`] does NOT bound a re-parked frame -- the 
reconciler
-    /// sweep ages a namespace only while it is un-materialised, and by here it
-    /// is materialised. [`Self::repark_partition_frames`] arms the pump-side
-    /// retry instead, and the sweep's backstop for an inbox that never drains 
is
-    /// `partition_reconciler::reconcile_parked_frames`, which now ages a
-    /// materialised namespace too.
+    /// [`MAX_PARKED_PASSES`] does not bound a re-parked frame: the sweep ages 
a
+    /// namespace only while un-materialised, and by here it is materialised.
+    /// [`Self::repark_partition_frames`] arms the pump retry instead;
+    /// `partition_reconciler::reconcile_parked_frames` is the backstop for an
+    /// inbox that never drains.
     fn redispatch_parked_frames(&self, namespace: IggyNamespace, epoch: u64)
     where
         B: MessageBus + 'static,
@@ -2079,7 +2127,9 @@ where
             epoch,
             "re-dispatching parked partition frames after materialisation"
         );
-        let mut refused_frames: Vec<ParkedFrame> = Vec::new();
+        // Incarnation filter first, independent of the sender: a prior
+        // incarnation is rejected whether or not this shard can re-queue.
+        let mut servable: Vec<ParkedFrame> = Vec::with_capacity(frames.len());
         for frame in frames {
             // Only a stamp that exists and disagrees is evidence of a prior
             // incarnation; see this function's docs on why `None` is not.
@@ -2087,54 +2137,61 @@ where
                 && parked_epoch != epoch
             {
                 self.reject_stale_parked_frame(namespace, epoch, frame);
-                continue;
+            } else {
+                servable.push(frame);
             }
-            let Some(sender) = self.senders.get(self.id as usize) else {
-                continue;
-            };
+        }
+        let Some(sender) = self.senders.get(self.id as usize) else {
+            self.retire_parked_frames(servable);
+            return;
+        };
+        let mut refused_frames: Vec<ParkedFrame> = Vec::new();
+        let mut remaining = servable.into_iter();
+        while let Some(frame) = remaining.next() {
             let passes = frame.passes;
             let parked_epoch = frame.epoch;
             let Err(error) = sender.try_send(ShardFrame::consensus(self.id, 
frame.message)) else {
                 continue;
             };
-            self.metrics.record_frame_drop(
-                crate::metrics::frame_drop_variant::PARTITION,
-                crate::coordinator::classify_try_send_err(&error),
-            );
             let (refused, disconnected) = match error {
                 TrySendError::Full(frame) => (frame, false),
                 TrySendError::Disconnected(frame) => (frame, true),
             };
             let ShardFrame::Consensus { message, .. } = refused else {
-                continue;
+                unreachable!("try_send returns the frame it was handed");
+            };
+            let refused_frame = ParkedFrame {
+                epoch: parked_epoch,
+                passes,
+                message,
             };
             if disconnected {
-                // The pump is gone, so re-parking would hold the frame until
-                // process exit. Answer a client request; a prepare has nothing
-                // left to serve it.
+                // Pump gone: re-parking holds the frame until process exit, 
and
+                // every later send hits the same dead channel.
+                self.metrics.record_frame_drop(
+                    crate::metrics::frame_drop_variant::PARTITION,
+                    crate::metrics::frame_drop_reason::DISCONNECTED,
+                );
                 tracing::warn!(
                     shard = self.id,
                     namespace_raw = namespace.inner(),
-                    "re-dispatch of parked partition frame refused: inbox 
disconnected"
+                    "re-dispatch of parked partition frames refused: inbox 
disconnected"
                 );
-                if message.header().command == Command2::Request
-                    && let Ok(request) = 
message.try_into_typed::<RequestHeader>()
-                {
-                    self.stage_transient_deny(request.header());
-                }
-                continue;
+                let mut stranded = vec![refused_frame];
+                stranded.extend(remaining);
+                self.retire_parked_frames(stranded);
+                return;
             }
+            refused_frames.push(refused_frame);
+            refused_frames.extend(remaining);
             tracing::debug!(
                 shard = self.id,
                 namespace_raw = namespace.inner(),
+                count = refused_frames.len(),
                 passes,
-                "re-parking parked partition frame: inbox full"
+                "re-parking parked partition frames: inbox full"
             );
-            refused_frames.push(ParkedFrame {
-                epoch: parked_epoch,
-                passes,
-                message,
-            });
+            break;
         }
         if !refused_frames.is_empty() {
             self.repark_partition_frames(namespace, refused_frames);
@@ -2149,21 +2206,18 @@ where
     /// moment ago, and shedding here would answer a frame the inbox merely
     /// deferred.
     ///
-    /// Arming [`Self::reparked_partition_namespaces`] is what makes the 
deferral
-    /// a deferral. Every other exit from the park map is closed for a
-    /// materialised namespace: the reconciler sweep only ages one it has not
-    /// built, and `reconcile_additions` never stages a second `InsertOwned` 
for
-    /// one already in `IggyPartitions`.
+    /// Arming [`Self::reparked_partition_namespaces`] is what makes it a
+    /// deferral. Every other exit is closed once materialised: the sweep only
+    /// ages a namespace it has not built, and `reconcile_additions` stages no
+    /// second `InsertOwned` for one already in `IggyPartitions`.
     fn repark_partition_frames(&self, namespace: IggyNamespace, frames: 
Vec<ParkedFrame>) {
-        let restored: usize = frames
-            .iter()
-            .map(|frame| parked_footprint(frame.message.as_slice().len()))
-            .sum();
-        self.pending_partition_frames
-            .borrow_mut()
-            .entry(namespace)
-            .or_default()
-            .extend(frames);
+        let restored: usize = frames.iter().map(ParkedFrame::footprint).sum();
+        let mut pending = self.pending_partition_frames.borrow_mut();
+        let entry = pending.entry(namespace).or_default();
+        for frame in frames {
+            entry.push(frame);
+        }
+        drop(pending);
         self.parked_partition_bytes
             .set(self.parked_partition_bytes.get().saturating_add(restored));
         self.reparked_partition_namespaces
@@ -2171,60 +2225,50 @@ where
             .insert(namespace);
     }
 
-    /// Age every frame parked under `namespace` by one reconciler pass and
-    /// retire the ones that have outlived [`MAX_PARKED_PASSES`]. Returns the
-    /// number retired -- client requests answered plus prepares destroyed, the
-    /// latter counted under
-    /// `frame_drops_total{variant=partition,reason=park_dropped}`.
+    /// Age every frame under `namespace` by one pass, answering CLIENT 
REQUESTS
+    /// past [`MAX_PARKED_PASSES`]. Returns the number answered.
+    ///
+    /// Prepares age but never expire. Expiry destroys a committed op with
+    /// nothing to recover it (see [`ParkedFrame::passes`]), and passes are
+    /// commit-driven: a non-empty buffer defeats the reconciler fast-skip, so 
a
+    /// create burst elapses four in milliseconds, across every parked 
namespace
+    /// rather than the one it concerns. Byte budgets bound them instead. Only
+    /// [`Self::discard_parked_partition_frames`] still destroys a prepare.
     ///
-    /// The bound is in passes rather than wall-clock so the simulator's 
virtual
-    /// clock governs it like everything else. It exists to bound residency: a
-    /// namespace can stay un-materialised indefinitely, and the buffer must 
not
-    /// grow with it. See [`ParkedFrame::passes`] for why this is not also
-    /// staleness protection -- the SDK replays the same request, so answering 
a
-    /// late frame does not prevent its operation from being applied late.
+    /// Passes, not wall-clock, so the simulator's virtual clock governs it.
     pub fn age_parked_partition_frames(&self, namespace: IggyNamespace) -> 
usize {
         let expired = {
             let mut pending = self.pending_partition_frames.borrow_mut();
-            let Some(frames) = pending.get_mut(&namespace) else {
+            let Some(entry) = pending.get_mut(&namespace) else {
                 return 0;
             };
-            for frame in frames.iter_mut() {
-                frame.passes += 1;
+            for frame in &mut entry.frames {
+                frame.passes = frame.passes.saturating_add(1);
             }
-            let expired: Vec<ParkedFrame> = frames
-                .extract_if(.., |frame| frame.passes > MAX_PARKED_PASSES)
-                .collect();
-            if frames.is_empty() {
-                pending.remove(&namespace);
+            let (expired, freed) =
+                entry.extract(|frame| !frame.is_replicated() && frame.passes > 
MAX_PARKED_PASSES);
+            let emptied = entry.frames.is_empty();
+            drop(pending);
+            if emptied {
+                // Through the shared remover so the pump-retry set is disarmed
+                // with it; the entry is already empty, so this only unhooks 
it.
+                self.take_parked_partition_frames(namespace);
             }
-            let freed: usize = expired
-                .iter()
-                .map(|frame| parked_footprint(frame.message.as_slice().len()))
-                .sum();
             self.parked_partition_bytes
                 .set(self.parked_partition_bytes.get().saturating_sub(freed));
             expired
         };
         let count = expired.len();
         if count > 0 {
-            let mut answered = 0;
-            for frame in expired {
-                if self.deny_parked_client_request(frame) {
-                    answered += 1;
-                } else {
-                    self.metrics.record_frame_drop(
-                        crate::metrics::frame_drop_variant::PARTITION,
-                        crate::metrics::frame_drop_reason::PARK_DROPPED,
-                    );
-                }
-            }
+            // Never replicated traffic (the predicate excludes it), so this is
+            // a request whose deny the pump refused: destroyed, and counted.
+            let (answered, unanswered) = self.retire_parked_frames(expired);
             tracing::warn!(
                 shard = self.id,
                 namespace_raw = namespace.inner(),
                 answered,
-                dropped = count - answered,
-                "retiring parked partition frames that outlived their 
admission window"
+                unanswered,
+                "answering parked partition requests that outlived their 
admission window"
             );
         }
         count
@@ -2241,33 +2285,27 @@ where
         self.pending_partition_frames
             .borrow()
             .get(&namespace)
-            .map_or(0, Vec::len)
+            .map_or(0, |entry| entry.frames.len())
     }
 
-    /// Answer every frame parked under `namespace` and drop the entry, without
-    /// touching the routing table. Used by the reconciler for a namespace it 
has
-    /// given up on materialising this pass.
-    pub fn reclaim_parked_partition_frames(&self, namespace: IggyNamespace) {
-        self.discard_parked_partition_frames(namespace);
-    }
-
-    /// Retire a parked frame that will never be served: a client request gets 
a
-    /// transient deny so it can re-issue, replicated traffic is destroyed.
-    /// Returns `true` when a reply was staged. Callers are synchronous
-    /// (`apply_reconcile_ops`, the reconciler sweep), so the deny rides the
-    /// pump's outbound lifecycle path instead of an inline bus send.
+    /// Retire a frame that will never be served: a client request gets a
+    /// transient deny, replicated traffic is destroyed. Returns `true` only 
when
+    /// a reply reached the pump.
     ///
-    /// A prepare gets no reply because there is no client on this node to send
-    /// one to, but "no reply" must not mean "no record": the primary only
-    /// retransmits an op that has not reached quorum, so a destroyed prepare 
on
-    /// a lagging backup is invisible loss. The `false` return makes callers
-    /// count it under 
`frame_drops_total{variant=partition,reason=park_dropped}`.
+    /// Callers are synchronous (`apply_reconcile_ops`, the reconciler sweep), 
so
+    /// the deny rides the pump's lifecycle path, not an inline bus send. A 
shard
+    /// with no sender stages nothing, hence forwarding
+    /// [`Self::stage_transient_deny`]'s verdict rather than assuming success.
+    ///
+    /// No reply must not mean no record: the primary retransmits only what has
+    /// not reached quorum, so a destroyed prepare is invisible loss. The 
`false`
+    /// return is what makes callers bump
+    /// `frame_drops_total{variant=partition,reason=park_dropped}`.
     fn deny_parked_client_request(&self, frame: ParkedFrame) -> bool {
         if frame.message.header().command == Command2::Request
             && let Ok(request) = 
frame.message.try_into_typed::<RequestHeader>()
         {
-            self.stage_transient_deny(request.header());
-            return true;
+            return self.stage_transient_deny(request.header());
         }
         false
     }
@@ -2282,15 +2320,37 @@ where
         materialised_epoch: u64,
         frame: ParkedFrame,
     ) {
-        tracing::warn!(
-            shard = self.id,
-            namespace_raw = namespace.inner(),
-            parked_epoch = ?frame.epoch,
-            materialised_epoch,
-            replicated = frame.message.header().command != Command2::Request,
-            "rejecting parked partition frame from a prior incarnation"
-        );
-        self.metrics.record_partition_frame_rejected_stale();
+        // Both directions reject: a frame stamped AHEAD must not be applied 
into
+        // the incarnation the staleness teardown is about to erase either. 
Only
+        // BEHIND is the anomaly `partition_frames_rejected_stale_total` is
+        // alerted on. Ahead means the recreate committed between the 
reconciler
+        // snapshotting `epoch` and the pump applying `InsertOwned`: expected
+        // churn, and counting it there fires the alert on a race by design.
+        let ahead = frame
+            .epoch
+            .is_some_and(|parked_epoch| parked_epoch > materialised_epoch);
+        let replicated = frame.is_replicated();
+        if ahead {
+            self.metrics.record_partition_frame_rejected_ahead();
+            tracing::debug!(
+                shard = self.id,
+                namespace_raw = namespace.inner(),
+                parked_epoch = ?frame.epoch,
+                materialised_epoch,
+                replicated,
+                "rejecting parked partition frame stamped ahead of the 
materialised incarnation"
+            );
+        } else {
+            self.metrics.record_partition_frame_rejected_stale();
+            tracing::warn!(
+                shard = self.id,
+                namespace_raw = namespace.inner(),
+                parked_epoch = ?frame.epoch,
+                materialised_epoch,
+                replicated,
+                "rejecting parked partition frame from a prior incarnation"
+            );
+        }
         self.deny_parked_client_request(frame);
     }
 
@@ -2341,48 +2401,71 @@ where
             .streams()
             .created_revision_for_namespace(namespace);
         let frame_cost = parked_footprint(message.as_slice().len());
+        let replicated = message.header().command() != Command2::Request;
         let mut pending = self.pending_partition_frames.borrow_mut();
         let parked_bytes = self.parked_partition_bytes.get();
         // Read the entry without `entry().or_default()`: inserting first would
-        // leave an empty Vec behind on the overflow path below, which reads 
as a
-        // parked namespace to the reconciler sweep and its fast-skip guard.
-        let existing = pending.get(&namespace);
-        let parked_len = existing.map_or(0, Vec::len);
-        let namespace_bytes: usize = existing.map_or(0, |frames| {
-            frames
-                .iter()
-                .map(|frame| parked_footprint(frame.message.as_slice().len()))
-                .sum()
-        });
-        // The per-namespace byte cap is waived for the first frame, or a frame
-        // bigger than the cap could never park at all; see
-        // [`MAX_PARKED_BYTES_PER_NAMESPACE`]. The shard-wide budget is not
-        // waived, so residency is unchanged.
-        let over_namespace_budget = parked_len > 0
-            && namespace_bytes.saturating_add(frame_cost) > 
MAX_PARKED_BYTES_PER_NAMESPACE;
-        if parked_len >= MAX_PARKED_PER_NAMESPACE
-            || over_namespace_budget
-            || parked_bytes.saturating_add(frame_cost) > MAX_PARKED_BYTES
-        {
+        // leave an empty entry behind on the overflow path below, which reads 
as
+        // a parked namespace to the reconciler sweep and its fast-skip guard.
+        let existing = pending.get_mut(&namespace);
+        let parked_len = existing.as_ref().map_or(0, |entry| 
entry.frames.len());
+        let namespace_bytes = existing.as_ref().map_or(0, |entry| entry.bytes);
+        // A prepare is never shed on a byte budget. No client to answer, and 
no
+        // recovery: `consensus::retransmit_targets` skips an op that already
+        // reached quorum and the plane opens a repair session only in
+        // `on_start_view`, so shedding one is permanent loss where shedding a
+        // request costs a retry. A request is refused the moment admitting it
+        // would cross a budget; a prepare only once one is already spent. Caps
+        // prepare residency at one frame of overshoot per budget (worst case
+        // `MAX_PARKED_BYTES` + `max_message_size`, 80 MiB per shard) instead 
of
+        // at the budget, and is what makes an oversize frame parkable at all.
+        let namespace_budget_spent = parked_len > 0
+            && if replicated {
+                namespace_bytes >= MAX_PARKED_BYTES_PER_NAMESPACE
+            } else {
+                namespace_bytes.saturating_add(frame_cost) > 
MAX_PARKED_BYTES_PER_NAMESPACE
+            };
+        let shard_budget_spent = if replicated {
+            parked_bytes >= MAX_PARKED_BYTES
+        } else {
+            parked_bytes.saturating_add(frame_cost) > MAX_PARKED_BYTES
+        };
+        if parked_len >= MAX_PARKED_PER_NAMESPACE || namespace_budget_spent || 
shard_budget_spent {
             self.metrics.record_frame_drop(
                 crate::metrics::frame_drop_variant::PARTITION,
                 crate::metrics::frame_drop_reason::PARK_OVERFLOW,
             );
-            // Shedding drops a frame, so it is logged at `warn` like every 
other
-            // drop site: the counter is not registered with a scrape endpoint 
yet
-            // (see the TODO in `crate::metrics`), so these logs are the only
-            // alertable signal. Rate-limited by the buffer being full -- once 
it
-            // is, the namespace is already the subject of one warning per
-            // arriving frame, which is the condition an operator needs to see.
-            tracing::warn!(
-                shard = self.id,
-                namespace_raw = namespace.inner(),
-                parked_frames = parked_len,
-                namespace_bytes,
-                parked_bytes,
-                frame_cost,
-                "park buffer at capacity; shedding partition frame"
-            );
+            // Warn once per namespace on the transition into shedding, then
+            // `debug`. A full buffer is this branch's trigger, not a rate 
limit:
+            // every later frame lands here too, and one formatted `warn` 
apiece
+            // is enough for the non-blocking appender to shed unrelated lines.
+            // The counter carries the volume.
+            let first_shed = existing.is_none_or(|entry| {
+                let first = entry.shed == 0;
+                entry.shed = entry.shed.saturating_add(1);
+                first
+            });
+            if first_shed {
+                tracing::warn!(
+                    shard = self.id,
+                    namespace_raw = namespace.inner(),
+                    parked_frames = parked_len,
+                    namespace_bytes,
+                    parked_bytes,
+                    frame_cost,
+                    replicated,
+                    "park buffer at capacity; shedding partition frames"
+                );
+            } else {
+                tracing::debug!(
+                    shard = self.id,
+                    namespace_raw = namespace.inner(),
+                    parked_bytes,
+                    frame_cost,
+                    replicated,
+                    "park buffer still at capacity; shedding partition frame"
+                );
+            }
             return ParkOutcome::Overflow(message);
         }
         tracing::debug!(
@@ -2397,6 +2480,7 @@ where
             passes: 0,
             message: message.into_generic(),
         });
+        drop(pending);
         self.parked_partition_bytes
             .set(parked_bytes.saturating_add(frame_cost));
         ParkOutcome::Parked
@@ -2414,9 +2498,9 @@ where
             IggyError::TransientNotAccepted.as_code(),
         );
         // Count only what the bus accepted, matching `stage_transient_deny` 
and
-        // the contract on `record_partition_request_denied_transient`: a deny
-        // the bus refused is a shed frame, not an answered one, and crediting 
it
-        // here hides exactly the silent-shed case this counter exists to 
expose.
+        // `record_partition_request_denied_transient`'s contract: a refused 
deny
+        // is a shed frame, and crediting it hides the silent shed this counter
+        // exists to expose.
         if let Err(error) = self
             .bus
             .send_to_client(request_header.client, 
reply.into_generic().into_frozen())
@@ -2442,7 +2526,10 @@ where
     /// hand the deny to this shard's own pump as a
     /// [`LifecycleFrame::ForwardClientSend`], whose handler performs the bus
     /// send (same funnel the parked-frame re-dispatch uses).
-    fn stage_transient_deny(&self, request_header: &RequestHeader) {
+    ///
+    /// Returns whether the pump took it. A shard with no sender stages 
nothing,
+    /// so assuming success logs an answer for a request destroyed unanswered.
+    fn stage_transient_deny(&self, request_header: &RequestHeader) -> bool {
         let reply = build_deny_reply_from_request_header(
             request_header,
             IggyError::TransientNotAccepted.as_code(),
@@ -2452,7 +2539,7 @@ where
             msg: reply.into_generic().into_frozen(),
         });
         let Some(sender) = self.senders.get(self.id as usize) else {
-            return;
+            return false;
         };
         // Count only what was actually handed to the pump: crediting before 
the
         // send reports an answer to a client that never received one, which is
@@ -2468,9 +2555,10 @@ where
                 operation = ?request_header.operation,
                 "dropping transient deny for discarded partition frame: inbox 
rejected: {error:?}"
             );
-            return;
+            return false;
         }
         self.metrics.record_partition_request_denied_transient();
+        true
     }
 
     #[allow(clippy::future_not_send)]
diff --git a/core/shard/src/metrics.rs b/core/shard/src/metrics.rs
index b73a99a54..5b8fa3438 100644
--- a/core/shard/src/metrics.rs
+++ b/core/shard/src/metrics.rs
@@ -116,11 +116,11 @@ pub mod frame_drop_variant {
 /// `PARK_OVERFLOW` ticks when a partition frame arrives for a namespace this
 /// shard has not materialised and the per-namespace park buffer is already at
 /// its cap, so the frame is shed with no reply. `PARK_DROPPED` ticks when a
-/// frame that did park leaves the buffer without being served: it outlived
-/// `MAX_PARKED_PASSES`, or its namespace was torn down. A client request also
-/// bumps `partition_requests_denied_transient_total` there, since it gets a
-/// reply; replicated traffic has nobody to answer, so this counter is the only
-/// record that the op was destroyed.
+/// frame that did park leaves unserved: its namespace became unreachable, or a
+/// request outlived `MAX_PARKED_PASSES` with no pump to take the deny. A 
request
+/// also bumps `partition_requests_denied_transient_total` when answered;
+/// replicated traffic has nobody to answer, so this is the only record the op
+/// was destroyed.
 pub mod frame_drop_reason {
     pub const FULL: &str = "full";
     pub const DISCONNECTED: &str = "disconnected";
@@ -131,6 +131,10 @@ pub mod frame_drop_reason {
     pub const PARK_DROPPED: &str = "park_dropped";
 }
 
+// Minted in full, so 7 x 7 includes pairs no drop site produces:
+// `park_overflow` and `park_dropped` pair only with `PARTITION`, leaving 12
+// unreachable. Free while nothing scrapes these (module `TODO(hubcio)`); mint
+// per drop site once a registry lands, so the scrape carries no permanent 
zeroes.
 const VARIANT_COUNT: usize = 7;
 const REASON_COUNT: usize = 7;
 
@@ -183,6 +187,7 @@ pub struct ShardMetrics {
     partitions_removed_total: Counter,
     partitions_reconcile_failures_total: Counter,
     partition_frames_rejected_stale_total: Counter,
+    partition_frames_rejected_ahead_total: Counter,
     partition_requests_denied_transient_total: Counter,
 }
 
@@ -214,6 +219,7 @@ impl ShardMetrics {
             partitions_removed_total: Counter::default(),
             partitions_reconcile_failures_total: Counter::default(),
             partition_frames_rejected_stale_total: Counter::default(),
+            partition_frames_rejected_ahead_total: Counter::default(),
             partition_requests_denied_transient_total: Counter::default(),
         }
     }
@@ -270,6 +276,16 @@ impl ShardMetrics {
         self.partition_frames_rejected_stale_total.inc();
     }
 
+    /// Bumped when a parked frame carries an epoch AHEAD of the one its
+    /// partition materialised at: the recreate committed between the 
reconciler
+    /// snapshotting the epoch for `InsertOwned` and the pump applying it. 
Split
+    /// from `partition_frames_rejected_stale_total` so that counter keeps 
meaning
+    /// caught correctness anomaly; this direction is an expected race and 
would
+    /// fire the alert on ordinary delete + recreate churn. Both still reject.
+    pub fn record_partition_frame_rejected_ahead(&self) {
+        self.partition_frames_rejected_ahead_total.inc();
+    }
+
     /// Total frame drops across every `{variant, reason}` pair.
     ///
     /// Simulator assertion hook: a run without injected loss must keep
@@ -337,6 +353,14 @@ impl ShardMetrics {
         self.partition_frames_rejected_stale_total.get()
     }
 
+    /// Snapshot of `partition_frames_rejected_ahead_total`. Test/simulator
+    /// accessor.
+    #[cfg(any(test, feature = "simulator"))]
+    #[must_use]
+    pub fn partition_frames_rejected_ahead_value(&self) -> u64 {
+        self.partition_frames_rejected_ahead_total.get()
+    }
+
     /// Snapshot of one `frame_drops_total{variant, reason}` pair, or 0 when 
the
     /// pair is not a known label combination.
     ///

Reply via email to