Repository navigation
Let a client delete a stored economy failure so the next request starts a new run - #4198
Merged
Merged
Conversation
…ts a new run DELETE on the annual economy URL takes the GET's query, parsed by the same AnnualEconomyQuery model, and selects the stored record the GET would answer from. It removes that record and its scope and recent index entries only when it is a failure recorded at least 10 seconds earlier (204), and refuses a more recent failure (409 with delete_after_seconds), a computing or completed record (409 with null), and answers 404 when nothing is stored. The removal is one Lua script that deletes the record only if it still holds the exact value that was read and checked, so a record rewritten in between, or a job a concurrent GET submits under a new key, is never removed; a changed record is selected and checked again, up to three times. A failed GET's 502 gains delete_after_seconds: the seconds until a DELETE is accepted, 0 once it is, and null whenever retry_after_seconds 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 #4198 +/- ##
==========================================
+ Coverage 88.52% 88.63% +0.11%
==========================================
Files 304 304
Lines 18313 18442 +129
Branches 3057 3084 +27
==========================================
+ Hits 16211 16346 +135
+ Misses 1363 1357 -6
Partials 739 739 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…s empty Review findings on the DELETE for a stored economy failure: - A late poll that read the failed job before the DELETE and wrote after it put the record back, so the next GET replayed the failure instead of starting a new run. ReformImpactCache.update now writes the record with Redis's SET XX, so it rewrites only a record that still exists. - When the cache could not be read, the selection found nothing and the DELETE answered 404 "nothing stored". It now confirms the cache is readable before a 404, and raises CacheCoordinationError (500) otherwise. - The script's refusal path is now tested: the record is rewritten after the scan reads it and before the script runs, on the in-memory fake and on real Redis (where an unconditional delete script fails the test). - New route tests: typed SPM failures, an older failure behind a newer job, a bare state code, an omitted version, a DELETE for another request, and the DELETE's 502. - The 10-second cap on the wait is documented as such: with clock skew between instances a DELETE sent after the wait can still get a 409. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v1 route contract test loads the economy routes with a stand-in economy_service module that defines only the names the routes used before, so importing the new types through economy_service failed there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its tests Second review round: - One record read that timed out still made the DELETE answer 404 "nothing stored": the lookups read any cache error as an empty result, and the readability check only read the recent index. The DELETE now selects inside ReformImpactCache.reads_fail_closed, a context in which those same lookups raise CacheCoordinationError instead, so the selection stays the GET's and an unreadable record or cache gives 500, never 404. confirm_readable is removed. - The budget-window 502 for an unreachable simulation service no longer carries delete_after_seconds, since that URL takes no DELETE. - Tests now prove the null delete_after_seconds when a stored failure's lifetime cannot be read, the Lua script's removal of both index entries on real Redis, a DELETE with another version, and the 500 body through the application's error handlers. - The docs say each instance counts the 10 seconds on its own clock, and that after a deploy that changes the worker app the DELETE answers 404 while the next GET starts a new run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the wording Third review round: - New tests: a DELETE half a second before the 10 seconds end is refused with 1 second left, and a failure stored under a worker app the GET no longer resolves to is left in place with a 404. Each fails if the lockout is shortened or the selection skips the worker-app check. - A record whose stored value cannot be decoded counts as nothing stored for the DELETE, as for the GET; a test pins it, and the docstrings and docs/canonical-spm.md now say that only an unreachable cache or a record the cache cannot return gives 500. - A 404 is followed by a GET that submits a new job, or answers computing while another request submits one. - The two-minute retry note on state and local-area failures now also says a new run can be requested 10 seconds after the failure by first sending DELETE to the URL. 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 #4197
Summary
DELETE /<country_id>/economy/<policy_id>/over/<baseline_policy_id>economy_routes.py). The query is parsed byAnnualEconomyQuerythroughparse_multidict_query, the GET's model, and both methods build the request with one helper,_annual_impact_request.EconomyService.delete_failed_economic_impactvalidates the request as the GET does and selects the stored record with the GET's_get_reusable_impact, including its worker-app check, on a fresh copy of the options for each attempt. It submits and polls nothing.FAILED_IMPACT_DELETE_LOCKOUT_SECONDS, from itsend_time). It is removed with its scope and recent index entries, so the next GET submits a new job.{"status": "error", "message": …, "result": null, "delete_after_seconds": N}: a more recent failure.delete_after_seconds: null: a computing or completed record.{"status": "error", "message": …, "result": null}: nothing is stored that the GET would answer from. That includes a record whose value cannot be decoded, and a failure stored under a worker app the request no longer resolves to; the GET would submit a new job for both.status,messageandresult: a request to the simulation service got no response.CacheCoordinationError). The selection runs insideReformImpactCache.reads_fail_closed(), aContextVarmode in which the GET's own lookups raise instead of reading an error as nothing stored. Outside that block the lookups behave as before.delete_after_seconds.Removing only the failure that was checked
ReformImpactCache.delete_failurefinds the failure's record key through its scope index, which also finds typed failures whose worker handle was cleared. One Lua script,DELETE_UNCHANGED_RECORD, deletes the record and both index entries only if the record still holds exactly the value read.FAILED_IMPACT_DELETE_ATTEMPTS(3) times.ReformImpactCache.update, used for every completion and failure write, now writes the record with RedisSET … XX. A late status poll that read the failed job before the DELETE can therefore no longer write it back after the 204, andupdatereturns None, as it does for a missing record. The index entries such a refused write adds point at no record and are skipped on read, as an expired record's are. First writes (set_reform_impact) are unchanged.delete_after_secondson the failed annual GET's 502retry_after_secondsis null, because it is computed only when that field is.ReformImpactsServicetakes an injectablenowclock, which stampsend_timeand counts the lockout. Each instance counts on its own clock, and the value is capped at 10. The docs say what clock skew between instances does to the wait.delete_after_seconds: null; the budget-window 502 is unchanged.Other
CORSMiddlewarealready allowsDELETEand answers the preflight before Flask, and Flask-CORS's default methods includeDELETE. A test covers both preflights and the CORS headers on a 409 and a 204.docs/canonical-spm.md, where Say in a failed economy answer how long its failure is kept #4172 documentedretry_after_seconds, now describes the DELETE and the new field.tests/contract/registry.pyhas no entry for the annual GET, andmigration_registry.pyassigns requests by path segment, soscripts/export_migration_contracts.pyproduces no change.failure-delete(deleted,delete-skipped,coordination-failed) and fail-closedindex-readevents. The service logsDeleted a stored economic impact failurewith the removed run'sdeleted_observability_id. The DELETE adds no stage span, and no span names.InMemoryCacheBackendnow runs the new script, andsetacceptsxx. Any other script text is still taken as the claim store's compare-and-delete.economy_impact_types, because the v1 route contract test loads the routes with a stand-ineconomy_servicemodule.No inline
noqasuppressions are added.Testing
Only the added and modified tests were run, each selected by node ID (103 cases):
tests/unit/routes/test_economy_failure_deletion.py(new, 40 cases). It covers:error_bp);tests/unit/runtime_cache/test_reform_impact_failure_deletion.py(new, 17 cases). It covers:Real Redis, in
tests/integration/test_runtime_cache_redis.py(3 new tests, run against a local Redis withRUNTIME_CACHE_TEST_URL=redis://127.0.0.1:6391/0):Script mutations that delete unconditionally or skip the index removal fail these tests.
New unit tests elsewhere: the stand-in's script and
xxemulation, the service clock and countdown, the pass-through delete, and a stored failure whose lifetime cannot be read giving no waits.Existing tests updated for the new field and note text:
to_dicttests;ruff check,ruff format --check,mypyandscripts/run_quality_guards.pypass.🤖 Generated with Claude Code