Summary
Memory-pool shared-memory names are derived from a process-local counter, so a
node that crashes and restarts regenerates the exact same name as its previous
incarnation. If the old pool has not been reclaimed by then, the restarted node
cannot register a pool at all — ShmemConf::create() fails on the leftover segment,
and the daemon would independently reject the registration as a duplicate.
Split out of #2881, which fixed when pools are reclaimed. This is the separate
question of pool ids not being unique across incarnations.
Evidence
The counter is a process-global static seeded at 0
(apis/python/node/src/lib.rs:137-138):
/// Counter to make pinned memory buffer IDs unique across registrations.
static PINNED_COUNTER: LazyLock<std::sync::Mutex<u64>> = LazyLock::new(|| std::sync::Mutex::new(0));
It is unique across registrations within one process, but a fresh process always
starts at 1. The segment name mixes it with two values that do not change across a
restart (apis/python/node/src/lib.rs:1965-1968):
let shmem_name = format!("dora_pool_{}_{}_{}", self.dataflow_id, self.node_id, pool_counter);
So incarnation 2 of a node re-derives dora_pool_{dataflow}_{node}_1 and calls
ShmemConf::new().os_id(&shmem_name)…create(). That fails while the old segment
exists — confirmed against shared_memory_extended 0.13.0:
first create: Ok
second create: ERR <-- collision: Shared memory OS specific ID already exists
which is exactly what the call site's own error message anticipates: "name collision
with another node or leftover segment". Independently, the daemon would reject the
registration too — MemoryPoolManager::register_memory_pool returns
"Memory pool with ID {} already registered" for the still-present entry.
Impact
A node using the memory-pool transport under restart_policy: Always fails on its
first registration after any crash, for the remaining life of the dataflow. Since
register_memory_pool raises, the restarted node typically dies immediately — a
crash-restart loop that never recovers.
Why #2881 does not cover it
After #2881, a pool is reclaimed on node exit once no live node can still reach it,
which frees both the table entry and the /dev/shm segment — so the restart succeeds
whenever the crashed node's consumers are gone too. The collision remains in the
common case: sender crashes while its receiver keeps running. The pool is then
deliberately retained (the receiver may still read it), and the restarted sender walks
straight into its own leftover name.
Reclaiming it eagerly is not an option, for the reason established in #2881 — it would
cut off the receiver's in-flight transfer.
Suggested direction
Make the name unique per incarnation rather than per registration. The cheapest form
that preserves the on-wire id format is to seed PINNED_COUNTER with a random u64
instead of 0: the component stays a u64, so both existing parsers keep working —
the writer fast path (buffer_id.rsplit_once('_') then parse::<u64>(),
apis/python/node/src/lib.rs:2459-2462) and try_doradma_read, which reconstructs
dora_pool_{dataflow_id}_{node_id}_{counter} from pool_{node_id}_{counter}
(apis/python/node/src/lib.rs:3481-3512). Any scheme that adds a new component to
the name would need both parsers updated.
An alternative — letting a re-registration by the same node id take over a stale entry
whose registrar has exited — fixes the daemon table but not the /dev/shm collision,
which happens first, node-side. It would also weaken the duplicate-registration check
for the Rust API, where the caller supplies shared_memory_id directly.
Happy to prepare a PR if the random-seed direction looks right.
Summary
Memory-pool shared-memory names are derived from a process-local counter, so a
node that crashes and restarts regenerates the exact same name as its previous
incarnation. If the old pool has not been reclaimed by then, the restarted node
cannot register a pool at all —
ShmemConf::create()fails on the leftover segment,and the daemon would independently reject the registration as a duplicate.
Split out of #2881, which fixed when pools are reclaimed. This is the separate
question of pool ids not being unique across incarnations.
Evidence
The counter is a process-global static seeded at
0(
apis/python/node/src/lib.rs:137-138):It is unique across registrations within one process, but a fresh process always
starts at
1. The segment name mixes it with two values that do not change across arestart (
apis/python/node/src/lib.rs:1965-1968):So incarnation 2 of a node re-derives
dora_pool_{dataflow}_{node}_1and callsShmemConf::new().os_id(&shmem_name)…create(). That fails while the old segmentexists — confirmed against
shared_memory_extended0.13.0:which is exactly what the call site's own error message anticipates: "name collision
with another node or leftover segment". Independently, the daemon would reject the
registration too —
MemoryPoolManager::register_memory_poolreturns"Memory pool with ID {} already registered"for the still-present entry.Impact
A node using the memory-pool transport under
restart_policy: Alwaysfails on itsfirst registration after any crash, for the remaining life of the dataflow. Since
register_memory_poolraises, the restarted node typically dies immediately — acrash-restart loop that never recovers.
Why #2881 does not cover it
After #2881, a pool is reclaimed on node exit once no live node can still reach it,
which frees both the table entry and the
/dev/shmsegment — so the restart succeedswhenever the crashed node's consumers are gone too. The collision remains in the
common case: sender crashes while its receiver keeps running. The pool is then
deliberately retained (the receiver may still read it), and the restarted sender walks
straight into its own leftover name.
Reclaiming it eagerly is not an option, for the reason established in #2881 — it would
cut off the receiver's in-flight transfer.
Suggested direction
Make the name unique per incarnation rather than per registration. The cheapest form
that preserves the on-wire id format is to seed
PINNED_COUNTERwith a randomu64instead of
0: the component stays au64, so both existing parsers keep working —the writer fast path (
buffer_id.rsplit_once('_')thenparse::<u64>(),apis/python/node/src/lib.rs:2459-2462) andtry_doradma_read, which reconstructsdora_pool_{dataflow_id}_{node_id}_{counter}frompool_{node_id}_{counter}(
apis/python/node/src/lib.rs:3481-3512). Any scheme that adds a new component tothe name would need both parsers updated.
An alternative — letting a re-registration by the same node id take over a stale entry
whose registrar has exited — fixes the daemon table but not the
/dev/shmcollision,which happens first, node-side. It would also weaken the duplicate-registration check
for the Rust API, where the caller supplies
shared_memory_iddirectly.Happy to prepare a PR if the random-seed direction looks right.