PolicyEngine / PolicyEngine/policyengine-household-api
Decide the Modal worker concurrency model: async handler with concurrent inputs, or sync handler with max_inputs=1
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1
- Forks
- 3
- Avg merge
- 6h 34m
- Merged PRs (30d)
- 8
Description
Follow-up to #1609 / #1610. The worker currently runs a synchronous handle_household_request under @modal.concurrent (interim config after #1610: max_inputs=3, target_inputs=2, cpu=2.0). That combination carries a structural hazard confirmed both in the 2026-07-06 incident logs and in Modal's documentation: for synchronous functions with input concurrency, "a single input cancellation will terminate the entire container" — one cancelled or timed-out request destroys the warm container and reschedules its sibling inputs, and the replacement container can pay a ~70–90s snapshot rebuild. Modal's guidance also calls input concurrency for CPU-bound workloads "likely not as effective (or even counterproductive)."
We should commit to one of two clean end states rather than keep the interim shape indefinitely:
Option A — async handler, keep concurrent inputs per container
Convert the method to async def and offload the blocking WSGI dispatch to a bounded thread pool (await asyncio.to_thread(dispatch_to_flask_app, self.flask_app, payload)).
- Cancellation becomes per-input: Modal raises
asyncio.CancelledErrorin the affected task only; the container survives and sibling inputs are untouched. - Keeps warm-pool economics: several cheap requests share one container.
- Caveat: the underlying calculation thread cannot be interrupted, so a cancelled request leaves an orphaned calculation burning CPU until it completes (bounded by
max_inputs). The thread pool must be sized tomax_inputs. - Thread-safety is not a new requirement — the sync+concurrent setup already runs the dispatch on concurrent threads today.
- Should ship with a cancellation test against a throwaway Modal environment verifying the container outlives a cancelled input.
Option B — stay sync, drop input concurrency (max_inputs=1)
- Simplest possible semantics: one request per container; a cancellation costs only that container with zero sibling collateral; no GIL/BLAS contention between requests, so per-request latency is the solo latency.
- Matches Modal's guidance for CPU-bound work.
- Cost: every concurrent request needs its own container — more cold starts (mitigated by memory snapshots) and a larger container fleet under load; autoscaler responsiveness becomes the latency bound for bursts.
Decision inputs
- Observe how
max_inputs=3 / target_inputs=2 / cpu=2.0(#1610) behaves over a release cycle or two: per-request latency spread on heavy calculates, container fleet size, and cost. - Measure the warm-pool economics that concurrency actually buys for the real traffic mix (routine calculates are <1 CPU-second; the heavy customer-regression households are 12–50 CPU-seconds).
- Open question flagged in #1609: whether a worker-side timeout on a sync concurrent container also recycles the container (docs are explicit only about caller cancellation). If it does, Option A is the only shape that fully removes the capacity-destruction failure class.
Related: #1609 (incident diagnosis), #1610 (interim mitigation).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the worker's handle_household_request entry point and the dispatch_to_flask_app call, then review #1609 and #1610 for the incident and interim configuration. Measure latency, fleet size, cost, and cancellation or timeout behavior over a release cycle for max_inputs=3 and target_inputs=2. Done means selecting and documenting one concurrency model, with the cancellation test requested for the async option.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100