Skip to content

Carry out the session control directives PRRTE can honor - #2596

Merged
rhc54 merged 5 commits into
openpmix:masterfrom
rhc54:topic/session-control
Aug 2, 2026
Merged

Carry out the session control directives PRRTE can honor#2596
rhc54 merged 5 commits into
openpmix:masterfrom
rhc54:topic/session-control

Conversation

@rhc54

@rhc54 rhc54 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #2589

The problem

PMIx_Session_control is the API a scheduler uses to create a session, operate on
it while it runs, and reclaim it. PRRTE accepted the call and did almost none of
it: every operational directive returned PMIX_ERR_NOT_SUPPORTED, and an
instantiation built a session and a job object but never launched anything.

So a scheduler driving PRRTE had no way to start work in a session it had just
created, and no way to pause, signal, extend or terminate one afterwards. The
placeholder in docs/how-things-work/schedulers/flow_of_control.rst has been
waiting for this.

What this does

Implements the whole surface. Instantiation gives the session the nodes it was
described with, extends the DVM across any that are new, and launches the
applications it was given into it — answering with the session id, the allocation
id, and the launched namespace. Pause and resume stop and continue every job in
the session; preempt and restore do the same for the jobs a PMIX_NSPACE
qualifier names; signal delivers an arbitrary signal; extend adds nodes or revises
the time limit; terminate kills the jobs and reclaims the session once they have
retired. A session id of UINT32_MAX addresses every session the caller controls.

A scheduler may impose a lifetime on a session (PMIX_ALLOC_TIME/PMIX_TIMEOUT);
when it expires the session is terminated exactly as an explicit request would.
What becomes of a reclaimed session's nodes follows its PMIX_ALLOC_INHERITANCE
disposition, defaulting to the DVM's general pool — handing them back to the
scheduler is the destructive choice and has to be asked for.

The user-facing description of every directive, including where PRRTE deviates and
why, is the new docs/how-things-work/schedulers/session_control.rst.

Two deliberate deviations

Preemption is suspension-based. The attribute describes preemption as halting
the jobs and recovering their resources. Recovering them means checkpointing the
jobs out and restoring them later, which PRRTE cannot do — and terminating them
instead would make the paired PMIX_SESSION_RESTORE impossible to honor at all.
So a preempted job stops and holds its resources, and a restored job resumes where
it was. A scheduler that genuinely needs the nodes back should terminate the
session.

PMIX_SESSION_COMPLETE is outbound only. It is what an RTE says to its
scheduler, not the other way round: PRRTE is the only party that knows when the
jobs have finished and the nodes are free. Inbound it is refused; PRRTE now sends
it — carrying each job's termination status — when a session the scheduler
instantiated is reclaimed.

Two design points worth review

A session created through this API is detached by default, which is the
opposite of an allocation reservation. A reservation is nodes a tool took for its
own use and dies with that tool. A session is named, addressable by anyone
authorized, and destroyed by an explicit terminate, by its own time limit, or —
when it was created to run apps — when those apps finish. Following the requester
instead makes the API unusable for what it exists to do: the requester is a tool,
its namespace lasts only as long as the request, and the session was being
reclaimed out from under its own jobs the moment the caller disconnected (this is
not hypothetical — it is what the multi-node harness caught first).
PMIX_SESSION_SEP given as false opts back into requester lifetime.

A DVM with no scheduler answers for itself. The scheduler owns sessions when
there is one, but refusing session control outright when there is not left the
capability unreachable on an ordinary prte DVM — and impossible to test. Requests
are gated on the same ownership check that governs spawning into a reservation, so
a caller can only act on an allocation it owns. The relay from a non-master daemon
makes the same decision, so a caller gets the same answer whichever daemon it
happens to be attached to.

Pre-existing bugs this uncovered

Each is a separate commit ahead of the feature, so they can be reviewed and
bisected on their own. The middle two were only ever reachable from a non-master
daemon
, which is why they had survived: both APIs are normally driven from the
head node, where the master answers directly and the relay never runs.

  1. prte_plm_base_prted_signal_local_procs() packed the wrong bytes as the
    target namespace.
    A pmix_nspace_t is an array type, so as a parameter it
    has decayed to a char * — and &job is the address of that pointer variable,
    not of the name. PMIX_PROC_NSPACE copies PMIX_MAX_NSLEN+1 bytes from
    whatever it is handed, so a stack fragment went on the wire, every daemon
    matched no job, and the signal was silently dropped. Every signal delivered
    through prte_plm.signal_job() was lost; PMIx_Job_control signals were
    unaffected (that path uses a real local array).

  2. pmix_server_alloc_request_resp() answered a relayed request into the void.
    It passed the request tracker as the callback data where the original caller's
    cbdata belongs. PMIx uses that value to find the operation it is completing,
    so it was handed a pointer it had never issued: the client sat in its PMIx call
    until it timed out, or the library faulted on the unrecognized pointer and took
    the daemon with it. This affected every allocation request as well as every
    session-control request issued from a non-master daemon.

  3. prte_pmix_set_scheduler() re-probed on every request.
    PMIx_tool_attach_to_server is a blocking call that hunts the filesystem for a
    rendezvous and tries to connect to what it finds — and it ran on the PRRTE
    progress thread, the only thread driving the DVM, in front of every allocation
    and every session-control request. On a DVM with no scheduler (the ordinary case,
    where it can only ever fail) that is pure cost on the one thread that must not be
    spent. It now looks once; a scheduler that appears later announces itself by
    attaching to us.

  4. prte_ras_base_teardown_reservation() could shrink the master out of its own
    DVM.
    When handing a reservation's nodes back to the scheduler it collected the
    daemon rank of every member node, with nothing excluding our own. A reservation
    may legitimately contain the head node, and ordering our own daemon to depart
    terminates the DVM, taking every other job with it.

