Skip to content

Commit d34e366

Browse files
fix: publish registry slot state only after its payload commits under the seqlock
1 parent 2ed0026 commit d34e366

2 files changed

Lines changed: 23 additions & 5 deletions

File tree

‎rmw_unix_socket_cpp/src/registry.cpp‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,9 @@ static bool snapshot_slot(RegistryEntrySlot * slot, RegistryEntrySlot * out)
170170
continue;
171171
}
172172
uint8_t st = slot->state.load(std::memory_order_acquire);
173-
if (st == ENTRY_EMPTY) {
173+
if (st == ENTRY_EMPTY || st == ENTRY_RESERVED) {
174+
// RESERVED = a writer claimed the slot but has not yet published its
175+
// payload. Treat it as empty so we never snapshot a half-built slot.
174176
return false;
175177
}
176178

@@ -230,12 +232,17 @@ static int32_t try_add_once(RegistryHeader * header, const RegistryEntry & entry
230232

231233
for (uint32_t i = 0; i < max; ++i) {
232234
uint8_t expected = ENTRY_EMPTY;
233-
// CAS state EMPTY -> entry.type. Winner owns the slot.
235+
// CAS state EMPTY -> RESERVED. Winner owns the slot, but readers still see
236+
// it as empty until we publish the real type below — so no reader can
237+
// snapshot the slot before its payload is committed under the seqlock.
234238
if (slots[i].state.compare_exchange_strong(
235-
expected, static_cast<uint8_t>(entry.type),
239+
expected, static_cast<uint8_t>(ENTRY_RESERVED),
236240
std::memory_order_acq_rel, std::memory_order_relaxed))
237241
{
238242
write_slot_payload(&slots[i], entry);
243+
// Payload is committed (seq even). Publish the real type with release
244+
// ordering so any reader that now sees it also sees the payload.
245+
slots[i].state.store(static_cast<uint8_t>(entry.type), std::memory_order_release);
239246
header->generation.fetch_add(1, std::memory_order_acq_rel);
240247
return static_cast<int32_t>(i);
241248
}
@@ -347,6 +354,11 @@ void registry_cleanup_stale(RegistryHeader * header)
347354
if (!snapshot_slot(&slots[i], &snap)) {
348355
continue;
349356
}
357+
if (snap.pid == 0) {
358+
// pid==0 is never a real entry — only a transient snapshot of a slot
359+
// whose payload is not yet written. Never reclaim it.
360+
continue;
361+
}
350362
if (pid_is_alive_or_unreachable(snap.pid)) {
351363
continue;
352364
}

‎rmw_unix_socket_cpp/src/registry.hpp‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ enum RegistryEntryType : uint8_t
2727
ENTRY_SUBSCRIPTION,
2828
ENTRY_SERVICE,
2929
ENTRY_CLIENT,
30+
// Transient claim state: a writer won the slot but has not yet committed its
31+
// payload. Readers treat it like ENTRY_EMPTY so they never observe a slot
32+
// before its payload is published. Never stored in a RegistryEntry; lives
33+
// only in the slot's atomic state field, so it costs no struct layout.
34+
ENTRY_RESERVED = 0xFF,
3035
};
3136

3237
// User-facing struct, passed to registry_add. Plain data, no atomics.
@@ -52,11 +57,12 @@ struct RegistryEntry
5257
// seqlock + atomic state machine so readers never need a global mutex.
5358
//
5459
// Protocol (writer):
55-
// 1. CAS state EMPTY -> target_type (claims the slot atomically)
60+
// 1. CAS state EMPTY -> RESERVED (claims the slot; readers treat as EMPTY)
5661
// 2. seq.fetch_add(1, acq_rel) -> seq becomes odd (write in progress)
5762
// 3. memcpy payload fields
5863
// 4. seq.fetch_add(1, acq_rel) -> seq becomes even (snapshot stable)
59-
// 5. generation.fetch_add(1, acq_rel) -> wakes cached readers
64+
// 5. state.store(target_type, release) -> publishes the committed payload
65+
// 6. generation.fetch_add(1, acq_rel) -> wakes cached readers
6066
//
6167
// Protocol (reader):
6268
// 1. s1 = seq.load(acquire); if (s1 & 1) skip slot (writer in progress)

0 commit comments

Comments
 (0)