Skip to content

Share one Stage 12 child wait deadline and cancel unfinished children - #793

Closed
anth-volk wants to merge 2 commits into
mainfrom
fix/stage12-child-wait-deadline
Closed

anth-volk wants to merge 2 commits into
mainfrom
fix/stage12-child-wait-deadline

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #792

Same-Repository Draft PR Check

  • This PR is a draft.
  • This PR is from a branch in PolicyEngine/policyengine-sim-api, not a personal fork.

Summary

This is an interim change before #712. It keeps the sequential baseline-then-reform wait, but bounds it and cleans up after a failure.

  • Shared wait deadline:
    • One 3,000 s window (SIMULATION_WAIT_TIMEOUT_SECONDS, the same as the workers' Modal timeout) opens when coordinate_report starts. It covers coordinator setup (claiming, output planning including the country model load, retained-artifact reads and dispatch) and both child waits. Each call.get receives only the remaining time, never less than zero.
    • Setup time therefore shortens the children's waits, not the time after them. However long setup takes within the window, 1,500 s of the coordinator's 4,500 s Modal timeout remain once the window closes: 900 s for the production-result wait, and 600 s to aggregate, persist and record a failure. This matches the budget already described on coordinate_report in v2_app.py.
    • Previously a slow baseline could give reform a second full 3,000 s.
  • Cancel unfinished workers:
    • When a wait fails or times out, _cancel_unfinished_children cancels every unfinished child, best effort. This includes a timed-out child that is still running, and a call restored from an earlier coordinator attempt.
    • It records a cancelled sibling that is still RUNNING as FAILED with error_code="simulation_cancelled". A sibling that has already recorded SUCCEEDED or FAILED is left unchanged. Modal's InputCancellation is a BaseException, so the worker can't record its own outcome.
    • Cancellation and state-update errors never replace the original failure. Their warnings (stage12_child_cancel_failed, stage12_cancelled_child_state_update_failed) carry evaluation_id, the child's simulation_execution_id, simulation_role and error_type, but no error message.
  • ChildCall gains cancel(). Modal's FunctionCall.cancel already exists.
  • Behaviour change: a reform worker still queued when the shared window is used up now fails, instead of running past the coordinator's budget. Coordinator setup time now also comes out of the children's window.
  • No timeout or Modal configuration changes. No environment variables change.

Interaction with other PRs

Testing

  • New tests in tests/test_stage12_runtime.py:

    • The children share one wait window, and an exhausted window gives the next child no extra time.
    • With 400 s of coordinator setup, the baseline wait receives 2,600 s, and the declared Modal timeouts leave at least 900 s plus 600 s after the window. This test reads the timeouts from the Modal app declaration.
    • A baseline failure cancels and records the reform child, including a reform call restored from an earlier coordinator attempt.
    • A baseline timeout cancels both unfinished children.
    • A reform failure leaves a finished baseline alone.
    • Cancellation leaves a sibling that already recorded SUCCEEDED or FAILED unchanged.
    • Cancellation errors and sibling state errors don't mask the child failure, and their warnings identify the report and child without the error message.
  • Suites, on Python 3.13 with uv run --frozen:

    • All 117 Stage 12 executor tests pass.
    • The full executor suite passes: 744 passed, 23 skipped, 3 deselected.
  • Regression checks: I reverted each behaviour in turn. Each time, the matching new test failed:

    • anchoring the deadline at dispatch fails the setup-time test;
    • removing the RUNNING check fails both finished-sibling tests;
    • dropping the identifiers fails both warning tests.

    The original cancellation tests still fail against the code before this PR only at their clock fixture, because they need the new monotonic-clock hook.

  • Formatting and lint:

    • ruff format --check and git diff --check pass.
    • ruff check with the locked ruff (0.15.11) is clean on the changed files.
    • Ruff 0.16.8 reports four findings in tests/test_stage12_runtime.py, and the same four are already on main.
  • Not run: Pyright, which isn't installed in the test environment.

🤖 Generated with Claude Code

The coordinator waited up to 3,000 s for baseline and then up to a fresh
3,000 s for reform, so queued workers could push it past its own 4,500 s
Modal timeout before it recorded the failure. Both waits now draw on one
3,000 s window, leaving 1,500 s for the 900 s production-result wait and
600 s of aggregation and failure persistence.

When a child wait fails or times out, the coordinator now cancels every
unfinished child, best effort, and records a cancelled sibling as failed,
because Modal's InputCancellation is a BaseException the worker cannot
record itself. Cancellation and state-update errors are logged and never
replace the original child failure.

This keeps the sequential baseline-then-reform wait; concurrent
observation remains part of #712.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Anchor the shared child-wait deadline when coordinate_report starts, so
claiming, output planning, retained-artifact reads and dispatch shorten
the children's waits rather than the 1,500 s left for the
production-result wait, aggregation, persistence and failure recording.

Add evaluation_id and the child's simulation_execution_id to the
cancellation warnings, and cover finished siblings, restored calls and
the ImmediateCall cancel double. The coordinator timeout test now runs
the coordinator with simulated setup time against the declared Modal
timeouts, so it lives beside the other coordinator tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk anth-volk mentioned this pull request Oct 14, 2026
2 tasks done
@anth-volk

Copy link
Copy Markdown
Contributor Author

Closing this in favour of #712, which now covers #792 and lists Fixes #792. #712 opens the shared child wait window at coordinator entry, cancels every unfinished worker when one fails or times out (including failures during coordinator startup), and logs cancellations with the evaluation and simulation execution IDs. It observes baseline and reform together, which #792 always left to #712.

@anth-volk anth-volk closed this Oct 14, 2026
anth-volk added a commit that referenced this pull request Oct 14, 2026
…eouts

The coordinator stopped waiting 3,000 s after it started, although a US
simulation worker may run for 3,900 s and each of its region-group calls
for 3,600 s, so US simulations could never use the time Modal allowed
them. The coordinator's 5,100 s timeout was not derived from either.

Following the rule from #793, a new dependency-free stage12_timeouts
module defines the worker timeouts, sets the shared child-wait window to
the longest of them (3,900 s), and derives the coordinator's Modal
timeout as that window plus the 900 s production-result wait plus 600 s
to read results, aggregate, write artifacts and record the outcome:
5,400 s. A complete testing-environment US report took 288 s, so 600 s
of finishing time is ample. The Modal app, coordinator and comparison
step all read these constants.

The last poll no longer sleeps past the deadline, so the window closes
on time instead of up to 1 s late and the finishing time is not eroded.

Tests read the declared Modal timeouts and check that the window covers
every simulation worker's timeout and that the coordinator's timeout
leaves the production wait plus at least 600 s. They also check that
setup time shortens the window and that an exhausted window gives the
children no extra time. Only the Modal timeout values change: no image,
secret or environment variable attachment changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
Adopt #793's cancellation practice in the concurrent coordinator:

- Every simulation the coordinator stops, after a peer failure, a wait
  timeout or a coordinator failure, is recorded as failed with
  simulation_cancelled, and its error_summary gives the reason, for
  example "Cancelled after the baseline simulation failed.". The
  cancelled_after_peer_failure and cancelled_after_coordinator_failure
  codes and the incomplete status for cancellations are gone; the wait
  timeout stays the report's error code.
- Only a running record changes, so a simulation that already succeeded
  or failed keeps its outcome. Workers already treat any status other
  than running as a cancellation, so a segmented worker still sees the
  failed record and cancels its own region-group calls.
- An unsegmented simulation is always cancelled through Modal, even when
  its record could not be written. A segmented simulation is cancelled
  through Modal only when its record could not be written, because Modal
  does not cancel spawned calls recursively. Cancellation and
  state-update errors are still logged without replacing the original
  failure.

Carry over #793's tests for a cancelled and recorded reform, a finished
baseline left alone, a restored call cancelled on retry, finished
siblings left unchanged, and both unfinished children cancelled on
timeout. Add tests for the Modal fallback after a failed state write,
for segmented and unsegmented simulations. ImmediateCall now counts
cancellations, and the entry and persistence fixtures use the new
cancellation record. Add a changelog line, as #793 did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
…eouts

The coordinator stopped waiting 3,000 s after it started, although a US
simulation worker may run for 3,900 s and each of its region-group calls
for 3,600 s, so US simulations could never use the time Modal allowed
them. The coordinator's 5,100 s timeout was not derived from either.

Following the rule from #793, a new dependency-free stage12_timeouts
module defines the worker timeouts, sets the shared child-wait window to
the longest of them (3,900 s), and derives the coordinator's Modal
timeout as that window plus the 900 s production-result wait plus 600 s
to read results, aggregate, write artifacts and record the outcome:
5,400 s. A complete testing-environment US report took 288 s, so 600 s
of finishing time is ample. The Modal app, coordinator and comparison
step all read these constants.

The last poll no longer sleeps past the deadline, so the window closes
on time instead of up to 1 s late and the finishing time is not eroded.

Tests read the declared Modal timeouts and check that the window covers
every simulation worker's timeout and that the coordinator's timeout
leaves the production wait plus at least 600 s. They also check that
setup time shortens the window and that an exhausted window gives the
children no extra time. Only the Modal timeout values change: no image,
secret or environment variable attachment changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
Adopt #793's cancellation practice in the concurrent coordinator:

- Every simulation the coordinator stops, after a peer failure, a wait
  timeout or a coordinator failure, is recorded as failed with
  simulation_cancelled, and its error_summary gives the reason, for
  example "Cancelled after the baseline simulation failed.". The
  cancelled_after_peer_failure and cancelled_after_coordinator_failure
  codes and the incomplete status for cancellations are gone; the wait
  timeout stays the report's error code.
- Only a running record changes, so a simulation that already succeeded
  or failed keeps its outcome. Workers already treat any status other
  than running as a cancellation, so a segmented worker still sees the
  failed record and cancels its own region-group calls.
- An unsegmented simulation is always cancelled through Modal, even when
  its record could not be written. A segmented simulation is cancelled
  through Modal only when its record could not be written, because Modal
  does not cancel spawned calls recursively. Cancellation and
  state-update errors are still logged without replacing the original
  failure.

Carry over #793's tests for a cancelled and recorded reform, a finished
baseline left alone, a restored call cancelled on retry, finished
siblings left unchanged, and both unfinished children cancelled on
timeout. Add tests for the Modal fallback after a failed state write,
for segmented and unsegmented simulations. ImmediateCall now counts
cancellations, and the entry and persistence fixtures use the new
cancellation record. Add a changelog line, as #793 did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
…eouts

The coordinator stopped waiting 3,000 s after it started, although a US
simulation worker may run for 3,900 s and each of its region-group calls
for 3,600 s, so US simulations could never use the time Modal allowed
them. The coordinator's 5,100 s timeout was not derived from either.

Following the rule from #793, a new dependency-free stage12_timeouts
module defines the worker timeouts, sets the shared child-wait window to
the longest of them (3,900 s), and derives the coordinator's Modal
timeout as that window plus the 900 s production-result wait plus 600 s
to read results, aggregate, write artifacts and record the outcome:
5,400 s. A complete testing-environment US report took 288 s, so 600 s
of finishing time is ample. The Modal app, coordinator and comparison
step all read these constants.

The last poll no longer sleeps past the deadline, so the window closes
on time instead of up to 1 s late and the finishing time is not eroded.

Tests read the declared Modal timeouts and check that the window covers
every simulation worker's timeout and that the coordinator's timeout
leaves the production wait plus at least 600 s. They also check that
setup time shortens the window and that an exhausted window gives the
children no extra time. Only the Modal timeout values change: no image,
secret or environment variable attachment changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
Adopt #793's cancellation practice in the concurrent coordinator:

- Every simulation the coordinator stops, after a peer failure, a wait
  timeout or a coordinator failure, is recorded as failed with
  simulation_cancelled, and its error_summary gives the reason, for
  example "Cancelled after the baseline simulation failed.". The
  cancelled_after_peer_failure and cancelled_after_coordinator_failure
  codes and the incomplete status for cancellations are gone; the wait
  timeout stays the report's error code.
- Only a running record changes, so a simulation that already succeeded
  or failed keeps its outcome. Workers already treat any status other
  than running as a cancellation, so a segmented worker still sees the
  failed record and cancels its own region-group calls.
- An unsegmented simulation is always cancelled through Modal, even when
  its record could not be written. A segmented simulation is cancelled
  through Modal only when its record could not be written, because Modal
  does not cancel spawned calls recursively. Cancellation and
  state-update errors are still logged without replacing the original
  failure.

Carry over #793's tests for a cancelled and recorded reform, a finished
baseline left alone, a restored call cancelled on retry, finished
siblings left unchanged, and both unfinished children cancelled on
timeout. Add tests for the Modal fallback after a failed state write,
for segmented and unsegmented simulations. ImmediateCall now counts
cancellations, and the entry and persistence fixtures use the new
cancellation record. Add a changelog line, as #793 did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
…eouts

The coordinator stopped waiting 3,000 s after it started, although a US
simulation worker may run for 3,900 s and each of its region-group calls
for 3,600 s, so US simulations could never use the time Modal allowed
them. The coordinator's 5,100 s timeout was not derived from either.

Following the rule from #793, a new dependency-free stage12_timeouts
module defines the worker timeouts, sets the shared child-wait window to
the longest of them (3,900 s), and derives the coordinator's Modal
timeout as that window plus the 900 s production-result wait plus 600 s
to read results, aggregate, write artifacts and record the outcome:
5,400 s. A complete testing-environment US report took 288 s, so 600 s
of finishing time is ample. The Modal app, coordinator and comparison
step all read these constants.

The last poll no longer sleeps past the deadline, so the window closes
on time instead of up to 1 s late and the finishing time is not eroded.

Tests read the declared Modal timeouts and check that the window covers
every simulation worker's timeout and that the coordinator's timeout
leaves the production wait plus at least 600 s. They also check that
setup time shortens the window and that an exhausted window gives the
children no extra time. Only the Modal timeout values change: no image,
secret or environment variable attachment changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
Adopt #793's cancellation practice in the concurrent coordinator:

- Every simulation the coordinator stops, after a peer failure, a wait
  timeout or a coordinator failure, is recorded as failed with
  simulation_cancelled, and its error_summary gives the reason, for
  example "Cancelled after the baseline simulation failed.". The
  cancelled_after_peer_failure and cancelled_after_coordinator_failure
  codes and the incomplete status for cancellations are gone; the wait
  timeout stays the report's error code.
- Only a running record changes, so a simulation that already succeeded
  or failed keeps its outcome. Workers already treat any status other
  than running as a cancellation, so a segmented worker still sees the
  failed record and cancels its own region-group calls.
- An unsegmented simulation is always cancelled through Modal, even when
  its record could not be written. A segmented simulation is cancelled
  through Modal only when its record could not be written, because Modal
  does not cancel spawned calls recursively. Cancellation and
  state-update errors are still logged without replacing the original
  failure.

Carry over #793's tests for a cancelled and recorded reform, a finished
baseline left alone, a restored call cancelled on retry, finished
siblings left unchanged, and both unfinished children cancelled on
timeout. Add tests for the Modal fallback after a failed state write,
for segmented and unsegmented simulations. ImmediateCall now counts
cancellations, and the entry and persistence fixtures use the new
cancellation record. Add a changelog line, as #793 did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
…eouts

The coordinator stopped waiting 3,000 s after it started, although a US
simulation worker may run for 3,900 s and each of its region-group calls
for 3,600 s, so US simulations could never use the time Modal allowed
them. The coordinator's 5,100 s timeout was not derived from either.

Following the rule from #793, a new dependency-free stage12_timeouts
module defines the worker timeouts, sets the shared child-wait window to
the longest of them (3,900 s), and derives the coordinator's Modal
timeout as that window plus the 900 s production-result wait plus 600 s
to read results, aggregate, write artifacts and record the outcome:
5,400 s. A complete testing-environment US report took 288 s, so 600 s
of finishing time is ample. The Modal app, coordinator and comparison
step all read these constants.

The last poll no longer sleeps past the deadline, so the window closes
on time instead of up to 1 s late and the finishing time is not eroded.

Tests read the declared Modal timeouts and check that the window covers
every simulation worker's timeout and that the coordinator's timeout
leaves the production wait plus at least 600 s. They also check that
setup time shortens the window and that an exhausted window gives the
children no extra time. Only the Modal timeout values change: no image,
secret or environment variable attachment changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
Adopt #793's cancellation practice in the concurrent coordinator:

- Every simulation the coordinator stops, after a peer failure, a wait
  timeout or a coordinator failure, is recorded as failed with
  simulation_cancelled, and its error_summary gives the reason, for
  example "Cancelled after the baseline simulation failed.". The
  cancelled_after_peer_failure and cancelled_after_coordinator_failure
  codes and the incomplete status for cancellations are gone; the wait
  timeout stays the report's error code.
- Only a running record changes, so a simulation that already succeeded
  or failed keeps its outcome. Workers already treat any status other
  than running as a cancellation, so a segmented worker still sees the
  failed record and cancels its own region-group calls.
- An unsegmented simulation is always cancelled through Modal, even when
  its record could not be written. A segmented simulation is cancelled
  through Modal only when its record could not be written, because Modal
  does not cancel spawned calls recursively. Cancellation and
  state-update errors are still logged without replacing the original
  failure.

Carry over #793's tests for a cancelled and recorded reform, a finished
baseline left alone, a restored call cancelled on retry, finished
siblings left unchanged, and both unfinished children cancelled on
timeout. Add tests for the Modal fallback after a failed state write,
for segmented and unsegmented simulations. ImmediateCall now counts
cancellations, and the entry and persistence fixtures use the new
cancellation record. Add a changelog line, as #793 did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
…eouts

The coordinator stopped waiting 3,000 s after it started, although a US
simulation worker may run for 3,900 s and each of its region-group calls
for 3,600 s, so US simulations could never use the time Modal allowed
them. The coordinator's 5,100 s timeout was not derived from either.

Following the rule from #793, a new dependency-free stage12_timeouts
module defines the worker timeouts, sets the shared child-wait window to
the longest of them (3,900 s), and derives the coordinator's Modal
timeout as that window plus the 900 s production-result wait plus 600 s
to read results, aggregate, write artifacts and record the outcome:
5,400 s. A complete testing-environment US report took 288 s, so 600 s
of finishing time is ample. The Modal app, coordinator and comparison
step all read these constants.

The last poll no longer sleeps past the deadline, so the window closes
on time instead of up to 1 s late and the finishing time is not eroded.

Tests read the declared Modal timeouts and check that the window covers
every simulation worker's timeout and that the coordinator's timeout
leaves the production wait plus at least 600 s. They also check that
setup time shortens the window and that an exhausted window gives the
children no extra time. Only the Modal timeout values change: no image,
secret or environment variable attachment changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
Adopt #793's cancellation practice in the concurrent coordinator:

- Every simulation the coordinator stops, after a peer failure, a wait
  timeout or a coordinator failure, is recorded as failed with
  simulation_cancelled, and its error_summary gives the reason, for
  example "Cancelled after the baseline simulation failed.". The
  cancelled_after_peer_failure and cancelled_after_coordinator_failure
  codes and the incomplete status for cancellations are gone; the wait
  timeout stays the report's error code.
- Only a running record changes, so a simulation that already succeeded
  or failed keeps its outcome. Workers already treat any status other
  than running as a cancellation, so a segmented worker still sees the
  failed record and cancels its own region-group calls.
- An unsegmented simulation is always cancelled through Modal, even when
  its record could not be written. A segmented simulation is cancelled
  through Modal only when its record could not be written, because Modal
  does not cancel spawned calls recursively. Cancellation and
  state-update errors are still logged without replacing the original
  failure.

Carry over #793's tests for a cancelled and recorded reform, a finished
baseline left alone, a restored call cancelled on retry, finished
siblings left unchanged, and both unfinished children cancelled on
timeout. Add tests for the Modal fallback after a failed state write,
for segmented and unsegmented simulations. ImmediateCall now counts
cancellations, and the entry and persistence fixtures use the new
cancellation record. Add a changelog line, as #793 did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 19, 2026
* Add Stage 12 segmentation foundations

* Add Stage 12 segment calculation and merge

* Add Stage 12 cache identity and loading

* Add Stage 12 cache precompute app

* Bake Stage 12 caches into US workers

* Fan out Stage 12 US simulations

* Qualify and document Stage 12 execution

* Fix Stage 12 precompute image sources

* Validate Stage 12 cache deploy access

* Keep Stage 12 deployment validation independent

* Validate Stage 12 reform intervals

* Preserve Stage 12 failure details

* Observe Stage 12 simulations concurrently

* Document Stage 12 failure handling

* Reconcile segmented Stage 12 execution with current typed provenance

* Identify the report and child in Stage 12 cancellation logs

Cancellation state-update and cancel-request failures previously logged
only the simulation role and error type. They now also log the report's
evaluation_id and the child's simulation_execution_id, in the message and
as record attributes, and still omit the error's own message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Stop started Stage 12 workers when coordinator startup fails

Only a failed spawn used to cancel already-started children. Any other
coordinator failure after the first spawn, such as a failed dispatch
placeholder write, a failed invocation attach, a failed restore, or a
retained-artifact error, reached the report failure handler, which marked
the report failed but left every started worker running until its own
timeout.

The handler now stops every started, unfinished child with the existing
cancellation path: it records the child as incomplete with
cancelled_after_coordinator_failure, cancels an unsegmented call, and
lets a segmented call stop itself once it reads that record. Children
that already finished are left unchanged. Child, dispatch and wait-timeout
failures keep their own peer cancellation, and cancellation or
state-update errors are logged without replacing the original failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Open the Stage 12 child wait window at coordinator entry

The child-wait deadline used to start when observation began, after
claiming, output planning (including the country model load),
retained-artifact reads and dispatch. Setup time therefore came out of
the coordinator's remaining Modal time rather than the children's
window.

The 3,000 s window now opens when coordinate_report starts, so setup
shortens the children's window and the coordinator always keeps 2,100 s
of its 5,100 s Modal timeout after the window: up to 900 s for the
production-result wait and 1,200 s to read results, aggregate, write
artifacts and record the outcome. The timeout summary now says the
window is measured from the coordinator's start.

A new test runs the coordinator with 0, 400 and 2,000 s of simulated
setup, checks that the window closes 3,000 s after entry, and checks the
declared Modal timeouts still leave the production wait plus at least
600 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Size the Stage 12 wait window and coordinator timeout from worker timeouts

The coordinator stopped waiting 3,000 s after it started, although a US
simulation worker may run for 3,900 s and each of its region-group calls
for 3,600 s, so US simulations could never use the time Modal allowed
them. The coordinator's 5,100 s timeout was not derived from either.

Following the rule from #793, a new dependency-free stage12_timeouts
module defines the worker timeouts, sets the shared child-wait window to
the longest of them (3,900 s), and derives the coordinator's Modal
timeout as that window plus the 900 s production-result wait plus 600 s
to read results, aggregate, write artifacts and record the outcome:
5,400 s. A complete testing-environment US report took 288 s, so 600 s
of finishing time is ample. The Modal app, coordinator and comparison
step all read these constants.

The last poll no longer sleeps past the deadline, so the window closes
on time instead of up to 1 s late and the finishing time is not eroded.

Tests read the declared Modal timeouts and check that the window covers
every simulation worker's timeout and that the coordinator's timeout
leaves the production wait plus at least 600 s. They also check that
setup time shortens the window and that an exhausted window gives the
children no extra time. Only the Modal timeout values change: no image,
secret or environment variable attachment changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Record cancelled Stage 12 simulations as failed and always stop them

Adopt #793's cancellation practice in the concurrent coordinator:

- Every simulation the coordinator stops, after a peer failure, a wait
  timeout or a coordinator failure, is recorded as failed with
  simulation_cancelled, and its error_summary gives the reason, for
  example "Cancelled after the baseline simulation failed.". The
  cancelled_after_peer_failure and cancelled_after_coordinator_failure
  codes and the incomplete status for cancellations are gone; the wait
  timeout stays the report's error code.
- Only a running record changes, so a simulation that already succeeded
  or failed keeps its outcome. Workers already treat any status other
  than running as a cancellation, so a segmented worker still sees the
  failed record and cancels its own region-group calls.
- An unsegmented simulation is always cancelled through Modal, even when
  its record could not be written. A segmented simulation is cancelled
  through Modal only when its record could not be written, because Modal
  does not cancel spawned calls recursively. Cancellation and
  state-update errors are still logged without replacing the original
  failure.

Carry over #793's tests for a cancelled and recorded reform, a finished
baseline left alone, a restored call cancelled on retry, finished
siblings left unchanged, and both unfinished children cancelled on
timeout. Add tests for the Modal fallback after a failed state write,
for segmented and unsegmented simulations. ImmediateCall now counts
cancellations, and the entry and persistence fixtures use the new
cancellation record. Add a changelog line, as #793 did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test that a closed Stage 12 wait window is reported first

The rebase onto #789 reports a closed child-wait window to telemetry
before cancellation and report failure records are written, as every
other Stage 12 failure is. This test makes those writes fail and checks
that the timeout still reached telemetry first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep a failed Stage 12 child's failure when its own record errors

The coordinator read a failed child's record, to choose its failure detail,
outside any guard. One database error on that read reached the report
handler: the report was recorded as report_coordination_failed, and the
failed child and its peer were both recorded as cancelled "after the
coordinator failed".

Reading and writing the failed child's own record, in both the child-wait
and the dispatch-failure paths, is now non-fatal in the same way as the
cancellation writes. The error is logged with the report and simulation
identifiers and its type, but not its message. The report keeps the child's
failure as its code and cause, the failed child is recorded as failed with
its own cause where the record can still be written and is never recorded
as cancelled, and its peers are cancelled "after the <role> simulation
failed".

Each failure is now also reported once. The coordinator, the simulation
worker and the segment fan-out reported an unexpected error both through
the original-failure report and again through the correlation-ID log. One
report now carries the correlation ID that the redacted summary persists,
with the report and simulation identifiers. A segment failure is reported
where it occurs, and the worker does not report it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Mount only the read-only Hugging Face secret in the Stage 12 precompute app

The Stage 12 cache precompute app, which runs on every Stage 12 deployment,
mounted the retiring policyengine-data-credentials and huggingface-token
(HF_TOKEN) Modal secrets on its image build and all five functions. The
credential runbook removes both from simulation code.

Its image build and functions now mount pe-uk-private-hf-read-token, which
supplies HUGGING_FACE_TOKEN, declared through the same constants and
required key as the executor and v2 apps. The US data the app reads comes
only from the public policyengine/populace-us dataset repository, which the
v2 US worker image already downloads with this token. The guard test that
keeps the retired names out of deployment sources now covers this app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Register the Stage 12-only integration test in the rules registry

The rules registry guard that landed on main with #752 requires every
deployment-asset guard test to belong to a rule. Add the test that keeps
the Stage 12-only deploy from running the existing stack's complete
integration suite to sim.stage12.deployment-scope, widen the statement to
say so, and re-render the rules page and the AGENTS.md block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Return valid aggregates from the Stage 12 test aggregators

#787 made the coordinator tests' artifact double write aggregates through
the real Stage12ArtifactStore, which rejects anything that is not a valid
AggregateReportArtifactPayload. Use _aggregate_payload for this PR's
aggregator stubs that must not run, as #787 did for main's, so an
unexpected run shows up as a written aggregate instead of as a rejection
hidden by the failure the test expects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the v2 manifest readable by readers released before the cache

The manifest models forbid unknown fields, so readers already deployed
reject a manifest carrying the new cache release fields, and a cold
reader then has no valid manifest. The manifest is published before
the reading revision is promoted, staging deploys never promote, and a
rollback or revert would read the same document.

The publisher now writes the manifest under the active-with-cache key
and the same manifest without cache releases under the existing active
key. Current readers read active-with-cache, falling back to active
only before the first publish that writes both. A test pins the fields
earlier readers accept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix Stage 12 cache file names, forced rebuilds and the determinism gate

Name each baseline file as .py's Simulation.storage_id names the file
it saves and loads. A US simulation always resolves an SPM selection,
so .py appends its digest; the cache used the bare simulation ID, so
precompute never found the file it had calculated and no worker could
load a baked one. The identity now holds the resolved selection the
worker builds the simulation with, and a test compares the planned
name with the real worker model's.

Remove force from the Stage 12 cache. Stored files are immutable and
create-only, and HDF5 baselines are not byte-reproducible, so v1's
force_recompute input made every forced run fail.

Record a passing determinism check in the cache bucket and publish the
manifest only when that record exists. Previously a run that computed
no baselines skipped the check, so running again after a failure
published baselines that had failed it. The check now also requires
the stored baseline to load as a cache hit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remove the Stage 12 ruff suppressions the main merge added

Fix the findings instead of suppressing them; only deliberate catch-all
handlers keep a suppression, each with its reason.

- Import at module level everywhere the deferred imports had no real
  need: the precompute app, the image-build download, the cache build,
  worker validation, qualification, segment fan-out and the v2 app.
  Modules that tests patch are imported whole and called through.
- Break the two import cycles. Stage12CachedSimulation moves from
  stage12_cache.runtime to stage12_runtime.cached_simulation, and
  should_segment_simulation moves into the partition module, so the
  simulation module imports both at module level. The stage12_cache
  package no longer imports .py.
- When a stored baseline fails its output plan, clear .py's in-memory
  cache with its public clear() instead of replacing one private entry.
  The next ensure loads the saved file.
- Annotate the precompute's Modal handles as modal.Function and drop the
  duplicated RemoteFunction protocol, and type the fan-out's Modal module.
- Move two raises out of try blocks into helpers, and raise TypeError
  for the image build's two manifest type checks.
- Give the six catch-all suppressions this PR already had their reasons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Install only US data in the Stage 12 cache precompute image

The precompute image ran `policyengine bundle install` with --venv, which
asks .py to pip-install the bundle's packages into the uv environment.
That environment has no pip, so with .py 6.7.5 the image build failed
with "No module named pip" in the first testing deploy. The locked
modal-simulation-image group already installs .py and the country
packages, so the image now installs only the US data through
bundle_data_install_command, as the v2 worker images do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Anthony Volk <14987227+anth-volk@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <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.

Stage 12 coordinator can outlive its timeout while waiting and leaves workers running

1 participant