Skip to content

feat(slurm): adopt a mirror's live job instead of allocating a second - #25

Merged
ligon merged 1 commit into
mainfrom
feat/launch-adopts-live-job
Sep 18, 2026
Merged

ligon merged 1 commit into
mainfrom
feat/launch-adopts-live-job

Conversation

@ligon

@ligon ligon commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Issue #19's option 3 — and the leak the existing reuse probe could not see.

What was wrong

_launch_confined already refused to resubmit while a job was live. But it asked the session record~/.sucoder/sessions/<mirror>--<target>.yaml, which holds one job id, and which RemoteSession.load returns blank for when the file is missing or unreadable.

So a record clobbered by a later launch, lost, or written under a different -T spelling read as "no job at all", and the launch went straight to sbatch — over a job that was still running. That is the configuration #19 was filed about, arrived at from the other direction: not two deliberate launches racing, but one launch that could no longer see the first.

Slurm has known the answer all along. Every confined launch carries --job-name=sucoder-<token>.

What it does now

When the record finds nothing, ask the scheduler: squeue --me --name=sucoder-<token>. A live job for this mirror on this target is adopted — attached to, and written back into the record, so attach, release and renew can reach it again — instead of allocated beside.

That last part matters as much as the refusal: it heals the orphan rather than just declining to make another one.

Three deliberate non-blocks

A job on a different target does not stop the launch. One mirror on two targets is a deliberate configuration, not a mistake; it is reported and the new job allocated. Targets are told apart by partition/account/qos via sessions_report.match_target — the same signature sucoder sessions groups by, so a target pinning none of the three claims nothing rather than everything.

A failed squeue does not stop it either. This is a safety net over the record-keyed probe, not a gate, and that probe already refuses to resubmit on an unknown answer for a job it knows about. Making this one fail closed would turn a controller blip into "cannot launch".

An array element like 4242_3 is not adopted. int() reads underscores as digit separators, so it would have become job 42423 — the same trap that bit parse_squeue in #23.

salloc jobs get a name

salloc --no-shell carried no --job-name at all. Nothing reuses by name on that path yet — it reuses via the record and adopts by node — but without the name an allocation is invisible to sucoder sessions, which filters on the sucoder- prefix, and there is nothing for a name-keyed probe to match on later. Sanitized identically to confined_tmux_target, or the two paths disagree about what a mirror is called.

Testing

886 passed (from 880 on main). The two reuse paths now share _reuse_confined_job so they cannot drift.

All three changes are mutation-checked — disabling the name probe, adopting regardless of target, and dropping the salloc name each fail a test. The _confined_responder stub asserts the argv shape of both squeue forms, so a drift in either command fails loudly rather than silently mis-routing to a green pass.

tests/test_direct_remote.py::test_direct_collaborate_rejects_node fails in my environment before and after (a chmod-based readability check); CI is green on it.

Not here

The unconfined path still reuses via the record and adopts by node, and can still double-allocate if the record is lost. Naming the jobs is the prerequisite for fixing that, and it is in this PR; the fix itself is not, because _ensure_slurm_node has a documented persist-before-query window that deserves its own change rather than a rider on this one.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Lb2GYSyS7vteb34ziwHQ4c

Issue 19's option 3, and the leak the existing reuse probe could not see.

_launch_confined already refused to resubmit while a job was live, but it
asked the session record -- ~/.sucoder/sessions/<mirror>--<target>.yaml,
which holds ONE job id, and which RemoteSession.load returns blank for when
the file is missing or unreadable.  A record that was clobbered by a later
launch, lost, or written under a different -T spelling therefore read as "no
job at all", and the launch went straight to sbatch over a job that was
still running.  That is the configuration issue 19 was filed about, arrived
at from the other direction.

Slurm has known the answer all along: every confined launch carries
--job-name=sucoder-<token>.  So when the record finds nothing, ask the
scheduler.  A live job for this mirror on this target is adopted -- attached
to, and written back into the record, so attach/release/renew can reach it
again -- rather than allocated beside.

Three deliberate non-blocks:

A job on a DIFFERENT target does not stop the launch.  One mirror on two
targets is a deliberate configuration, not a mistake; it is reported and the
new job is allocated.  Targets are told apart by partition/account/qos via
sessions_report.match_target, the same signature `sucoder sessions` groups
by, so a target pinning none of the three claims nothing rather than
everything.

A failed squeue does not stop it either.  This is a safety net over the
record-keyed probe, not a gate, and that probe already refuses to resubmit
on an unknown answer for a job it knows about.

An array element like `4242_3` is not adopted: int() reads underscores as
digit separators, so it would have become job 42423.

salloc launches now carry the same --job-name.  Nothing reuses by name on
that path yet -- it reuses via the record and adopts by node -- but without
it an allocation is invisible to `sucoder sessions`, which filters on the
`sucoder-` prefix, and there is nothing for a name-keyed probe to match on
later.

The two reuse paths share _reuse_confined_job so they cannot drift.  All
three changes are mutation-checked: disabling the name probe, adopting
regardless of target, and dropping the salloc name each fail a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lb2GYSyS7vteb34ziwHQ4c
@ligon
ligon merged commit 2ee5c5d into main Sep 18, 2026
4 checks passed
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