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

Applies to builds where tr::mem::kSpinWaitSafe is true

This is the one build-configuration-dependent example in the wire and view domains. A single-core priority-preemptive target sets tr::mem::kSpinWaitSafe = false in libtracer/config_override.hpp; there a spinner that outranks the lock holder never yields the CPU the holder needs, so binding spin_sync_t is not “slower” — it is a hang, and synchronized_pool_t refuses the instantiation outright. The target is still always built and always linked: it follows the binding with if constexpr, prints skipped: this build sets tr::mem::kSpinWaitSafe = false and passes.

An example a binding does not apply to must skip at run time, never fail to compile. The mechanics differ from sub_unsubscribe_from_dispatch, and the difference is a two-layer trap worth stating outright. First, if constexpr alone is not enough: in a non-template function the discarded branch is still fully instantiated, so declaring the pool inside a discarded branch of main would trip the assert regardless. Second — and this is the one that is easy to miss — being a template is not enough either: a template’s non-dependent constructs are checked at definition time, so spelling the concrete tr::mem::sync_pool_t inside the discarded branch trips it too. The pool type has to depend on a template parameter, which is why the helper takes the sync policy as one. Line 1 of a run prints the bound value, so the output says which arm it took. Every other example in these two domains is unconditional.

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 policy, because only the target knows its concurrency model. A multi-core host wants the spin_sync_t spinlock: negligible contention on an O(1) section, and it avoids the ~2 µs OS-mutex round trip that would dominate a ~120 ns free-list operation. A single-core MCU wants an interrupt-disable critical section instead. The choice is a template argument — no branch, no vtable, no per-alloc indirection.

  • tr::mem::sync_pool_t is the host spelling, and it is the discoverable short name — which is why a build that forbids spin-waiting rejects it loudly rather than shipping 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. A spinlock is is_nonblocking but not is_isr_safe, and the example asserts the latter — the traits forward the policy’s guarantees rather than inventing them.

  • 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 policy — because only the target knows its concurrency
 16 * model. `tr::mem::sync_pool_t` is the multi-core-host spelling, `spin_sync_t`.
 17 *
 18 * THIS EXAMPLE IS BUILD-CONFIGURATION-DEPENDENT, and it is the only one in the wire/view set
 19 * that is. A single-core priority-preemptive target sets `tr::mem::kSpinWaitSafe = false` in
 20 * `libtracer/config_override.hpp` — there a spinner that outranks the lock holder never
 21 * yields the CPU the holder needs, so binding `spin_sync_t` is a hang, and
 22 * `synchronized_pool_t` refuses the instantiation. An example a binding does not apply to
 23 * must SKIP AT RUN TIME, never fail to compile, so the body lives in a template: in a
 24 * non-template function the discarded `if constexpr` branch is still instantiated, and
 25 * declaring the pool there would trip the assert anyway. Line 1 of a run says which arm it
 26 * took.
 27 *
 28 * Runs under ctest as `example_view_sync_pool`; returns non-zero on any failed check. Where
 29 * `kSpinWaitSafe` is false it prints a skip line and passes.
 30 */
 31
 32#include <array>
 33#include <atomic>
 34#include <cstddef>
 35#include <cstdio>
 36#include <optional>
 37#include <thread>
 38#include <vector>
 39
 40#include "libtracer/tracer.hpp"
 41
 42namespace {
 43
 44/** @brief Report expectation @p what and record a failure on @p ok. */
 45void check(bool& ok, bool cond, const char* what) {
 46    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
 47    ok = ok && cond;
 48}
 49
 50/** @brief Slots per thread in the churn below — enough to interleave, small enough to be quick. */
 51constexpr int kRounds = 2000;
 52
 53/**
 54 * @brief The pool exercise — a TEMPLATE so the discarded branch is never instantiated.
 55 *
 56 * Two things are load-bearing about this signature, and both are easy to get wrong. First,
 57 * `if constexpr` alone is not enough: in a NON-template function the discarded branch is still
 58 * fully instantiated, so declaring the pool inside a discarded branch of `main` would trip the
 59 * assert regardless. Second, being a template is not enough EITHER — a template's
 60 * NON-dependent constructs are checked at definition time, so spelling the concrete
 61 * `tr::mem::sync_pool_t` here would trip it too. The type has to depend on a template
 62 * parameter, which is what @p Sync is for.
 63 *
 64 * @tparam Enabled `tr::mem::kSpinWaitSafe`, i.e. whether this target may bind a spin-waiting
 65 *         critical section at all.
 66 * @tparam Sync The synchronization policy to bind. Defaulted rather than hard-coded purely so
 67 *         that `synchronized_pool_t<Sync>` below is a DEPENDENT type and is therefore left
 68 *         uninstantiated when the branch is discarded.
 69 * @return True when every expectation held (and trivially true on the skip arm).
 70 */
 71template <bool Enabled, class Sync = tr::mem::spin_sync_t>
 72bool run_sync_pool() {
 73    if constexpr (Enabled) {
 74        alignas(std::max_align_t) std::array<std::byte, 4096> slab{};
 75        tr::mem::synchronized_pool_t<Sync> pool{slab, 32};  // == tr::mem::sync_pool_t
 76        std::printf("sync_pool_t over a %zu-byte slab: %zu slots, two threads\n", slab.size(),
 77                    pool.capacity());
 78
 79        std::atomic<int> served{0};
 80        const auto churn = [&pool, &served] {
 81            for (int i = 0; i < kRounds; ++i) {
 82                // alloc on this thread, drop on this thread — but the two threads race for
 83                // the same free list, which is exactly what the policy guards.
 84                tr::view::segment_ptr_t seg = tr::view::segment_alloc(pool, 8);
 85                if (seg) served.fetch_add(1, std::memory_order_relaxed);
 86            }
 87        };
 88        std::thread a(churn);
 89        std::thread b(churn);
 90        a.join();
 91        b.join();
 92
 93        // The free list has no `available()` counter through the synchronized facade, so the
 94        // honest check is to drain it: every slot must still be reachable afterwards.
 95        std::vector<tr::view::segment_ptr_t> drained;
 96        while (auto seg = tr::view::segment_alloc(pool, 8)) drained.push_back(std::move(seg));
 97
 98        bool ok = true;
 99        std::printf("%d of %d allocations served; %zu/%zu slots reachable afterwards\n",
100                    served.load(), 2 * kRounds, drained.size(), pool.capacity());
101        check(ok, served.load() == 2 * kRounds, "every request was served — the slab never leaked");
102        check(ok, drained.size() == pool.capacity(),
103              "and the free list is whole: no slot was lost to a race");
104        check(ok, !tr::mem::synchronized_pool_t<Sync>::is_isr_safe,
105              "a spinlock is not an ISR critical section — the policy says so in a constant");
106        return ok;
107    } else {
108        std::printf(
109            "skipped: this build sets tr::mem::kSpinWaitSafe = false — sync_pool_t spin-waits "
110            "and this target must bind an interrupt-disable policy instead\n");
111        return true;
112    }
113}
114
115}  // namespace
116
117int main() {
118    std::printf("tr::mem::kSpinWaitSafe bound by this build: %d\n",
119                static_cast<int>(tr::mem::kSpinWaitSafe));
120    const bool ok = run_sync_pool<tr::mem::kSpinWaitSafe>();
121    std::printf("RESULT %s\n", ok ? "ok" : "FAILED");
122    return ok ? 0 : 1;
123}

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