Skip to content

feat(server,cli): replace the oldest slot when a period is full - #378

Merged
filvecchiato merged 7 commits into
mainfrom
tg/statement-slot-eviction
Aug 13, 2026
Merged

feat(server,cli): replace the oldest slot when a period is full#378
filvecchiato merged 7 commits into
mainfrom
tg/statement-slot-eviction

Conversation

@TarikGul

Copy link
Copy Markdown
Member

A full statement-store period is no longer a dead end. The runtime allows taking over a slot once it has aged past Resources.StmtStoreReplacementCooldown (60s on paseo-next-v2), so registration replaces the oldest slot that is neither the target's own nor still inside that cooldown, and reports NoFreeStatementStoreSlot only when nothing is replaceable.

The scan carries each occupied slot's (seq, account_id, since) rather than discarding two of the three. slot::replaceable_slot is a pure function over a supplied candidate list, so widening the pool later — pooling across membership collections — changes the candidates without touching the rule. Ties on since break to the lowest seq, so a retry cannot oscillate between two slots of equal age.

RegistrationParams carries now_seconds, matching how current_period and next_tick_delay already take their clock, which keeps the choice deterministic under test. alloc-check prints each occupied slot's age, whether it is replaceable, and which slot it would take.

Part of #334, covering the item "Statement store allowances: when no slot are available - Native replace the oldest one that can be replaced and replace it, rust does not."

Every caller of register_statement_account inherits this, so the renewal pass added in #308 now replaces a slot instead of reporting the period exhausted.

Verification

Offline: the rule itself (oldest wins, cooldown respected, the target's own slot never taken, excluded slots skipped, ties deterministic, cooldown read from metadata), plus scripted registration for a full table that replaces and a full table inside the cooldown that still fails.

Live against paseo-next-v2: filled all ten slots, watched the diagnostic hold back slots aged 23-59s while offering those past 60s, replaced the oldest at 18551s, and confirmed the slot's account and since both moved and the next candidate advanced.

Exhaustion is covered offline only. Filling ten slots takes about 120s at ~12s per block, so a sequential host cannot get every slot inside a 60s cooldown.

A full statement-store period was a dead end: the scan reported `Full` and
registration failed with `NoFreeStatementStoreSlot`, even though the runtime
allows taking over a slot once it has aged past
`Resources.StmtStoreReplacementCooldown` (60s on paseo-next-v2).

The scan now carries each occupied slot's `(seq, account_id, since)` instead of
discarding two of the three, and `slot::replaceable_slot` picks the oldest slot
that is neither the target's own nor still inside the cooldown. Registration
replaces that slot and only reports `NoFreeStatementStoreSlot` when nothing is
replaceable.

The rule is a pure function over a supplied candidate list rather than a scan of
its own, so widening the pool later (pooling across membership collections)
changes the candidates without touching the rule. Ties on `since` break to the
lowest `seq`, so a retry does not oscillate between two slots of equal age.

`RegistrationParams` carries `now_seconds`, matching how `current_period` and
`next_tick_delay` already take their clock, which keeps the choice deterministic
under test. `alloc-check` prints each occupied slot's age, whether it is
replaceable, and which slot it would take.

Verified against paseo-next-v2: filled all ten slots, watched the diagnostic
hold back slots aged 23-59s while offering those past 60s, replaced the oldest
(18551s), and confirmed the slot's account and `since` both moved and the next
candidate advanced. Exhaustion is covered offline only: filling ten slots takes
about 120s at ~12s per block, so a sequential host cannot get every slot inside
a 60s cooldown.
@TarikGul
TarikGul requested a review from a team August 13, 2026 02:15

@valentinfernandez1 valentinfernandez1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚀

Comment thread rust/crates/truapi-server/src/runtime/statement_allowance.rs
Comment thread rust/crates/truapi-server/src/runtime/statement_allowance/slot.rs Outdated
Comment thread rust/crates/truapi-server/src/runtime/statement_allowance.rs
TarikGul and others added 6 commits August 13, 2026 09:41
…ndary

`since` is a chain timestamp and the runtime re-checks the replacement cooldown
against its own clock when it validates the extrinsic. Ages were computed from
the host clock, which on paseo-next-v2 runs 10-18s ahead of `Timestamp.Now`
because the chain advances it once per 12s block. A slot whose host age was just
past the 60s cooldown could still be inside it from the chain's view.

The runtime also requires `now > since + cooldown` rather than `>=`, so a slot at
exactly the cooldown is refused.

Either mistake offers a slot the chain rejects, and it rejects at submit with
`Invalid Transaction (1010)`, which `duplicate_submit_error` does not match, so
the registration aborts with no retry and no exhaustion signal.

Ages are now read from `Timestamp.Now` at the point of choosing, and the
comparison is strict. Reading the clock where it is used also removes the
`now_seconds` field that was threaded through `RegistrationParams` and its five
call sites.
…ssion

`skipped_duplicate_slots` was serving two purposes. It records slots this call
already submitted for, so the scan stops offering them, and it was also handed to
the replacement rule. If the only free slot was one of those, the scan reported
the period full and registration replaced a live slot while an empty one sat
there, waiting for a submission that was about to resolve.

The scan now separates the two cases: no free slot at all is `Full` and may be
replaced into, whereas free-but-excluded is `FreeSlotsExcluded` and returns
`SlotError::FreeSlotsAwaitingSubmission` so the caller retries once the earlier
submission settles.
The duplicate-submit retry rescans and picks another slot. That was written for
free slots, where a second attempt costs nothing. On a full period every attempt
is a revocation, and the submission that looked like a duplicate can still land,
so a retry could leave two allowances revoked to place one.

A registration now performs at most one takeover: once a slot has been replaced,
a retry that finds the period full reports `NoFreeStatementStoreSlot` instead of
replacing again.
The renewal pass registers targets one after another. On a full period each one
replaced the oldest slot, and after enough registrations the oldest slot was one
this same pass had just claimed, so the run took allowances back off targets it
had only just renewed. With more targets than the period has slots that never
settles, and the pass runs again every hour.

`RegistrationParams::protected` lists slots the caller has already claimed and
must not lose, and the pass accumulates every seq it registers or finds already
allocated. Once every replaceable slot belongs to the pass, registration reports
slot exhaustion, which is the honest answer when the ledger wants more slots than
the period has: it surfaces the capacity problem instead of churning.
The runtime re-checks the replacement cooldown against its own clock and rejects
at validation, so a takeover that looked eligible can still be refused: the chain
answers `Invalid Transaction (1010)` before the extrinsic reaches a block.
`duplicate_submit_error` does not match that, so the registration surfaced a raw
RPC failure carrying no indication of what happened or whether retrying helps.

A refusal after a takeover now returns `SlotError::ReplacementRefused` naming the
period and slot. Nothing was revoked, since the rejection happens before
inclusion, so the caller can retry on the next tick.
@filvecchiato
filvecchiato enabled auto-merge August 13, 2026 13:59
@filvecchiato
filvecchiato added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit b573411 Aug 13, 2026
14 checks passed
@filvecchiato
filvecchiato deleted the tg/statement-slot-eviction branch August 13, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants