PolicyEngine / PolicyEngine/policyengine-household-api

Decide the Modal worker concurrency model: async handler with concurrent inputs, or sync handler with max_inputs=1

Open
#1,611 0 comments 0 reactions 0 assignees View on GitHub

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.CancelledError in 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 to max_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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.