Skip to content

Commit 0aa89b4

Browse files
timsaucerclaude
andcommitted
Reap worker processes on every driver exit path
An unreadable report from a worker that exited 0 used to raise mid-loop, leaving the remaining workers running with no communicate() and, in run_tpch.py, a shuffle directory about to be deleted out from under them. Failures of both kinds now accumulate so every worker is waited on, and a finally kills stragglers on the paths that never finish the loop at all: a spawn that fails partway through the fan-out, or an interrupt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 7c91155 commit 0aa89b4

1 file changed

Lines changed: 48 additions & 20 deletions

File tree

  • examples/distributed/engine-library/python/dfx_engine

‎examples/distributed/engine-library/python/dfx_engine/driver.py‎

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,53 @@ def _report(task: tuple[int, int], stdout: str) -> int:
162162
raise RuntimeError(message) from err
163163

164164

165+
def _fan_out(
166+
envelopes: list[dict], tasks: list[tuple[int, int]], task_dir: pathlib.Path
167+
) -> dict[tuple[int, int], int]:
168+
"""One process per task, all in flight together; rows produced per task.
169+
170+
This is the claim the example is making: each worker reads a different
171+
file and writes a different result, so they need no coordination beyond
172+
the directory.
173+
174+
Every failure -- a nonzero exit or an unreadable report -- is accumulated
175+
rather than raised mid-loop, so every worker gets its `communicate` before
176+
anything is reported. The `finally` covers the paths that never reach the
177+
loop's end at all: a spawn that fails partway through the fan-out, or an
178+
interrupt while collecting. A worker abandoned running would keep writing
179+
into a shuffle directory its caller believes is settled -- or, in
180+
`run_tpch.py`, one that is about to be deleted.
181+
"""
182+
workers: list[subprocess.Popen[str]] = []
183+
task_rows: dict[tuple[int, int], int] = {}
184+
failures = []
185+
try:
186+
for envelope, (stage_id, partition) in zip(envelopes, tasks, strict=True):
187+
workers.append(_dispatch(envelope, task_dir, stage_id, partition))
188+
189+
for task, worker in zip(tasks, workers, strict=True):
190+
stdout, stderr = worker.communicate()
191+
if worker.returncode != 0:
192+
stage_id, partition = task
193+
failures.append(
194+
f"stage {stage_id} partition {partition} failed:\n{stderr}"
195+
)
196+
continue
197+
try:
198+
task_rows[task] = _report(task, stdout)
199+
except RuntimeError as err:
200+
failures.append(str(err))
201+
finally:
202+
for worker in workers:
203+
if worker.poll() is None:
204+
worker.kill()
205+
worker.wait()
206+
207+
if failures:
208+
raise RuntimeError("\n".join(failures))
209+
return task_rows
210+
211+
165212
def require_empty_shuffle(shuffle_dir: pathlib.Path) -> None:
166213
"""Refuse a directory that already holds stage output.
167214
@@ -266,26 +313,7 @@ def run_distributed(
266313
}
267314
)
268315

269-
# One process per task, all in flight together. This is the claim the
270-
# example is making: each worker reads a different file and writes a
271-
# different result, so they need no coordination beyond the directory.
272-
workers = [
273-
_dispatch(envelope, task_dir, stage_id, partition)
274-
for envelope, (stage_id, partition) in zip(envelopes, tasks, strict=True)
275-
]
276-
277-
task_rows: dict[tuple[int, int], int] = {}
278-
failures = []
279-
for task, worker in zip(tasks, workers, strict=True):
280-
stdout, stderr = worker.communicate()
281-
if worker.returncode != 0:
282-
stage_id, partition = task
283-
failures.append(f"stage {stage_id} partition {partition} failed:\n{stderr}")
284-
continue
285-
task_rows[task] = _report(task, stdout)
286-
287-
if failures:
288-
raise RuntimeError("\n".join(failures))
316+
task_rows = _fan_out(envelopes, tasks, task_dir)
289317

290318
# Now run the whole query here. Every stage partition has a file, so the
291319
# stage nodes stream them instead of recomputing -- the driver does only

0 commit comments

Comments
 (0)