Skip to content

Run the worker in a subprocess so remote bugs cost seconds, not an hour - #157

Merged
rajeeja merged 3 commits into
mainfrom
rajeeja/fake-endpoint-harness
Sep 21, 2026
Merged

rajeeja merged 3 commits into
mainfrom
rajeeja/fake-endpoint-harness

Conversation

@rajeeja

@rajeeja rajeeja commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Overview

Every remote defect in the CONUS-RRM work took about half an hour to see: SSH with MFA, a redeploy, a Slurm queue, then one tool call. Four bugs hid behind that latency — a stale checkout, an orphaned PID file, a client timeout shorter than the job, and an OOM-killed worker — and each was only distinguishable from the others after paying the cost again. None of them needed a cluster. They needed a worker that is a separate process with its own memory limit and its own installed packages, which is all a subprocess is.

tests/fake_endpoint.py patches globus_compute_sdk.Executor and runs the submitted function in a child interpreter. It is faithful where it matters and documents where it is not: the function really is serialized and shipped by value, a memory cap really is RLIMIT_AS, and the worker can be pointed at a different PYTHONPATH to reproduce version drift. There is no Slurm, no network and no MPI launcher, so it does not replace test_remote_remap_live.py.

Also fixes what it was built to find

A killed worker normalized to Remote execution failed on chrysalis: *****, because the code kept the last line of the Globus message — which is boilerplate about Python versions, not the cause. Diagnosing the real failure meant bypassing this server entirely. It now names the host, the worker id, and memory as the usual cause. Every _worker_runtime envelope also carries peak_rss_gib, the number today's diagnosis needed and had to get out-of-band.

Two findings from building it

dill ships closures, so my first "closures fail remotely" test was simply wrong; it now pins the opposite, and the real hazard — importing something the worker lacks — is the one asserted. And macOS refuses RLIMIT_AS, RLIMIT_DATA and RLIMIT_RSS, so a cap there would silently not apply; those tests skip on darwin rather than pretend, and CI is Linux.

Verification

  • 1287 pass locally, pre-commit clean.
  • The normalizer test was checked by sabotage. It initially passed with the fix reverted, because my synthetic message was 268 characters and the normalizer only rewrites messages over 600 — a tidy fixture took a different code path. The fixture is now 6.3k characters, matching the real thing, and the sabotage fails as it should.

Every remote defect in the CONUS-RRM work took about half an hour to
see: SSH with MFA, a redeploy, a Slurm queue, one tool call. Four bugs
hid behind that latency -- a stale checkout, an orphaned PID file, a
client timeout shorter than the job, and an OOM-killed worker -- and
each was only distinguishable from the others after paying the cost
again. None needed a cluster. They needed a worker that is a separate
process with its own memory limit and its own installed packages, which
is what tests/fake_endpoint.py provides.

It is faithful where faithfulness matters and says where it is not: the
function really is serialized and shipped, a memory cap really is
RLIMIT_AS, and the worker can be pointed at a different PYTHONPATH to
reproduce version drift. There is no Slurm, no network and no MPI
launcher, so it does not replace the live lane.

Two findings from building it. dill ships closures, so the obvious
"closures fail remotely" test was wrong and now pins the opposite --
the real hazard is importing something the worker lacks. And macOS
refuses every memory rlimit, so those tests skip on darwin rather than
silently not applying a cap; CI is Linux and runs them.

Also fixes what the simulator was built to find: a killed worker was
normalized to a redacted message, because the code kept the last line
of the Globus text, which is boilerplate about Python versions. It now
names the host, the worker and memory. The synthetic message is 6.3k
characters for a reason -- the normalizer only rewrites over 600, so a
tidy fixture took a different path and let the bug through until the
fixture was made realistic.

Every _worker_runtime envelope also reports peak_rss_gib now. Today's
diagnosis needed that number and had to get it by bypassing the server.
remote_subset_bbox_plot computed the mean face area of the entire mesh
before subsetting, to express the crop's resolution as a ratio against
the rest. On the 300M-face np4 grid that killed the worker at ~400 s:
the caller asked for Texas and paid for the planet, then got nothing.

The subset now runs first, and the whole-mesh mean is skipped above the
same 50M-face cap used elsewhere, reported as mean_area_full_skipped
rather than silently dropped. The crop, the figure and the subset's own
areas are unaffected.

This does not make the np4 grid subsettable, and measurement is what
said so rather than reasoning. On the pg2 twin, open_grid is 33 GiB and
face_bounds_lon/lat adds 109 GiB more; scaled by face count the np4 file
needs roughly 363 GiB against a 236 GiB node. face_bounds is mandatory
for a bounding-box subset and is computed for every face before any crop
applies, so no reordering reaches it. The pg2 twin describes the same
geometry and crops in 113 s.

Tests pin the order of operations by reading the source rather than by
faking a 300M-face grid: a proxy large enough to trigger the guards
cannot also satisfy uxarray's subsetting internals, which divide by the
real face count, and a fixture that reimplements the grid only tests
itself.
@rajeeja
rajeeja merged commit 9c18c22 into main Sep 21, 2026
13 checks passed
@rajeeja
rajeeja deleted the rajeeja/fake-endpoint-harness branch September 21, 2026 22:48
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.

1 participant