Skip to content

Add budget-window batch economy endpoint#3387

Merged
anth-volk merged 27 commits into
masterfrom
codex/budget-window-batch
May 5, 2026
Merged

Add budget-window batch economy endpoint#3387
anth-volk merged 27 commits into
masterfrom
codex/budget-window-batch

Conversation

@MaxGhenis
Copy link
Copy Markdown
Collaborator

Summary

  • add a dedicated budget-window economy endpoint that batches yearly reform-impact work server-side
  • cap active yearly jobs and return aggregate progress, completed years, queued years, and final totals
  • cover completed, queued, and error flows in the economy service tests

Testing

  • FLASK_DEBUG=1 uv run --python 3.12 pytest tests/unit/services/test_economy_service.py
  • uv run --python 3.12 ruff format --check .

Companion app change: PolicyEngine/policyengine-app-v2#930

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 98.00000% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.94%. Comparing base (1690e20) to head (55e4f2f).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
policyengine_api/services/economy_service.py 97.91% 1 Missing and 2 partials ⚠️
policyengine_api/services/budget_window_cache.py 97.70% 1 Missing and 1 partial ⚠️
policyengine_api/routes/economy_routes.py 97.82% 1 Missing ⚠️
policyengine_api/utils/budget_window.py 96.29% 0 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@anth-volk
Copy link
Copy Markdown
Collaborator

I think it is dangerous for request-serving code to issue schema-management SQL (SHOW COLUMNS, ALTER TABLE ...) against production. Please move that to a manual migration step before the PR is pushed, or introduce a migration tool such as Alembic so schema changes are applied explicitly rather than opportunistically during live traffic. That will reduce the chance of production-breaking bugs around deploy order, permissions, or lock contention.

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.

Copy link
Copy Markdown
Collaborator

@anth-volk anth-volk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/batch or /simulate/economy/budget-window.
  • The request should accept either:
    • a base simulation payload plus start_year, window_size, and max_parallel, or
    • a fully expanded list of yearly payloads plus max_parallel.
  • 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 BudgetWindowEconomicImpactResult response
  • Remove the request-time thread pool, DB advisory locks, provisional execution IDs, stale-claim handling, and per-year reform_impact coordination.
  • 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.

@anth-volk anth-volk force-pushed the codex/budget-window-batch branch from 921c8f7 to 324b29f Compare May 1, 2026 16:51
@anth-volk anth-volk marked this pull request as ready for review May 5, 2026 16:15
@anth-volk anth-volk merged commit 9cffb43 into master May 5, 2026
7 checks passed
@anth-volk anth-volk deleted the codex/budget-window-batch branch May 5, 2026 16:15
DTrim99 added a commit to PolicyEngine/missouri-income-tax-elimination that referenced this pull request May 11, 2026
* 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>
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.

2 participants