Skip to content

Recovery hint attributes: declare which failure recovers how - #597

Merged
jeremydmiller merged 1 commit into
mainfrom
feature/recovery-hint-attributes
Jul 31, 2026
Merged

Recovery hint attributes: declare which failure recovers how#597
jeremydmiller merged 1 commit into
mainfrom
feature/recovery-hint-attributes

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

A tag or trait says this test is unreliable. A hint says which failure is unreliable and what fixes it — so an assertion failure on the same test is still reported as the bug it is, rather than retried away with everything else.

[ClearsOnRetry(typeof(TimeoutException), Because = "the broker warms up slowly")]
[ClearsOnRecycle("rabbit,kafka", typeof(BrokerUnavailableException))]
[ClearsInFreshProcess(typeof(BadImageFormatException))]
[NeverRecovers(typeof(SomeDeterministicBug), Because = "this is a real bug")]
public class OrderTests;

Why here rather than in a test runner

Any suite that would want these already references JasperFx — directly, or transitively through Marten, Wolverine or Polecat. Putting the vocabulary here means a project can write down what it knows about its own flakiness without taking a dependency on whatever ends up running it. A runner that understands them acts on them; a runner that does not is unaffected, because these carry no behaviour and start nothing.

DispositionKind moves here with them rather than a runner keeping its own copy mapped at a seam. Two enums meaning the same thing is how a vocabulary starts drifting.

NeverRecovers is what makes the set usable rather than merely expressive. Without it, the only way to stop a broad "retry three times" policy from re-running a deterministic bug is to remove the retry — which also stops the retries that were pulling their weight.

Deliberately not included

Anything that reads or acts on a hint. Matching a failure to a hint, resolving assembly < class < method scope, and deciding what a retry budget permits are runner policy and stay in the runner.

One rule worth stating for whoever implements that side: a hint must never widen a retry budget. What recovers is the author's knowledge; how much time a run may spend is the operator's. Conflating them lets a test author escape a ceiling set by whoever runs the suite.

Also worth being explicit: these are declarations, and there is no in-box reader yet. An out-of-process runner cannot see attributes at all without either projecting them as traits or emitting a build-time manifest — and that choice belongs with the runner that needs it, not here.

Verification

12 new tests covering the disposition each hint declares, resource parsing (splitting, trimming, empty-entry compaction), and discovery from class, method and assembly scope. 491 pass in CoreTests, 1 pre-existing skip.

No new AOT or trim warnings — attributes are declarations, so nothing here reflects.

🤖 Generated with Claude Code

https://claude.ai/code/session_012vmmUkRtSFMk1Pbf9E5eor

A tag or trait says *this test* is unreliable. A hint says *which failure* is
unreliable and what fixes it, so an assertion failure on the same test is still
reported as the bug it is rather than retried away with everything else.

    [ClearsOnRetry(typeof(TimeoutException), Because = "the broker warms up slowly")]
    [ClearsOnRecycle("rabbit,kafka", typeof(BrokerUnavailableException))]
    [ClearsInFreshProcess(typeof(BadImageFormatException))]
    [NeverRecovers(typeof(SomeDeterministicBug), Because = "this is a real bug")]
    public class OrderTests;

These live in JasperFx rather than in a test runner on purpose. Any suite already
referencing JasperFx -- directly, or transitively through Marten, Wolverine or
Polecat -- can now write down what it knows about its own flakiness without
taking a dependency on whatever ends up running it. A runner that understands
them acts on them; a runner that does not is unaffected, because these carry no
behaviour and start nothing.

DispositionKind moves here with them rather than a runner keeping its own copy
mapped at a seam. Two enums meaning the same thing is how a vocabulary drifts.

NeverRecovers is what makes the set usable rather than merely expressive. Without
it the only way to stop a broad "retry three times" policy re-running a
deterministic bug is to remove the retry, which also stops the retries that were
pulling their weight.

Deliberately NOT included: anything that reads or acts on a hint. Matching a
failure to a hint, resolving assembly < class < method scope, and deciding what a
budget permits are runner policy and stay in the runner. Note in particular that
a hint must never widen a retry budget -- what recovers is the author's
knowledge, how much time a run may spend is the operator's, and conflating them
would let a test author escape a ceiling set by whoever runs the suite.

The attributes are declarations only; there is as yet no in-box reader. An
out-of-process runner cannot see attributes at all without either projecting them
as traits or emitting a build-time manifest, and that choice belongs with the
runner that needs it.

12 new tests; 491 pass in CoreTests. No new AOT/trim warnings -- attributes are
declarations, so nothing here reflects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vmmUkRtSFMk1Pbf9E5eor
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