Testing

  • Unit — new coverage for the session time-limit parser in
    test/unit/prted/. (Writing it caught that PMIx_Argv_split discards empty
    tokens, so ":30" and "1::2" were being silently reinterpreted as valid
    durations rather than refused.)
  • make check — all suites pass.
  • Offline mapper harness — 1180/1180.
  • Docs — build warning-free; new session_control.rst wired into the
    scheduler-integration section.
  • Single nodeexamples/sessionctrl.c drives the full lifecycle:
    instantiate + launch, pause (procs observably in T), resume, preempt/restore,
    signal, time-limit expiry, terminate, and the error cases.
  • Multi node — new test_session phase in contrib/dockerswarm
    (14 cases): a reservation actually withholding its nodes from a general job, a
    request relayed from a non-master daemon, both nodes' procs stopping and
    resuming, a signal reaching the session's jobs on every node they occupy, a
    session created to run apps being reclaimed when they end, and a standing
    reservation persisting until explicitly terminated.
    Full suite: 395 passed, 0 failed, 2 skipped (both skips pre-existing PMIx
    capability gates, unrelated).

The multi-node run is what found items 1–3 above, plus a self-deadlock of my own
making: _send_alloc_resp releases the request twice — explicitly, and again
through its caddy's destructor — so a new caller has to hand it over holding the
extra PMIX_RETAIN the allocation branch already takes, or the last release runs
the destructor while the object lock is held. That path also has to be
thread-shifted out of the RML receive before it drives the PLM, as the allocation
branch does.

🤖 Generated with Claude Code

rhc54 added 5 commits August 1, 2026 15:31
prte_plm_base_prted_signal_local_procs() packed the address of its own
parameter as the target namespace.  A pmix_nspace_t is an array type, so
as a function parameter it has decayed to a char* - and &job is
therefore the address of that pointer variable, not of the name.  Packing
PMIX_PROC_NSPACE copies PMIX_MAX_NSLEN+1 bytes from whatever it is
handed, so what went on the wire was a fragment of the caller's stack.

Every daemon receiving the command then compared that garbage against
its local children, matched none of them, and delivered the signal to
nothing at all.  The command was accepted, the xcast succeeded, and the
target processes never saw the signal - which is why this survived: the
only symptom is silence.

Copy the name into real storage before packing it.  The equivalent code
in pmix_server_job_ctrl.c has always done so, which is why signals sent
through PMIx_Job_control were unaffected.

Signed-off-by: Ralph Castain <rhc@pmix.org>
prte_ras_base_teardown_reservation(), when told to hand a reservation's
nodes back to the scheduler, collected the daemon rank of every member
node and broadcast a shrink command naming them all.  Nothing excluded
the master's own daemon.

A reservation may legitimately contain the head node - a single-node DVM
whose whole allocation is carved into one session is the extreme case,
but any allocation that includes the head node reaches it - and ordering
our own daemon out of our own DVM terminates the DVM, taking every other
job still running on it with it.  The failure looks like the DVM simply
vanishing at the moment an unrelated reservation is released.

Skip our own rank when collecting shrink targets.  The head node is
detached from the reservation either way, so it reverts to the general
pool, which is the correct outcome for the node the master is standing on.

Signed-off-by: Ralph Castain <rhc@pmix.org>
pmix_server_alloc_request_resp() is how a non-master daemon completes an
allocation or session-control request the DVM master serviced on its
behalf.  It invoked the requester's callback with the request TRACKER as
the callback data, where the caller's own cbdata belongs.  PMIx uses
that value to find the operation it is completing, so it was handed a
pointer it had never issued: the client sat in its PMIx call until it
timed out, or the library faulted on the unrecognized pointer and took
the daemon with it.

Every allocation request and every session-control request issued from a
process attached to a non-master daemon went this way.  It survived
because both APIs are normally driven from the head node, where the
master answers directly and this relay never runs.

Pass req->cbdata.  Also clear the tracker's slot on the no-callback
branch, which released the request but left the array pointing at it,
and add traces on both halves of the relay - the response arriving and
the request it names being gone are the two things worth seeing when a
client hangs here.

