A shared seam needs a thread-safe backend (L0/L1 substrate)

A segment self-routes its reclaim on whatever thread drops the last reference — typically a subscriber or a transport receive thread, concurrent with a writer’s alloc. So any mem_backend_t injected at a shared seam must tolerate that (ADR-0060 §2): a graph_t’s value backend, a router’s flat, a transport vertex’s rx backend. tr::mem::synchronized_pool_t is the bounded answer — one pool_t whose O(1) free-list operations run inside a critical section.

What to notice

  • The mechanism is a compile-time guard, because only the target knows its concurrency model. Since RFC-0028 slice 10 the pool’s guard is the same tr::guard trait the last-known-value slot uses, and synchronized_pool_t<> binds the build’s one tr::graph::guard_t: on a host the tr::mutex_guard_t (a short bounded spin, then a nap — never a pure spin, so it cannot hang a priority-preemptive scheduler), on ESP-IDF the interrupt-masked critical_guard_t (tr::esp::critical_pool_t). The choice is a template argument — no branch, no vtable, no per-alloc indirection.

  • A guard that may pure-spin is refused where spinning is unsafe. A build that sets tr::mem::kSpinWaitSafe = false rejects any guard declaring may_spin = true at the instantiation, so a spinner that outranks the lock holder is a compile error, not a hang.

  • A single thread-safe pool, never per-stripe sharding. Sharding removes no race and adds partition imbalance.

  • ISR-safety and non-blocking are different facts. The pool forwards its guard’s is_isr_safe / is_nonblocking rather than inventing them, and the example checks that.

  • It is opt-in construction only. No seam defaults to it; heap_backend() remains the default everywhere.

Source

 1/*
 2 * SPDX-License-Identifier: Apache-2.0
 3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
 4 */
 5
 6/**
 7 * @file
 8 * @brief ONE CONCEPT — a backend at a SHARED seam must be thread-safe (ADR-0060 §2).
 9 *
10 * A segment self-routes its reclaim on whatever thread drops the last reference — typically a
11 * subscriber or a transport receive thread, concurrent with a writer's `alloc`. So any
12 * `mem_backend_t` injected at a shared seam (a `graph_t`'s value backend, a router's flat, a
13 * transport vertex's rx backend) must tolerate that. `tr::mem::synchronized_pool_t` is the
14 * bounded answer: one `pool_t` whose O(1) free-list ops run inside a critical section, with
15 * the MECHANISM as a compile-time guard — because only the target knows its concurrency
16 * model. Since RFC-0028 slice 10 the guard is the SAME trait the last-known-value slot uses
17 * (`tr::graph::guard_t`), so `synchronized_pool_t<>` binds the build's one guard: the
18 * host `mutex_guard_t` (a short bounded spin, then a nap — never a pure spin, so it is safe on
19 * every scheduler), or an ESP-IDF build's interrupt-masked `critical_guard_t`.
20 *
21 * Runs under ctest as `example_view_sync_pool`; returns non-zero on any failed check.
22 */
23
24#include <array>
25#include <atomic>
26#include <cstddef>
27#include <cstdio>
28#include <thread>
29#include <vector>
30
31#include "libtracer/tracer.hpp"
32
33namespace {
34
35/** @brief Report expectation @p what and record a failure on @p ok. */
36void check(bool& ok, bool cond, const char* what) {
37    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
38    ok = ok && cond;
39}
40
41/** @brief Slots per thread in the churn below — enough to interleave, small enough to be quick. */
42constexpr int kRounds = 2000;
43
44/** @brief The pool exercise: two threads race one free list through the synchronized pool. */
45bool run_sync_pool() {
46    alignas(std::max_align_t) std::array<std::byte, 4096> slab{};
47    tr::mem::synchronized_pool_t<> pool{slab, 32};  // the build's one guard
48    std::printf("synchronized_pool_t<%s> over a %zu-byte slab: %zu slots, two threads\n",
49                tr::graph::guard_t::name, slab.size(), pool.capacity());
50
51    std::atomic<int> served{0};
52    const auto churn = [&pool, &served] {
53        for (int i = 0; i < kRounds; ++i) {
54            // alloc on this thread, drop on this thread — but the two threads race for the
55            // same free list, which is exactly what the guard protects.
56            tr::view::segment_ptr_t seg = tr::view::segment_alloc(pool, 8);
57            if (seg) served.fetch_add(1, std::memory_order_relaxed);
58        }
59    };
60    std::thread a(churn);
61    std::thread b(churn);
62    a.join();
63    b.join();
64
65    // The free list has no `available()` counter through the synchronized facade, so the
66    // honest check is to drain it: every slot must still be reachable afterwards.
67    std::vector<tr::view::segment_ptr_t> drained;
68    while (auto seg = tr::view::segment_alloc(pool, 8)) drained.push_back(std::move(seg));
69
70    bool ok = true;
71    std::printf("%d of %d allocations served; %zu/%zu slots reachable afterwards\n", served.load(),
72                2 * kRounds, drained.size(), pool.capacity());
73    check(ok, served.load() == 2 * kRounds, "every request was served — the slab never leaked");
74    check(ok, drained.size() == pool.capacity(),
75          "and the free list is whole: no slot was lost to a race");
76    check(ok, tr::mem::synchronized_pool_t<>::is_isr_safe == tr::graph::guard_t::is_isr_safe,
77          "the pool forwards its guard's guarantees rather than inventing them");
78    return ok;
79}
80
81}  // namespace
82
83int main() {
84    const bool ok = run_sync_pool();
85    std::printf("RESULT %s\n", ok ? "ok" : "FAILED");
86    return ok ? 0 : 1;
87}

See also: backends · configuration · concurrency & scaling reference.