Skip to content

Re-run a stored economy or budget-window report, or a simulation, on the current .py version - #1059

Draft
anth-volk wants to merge 20 commits into
mainfrom
v2plan/c1-17-re-runs
Draft

anth-volk wants to merge 20 commits into
mainfrom
v2plan/c1-17-re-runs

Conversation

@anth-volk

@anth-volk anth-volk commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1056

Summary

The simulation service re-runs a stored economy report or simulation on the current .py version (the v2 version manifest's default worker), as a new run started by run ID, and keeps every earlier run. A report's re-run can force a recalculation of both simulations.

  • POST /reports/{report_id}/runs (simulation:submit), body {"force": false}. The service reads what the report calculates (its country, year, region code, dataset name, options and each simulation's policy and dynamic), finds the rows with that region code and dataset name in the catalog version of the default worker's .py version, and reads each stored policy and dynamic on it. It then creates, in one transaction, the report's new run (trigger manual) and a new run of each side's own simulation, and starts the coordinator with the run IDs, as POST /reports does (Coordinate economy runs through the run tables instead of the evaluation records #1051's coordination). Without force, a side reuses its own simulation's newest run that succeeded on the current versions, under the same conditions as a new report's reuse; force calculates both simulations again. It answers 201 with the create's fields, naming the new run.
  • Budget-window reports (Calculate budget windows on the detailed-save path, five years at a time #1054, merged): the same route re-runs one as a new budget_window_runs row (trigger manual, recalculate set by force) with a report run per year. Each year's sides are the simulations that year's report run of the report's newest window run names (a year that reused another report's run names that report's simulation), each reusing a run of them unless forced. The first years start through the budget-window service's own start (_start_first_years, _YearStarts): up to five at once under one shared wait for Modal. It answers 201 with BudgetWindowReportCreated, so the route's 201 is the union of the two report answers.
  • POST /simulations/{simulation_id}/runs (simulation:submit), no body: a stored economy simulation's new run, always calculated, started on the country's simulation function.
  • Nothing changes a stored record. The service role may not update reports or simulations, so a report keeps its simulations; each new run records the catalog version, dataset and region it calculates on.
  • Refusals, before any write: 404 unknown_report / unknown_simulation, also for a record deleted between the re-run's read and its insert (the persistence library raises RerunRecordDeletedError when the database refuses the new run's key to the record); 409 run_in_progress, naming the run in run_id (a report's budget-window run, or else its report run; a simulation's run), while the record has a run pending or running (checked under a transaction-level advisory lock on the record's ID, so two simultaneous re-runs of one record create one run); 409 rerun_not_supported for a household record, a budget window whose newest window run lacks a year, or one naming no simulation, region, dataset, year or country the service calculates; and the create's 400 and 422 codes, worded for the stored record (a 422 reason's loc starts with report or simulation). A report's re-run checks only the report's own runs, so a side whose simulation has a run under way for another report still gets a run of its own; a simulation's re-run checks all of its runs. When Modal refuses the new run's call, the run is failed and the 503 modal_call_refused names the record and the run.
  • Shared code: the economy submission service and the budget-window service are generic over their store type, and the re-run service extends the budget-window one without a constructor; a new window and a window re-run share the per-year reuse search, the years check, the year's report run row, its run IDs and its answer (typed by a protocol of the fields they read); its call start, record creation and input reads take run IDs and say whether the record is re-run; the economy routes' answer helpers are public so the re-run routes module uses them; find_reusable_run shares its run conditions with find_reusable_run_of, which searches one simulation's runs and compares the catalog version on the run row. POST /reports and POST /simulations answer as before.
  • Docs: the simulation entry README, the persistence README and docs/migration/stage-12-modal-worker-foundation.md describe re-runs. The changelog fragments are changelog.d/c1-17-reruns.added.md and changelog.d/c1-17-budget-window-reruns.added.md.

Waits for other PRs

Environment variables and secrets

None added or changed. The re-run routes use the economy routes' existing settings, which #1052 wired and this change doesn't touch:

  • SIMULATION_SERVICE_DATABASE_URL: a Secret Manager secret named by the GitHub variable SIMULATION_SERVICE_DATABASE_URL_SECRET_NAME, attached as a Cloud Run runtime secret by deploy-cloud-run-simulation-entry.sh.
  • SIMULATION_V2_MODAL_ENVIRONMENT and SIMULATION_V2_MANIFEST_NAME: set as Cloud Run environment variables by the same script, from the job's Modal environment and a fixed manifest name.
  • V2_SIMULATION_OUTPUT_BUCKET: the GitHub environment variable of the same name, passed to the script as V2_SIMULATION_OUTPUT_BUCKET_VALUE by simulation-deploy.reusable.yml.

Database grants

None needed. A re-run inserts into simulation_runs, report_runs and budget_window_runs (the service role holds SELECT, INSERT on each), reads reports, simulations, regions, datasets, tax_benefit_models and tax_benefit_model_versions (SELECT), and records or fails the new run's call through the column-level UPDATEs the creates already use; a window re-run's first years start through the budget-window store's advance, which writes only the window columns #1054's Public API grant gives the service. The advisory lock needs no privilege.

Testing

Each test this PR adds or changes, by node ID:

  • Persistence, against a disposable PostgreSQL 17 database built from the Public API's v2 migration chain at master with its privileges reconciled: the 23 test_economy_reruns.py tests (reading a stored report's and simulation's definition, reuse of a simulation's own run on a later catalog version, the re-run inserts and their refusal while a run is under way, the advisory lock held until the transaction ends, the re-run reads on the current catalog version) and the 11 test_economy_window_reruns.py tests (a window's newest window run's years, the window re-run's rows, years check and refusals, each year's reusable runs). The existing reuse, records, store and budget-window record tests that use the reworked reuse search, the shared window code and the test row helpers were run by node ID too.
  • Stage 12 persistence: the new budget-window store test (a report's later window run starts only its own years), against the same database as the Modal worker role. These store tests skip in CI, which sets no URLs for them.
  • Simulation entry (41 service, 25 route and 3 changed OpenAPI tests): test_economy_reruns.py (service, store and Modal faked, with the budget-window service's fake advancer for windows), test_economy_rerun_routes.py, and the budget-window submission tests whose start path changed.
  • Generated client: regenerated locally with ./scripts/generate-clients.sh; the four re-run tests in test_economy_generated_client.py ran against it.
  • CI's pinned Ruff (format and lint), the rules rendering, the quality guards, the lock checks and Pyright on the persistence, Stage 12 persistence and simulation entry packages pass.

🤖 Generated with Claude Code

anth-volk and others added 11 commits November 13, 2026 00:42
…log version (failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ent catalog version

A re-run keeps the record and every earlier run. It reads the record's
region code, dataset name and each simulation's policy and dynamic, so the
current catalog version's rows can be found; a report side may reuse a run
of its own simulation that succeeded on the current versions, compared on
the run's own catalog version, dataset and region. Nothing is created while
the record has a run pending or running, checked under a transaction-level
advisory lock on the record's ID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/{id}/runs and POST /simulations/{id}/runs (failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and POST /simulations/{id}/runs

A re-run creates the stored record's new run on the v2 version manifest's
default worker and starts it by run ID, as a new record's first run is
started; every earlier run is kept. A report's re-run reuses a run of each
side's own simulation that succeeded on the current versions unless force
is set, and its run's trigger is manual. A simulation's re-run is always
calculated. A record with a run pending or running, a budget-window or
household record, or one missing a row it calculates on is answered 409;
an unknown record 404.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…log fragment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… definition reads (failing)

The entry tests fail: a record deleted after a re-run's reads should be
answered 404, and a row deleted meanwhile should be refused naming the
report. The persistence tests cover existing behaviour and pass at once:
the reuse search's catalog-version condition on the run row, and the
household, filtered, other-model and other-type definition reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ored record's

A record deleted after a re-run read it is answered 404, and any other row
the database refuses meanwhile is named as the report's or simulation's.
The economy submission service is generic over its store, so the re-run
service needs no constructor of its own; the re-run records share the
economy report type; the docs say how the caller-row codes apply to a
re-run, that the report body is required, and how the in-progress check
differs between a report and a simulation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s own error (failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d re-run record apart

insert_report_rerun and insert_simulation_rerun raise RerunRecordDeletedError
when the database refuses the new run's key to the record itself, so the
re-run service answers 404 without naming tables or columns. The docs say a
report deleted with its simulations answers 400 missing_reference, and the
re-run routes' 400 names every code that refers to the stored record's rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, and when a deleted report answers 404

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 9 commits November 13, 2026 02:54
Brings in the budget windows (#1054). Both sides' imports are kept; the
call-refused answer is public under main's name with the re-run keyword;
the report route dispatches budget windows as main does, with the public
answer helpers. The budget-window service is generic over its store, and
the re-run service extends it, so the app builds one service that creates,
re-runs and starts budget windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion (failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s (failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A budget window's re-run creates a budget-window run (trigger manual,
recalculate set by force) with a report run per year, each year on the
simulations that year's report run of the report's newest window run
names, reusing a run of them that succeeded on the current versions unless
forced. Its first years start as a new window's do: the budget-window
service's start of the first years now takes the window's IDs. A report
re-run, of either kind, is refused while a budget-window run or a report
run of the report is pending or running, and a window whose newest run
lacks a year answers 409 rerun_not_supported. The re-run answers with the
budget-window create's shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fail: a window re-run checks its years are its window's, and a year naming
no reform is left out of a window's definition. Pass at once, covering
existing behaviour: a window re-run of a deleted report or year
simulation, one refused while a year still runs after its window ended,
and the budget-window store starting only a report's later window run's
years.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s years

A window re-run's dataclass checks its years as a new window's does, with
the same check; a window's definition leaves out a year naming no reform,
so the service needs no copy of each year. A new window and a re-run share
the per-year reuse search, the year's report run row and the year's answer;
the budget-window service reads its store by one name. The docs say which
run a 409 run_in_progress names, that up to five years start at once, and
that a deleted budget-window report always answers 404.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ad, and test the insert order

The shared year row and year answer take a protocol of the fields they read,
so the new-window modules no longer name the re-run's year type. A new test,
which passes at once, shows a window re-run of a report deleted with a year's
simulation is refused as a deleted report, since its window run is inserted
first. The years check, the window years read and the README say what they
do in full.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk anth-volk changed the title Re-run a stored economy report or simulation on the current .py version Re-run a stored economy or budget-window report, or a simulation, on the current .py version Nov 12, 2026

This branch has not been deployed

No deployments
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.

Re-run a stored simulation or report on the current .py version

1 participant