Repository navigation
Conversation
… (failing) The Public API's next revision (C1 · 25) gives simulations and reports a key_version and definition_key, unique together, reports a dataset_independent_key, and both run tables a forced marker and a partial unique index allowing one unfailed, unforced run per definition and version set. Find-or-create names these as its conflict targets, so the models must declare them under the same names and conditions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sim's copies of the columns C1 · 25's revision adds: key_version and definition_key on simulations and reports (with dataset_independent_key on reports), each nullable, with the Public API's checks, the unique constraint find-or-create targets and the dataset-independent index; forced on simulation_runs and report_runs, false unless a forced re-run inserts it; and the partial unique indexes allowing one unfailed, unforced run per definition and version set. The models now match the Public API's tables at revision 94dc43079186. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A key is the lowercase SHA-256 of a definition's canonical JSON: keys sorted, no spaces, UTF-8, UUIDs as lowercase strings, missing values null, and no value JSON can't write exactly. An economy report's object names its key version, record type, country, each side's policy and dynamic, dataset name, region code, year, window size and options, as C1 · 24's answer 1a sets out; the proposal's worked example gives three reports' keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
definition_keys hashes a definition's canonical JSON with SHA-256, the format key_version 1 names, and builds an economy report's key object: each side's policy and dynamic, the dataset name, region code and year both sides share, the window size and the stored options. Names and codes, not catalog IDs, so a report keeps its key across releases. The dataset-independent key waits for C1 · 24's revised answer 1a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling) Each lookup inserts against the Public API's unique index for the row's definition or version set and, when that does nothing, reads the row that won: a simulation or report by its key, a simulation run by simulation, year, catalog version and versions, a report run by report and versions. A failed run is no longer joined, a forced run is never joined and never takes a run's place, a transaction inserting a run another has inserted waits for it and joins it, and a run that fails between the insert and the read is inserted again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shared_rows' four lookups each insert with ON CONFLICT DO NOTHING against the Public API's unique index for the row (a simulation's or report's definition key; a run's simulation or report, catalog version and versions, among runs neither failed nor forced) and, when that does nothing, read the row that won. A run that fails between the two is inserted again, up to three times. The partial indexes' predicates come from the models, so the conflict targets match the indexes. Forced runs and a budget window's year runs are never looked up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answer 3: a forced re-run is an extra run. Its report run, or each year's report run of a budget window, and the simulation runs it calculates are marked forced, so the Public API's one-unfailed-run indexes leave them out: a forced re-run is inserted beside a pending, running or succeeded run on the same versions, and is never refused while one is under way. A forced report re-run calculates both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A forced report re-run's report run and new simulation runs, and a recalculating budget-window re-run's year runs and simulation runs, are inserted with forced set, so the Public API's one-unfailed-run indexes leave them out. Such a re-run takes no re-run lock and is never refused while a run is under way; a forced report re-run reuses no run. Unforced re-runs are unchanged until C1 · 24's answers 4 and 8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service passes a re-run's force to its new run, which the store then inserts as an extra run marked forced, never refused while a run is under way. A budget window's recalculate already did. The README and the module's steps say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once identical requests join existing runs, the check's report request
ends on a run a check on the same versions already calculated. The check
then forces a new run with POST /reports/{report_id}/runs and
{"force": true}, asked once, and waits for that run; a refused or
unanswered re-run, or one naming no run, fails the check before any read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check finds or creates its Wales report as before, then sends
POST /reports/{report_id}/runs with {"force": true}, asked once, and
waits for that run. Once identical requests join existing runs, the
report request alone would end on a run an earlier check on the same
versions calculated; a forced run, which nothing joins, calculates on
every deploy (C1 · 24's answer 3). The re-run needs the same
simulation:submit permission and reads no new variable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling) C1 · 24's answer 3: a re-run joins a run under way, so #1059's 409 run_in_progress goes, and a re-run on unchanged versions joins the succeeded run there. Answer 2: a failed run is never joined, so the next re-run inserts a new one. The re-run answers the run it ended on, whether it inserted it, and its status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A simulation re-run now looks up the simulation's run that hasn't failed and wasn't forced on the same year, catalog version and versions, pending, running or succeeded, and creates one only when there is none (C1 · 24's answers 2 and 3). It takes no re-run lock and raises no RunInProgressError: the Public API's index makes concurrent re-runs end on one run. It returns the run, its status and whether it was created, so the service starts a Modal call only for a run it created. The store's creates return what their insert returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once the store joins the simulation's run on the re-run's versions, the service answers that run and its status, and starts and records no Modal call for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /simulations/{simulation_id}/runs now answers the run the store
joined, pending, running or succeeded, with its status and no Modal call,
and starts and records a call only for a run the store created. The
service's create helpers return what the store's create returns. The
route's description, the module's steps and the README say so, and that
409 run_in_progress is a report's only.
The checks of which stored records the service re-runs, and what each
calculates on, move unchanged from economy_reruns into
rerunnable_records, which keeps economy_reruns under the 800-line limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answer 3: a re-run while the report has a run pending, running or succeeded on the current versions joins that run, with the simulation runs it names, and creates nothing, so #1059's 409 run_in_progress goes. A run still under way on older versions is no run to join: the re-run creates its own on the current versions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iling) Another request may create the report's run on the re-run's versions between the re-run's lookup and its insert. The re-run then joins that run and takes back the simulation runs it had inserted, so no simulation run is left that no report run names. The fixtures committing a catalog for tests of two transactions move to conftest, the deletion of its rows to committed_rows, so both test modules use them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unforced report re-run, under the report's re-run lock, joins the report's run on the current versions that hasn't failed and wasn't forced, pending, running or succeeded, with the simulation runs it names, and creates nothing (C1 · 24's answers 2 and 3). Only a budget window's run under way still refuses it. Otherwise it creates its run inside a savepoint, inserting the report run against the Public API's index: one another request created after the lookup is joined, and the savepoint takes back this re-run's simulation runs. It returns the run, its status and pair, and whether it created it. shared_rows gains the lookup of a report's joinable run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once the store joins the report's run on the re-run's versions, the service answers that run, its status and the simulation runs it names, with both sides reused, and starts and records no coordinator call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /reports/{report_id}/runs now answers the run the store joined,
pending, running or succeeded on the current versions, with its status,
the simulation runs it names and both sides reused, and starts no
coordinator. Only a run the store created gets its call. The 409
run_in_progress is left to budget windows, whose re-runs C1 · 24's answer
8 decides; the README, the route's notes and the module's steps say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1's bug: the reuse search behind find_reusable_run and find_reusable_run_of has no condition on forced, so a forced re-run's succeeded simulation run is offered to the next identical report, and to an unforced re-run of the same report. C1 · 24's answer 3 says nothing joins a forced run. The test rows' run shape gains the forced marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reuse search behind find_reusable_run and find_reusable_run_of now leaves out runs marked forced, so a forced re-run's simulation runs are never named by another report run, as C1 · 24's answer 3 requires. Fixes review round 1's bug. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The report re-run join test counts report runs for the re-run's report run ID, and simulation runs for its simulation run IDs, rather than looking for the report run among simulation runs. The two-transaction test commits the first transaction only once a backend waits on a lock that transaction holds (pg_blocking_pids), not on any lock in the database, and re-raises a thread's error instead of failing on a missing result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1: SharedRowNotSettledError, raised when the run a lookup lost to keeps failing before it can be read, was answered as a 500. Nothing was written, so the re-run should be answered unavailable and asked again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service's create answers SharedRowNotSettledError, raised when the run a lookup lost to keeps failing before it can be read, as the 503 database_unavailable it gives a database error: its transaction wrote nothing, so the caller may ask again. Fixes a review round 1 finding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1: the OpenAPI document still says a re-run's 201 is a new run, created and started, and that a simulation's re-run answers 409 run_in_progress, which it no longer can: it joins its run. The 201 must say the run may have been joined, and only the report route's 409 names run_in_progress. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…und 1) Review round 1's documentation findings: - The re-run routes' 201 says the run may have been joined, and only the report route's 409 names run_in_progress, for a budget-window or household report with a run under way; a simulation's re-run joins. - The report route's description, ReportRerunRequest, the conflict codes, and the created answers' status and reused fields describe joining. - The READMEs say which re-runs take the advisory lock, that a household report's re-run is still refused while a run is under way, that an unforced re-run whose side has an unreused run on the current versions answers 500 until sides are found by version set, and that the GET routes read the newest run, which may be later than the one answered. - The staging check's script, guide, tests and workflow comment say its report request reuses simulation runs (and, once reports are found by key, the report run), so the forced run is what calculates. - insert_report_rerun says when its sides' plain inserts are refused, and the budget-window refusal shares one query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven tests stored two runs that haven't failed and weren't forced on one version set, which the Public API's next revision refuses; each failed with a unique violation against it and passes now, its intent kept: - the test rows' report run takes its versions, and the later run the re-run tests reuse is used by another report's run on the later versions, not by a run of the stored report, which a re-run would now join (five re-run tests); - the re-run after a failed re-run fails that re-run's simulation runs too, as its runs have ended; - the newest-run read's two later runs are forced re-runs' runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… test 07a1f44 moved _contract_reasons to household_report_calculations; the test of a refused missing-variables option called it through household_report_submissions and failed. Run by node ID, passing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answers 3 and 8 and interim answer B leave no re-run refused while a run is under way: an unforced re-run of a keyed economy or household report joins that run, and a forced one, as every re-run of a budget window or of a report without keys is, is an extra run. So the OpenAPI document must name no run_in_progress code, and the two tests of that 409 go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No re-run the service sends is refused while a run is under way any more (C1 · 24's answers 3 and 8, interim answer B), so the code leaves EconomyConflictCode, EconomyRerunConflictError loses its run_id, and rerun_refusals no longer turns the store's RunInProgressError into a 409. The re-run routes' 409 answers rerun_not_supported alone. API v1's re-run client (C1 · 28) was told the 409 goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The create and re-run routes, HouseholdReportCreated and the entry README say a household report and its simulations are found or created by key, its run joined or created, a policy compared with itself named on one run, and a call started only for a run the request created; that Modal's refusal fails only that report run; and that no re-run is refused while a run is under way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 25's schema at its pushed 63cce8273 (PolicyEngine/policyengine-api#4303): answer 6 makes reports.label, report_runs.trigger and budget_window_runs.trigger nullable, so the service can stop writing them; answer 8 adds budget_window_runs.policyengine_version and uq_budget_window_runs_unfailed_unforced_versions, one window run per report and version set unless recalculated. B1 · 5's comparison with this test database, rebuilt from that commit, shows those five differences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Matching the Public API at C1 · 25's 63cce8273: reports.label, report_runs.trigger and budget_window_runs.trigger are nullable (answer 6), and budget_window_runs has policyengine_version and uq_budget_window_runs_unfailed_unforced_versions on (report_id, tax_benefit_model_version_id, policyengine_version) where status <> 'failed' AND NOT recalculate (answer 8). B1 · 5's comparison with the rebuilt test database passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answer 6: a shared report holds no request's label, description or user, and a shared run no trigger (each user's report-run record holds it). Keyed economy and household creates, and economy and household re-runs, joined or forced, must leave all four null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answer 6, on C1 · 25's nullable columns: the keyed economy and household creates write no reports.label, description or user_id and no report_runs.trigger, and re-runs, joined or forced, write no trigger. Each user's saved report and report-run record (API v1, C1 · 28) hold them. KeyedEconomyReport and KeyedHouseholdReport lose label and description, which the service no longer passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d (failing) C1 · 24's answer 6: the Public API keeps a report's name and description on each user's saved report, and sim stores neither, so its report requests may leave out the label and say it is not stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… storing them C1 · 24's answer 6: both are optional on EconomyReportRequest and HouseholdReportRequest and say they are not stored (each user's saved report in the Public API holds them); given ones are still checked, so the Public API's callers' requests stay valid. A budget window's follow with its keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answer 8, on C1 · 25's window run index: a window request finds each year's simulations by that year's key and the report by its key (the window size in it), then joins the report's window run on the current versions that hasn't failed and isn't a recalculation, with its year runs; otherwise it creates the window run, recording its .py version, and each year's report run on its sides' runs, found or inserted. Tested: the first request's rows and keys, no per-user columns (answer 6); an identical request joining and inserting nothing; a window year naming a single-year report's simulation run (review round 4's bug 3); a failed window run getting a new one on the years' runs; one run a year for a policy compared with itself; the store; and a definition whose length isn't its years'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answer 8, on C1 · 25's window run index. find_or_create_budget_window finds each year's simulations by that year's key, in key order across every year, and the report by its key (the window's first year and size in it); it joins the report's window run on the current catalog and .py versions that hasn't failed and isn't a recalculation, with its year runs, or else inserts the window run first (find_or_insert_window_run: a request inserting one another has inserted waits for it and joins it), then every year's side runs, found or inserted in simulation ID order, and each year's report run naming them. A window year so names a single-year report's simulation run, and the reverse (review round 4's bug 3). No per-user column is written, and the window run records its .py version. EconomyRunStore.find_or_create_budget_window runs it in a transaction of its own. The economy create's simulation and report rows are built by economy_simulation_row and economy_report_row, which the window shares, and FoundSide says whether a side's run is reused, as answers do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one (failing) C1 · 24's answers 3, 6 and 8: an unforced window re-run joins the report's window run on its versions, with its year runs, or else creates one whose years' sides join their simulations' runs; a forced one (recalculate) is an extra window run on new runs of every year, all forced. Both record the window run's .py version and no trigger, return the window run and years they ended on, and name no reused side. No window re-run is refused while a run is under way any more; a single-year re-run of a window report still is, which the service never sends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answers 3, 6 and 8. insert_window_rerun, unforced, takes the report's re-run lock and joins its window run on the re-run's catalog and .py versions that hasn't failed and isn't a recalculation, with its year runs; otherwise it inserts the window run (a concurrent one is joined) and each year's report run on its sides' runs, found or inserted, as a keyed window's create does. A forced re-run inserts an extra window run on new runs of every year, all forced. Both record the window run's .py version and no trigger, and return the window run and years they ended on, as EconomyRunStore.create_window_rerun does. No window re-run is refused while a run is under way, and none names a reused side. insert_window_years is the year insert the window's create and re-runs share; stored_window_years reads a joined window run's years. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ice (failing) C1 · 24's answer 8, in the entry: a window request reads no run to reuse, finds or creates its window by key (its definition holding its first year, size and SPM selection, its inputs the selection alone), starts its first years only for a window run it created, on the year runs the store names (a year may name another report's run), and answers a joined window run as it is, starting no year. A keyed window's unforced re-run joins its window run the same way; force recalculates. The fakes find or create windows and can join. Also the pinned formatter's one-line change to the household builder's test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…indow re-runs C1 · 24's answer 8, in the entry. A window request reads no run to reuse, checks its first year's calculation on the run IDs it proposes, and finds or creates its window by key (KeyedBudgetWindow: the definition holds its first year, size and SPM selection; the report stores the selection alone); only for a window run it created does it start the first years, on the year runs the store names, which may be another report's. A joined window run is answered as it is. A keyed window's unforced re-run joins its window run the same way, and starts its first years only for a window run it created; force, or a window without keys, recalculates. The economy service's reusable-side helper, which only the window create used, goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (failing) C1 · 24's answer 6: the window's report stores neither, as an economy or household report's doesn't. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 24's answer 6, as for economy and household reports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… force The routes, ReportRerunRequest, the re-run modules and both READMEs say a budget window, its years' simulations and its window run are found or created by key, a window year sharing a single-year report's runs; that only a window run the request created has its first years started; that a keyed window's re-run joins its window run, force or a window created before keys recalculating; and that no keyed create writes a per-user column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 5's finding 4 and 6. insert_report_rerun no longer refuses while a window run of the report is under way: the service re-runs a window with insert_window_rerun, and nothing maps the refusal any more, so RunInProgressError, its query and the test of it go. The unused ServiceSettings.reuse_prefix and the test-only FoundEconomyReport.inserted_simulation_run_ids go too (the tests check each side's run_inserted instead). find_or_insert_report_run_on_sides returns no side runs after a lost race, since the savepoint took back what it inserted. The output bucket setting stays, read by nothing, until the reuse search is removed with its deployment setting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 5's findings 1 to 3: no re-run writes a trigger (answer 6); a keyed window's re-run joins after the re-run lock, as a report's does; a household report's re-run joins or forces as an economy one's does; window answers name the window run a request ended on, joined or created, and a side reused when this request didn't insert its run; request options are what the calculation reads, the report storing the SPM selection alone; include_cliffs: true is refused until C1 · 39; the old create functions without keys are no longer the service's, kept for tests; the persistence README lists the household keys. The window options test is renamed for what it checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 5's finding 5. Both find the stored window, whose earlier window run failed, so neither finds a run to join; the first inserts a window run and holds its transaction open until the second waits for it at the window run's index, then the second joins it with its year runs, and only the first starts its years. It passes against the code already there. The two-transaction helpers moved from the economy test module to committed_rows, which both use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er, test isolation - Docs: the persistence README no longer names the removed run-in-progress refusal; the entry README's economy re-run names no trigger; the deployment notes say the service no longer reads the output bucket; the Stage 12 document's re-run and household re-run passages describe joining and forced runs with no trigger. - budget_window_records' year run row, used only by the window insert without keys, is private and writes initial and unforced itself; the module docstring says such a window could name a succeeded run on a year's side. - The window two-transaction test keys its window under the committed catalog's own country, and asserts that the second request waited at the window run's insert (wait_until_another_backend_waits_for now returns the waiting statement); test_shared_rows uses committed_rows' helpers instead of its own copies. The pinned formatter's changes to three test files come with them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a race in a test helper - The entry README names the Public API's per-user report-run record, not API v1's. - The deployment notes say the deploy still sets and checks V2_SIMULATION_OUTPUT_BUCKET, the service reads it into its settings and uses it for nothing, and both go with the run-reuse search later. - The Stage 12 document's re-run passage says a simulation's re-run joins or creates its run whether or not the simulation has a key, and takes no force. - budget_window_records' year run row docstring says it builds each year's run of a window's first window run. - test_shared_rows loses a comment its moved constant left behind. - wait_until_another_backend_waits_for reads the waiting backend's statement again in a transaction of its own once it has found it, so the statement is the one the backend waits in, not the one before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The settings comment, the Cloud Run deploy script's comment above the bucket check, the economy deployment test's module docstring and ServiceSettings now say the same as the deployment notes: every deploy sets and checks V2_SIMULATION_OUTPUT_BUCKET, the service reads it into its settings and uses it for nothing, and both go with the run-reuse search. The two-transaction helper's docstring says the statement is read once the waiting backend's pid is found, and test_shared_rows is formatted again. Comments only in the deploy script: no variable or check changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d until it merges The Public API schema job built the tables at 429d348ae, before #4303, so every test needing the key columns, forced markers and nullable per-user columns failed. 63cce8273 is #4303's pushed head, which this branch's models match (B1 · 5's comparison passes locally against it). The job's last step requires a commit on the Public API's master, so it fails until #4303 merges; then the pin moves to #4303's merge commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…un test The Test integration job generates the client from the entry's OpenAPI document, which no longer lists run_in_progress, so the test's sample 409 with that code failed to parse. No re-run is refused while a run is under way; the 409 left is rerun_not_supported. Run by node ID against the regenerated client, passing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been 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 #1073
Identical requests now end on the same simulation, report and run rows, together or apart, and the Simulation Entrypoint starts a Modal call only for a run its own transaction inserted (tracker C1 · 27, following C1 · 24's decisions in the shared-reports proposal).
What changes
definition_keysbuilds each record's key: the lowercase SHA-256 of a definition's canonical JSON, withkey_version1. An economy simulation is keyed by its country, policy, dynamic, dataset name, region code, year, a null filter and the SPM selection; an economy report or budget window by both sides, the dataset, region, year and SPM selection they share, and the window size, with no other option; a household simulation by its country, policy, dynamic, stored household and year; a household report by its sides, household and year. Each report also stores adataset_independent_key.shared_rowsdoes oneINSERT … ON CONFLICT DO NOTHINGper row against the Public API's unique indexes (simulations and reports by key; simulation runs, report runs and budget-window runs among runs that haven't failed and weren't forced), then reads the row that won.keyed_economy_records,keyed_household_recordsandkeyed_window_recordsrun a create in one transaction: simulations by key in key order, the report by key, the report's (or window's) run on the current versions joined when there is one, otherwise each side's simulation run found or inserted and the report run (or window run and each year's report run) naming them. A competing request's report run is joined and the savepoint takes back this request's simulation runs. A budget-window year and a single-year report of that year share simulations and runs.POST /reportsfor economy, budget-window and household reports;POST /simulations) find or create by key, and start a coordinator, household function or simulation function only for a run they inserted; a joined run is answered as it is, 201.{"force": true}, and every re-run of a report created before keys (interim answer B), is an extra run markedforced(a window'srecalculate), which nothing joins and the run indexes leave out. A simulation's re-run joins its run. No re-run is refused while a run is under way, so 409run_in_progressis gone from the API.include_cliffs: trueanswers 422cliffs_not_available, and so does a re-run of a stored report whose options hold it, until C1 · 39 calculates cliffs per simulation run.reports.label,descriptionoruser_id, or areport_runs/budget_window_runstrigger; the requests acceptlabelanddescriptionas optional and don't store them.tables.pymodels those columns as nullable, andbudget_window_runs.policyengine_versionwith its index, matching Share detailed-save reports and runs by key, and record each user's report runs policyengine-api#4303.ReportRunIdsandReportExecutionInput, householdHouseholdReportRunCalculation).{"force": true}and follows that forced run, so every deploy still calculates.C1 · 24's answers it follows
1a to 1c (keys, dynamics by ID, key columns with random row IDs), revised 1a (
dataset_independent_keywithout the dataset), 2 (a failed run never counts), 3 (forcemakes an extra run), 4 (SPM in the simulation key, no options in the report key, each run storing its own simulation's plan), 5 (a coordinator waits for a run another report run is calculating), 6 (no per-user columns on shared rows), 8 (household reports and budget windows by key), 10, who may sendforce, and the interim answers A and B.Deploy first
94dc43079186) applied in staging and production before this deploys; thenpublic-api-commitmoves to its merge commit. Once this writes nulls, the Public API can't roll back below that revision.Deployment code and environment variables
.github/scripts/simulation-staging-economy-check.shchanges, as do a step name and a comment in.github/workflows/simulation-deploy.reusable.ymland a comment above the bucket check in.github/scripts/deploy-cloud-run-simulation-entry.sh; no Dockerfile, Terraform or Modal change, and no variable is added, removed or rewired. The staging check reads:SIMULATION_TEST_AUTH_ISSUERSIMULATION_ENTRYPOINT_AUTH_ISSUER(beta)simulation-deploy.ymlcallssimulation-deploy.reusable.ymlwithsecrets: inheritandrelease_environment: beta; thestaging_economy_checkjob runs in that environment and maps the secret into the step'senv:SIMULATION_TEST_AUTH_AUDIENCESIMULATION_ENTRYPOINT_AUTH_AUDIENCE(beta)SIMULATION_TEST_AUTH_CLIENT_IDSIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_ID(beta)SIMULATION_TEST_AUTH_CLIENT_SECRETSIMULATION_ENTRYPOINT_TEST_AUTH_CLIENT_SECRET(beta)ECONOMY_CHECK_DEADLINE_SECONDS,ECONOMY_CHECK_POLL_SECONDSThe re-run route needs the same
simulation:submitpermission asPOST /reports. Not verified: that the test client's token carries it for the re-run route in staging, which only a staging deploy shows.V2_SIMULATION_OUTPUT_BUCKET(GitHub Actions environment variable inbetaandprod, passed bysimulation-deploy.reusable.ymlasV2_SIMULATION_OUTPUT_BUCKET_VALUEto the deploy step, made a runtime variable bydeploy-cloud-run-simulation-entry.sh) is unchanged; the service reads it intoServiceSettings.output_bucketand now uses it for nothing, as reports join their sides' runs by version set.Meeting other open PRs
ReportRunIds,ReportExecutionInput,HouseholdReportRunCalculation), identical to its commits.dataset_namefrom the requests and moves the catalog reads toeconomy_catalog_rows; whichever merges second takesEconomyRequest(dataset_name=None)ineconomy_request_for, and its local-area test's expectedreports.inputsbecomes the SPM selection alone.Follow-up
The service no longer calls the old create functions without keys (
insert_economy_report,insert_household_report,insert_budget_window_report), which tests keep using to store records created before keys, nor the run-reuse search (find_reusable_run,RunReuseand thereuseparameters, always None now), andRerunSide.reusedis always refused. Removing them, with the output bucket setting and its deploy check, is a separate cleanup.Tests
Eight read-only review rounds (the last four found no bugs; their quality findings are fixed). Every new or changed test was seen failing first, then run by node ID only, the PostgreSQL ones against a database built from PolicyEngine/policyengine-api#4303's head
63cce8273; Ruff format and lint, Pyright and the quality guards passed for each changed package before each commit. A test sends two identical requests at once and checks they end on the same rows with exactly one Modal call.public-api-commitpins PolicyEngine/policyengine-api#4303's pushed head63cce8273for now, so the Public API schema job runs every schema test against the tables this PR needs; that job's last step, which requires the pinned commit on the Public API'smaster, fails until PolicyEngine/policyengine-api#4303 merges. The pin then moves to its merge commit, after it is applied in staging and production.🤖 Generated with Claude Code