Skip to content

provider-usage 0.6.0: disclose a reading served through an ongoing failure - #12

Merged
ualtinok merged 1 commit into
masterfrom
stale-disclosure
Aug 13, 2026
Merged

provider-usage 0.6.0: disclose a reading served through an ongoing failure#12
ualtinok merged 1 commit into
masterfrom
stale-disclosure

Conversation

@ualtinok

@ualtinok ualtinok commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Additive field on ProviderUsage, requested by a consumer that built compensating machinery in its absence.

The gap

A preserved last-known-good entry — one a producer keeps serving through a transient upstream failure rather than blanking the window — is currently byte-identical to a fresh entry apart from fetchedAt. It carries no error and no errorClass, because it is a clone of the last healthy read.

So a consumer cannot separate:

  • this figure is old because the producer has been unable to reach the provider, and
  • this figure is old because nothing polled recently.

Those have opposite remedies. The first is a reason to stop acting on the number; the second is not. With only a timestamp, the only available tool is a wall-clock threshold — which denies fresh-enough data in order to catch stale data.

That is not hypothetical. A consumer of usage.get (insula#5) reports two pieces of machinery built solely to compensate: a 5-minute TTL on lane verdicts invalidated on wall clock because nothing else can invalidate it, and a fail-closed gate denying dispatches on snapshots older than 10 minutes. Both have fired in production; the sharpest instance blocked a dispatch on a 58-minute-old snapshot. That snapshot was genuinely stale — the producer's refresh interval is 60s, so 58 minutes means an hour of failing fetches — but it was indistinguishable from a slow poll at the moment the decision was made.

The shape

pub stale: Option<Stale>,

pub struct Stale {
    pub since: String,          // RFC3339, when the producer first failed to refresh
    pub class: Option<String>,  // same vocabulary as error_class
}

Absent on a fresh entry. Present when the entry is a preserved reading served through an ongoing failure.

since is deliberately not fetchedAt. The reading was taken when it was taken; the failure began afterwards. The gap between the two is how long the producer has been blind, which is the quantity a staleness policy actually wants — an entry can be minutes old with the producer perfectly healthy, or seconds old with the producer failing since just after the read.

class is optional so a producer that cannot classify a failure still discloses the state. Disclosing "stale, cause unstated" beats looking fresh.

Compatibility

skip_serializing_if + default, so a fresh entry emits no key and today's shape is byte-identical. Two tests pin both directions: a fresh entry's JSON contains no stale key at all, and an entry from a producer predating the field decodes unchanged.

Four unit tests total, cargo test green, clippy 0, fmt clean.

Note on what this is not

A producer serving preserved readings is behaving correctly — a brief upstream failure should not blank a window. This field discloses that it is happening; it does not report a fault. Consumers should read it as "the producer's confidence in this number has a known age", not as an error.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Discloses when a ProviderUsage reading is being preserved through an ongoing failure. Previously a preserved reading was indistinguishable from a fresh one except for fetchedAt; now a stale field conveys failure start time and optional class so consumers can act correctly.

  • Field shape: stale: { since: RFC3339, class?: String } on ProviderUsage in cortexkit-provider-usage 0.6.0; absent on fresh entries and only serialized when present. class uses the same vocabulary as error_class.
  • Semantics: since is when the producer first failed to refresh, not fetchedAt.
  • Compatibility: additive with skip_serializing_if and default; existing producers/consumers continue to interoperate. Tests pin absence/presence and round-tripping.
  • Adoption: Producers may set stale when serving preserved readings; consumers can gate decisions on stale.since instead of wall-clock TTLs. No required migration.

Written for commit 97a7eae. Summary will update on new commits.

Review in cubic

A preserved last-known-good entry is currently byte-identical to a fresh
one apart from fetchedAt, so a consumer cannot separate "this figure is
old because the producer cannot reach the provider" from "this figure is
old because nothing polled recently". Those have opposite remedies, and a
consumer with only a timestamp has to guess with a wall-clock threshold
-- which denies fresh-enough data in order to catch stale data. One
consumer built exactly that and had a dispatch blocked on a 58-minute
snapshot, which was genuinely stale but indistinguishable from a slow
poll at the moment it mattered.

`since` is deliberately not fetchedAt: the reading was taken when it was
taken, and the failure began afterwards. The gap between them is how long
the producer has been blind, which is what a staleness policy wants.

Additive and absent on a fresh entry, so today's shape is byte-identical
and a consumer predating the field decodes unchanged -- both pinned.
@ualtinok
ualtinok merged commit 7d8e415 into master Aug 13, 2026
8 checks passed
@ualtinok
ualtinok deleted the stale-disclosure branch August 13, 2026 15:29
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.

1 participant