Skip to content

feat(server): let a host read what the last renewal pass achieved - #447

Open
TarikGul wants to merge 4 commits into
mainfrom
tg/renewal-scheduler-docs
Open

feat(server): let a host read what the last renewal pass achieved#447
TarikGul wants to merge 4 commits into
mainfrom
tg/renewal-scheduler-docs

Conversation

@TarikGul

Copy link
Copy Markdown
Member

The in-process renewal loop computes a report on every tick and has no caller to hand it to, so a host that drives the loop rather than calling a pass directly cannot learn that a period filled up and an allowance went unrenewed. Exhaustion
reaches a log line and stops there.

last_statement_renewal_report returns the most recent pass from either path. It is exported on both native types and forwarded by both host shells, including the Swift protocol. None means no pass has run, which is not the same as a healthy one. A tick that could not run at all leaves the previous report in place, since the last thing known beats nothing.

Recording and logging live in absorb_tick, separate from the loop body so the wiring is reachable from a test. A loop that logs but forgets to record is the regression worth catching, and a test that only exercises the state container does not catch it.

How a host answers its scheduler

The mapping from a report to a scheduler result is documented in both host READMEs and in the renewal module docs, because it is a decision every host would otherwise make separately from the same data:

  • every target Registered or AlreadyAllocated: success.
  • any target Failed: worth another attempt, on the next opportunistic wake rather than a tight loop.
  • slots_exhausted: success as far as the scheduler is concerned, because retrying cannot free a slot, only time or a replacement can. It does mean an allowance went unrenewed, so it belongs in front of a person rather than only in a log, which is what the new getter makes possible for a loop-driven host.

The docs also name the two layers either side of the scheduled wake, a pass on session activation and on-demand allocation, so a missed wake reads as ordinary. Resources.StmtStoreGraceWindow is 48 hours on top of a 24 hour period, so the deadline is soft by days.

Scope

Products still receive NotAvailable for exhaustion, the same as for a chain that does not offer the resource. Distinguishing them needs a new AllocationOutcome variant, which lands in the only payload version that exists and breaks decode for clients that do not know the index. That belongs in the deliberate breaking-change window #259 is waiting for.

The host READMEs already cover the cadence, the grace window and the cold-start
session problem, but not what to tell the operating system once a pass returns.
That decision lived only in the tracking issue, so each host would have arrived at
its own mapping from the same report.

Every target registered or already allocated is success. A failure is worth
another attempt, and the grace window means it can wait for the next opportunistic
wake rather than a tight loop. Exhaustion is success as far as the scheduler is
concerned, because retrying cannot free a slot, only time or a replacement can,
and a retry there only spends background budget. Exhaustion still reaches nobody
who can act on it, which is called out where a host would otherwise assume
silence means success.

Also names the two layers either side of the scheduled wake, a pass on session
activation and on-demand allocation, so a missed wake reads as ordinary rather
than as the only line of defence.
The in-process renewal loop computed a report on every tick and dropped it, so a
host that drives the loop rather than calling a pass directly had no way to learn
that a period filled up and an allowance went unrenewed. Exhaustion was a log line
and nothing else.

`last_statement_renewal_report` returns the most recent pass from either path,
exported on both native types and forwarded by both host shells. `None` means no
pass has run, which is not the same as a healthy one. A tick that could not run
leaves the previous report in place, since the last thing known beats nothing.

Recording and logging move into `absorb_tick`, separated from the loop body so the
wiring is reachable from a test. A loop that logs but forgets to record is the
regression worth catching, and testing the container alone did not catch it.

Also documents how a host answers its scheduler once a pass returns: every target
registered or already allocated is success, a failure is worth another attempt on
the next opportunistic wake, and exhaustion is success as far as the scheduler is
concerned because retrying cannot free a slot. It still means an allowance went
unrenewed, so it belongs in front of a person rather than only in a log. Names the
two layers either side of the scheduled wake as well, so a missed wake reads as
ordinary rather than as the only line of defence.
@TarikGul
TarikGul requested a review from a team August 18, 2026 14:59
@TarikGul TarikGul changed the title Tg/renewal scheduler docs feat(server): let a host read what the last renewal pass achieved Aug 18, 2026
The committed copy was generated while the two renewal doc comments were attached
to the wrong methods, which uniffi carries into the generated interface and into
its method checksums. Regenerating from the corrected source moves the docs onto
the methods they describe and settles the checksums at values both stable and
nightly reproduce.

@filvecchiato filvecchiato 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.

one minor nit

) -> Result<crate::runtime::statement_allowance::renewal::StatementRenewalReport, String> {
allowance_renewal::renew_now(&self.services, self).await
let report = allowance_renewal::renew_now(&self.services, self).await?;
self.renewal.record_report(&report);

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.

No test reaches this line: deleting it leaves all 643 green, while deleting the same call in absorb_tick fails three. Same getter, only the loop path guarded

The direct entry point recorded its report alongside returning it, and no test
reached that line: deleting it left the suite green, while deleting the same call
in `absorb_tick` fails three. Driving it would need a `SigningHost` with real
entropy, storage and chain services, which is why it was never covered.

It is also redundant. A caller of `renew_statement_allowances` already holds the
report; the loop is what has nowhere to return one to. So the recording belongs to
the loop alone, and the getter now says that rather than claiming either path.

Corrects the same claim on both host shells, both host guides and the runtime
export, and regenerates the Swift bindings the merge left conflicting.
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.

2 participants