One NAME, one slot — the whole routing table (L4 routing)

fwd_router_t has exactly one table: child_registry_t, keyed on this node’s own local name for each link (ADR-0037). Forwarding, replying and advertising all resolve through it. There is no second table, and no destination ever appears in it.

This example routes no frames at all. It is about the table’s two rules — what a refused registration leaves behind, and how many slots a name can ever own.

What to notice

  • A refusal registers NOTHING, and the bool is the only warning you get. add_child returns false for a name no address could ever spell (empty, containing an empty route segment, wider than graph::kMaxSegments) and for a registry that could not grow. Discarding it is how #930 shipped: a connection reported UP whose every forward missed and fell through to the terminus with no error anywhere. [[nodiscard]] does not forbid ignoring the result — it makes ignoring it a deliberate, greppable (void).

  • A name owns exactly one slot, for the router’s life (#884). Re-adding a live name rebinds its slot; a second slot would shadow the first on every name-keyed lookup — returning the dead one — and churn on a stable name set would grow the chain every lookup walks without bound.

  • Removal is a tombstone, not an erase. live_size() falls, size() does not. A lock-free reader may be walking that slot right now, so it stays put and stops answering; a later re-add revives it in place (ADR-0063). Hence two counters: live_size() is what routes, size() is what lookups walk.

  • remove_child is removal, not departure. A link that merely dropped wants link_down, because under RFC-0014 a DIAL connection’s vertex outlives its socket and self-heals — evicting the registry entry would permanently unroute a link that is only reconnecting.

  • Every mutation is its own statement. The example never puts a call that changes a counter and a read of that counter in the same printf argument list; C++ leaves that order unspecified, and an example about counters cannot afford it. (This bit the first draft.)

  • This target needs the FWD net plane. It is built only when LIBTRACER_NET_PLANE is on (the default). Nothing in it is conditional at run time.

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 — the routing table is one NAME→link map with one slot per name, and
  9 *        `add_child`'s `[[nodiscard]] bool` is the only thing between you and a child that is
 10 *        published UP but reachable by no `dst`.
 11 *
 12 * `fwd_router_t` has exactly one table: `child_registry_t`, keyed on this node's own local name
 13 * for each link (ADR-0037). Everything the routing plane does — forward, reply, advertise —
 14 * resolves through it. There is no second table, and no destination ever appears in it.
 15 *
 16 * Two properties are worth knowing before you wire a node:
 17 *
 18 *  1. **A refusal registers NOTHING.** `add_child` returns false for a name no address could
 19 *     ever spell (empty, containing an empty route segment, wider than `graph::kMaxSegments`) and
 20 *     for a registry that could not grow. Discarding that `bool` is how #930 shipped: a
 21 *     connection reported UP whose every forward missed and fell through to the terminus with
 22 *     no error anywhere. The attribute does not forbid ignoring the result — it makes ignoring
 23 *     it a deliberate, greppable `(void)`.
 24 *  2. **A NAME owns exactly one slot, for the router's life** (#884). Re-adding a live name
 25 *     REBINDS its slot rather than appending a second one, and `remove_child` TOMBSTONES the
 26 *     slot instead of erasing it, so a later re-add revives it in place. A second slot would
 27 *     shadow the first on every name-keyed lookup — returning the DEAD one — and churn on a
 28 *     stable name set would grow the chain every lookup walks without bound.
 29 *
 30 * Hence the two counters this example watches: `live_size()` is what routes, `size()` is what
 31 * lookups walk. Registration/removal churn moves the first and must not grow the second.
 32 *
 33 * No frames are routed here — this example is about the table alone. Runs under ctest as
 34 * `example_route_child_table`; returns non-zero on any failed check.
 35 */
 36
 37#include <cstddef>
 38#include <cstdio>
 39#include <span>
 40#include <string>
 41#include <vector>
 42
 43#include "libtracer/fwd_router.hpp"
 44#include "libtracer/tracer.hpp"
 45
 46namespace {
 47
 48using tr::graph::graph_t;
 49
 50/** @brief Report expectation @p what and record a failure on @p ok. */
 51void check(bool& ok, bool cond, const char* what) {
 52    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
 53    ok = ok && cond;
 54}
 55
 56/** @brief A `transport_t` that keeps every frame handed to it — an identity for the table. */
 57struct recording_link_t : tr::net::transport_t {
 58    std::vector<std::vector<std::byte>> sent; /**< @brief Frames emitted on this link, in order. */
 59    void send(std::span<const std::byte> frame) override {
 60        sent.emplace_back(frame.begin(), frame.end());
 61    }
 62    void send(std::span<const std::span<const std::byte>> iov) override {
 63        std::vector<std::byte> flat;
 64        for (const auto part : iov) flat.insert(flat.end(), part.begin(), part.end());
 65        sent.push_back(std::move(flat));
 66    }
 67};
 68
 69}  // namespace
 70
 71int main() {
 72    bool ok = true;
 73    graph_t g;
 74    tr::net::fwd_router_t router(g);
 75    recording_link_t first, second;
 76
 77    // Every mutation is its own statement, and every observation is a separate one. Mixing
 78    // them into one printf() argument list would leave the order unspecified — this example
 79    // is about counters that a call CHANGES, so the sequencing has to be explicit.
 80    const bool added = router.add_child("up", first);
 81    check(ok, added, "add_child returns true and the child is registered");
 82    check(ok, router.registry().live_size() == 1 && router.registry().size() == 1,
 83          "one live slot, one slot walked");
 84
 85    // A name no `dst` could ever spell is refused ALWAYS (not just in debug builds), and the
 86    // table is exactly as it was.
 87    const bool empty_name = router.add_child("", second);
 88    const bool empty_segment = router.add_child("a//b", second);
 89    check(ok, !empty_name && !empty_segment, "an unaddressable name is refused, both spellings");
 90    check(ok, router.registry().size() == 1, "and a refusal registered NOTHING — no ghost slot");
 91
 92    // Re-adding a LIVE name rebinds its one slot. The link the name resolves to changes; the
 93    // number of slots does not.
 94    const bool rebound = router.add_child("up", second);
 95    check(ok, rebound, "re-adding a live name succeeds");
 96    check(ok, router.registry().by_name("up") == &second,
 97          "and it REBINDS: the name now resolves to the new link");
 98    check(ok, router.registry().size() == 1 && router.receiver_ctx_count() == 1,
 99          "one name still owns exactly one slot and one receiver context");
100
101    // Removal is a tombstone, not an erase: nothing routes through the name any more, but the
102    // slot a lock-free reader may be walking right now stays put.
103    const bool removed = router.remove_child("up");
104    check(ok, removed, "remove_child reports that it named a live child");
105    check(ok, router.registry().by_name("up") == nullptr, "the name resolves to nothing now");
106    check(ok, router.registry().live_size() == 0 && router.registry().size() == 1,
107          "live_size() fell to zero; size() did not — the slot is tombstoned, not erased");
108    check(ok, !router.remove_child("up"), "removing it again names no live child");
109
110    // The re-add revives the tombstone in place. This is the churn bound: N add/remove cycles
111    // on a stable name set leave the chain at the number of distinct NAMES, not of calls.
112    const bool revived = router.add_child("up", first);
113    check(ok, revived, "a re-add after removal succeeds");
114    check(ok, router.registry().live_size() == 1 && router.registry().size() == 1,
115          "and revives the SAME slot — churn on a stable name set grows nothing");
116    check(ok, router.receiver_ctx_count() == 1, "the receiver-context chain is likewise unchanged");
117
118    std::printf("one name, one slot: %zu live of %zu walked, %zu receiver context(s)\n",
119                router.registry().live_size(), router.registry().size(),
120                router.receiver_ctx_count());
121    return ok ? 0 : 1;
122}

See also: fwd-router module · transport module · transports are vertices · terminus or forward · qualified mounts.