+2
−1
+53
−16
Loading
Previously `check` was first-match-wins: the first rule whose match
conditions were satisfied terminated evaluation, so a later, tighter rule
never counted and never blocked. Only `:log` rules were non-terminating.
Now every matching rule is evaluated and counted. Two cases still terminate:
a matched `:skip` rule, and a `:block` rule that is over its limit (there is
nothing left to learn once the request is rejected). A `:block` rule under
its limit behaves exactly like `:log`.
Because several rules can match without any of them blocking, the single
returned Result reports the most constraining one: fewest `remaining`, ties
broken by declaration order. `remaining` is floored at 0, so an exceeded rule
would tie with one sitting exactly on its limit; exceeded is ranked first so a
breach is never hidden by a rule that merely reached its limit.
`peek` mirrors this and no longer excludes `:log` rules, since `check` can now
report a `:log` rule's Result.
Caller-visible changes:
- `cost` is debited from every matching rule, not just the first.
- A `:log`-only path returns `matched? == true` and emits no `rule="unmatched"`
metric; an exceeded `:log` rule surfaces as `exceeded? == true` with
`action == :log`. Shadow rules are now observable without gaining the
ability to block.
- `calls_total` is emitted per matched rule, so summing it by `rate_limiter`
counts rule evaluations rather than requests.
- `RateLimit-Reset` may point further out, since `remaining` is compared
across rules with different periods.
Error handling stays whole-check: a raise part-way through discards the
verdicts of rules already evaluated, though their counters were incremented.
No blocking verdict is lost that way, but a rule declared after the failing
one loses its chance to block.
Co-Authored-By:
Claude Opus 5 (1M context) <noreply@anthropic.com>