Add budget-window batch economy endpoint#3387
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3387 +/- ##
==========================================
+ Coverage 76.66% 78.94% +2.27%
==========================================
Files 63 65 +2
Lines 3446 3762 +316
Branches 621 662 +41
==========================================
+ Hits 2642 2970 +328
+ Misses 629 614 -15
- Partials 175 178 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
I think it is dangerous for request-serving code to issue schema-management SQL ( Separately, the new caching / coordination flow feels unnecessarily complex for what should be a standard cache. Using DB rows plus advisory locks plus provisional execution IDs introduces a lot of race-prone state management. Please redo this around Redis instead of SQL locking, so we have a conventional cache / in-flight coordination mechanism rather than custom lock orchestration in request code. |
There was a problem hiding this comment.
I think this should be reworked so the orchestration lives in the simulation API repo rather than in API v1.
Benefits:
- Much less request-time table editing and coordination logic in API v1. We would not need request-serving code to manage provisional rows, execution ID promotion, stale-claim cleanup, or schema-related DB behavior.
- Better scalability. The simulation API repo is already the natural boundary for spawning and tracking simulation work, and Modal can scale the worker side horizontally for us.
- Fewer race-condition risks. The current DB-lock/provisional-claim flow is a lot of custom concurrency machinery for what is essentially a batch execution problem.
- More leverage from Modal. We already get async job spawning, polling by job ID, worker isolation, and container scaling. We should use that instead of rebuilding orchestration in Flask request code.
- Easier eventual integration into API v2 alpha. If the batch orchestration lives in the simulation API repo, API v1 and API v2 alpha can both consume the same backend capability instead of duplicating orchestration logic in each API layer.
Implementation outline:
In the simulation API repo:
- Add a batch endpoint for this workflow, e.g.
/simulate/economy/comparison/batchor/simulate/economy/budget-window. - The request should accept either:
- a base simulation payload plus
start_year,window_size, andmax_parallel, or - a fully expanded list of yearly payloads plus
max_parallel.
- a base simulation payload plus
- On submission, create one parent batch job and return a single batch job ID immediately.
- The batch job should fan out child yearly simulations as separate Modal executions, up to the requested parallelism limit.
- Track child job IDs and child statuses under the parent batch job.
- Expose polling for the parent batch job so the caller can retrieve:
- overall status
- progress
- completed years
- running years
- queued years
- failed years / error message
- final aggregated annual impacts and totals once complete
- The aggregation logic for the budget window should also live there, so the backend that owns the fan-out also owns the child-job status and final merged result.
In API v1:
- Keep the budget-window route and request parsing.
- Keep the budget-window response contract, since this PR is already moving toward the right shape for the frontend.
- Replace the current per-year orchestration with a thin adapter:
- build one batch request
- submit it to the simulation API repo
- store/poll one parent batch job ID
- map the batch status/result into the existing
BudgetWindowEconomicImpactResultresponse
- Remove the request-time thread pool, DB advisory locks, provisional execution IDs, stale-claim handling, and per-year
reform_impactcoordination. - If caching is still needed in API v1, use Redis as a standard cache:
- cache completed batch results by a canonical request key
- optionally cache “batch job currently in progress for this key”
- do not use SQL locking as the cache coordination mechanism
That would leave API v1 as a thin HTTP adapter and move the concurrency/orchestration concerns into the simulation API repo, where Modal is already doing the real execution work. It should be simpler to reason about, easier to scale, and substantially less race condition-prone than the current lock-based design.
If this direction makes sense, I’d be happy to take it on in follow-up and flag @MaxGhenis.
8ded7c1 to
b82ace8
Compare
The axes code path silently discarded all exceptions (`pass`), causing variables like NJ gross income to return null with no error trace. Now logs the full traceback via logging.exception(). Fixes #3322 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
921c8f7 to
324b29f
Compare
* Use budget-window batch endpoint for state revenue runs The PolicyEngine API now exposes a budget-window batch endpoint that queues every year in one request and reports aggregate progress server-side (PolicyEngine/policyengine-api#3387). Switching to it lets Missouri's state-revenue calculation make a single polling URL instead of nine parallel per-year polls — server-side queueing replaces the client-side STATE_CONCURRENCY=3 cap, and identical reform-window combinations can hit the server's batch cache. Changes: - frontend/lib/api.ts: add `pollBudgetWindowImpact` plus the typed response shapes (`AnnualBudgetImpact`, `BudgetWindowResult`, `BudgetWindowProgress`). Mirrors `pollEconomicImpact`'s polling semantics with an `onProgress` callback that streams the server's `progress` percent and `completed_years` / `computing_years` / `queued_years` arrays. - frontend/hooks/useStateImpact.ts: replace the `runWithConcurrency` per-year loop with one `pollBudgetWindowImpact` call covering 2027-2035. The unchanged-years short-circuit is preserved (years where the reform exactly matches the 2025 baseline still avoid the network entirely). When the batch resolves, `result.annualImpacts` is mapped from the API's camelCase fields back to the snake-case `BudgetImpact` the rest of the app already consumes. - The status-badge UX is preserved by translating the server's per-year progress arrays into the existing `pending` / `computing` / `ok` / `error` states. Per-year payloads still arrive only once the batch is fully done, so the bar chart fills in at the end rather than streaming year-by-year — fine since the whole window typically completes inside a few minutes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add Distributional, Winners & losers, and Poverty sub-tabs Calculate now fires the budget-window batch in parallel with nine per-year /us/economy polls so the new sub-tabs populate the same way the existing budgetary chart does — without a separate user action. The Modal Simulation Gateway's /budget-window endpoint returns only budget aggregates (taxRevenueImpact, federalTaxRevenueImpact, stateTaxRevenueImpact, benefitSpendingImpact, budgetaryImpact) per year, confirmed against the live OpenAPI schema. Full economic impact data — decile, intra_decile, poverty, by_income_bracket — still requires per-year /us/economy calls, so the new useFullEconomyImpact hook runs them in parallel with ECONOMY_CONCURRENCY=3 (matches the original cap that was added to dodge gateway aborts on bursts). Changes: - frontend/hooks/useFullEconomyImpact.ts: new hook. Same shape as useStateImpact — accepts (reform, unchangedYears), creates a policy, fans out concurrency-capped /us/economy polls, surfaces per-year pending/computing/ok/error status, abort-aware. Years where every bracket equals the 2025 baseline short-circuit to a synthesised empty payload (zero decile/intra-decile/poverty) without hitting the network. - frontend/components/YearPicker.tsx: shared year-picker dropdown used by the three new tabs. Annotates years with their status badge so the user can see what's still computing. - frontend/components/DistributionalImpact.tsx: decile bar chart with relative/absolute toggle and year picker, ported from SC's AggregateImpact distributional section. - frontend/components/WinnersLosersImpact.tsx: winners/no-change/losers headline cards plus intra-decile stacked bar by decile. - frontend/components/PovertyImpact.tsx: overall / child / deep / deep child poverty rate change bar chart. - frontend/app/(shell)/page.tsx: handleDone now resets and runs useFullEconomyImpact alongside useStateImpact and the household query. The bottom of the impact section is now a four-tab section: Budgetary (existing StateImpact, unchanged), Distributional, Winners & losers, Poverty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Normalise intra-decile keys from /us/economy PolicyEngine's /us/economy endpoint returns intra-decile buckets keyed by human-readable labels — "Gain more than 5%", "Lose less than 5%", etc. — but the dashboard's IntraDecile types and the WinnersLosersImpact component expect snake-case keys like gain_more_than_5pct. Without translation, intra.deciles[c.key] is undefined and the Winners & losers tab crashes on render. Add normaliseIntraDecile() to the extract pipeline so the hook returns a uniformly snake-cased payload regardless of which key shape the API returns. The zero-impact synthesised payload for unchanged years already uses snake_case keys, and the function passes those through unchanged via a "quick path" check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Switch year picker to prev/next buttons Mirror the household-impact tab's existing prev/next navigator instead of the dropdown. The new statewide sub-tabs (Distributional, Winners & losers, Poverty) now share the same year-picker style — arrow buttons on either side of a centred year label, with a "{completed}/{total} computed" hint underneath. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Use NC-style year pill buttons in statewide impact tabs Replace the prev/next arrow navigator with a row of pill buttons — one per year in the budget window — matching NC's "Tax year:" selector. With 9 years (2027-2035) the row stays readable and lets the user jump directly to any year. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Testing
Companion app change: PolicyEngine/policyengine-app-v2#930