6.2. Client Init/Finalize Lifecycle and State Management
This document specifies how PMIx manages the state associated with a
client across the PMIx_Init / PMIx_Finalize boundary. It has two
halves: the server-side management of the per-client and
per-namespace state the server holds on a client’s behalf, and the
client-side teardown of the client library itself. Its purpose is to
define the intended behavior precisely enough to implement and test it,
and in particular to guarantee one property:
The cycling guarantee
A client may cycle PMIx_Init → (work: fences, gets, puts,
events, …) → PMIx_Finalize and then PMIx_Init again, any
number of times, reusing the same namespace and rank, and each
new PMIx_Init must present a fresh interaction state on the
server. No state produced in one cycle may leak into the next. The
host environment is not required to deregister and re-register the
client between cycles for this to hold.
For a code-oriented view of the socket connection that underlies each init, see Transport Layer (Client/Server/Tool Connections). This document is concerned with the state lifecycle, not the transport.
6.2.1. The Problem
When a client calls PMIx_Init, the server accumulates state on its
behalf: a peer object holding the connection, event-loop registrations,
tag counters, and a datastore (gds) module assignment; entries in
collective trackers as the client fences; event and I/O-forwarding
registrations; cached notifications; and committed key/value data. The
server also carries longer-lived, job-scoped state in the namespace
object: the roster of ranks, the count of local processes, the GDS job
data, and network/programming-model resources reserved for the job.
A single PMIx_Init/PMIx_Finalize pass, followed by the host
tearing the job down, is the common case and works. The difficulty is
the cycling case. PMIx_Finalize on the client side tears the client
library all the way down (including the class system), so a subsequent
PMIx_Init is a brand-new library instance that opens a new socket
and reconnects. On the server, the namespace and the rank are still
registered — the host did not tear them down — so the second init must
reconnect onto existing job-scoped state while starting from a clean
per-client slate. If per-client state from the first cycle survives into
the second, the client sees stale data, duplicated accounting, or leaked
objects.
The design below draws a sharp line between job-scoped state, which belongs to the namespace and persists until the host deregisters it, and client-scoped state, which belongs to a single init/finalize cycle and must not survive one.
6.2.2. The Actors
pmix_namespace_t(nptr)The job. Holds the
rankslist,nlocalprocs(the number of local clients registered for this namespace),nfinalized(how many of those have finalized), the compatibility modules negotiated for the job, the namespace-level epilog, and the association to the GDS job data. Persists until the host callsPMIx_server_deregister_nspace.pmix_rank_info_t(info)One entry per rank on
nptr->ranks. Records the rank’s identity (pname),uid/gid, the host’sserver_object, the index (peerid) of the peer object serving this rank in the server’sclientsarray, andproc_cnt— the number of live clones of this rank (see below). This is registration state: it is created byPMIx_server_register_client(or lazily on first connect) and lives until the client or the namespace is deregistered. It is not torn down by finalize.pmix_peer_t(peer)The per-connection object: socket, send/receive queues, event registrations, dynamic-tag counters, the
finalizedflag, the peer’s assignedgdsmodule, and the peer-level epilog. Held inpmix_server_globals.clientsand pointed to byinfo->peerid. This is the primary carrier of client-scoped state.- Clones
A client may
fork/execa child that also callsPMIx_Initusing the same namespace and rank as its parent — in PMIx terminology, a clone of that client. Each clone opens its own socket and is tracked byinfo->proc_cnt. A rank is only fully finalized when the original client and all of its clones have finalized — i.e. whenproc_cntreturns to zero.
6.2.3. The Three Teardown Triggers
Server-side cleanup is driven by exactly three events. Each has a distinct, non-overlapping responsibility. The remainder of this document is normative: “must” denotes a required behavior.
- Client finalize (all clones included)
A client, together with all of its clones, has finalized — the rank’s
proc_cnthas dropped to zero. The peer object serving that rank is left in place as an inert tombstone at its existing client-array slot (it is not freed, its slot is not nulled, andinfo->peeridis not moved), while the rank’s registration (thepmix_rank_info_tonnptr->ranks) is preserved so the client can init again. The tombstone is reclaimed later, at the next reconnect for the rank or at namespace deregistration. See Tombstoning the peer on finalize. A tool peer takes a lighter path — it is freed at socket-close rather than tombstoned, because nothing resolves a tool throughinfo->peerid; see Tombstone mechanics.
- Host deregisters a client (
PMIx_server_deregister_client) The server removes the peer object from the namespace and releases it. See Deregistering a client.
- Host deregisters a client (
- Host deregisters the namespace (
PMIx_server_deregister_nspace) The server removes all GDS-registered job data for the namespace, removes and releases all peer objects belonging to the namespace, and cleans up all remaining references to the namespace. See Deregistering the namespace.
- Host deregisters the namespace (
The essential distinction is that finalize is reversible and deregistration is terminal. Finalize leaves the registration intact so the client can init again; only the host, by deregistering, declares that the client or the whole job is gone for good and its objects may be freed.
6.2.4. Tombstoning the Peer on Finalize
This section describes the client peer path, where the tombstone is required; a tool peer is retired more simply (freed at socket-close) and is covered under Tombstone mechanics below.
When the last live process for a client rank finalizes (proc_cnt
reaches zero), the server must not free the peer object, null its
client-array slot, or move info->peerid at socket-close time. Instead
it leaves the peer in place as an inert tombstone:
Leave the object at its existing slot in
pmix_server_globals.clientswithfinalized == true. By the timepmix_server_peer_finalizedruns,lost_connectionhas already stopped the peer’s send/receive events and closed its socket, and thefinalizedflag short-circuits every send path (thePMIX_PTL_SEND_*macros andPMIX_SERVER_QUEUE_REPLY). The tombstone is therefore harmless: it cannot arm a doomed send on its closed socket, yet it keepsinfo->peeridpointing at a real, resolvable peer object for the rank.Leave the rank’s
pmix_rank_info_tonnptr->ranks(the registration), leave the namespace’s GDS job data in place, and leaveinfo->peeridunchanged.Reduce only the rank’s live-process count (
proc_cnt). The finalized count (nfinalized) is left as-is — the tombstone is still afinalizedpeer and must keep being counted as one.
The tombstone is reclaimed at a later, quiescent point:
On reconnect. When the client calls
PMIx_Initagain, the connection handler finds the stale tombstone atinfo->peerid, frees it (nulls the slot, drops thenfinalizedcount it was holding, andPMIX_RELEASEs it), then allocates a fresh peer for the rank. The prior cycle has fully finalized by the time a new init arrives, so no collective from it can still be in flight — this is a safe point to mutate the array and the count.On deregistration. If the rank never reconnects (the common case for a spawned child that runs once and exits), the tombstone is freed when the host deregisters the namespace.
Why leave a tombstone rather than free the peer at socket-close? Two
earlier designs freed (or destruct/reconstructed) the object the moment
the socket dropped, from inside lost_connection. Both mutated shared
per-namespace/per-rank state — nulling the clients slot, and in
particular clearing info->peerid to -1 — while a different
process’s spawn/connect/disconnect collective or direct-modex get was
still walking the clients array and resolving ranks through
info->peerid. A rank whose peerid had just been cleared could no
longer be resolved to its GDS module, so the in-flight collective or get
stalled: an architecture- and timing-sensitive hang of multi-local-process
MPI_Comm_spawn. Leaving the finalized peer in place keeps every
peerid valid and every array slot populated, so nothing a concurrent
collective touches changes underneath it; the object is reclaimed only
once no such operation can be in flight.
A tombstone whose slot has been taken over by a newer peer for the same
rank — a fork/exec’d clone that is still running, or a fast re-init that
reconnected before the socket-close was processed — is no longer named by
info->peerid. Such a stranded object is freed immediately in
pmix_server_peer_finalized (nulling only its own slot, never the live
referenced one, and never touching info->peerid), so a departing
clone or a raced reconnect cannot leak a peer or its finalized count.
Job-scoped accounting must stay honest across cycles. The rank’s
proc_cnt tracks live processes and returns to zero when the last one
departs; nfinalized counts finalized peers and is decremented only
when a tombstone is actually reclaimed (on reconnect) or freed (stranded),
so that nlocalprocs == nfinalized remains an accurate “all local
processes finalized” test no matter how many times clients cycle.
- What finalize does not do
Finalize does not remove the rank from
nptr->ranks, does not drop the namespace’s GDS job data, and does not touch network or programming-model resources reserved for the job. Those are job-scoped and survive until the host deregisters. Transient, client-scoped registrations that are keyed to the peer — event registrations, I/O-forwarding requests, in-flight direct-modex requests, and cached notifications targeting the client — are pruned as part of the teardown so they do not linger.
6.2.4.1. Tombstone mechanics
The tombstone is driven from lost_connection in the transport layer
when a cleanly-finalized client’s socket drops, and its correctness rests
on two per-namespace counters staying honest across every cycle.
Peer states. A client peer moves through three states:
State |
|
|
in |
|---|---|---|---|
Active |
|
≥ 0 |
yes |
Finalizing ( |
|
≥ 0 |
yes |
Tombstone (socket dropped, kept at |
|
−1 |
no |
A tombstone is deliberately left with finalized == true so it stays
inert to every finalized-guarded send path while it waits at
info->peerid to be reclaimed. It is removed from proc_cnt (its
process is gone) but remains counted in nfinalized (it is still a
finalized peer), and it stays resolvable through info->peerid so a
concurrent collective or get can still find the rank’s GDS module.
The two counters.
info->proc_cntis the number of live processes (clones) for the rank. It is incremented in the connection handler when a peer connects and decremented on every peer departure — a clean finalize drop and an abnormal termination. Decrementing on abnormal death too (inlost_connection) keeps a later clone’s tombstone/reclaim accounting correct.nptr->nfinalizedis the number of client peers currently in thefinalized == truestate, including tombstones. It is incremented whenFINALIZE_CMDis received and decremented only when a tombstone is reclaimed — on reconnect, or when a stranded tombstone is freed. Keeping this balanced is what lets thenlocalprocs == nfinalizedtest at namespace teardown fire the programming-model “all local processes finalized” callback. Because a tombstone stays counted innfinalizeduntil it is reclaimed, that equality is already satisfied before namespace teardown begins and holds for every rank the teardown then walks. The callback must therefore be fired only once per namespace — alocal_app_fini_firedlatch on the namespace guards it — rather than once per rank as the ranks are removed.
Finalize on socket drop. lost_connection in the transport layer
routes a cleanly-finalized peer whose socket has now dropped to
pmix_server_peer_finalized — a pure client so it can be tombstoned,
and a pure tool so it can be freed (a peer that is both a client and
a tool is left for its tool reconnect/deregistration reclaim; see below).
pmix_server_peer_finalized decrements proc_cnt, then, if the peer
is still the rank’s referenced peer (info->peerid == peer->index),
retires it according to its role:
An application client (
PMIX_PEER_IS_CLIENT) is left in place as a tombstone and nothing else is touched — a concurrent collective or get belonging to another local process may still resolve this rank throughinfo->peerid(see below), so its shared state must not change here.A pure tool is instead freed at once, and
info->peeridis repointed — to a surviving tool sibling sharing the sameinfoif one is still live, else to-1. A tool is not an application-job member: it has its own namespace and is never the peer a job collective or direct-modex get resolves throughinfo->peerid, so the tombstone that protects that race is unnecessary. And because each tool init is assigned a fresh namespace, a finalized tool never reconnects onto the same rank to be reclaimed there — leaving it would leak one peer per init/finalize cycle. Freeing it also restores the post-finalizeinfo->peerid == -1state thatprocess_tool_requestrelies on to recognize a reconnecting tool.
Otherwise a newer peer already owns the rank’s slot and this object is
stranded, so it is freed at once regardless of role: its own clients
slot is nulled (never the live referenced one), nfinalized is
decremented, and it is PMIX_RELEASEd. A reference still held by a
pending non-blocking collective (whose caddy sits on the tracker’s
local_cbs) or an active sensor is harmless in every case: a tombstone
is simply retained a while longer, and a released object is not torn down
until that last holder drops its reference.
Reclaim on reconnect. Before allocating a fresh peer for a
reconnecting rank, the connection path reclaims any tombstone the rank
left behind. For a client this is the main connection handler, which
checks info->peerid: if it names a finalized peer and the rank has
no live process (proc_cnt == 0), that tombstone is reclaimed — its
slot nulled, nfinalized decremented, and the object released — and
only then is a new peer allocated. A tool that was registered as a client
(a launcher child) keeps the client tombstone semantics above but
reconnects through process_tool_request rather than the client
handler; that path performs the same reclaim on the finalized peer at
info->peerid before assigning the reconnecting tool its fresh slot.
Either way info->peerid is then repointed, by the normal connection
assignment, to the freshly allocated peer.
``peerid`` stays valid for clients. Because a client finalize never
clears info->peerid to -1, a local PMIx_Get for the rank
(which resolves committed data only while info->peerid names a
resolvable peer) continues to work against the client tombstone until the
rank either reconnects or is deregistered. A pure tool, freed at
socket-close, instead leaves info->peerid == -1 — which is correct,
because no other process resolves a tool’s rank this way.
6.2.5. Deregistering a Client
PMIx_server_deregister_client is the host’s declaration that a
specific rank is gone and will not init again. Unlike finalize — which
tombstones the peer object but keeps the rank’s registration so it can
init again — deregistration also removes the registration:
Remove the peer object from the namespace: release the reference the namespace/registration holds, null out the client-array slot identified by
info->peerid, and remove thepmix_rank_info_tfromnptr->ranks.Release the peer object.
Restore any job resources that were charged to the rank (network allocations, monitoring/sensors) and honor the peer’s epilog, exactly as when a client terminates.
After a client is deregistered, the rank no longer exists on the server; a subsequent connection for that rank would be rejected unless the host re-registers it.
6.2.6. Deregistering the Namespace
PMIx_server_deregister_nspace is the host’s declaration that the
entire job is gone. This is the terminal, whole-namespace teardown and
must reclaim everything the namespace owned:
Remove all GDS-registered job data for the namespace (
gdsdel of the namespace), so no key/value or job-info data survives.Remove and release all peer objects still belonging to the namespace (any rank whose processes have not all finalized), nulling their client-array slots and freeing every
pmix_rank_info_tonnptr->ranks.Release the network and programming-model resources reserved for the job, run the namespace epilog, and prune every remaining reference to the namespace: event and I/O-forwarding registrations, cached notifications targeting any of its procs, and finally the namespace object itself.
Nothing that names the namespace may remain after this call returns.
6.2.7. State Ownership Summary
The following table classifies the state and states which trigger frees it. “Released” means the object is freed and its client-array slot nulled; “Tombstoned” means it is left inert in place and reclaimed later; “Kept” means the trigger leaves it in place.
State |
Client finalize (all clones) |
Deregister client |
Deregister nspace |
|---|---|---|---|
Peer object ( |
Tombstoned (kept at slot; reclaimed on reconnect/deregister) |
Released, slot nulled |
Released, slot nulled |
Event / IOF / direct-modex regs, cached notifications for the peer |
Pruned |
Pruned |
Pruned |
Peer epilog |
Executed |
Executed |
Executed (peer) + namespace epilog |
Rank info ( |
Kept (registration preserved) |
Removed, released |
Removed, released |
GDS job data for the namespace |
Kept |
Kept |
Removed |
Network / programming-model job resources |
Kept |
Rank’s share restored |
Released (whole job) |
Namespace object ( |
Kept |
Kept |
Released; all references purged |
6.2.8. Client-Side Teardown
Everything above concerns the state a server holds for a client. The
cycling guarantee has a second half: the client’s own library must tear
itself down on PMIx_Finalize so that a subsequent PMIx_Init in
the same process starts from a clean slate. Unlike the server side —
where finalize deliberately preserves registration state — the client
side has no such asymmetry to manage: a client PMIx_Finalize (once
the init reference count reaches zero) is a full teardown of the library,
and a later PMIx_Init is a brand-new library instance that opens a
fresh socket and reconnects.
The teardown runs PMIx_Finalize → pmix_rte_finalize → framework
closes plus pmix_finalize_util and the release of the progress
thread; it is the mirror image of PMIx_Init → pmix_rte_init →
pmix_init_util plus the framework opens. The governing principle is
symmetry: finalize must undo exactly what init did, leaving no
residue that the next init would either trip over or skip.
- The reference-count guard
PMIx_Initis reference-counted: nested init/init/…/finalize sequences only tear down on the last finalize, when the counter returns to zero. The full teardown described here applies to that final finalize. A nested (non-final) finalize must leave the library running and untouched.
Three requirements make the symmetry real, and an implementation must uphold all three:
Every one-time-init latch must be reset. Any
staticboolean or counter that guards a “do this once” step in init (module registration, key creation, subsystem setup) must be cleared by the corresponding finalize, so the step runs again on the next init. Leaving a latch set silently skips re-initialization on the second cycle — the failure is not a crash at finalize but wrong or missing state later. The multi-select frameworkinitializedflags, the MCA parameter-registration latch, and the attribute/output/show-help subsystems all follow this rule and are the model to copy.Every freed global must be nulled, and every per-cycle flag reset. A global pointer whose target is freed during finalize must be set to
NULLin the same step. Two hazards follow from not doing so:Dangling reuse. A non-
NULLpointer to freed memory defeats theif (NULL == p)guards that would otherwise make a between-cycles access safe; it reads as “still valid” and yields a use-after-free. The event base, the shared progress-thread tracker, and the cached server peer are pointers of this kind.Stale mode flags. A flag that records how this cycle was set up — most importantly the singleton indicator, which records that the process came up with no server and therefore constructed the server-side I/O-forwarding lists — must be reset on every fresh init, not only when it happens to be toggled. A stale singleton flag carried from a no-server cycle into a with-server cycle drives finalize to tear down lists the with-server path never built.
Init and finalize of the utility layer must stay in lockstep.
pmix_init_utilopens a fixed set of low-level subsystems (output, install-dirs, show-help, the keyval parser, the MCA base and its variable system, the network and interface helpers). Each must be closed on finalize. Whether the close lives inpmix_finalize_utilor inline inpmix_rte_finalizeis an implementation choice, but the two lists must be kept in correspondence: a subsystem added to init without a matching teardown persists across a cycle and breaks the clean-slate guarantee for the next init.
- Platform caveat: destructor support
On platforms that provide neither a compiler
destructorattribute nor linker-finisupport, the library cannot guarantee cleanup at unload, and re-initialization after finalize is explicitly unsupported — a secondPMIx_InitreturnsPMIX_ERR_NOT_SUPPORTEDrather than risk an inconsistent restart. The cycling guarantee therefore applies only where destructor or-finisupport is present (the mainstream case).
6.2.9. Tool-Side Teardown
The tool role has the same cycling guarantee as the client: a process
must be able to cycle PMIx_tool_init → work → PMIx_tool_finalize
repeatedly, each init a clean slate. PMIx_tool_init /
PMIx_tool_finalize are a separate entry-point pair from the client
PMIx_Init / PMIx_Finalize — they do not share the client’s
finalize code — so the three symmetry requirements above have to be
satisfied independently in the tool path. Two of them bite the tool
specifically:
- The reference-count guard and the one-time latch
PMIx_tool_initis reference-counted through its own counter, and it sets the shared one-time-init latch (pmix_globals.init_called).PMIx_tool_finalizemust decrement that counter, tear down only on the last matching finalize, and reset the latch. Forgetting the reset is the most visible cycling failure: the secondPMIx_tool_initsees the latch still set, concludes the library is already initialized, and returnsPMIX_ERR_INIT— the tool never comes back up.- The tool constructs server state every init
Because a tool may itself act as a server (a launcher connects up to its own server while listening for its children),
PMIx_tool_initunconditionally initializes theserver_globalsbookkeeping — constructing the client array and every server-side list — on every init, whether or not the tool will act as a server. Tool finalize must therefore destruct all of those lists, not just the subset a particular run happened to populate; any list left un-destructed leaks its contents each cycle when the next init reconstructs over it. For the same reason the tool must free (andNULL) thetmpdirandsystem_tmpdirstrings: the init-time code refreshes them only when they areNULL, so a surviving pointer both leaks and silently pins the previous cycle’s temporary-directory choice, ignoring a changed environment on the next init. The tool must likewise destruct the client-sidepeersarray backing store and the static IOF sinks, exactly as the client finalize does.The reset of the server module latch (
pmix_server_globals.module_set, set byPMIx_tool_set_server_module) is not part of this pair: that latch is owned by a separate registration call a launcher makes outsidePMIx_tool_init, and the server reference finalizer does not clear it either. A launcher that re-registers its server module between cycles is therefore out of scope here and handled separately.
6.2.10. Invariants
An implementation of the above must uphold these invariants:
No cross-cycle carryover. The finalized peer serving a rank is reclaimed (freed) no later than the rank’s next reconnect, at which point a fresh object is allocated, so a new cycle cannot inherit queues, event registrations, tag counters, or datastore assignments from the previous one.
No orphaned peers. Cycling init/finalize must not accumulate unreferenced peer objects in the
clientsarray. A finalized client peer is left as a single tombstone atinfo->peeridand is reclaimed when the rank reconnects (or when the namespace is deregistered); a peer that a newer connection has already displaced, and a finalized tool peer, are freed immediately. Either way at most one finalized object per rank is ever kept, and only while the rank’s registration itself persists — including across repeated tool init/finalize cycles that reuse the same namespace and rank, whose reconnect reclaim runs inprocess_tool_request.No shared-state mutation while a collective is in flight. Finalize must not free a peer, null a live
clientsslot, or move a liveinfo->peeridfromlost_connection: a concurrent spawn/connect/disconnect collective or direct-modex get may be walking that state for another process. All such mutation is deferred to a quiescent point (reconnect or deregistration), or confined to a stranded object no livepeeridnames.No counter drift. Per-namespace finalized accounting and per-rank
proc_cntmust return to values consistent with the actual number of connected processes after each cycle, so that the job-level “all local processes finalized” condition and any resource-release keyed on it remain correct no matter how many times clients cycle.Registration outlives finalize; only deregistration is terminal. The
pmix_rank_info_tand the client-array slot persist across finalize and are removed only by an explicit host deregister. GDS job data and job resources persist untilPMIx_server_deregister_nspace.The client library reinitializes from a clean slate. After the final
PMIx_Finalize, nostaticinit-once latch remains set, no global pointer references freed memory, and no per-cycle mode flag retains a value from the previous cycle. The nextPMIx_Initbehaves exactly as the first.