Signed-off-by: Ralph Castain <rhc@pmix.org>
prte_pmix_set_scheduler() is called at the head of every allocation and
every session-control request the DVM master handles, and when no
scheduler has attached it re-ran PMIx_tool_attach_to_server every time.

That call blocks.  It runs on the PRRTE progress thread, which is the
only thread driving the DVM, and it hunts the filesystem for a
rendezvous and tries to connect to whatever it finds.  On a DVM with no
scheduler - the ordinary case, and the one where it can only ever fail -
that put a filesystem scan and a connect attempt in front of every such
request, on the one thread that must never be spent.

Look exactly once and remember the answer.  Nothing is lost: a scheduler
that appears later announces itself by attaching to us, and the
tool-connect upcall records it, so the probe is only ever a startup
convenience for a scheduler that was already there.

Signed-off-by: Ralph Castain <rhc@pmix.org>
PMIx_Session_control is how a scheduler creates, operates on and reclaims
a session - PRRTE's allocation object.  PRRTE accepted the call and did
almost none of it: every operational directive returned
PMIX_ERR_NOT_SUPPORTED, and an instantiation built a session and a job
object but never launched anything.  A scheduler driving PRRTE therefore
had no way to start work in a session it had created, and no way to
pause, signal, extend or terminate one afterwards.

Implement the whole surface.  Instantiation now gives the session the
nodes it was described with, extends the DVM across any that are new, and
launches the applications it was given into it, answering with the
session id, the allocation id and the launched namespace.  Pause and
resume stop and continue every job in the session; preempt and restore do
the same for the jobs a PMIX_NSPACE qualifier names; signal delivers an
arbitrary signal; extend adds nodes or revises the time limit; terminate
kills the jobs and reclaims the session once they have retired.  A
session id of UINT32_MAX addresses every session the caller controls.

Two of these cannot be done as the attribute describes them, and the
deviations are deliberate.  Preemption is suspension-based: recovering a
preempted job's resources would require checkpointing it out and
restoring it later, which PRRTE cannot do, and terminating it instead
would make the paired PMIX_SESSION_RESTORE impossible to honor at all.
And PMIX_SESSION_COMPLETE is refused as an inbound directive, because it
is what an RTE says to its scheduler - PRRTE is the only party that knows
when the jobs have finished.  PRRTE now sends it, carrying each job's
termination status, when a session the scheduler instantiated is
reclaimed.

A session created here is DETACHED by default, which is the opposite of
an allocation reservation and is the point of the distinction.  A
reservation is nodes a tool took for its own use and dies with that tool;
a session is named, addressable by anyone authorized, and destroyed by an
explicit terminate, by its own time limit, or - when it was created to
run apps - when those apps finish.  Following the requester instead makes
the API unusable for what it exists to do: the requester is a tool, its
namespace lasts only as long as the request, and the session would be
reclaimed out from under its own jobs the moment the caller disconnected.
PMIX_SESSION_SEP given as false opts back into requester lifetime.

The request is parsed in full before anything acts on it.  An info array
has no defined order, and the previous single-pass version mis-handled an
instantiation that arrived ahead of its own PMIX_SESSION_JOB: the job was
built and then never attached to the session.  Conflicting operations are
now refused rather than silently applied in array order.

A DVM with no scheduler answers for itself instead of failing.  The
scheduler owns sessions when there is one, but refusing session control
outright when there is not left the capability unreachable on an ordinary
prte DVM - and unable to be tested.  Requests are gated on the same
ownership check that governs spawning into a reservation, so a caller can
only act on an allocation it owns.  The relay from a non-master daemon
makes the same decision, so a caller gets the same answer whichever
daemon it is attached to; that path runs the directive on the progress
thread rather than inside the RML receive, and hands the request over
holding the second reference send_alloc_resp's two releases consume.

A scheduler may also impose a lifetime on a session (PMIX_ALLOC_TIME or
PMIX_TIMEOUT); when it expires the session is terminated exactly as an
explicit request would, and an extend re-arms it.  What becomes of a
reclaimed session's nodes follows its PMIX_ALLOC_INHERITANCE disposition,
defaulting to the DVM's general pool: handing them back to the scheduler
is the destructive choice and has to be asked for.

Provisioning and any resource description that asks PRRTE to choose
machines are refused, since PRRTE runs on the nodes it is given and is
itself the thing being scheduled.

examples/sessionctrl.c drives all of this against a running DVM, and the
container harness uses it for the parts that only exist across nodes - a
reservation actually withholding its nodes, a request relayed from a
non-master daemon, a signal reaching the session's jobs everywhere they
run, and the two ways a session ends.

Signed-off-by: Ralph Castain <rhc@pmix.org>
@rhc54
rhc54 merged commit fbd2033 into openpmix:master Aug 2, 2026
18 checks passed
@rhc54
rhc54 deleted the topic/session-control branch August 2, 2026 00:31
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.

Implement remaining PMIx request support

1 participant