Repository navigation
Say in a failed economy answer how long its failure is kept - #4172
Merged
Merged
Conversation
A failed economy calculation's 502 body carried when a new run could be requested only as a sentence in its message, so a client had to parse the text to know when to offer Retry. The body now has `retry_after_seconds`: the whole seconds, rounded up, until the stored failure's record expires and the next request submits a new job. It is read from the record's own expiry in the cache (PTTL), after the failure is written and on every replay, so it is exact for a two-minute US state or local-area breakdown failure, for a failure kept the usual 30 days, and for one stored before failures were kept for two minutes. It is null when no failure is stored (a refused breakdown request) or when the record or the cache cannot be read. The message is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…x's expiry - When the cache refused the error write, retry_after_seconds was read from the still-computing record, about 30 days, though no failure was stored. The error write now returns the stored failure, and only a stored failure gets a value; otherwise it is null. - A lookup finds a record only through its scope index, which a later job in the same scope can give a shorter lifetime than the record's. The value is now the earlier of the two expiries, and 0 once the index has expired. - The value is documented as the longest wait: a deploy that changes the runtime cache version stops the record being found sooner. - The cache read catches RedisError and OSError instead of Exception, so it needs no inline suppression, and the PTTL reply constants live in runtime_cache/core.py. - The three to_dict tests expect the new key; new tests cover a refused error write, an index that expires first, Redis's no-expiry reply, and a new run once the wait has passed. Code comments name the Public API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A poll of a job found through another record's lookup (by the options-hash prefix, or under a worker app the request no longer resolves to), or a late poll of a job a newer one has replaced, writes its failure onto a record the next request does not select: that request starts a new run, or answers from the newer job. The answer still reported that record's lifetime, up to 30 days, so a client hid Retry while a new run could start at once. A fresh failure now gets retry_after_seconds only when the next request's own lookup, run on a copy of the request's options, answers from that same stored failure; otherwise it is null. A replayed failure is the one this request's lookup chose, so it keeps its value. The route, the result type and docs/canonical-spm.md describe the field and when it is null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #4172 +/- ##
===========================================
+ Coverage 53.93% 88.30% +34.37%
===========================================
Files 281 293 +12
Lines 16261 17506 +1245
Branches 2629 2875 +246
===========================================
+ Hits 8770 15459 +6689
+ Misses 6760 1335 -5425
+ Partials 731 712 -19 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The check that the next request answers from a fresh failure ran the full reusable-impact lookup, whose refresh asks the simulation API for the worker app. That added a request to the simulation API on every failing poll, where a poll had made none, and when that request failed the refresh was skipped and the failure kept its full lifetime anyway. The check is now the cache lookup alone (_get_most_recent_impact): it still gives no seconds for a late poll of a job a newer one replaced, and makes no request to the simulation API. A deploy that changes the worker app a request resolves to falls under the documented longest wait, as a version change does. The route, the result type, the two-minute failure's docstring and docs/canonical-spm.md now name the newer-job case; tests check that a failing poll asks the simulation API for no worker app, and give no seconds on the 30-day path as well as the two-minute one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4171
What this changes
A failed economy calculation's 502 body from
GET /<country_id>/economy/<policy_id>/over/<baseline_policy_id>now hasretry_after_seconds:It is the whole number of seconds, rounded up, until lookups stop finding the stored failure. Until then every request gets the same failure; the first request after it submits a new job. Clients can read this number instead of parsing the message ("A new run for this state can be requested 2 minutes after this failure.", from #4064), which PolicyEngine/policyengine-app-v2#1439 does today.
PTTLon both), and 0 once the index has expired. It is read after a fresh failure is written and again on every replay of a stored one, so it is exact for a two-minute US state or local-area breakdown failure, for a failure kept the usual 30 days, and for a US state failure stored before Let a failed US state economy run be retried after two minutes #4064.Changes by module
runtime_cache/reform_impacts.py:ReformImpactCache.seconds_left(impact), the earlier of the record's and scope index'sPTTL, rounded up; None without a worker handle, for a missing or never-expiring record, or onRedisError/OSError.runtime_cache/core.py,runtime_cache/fake.py:pttlon the cache backend protocol and the in-memory fake, and the PTTL reply constants (-2missing,-1no expiry).services/reform_impacts_service.py:get_reform_impact_seconds_left(impact).services/economy_impact_types.py:EconomicImpactResult.retry_after_seconds, also into_dict().services/economy_stored_impacts.py:_set_reform_impact_errorreturns the stored failure;_failure_the_next_request_answers_fromchecks a fresh failure against the next request's cache lookup (_get_most_recent_impact);_stored_failure_resultreads the seconds.services/economy_impact_execution.py: the three answers with a stored failure (fresh and kept 30 days, fresh and kept two minutes, replayed) go through those helpers.routes/economy_routes.py: the 502 body carries the field.docs/canonical-spm.md: the field and when it is null.No inline suppressions are added.
Known timing limits
Tests
Written first and seen failing, then passing, in four rounds:
to_dicttests, the late poll of a replaced breakdown job, and the two exact 502 bodies now expect the field; the sharedReformImpactsServicestand-in returns None for it.Only the added and changed tests were run locally, by node ID, with
ruff check,ruff format --check,mypyandscripts/run_quality_guards.py. The full suite runs in CI. Three independent review rounds were run on the branch; their findings are fixed in the second to fourth commits.Release
Additive: clients that don't read the field are unaffected. PolicyEngine/policyengine-app-v2#1439 reads it, and should be released only after this is deployed.
🤖 Generated with Claude Code