docs(arch): TCP+TLS as first-class owned transport — resolves OQ-60, dissolves OQ-61
ADR-083 revised: TCP+TLS moves from an external sibling loop calling public dispatch to a first-class owned transport via with_tcp_tls(listener, acceptor), running inside run() alongside the quinn and iroh accept loops. The endpoint owns all its accept loops; shutdown() stops them all. The multi-owner shutdown problem (OQ-61) does not arise — dissolved. The reason TCP+TLS was structurally excluded (ADR-010 Am. 1: the endpoint built transports internally, TCP+TLS couldn't fit) is gone after ADR-083 — the endpoint no longer builds transports; it runs accept loops on whatever it's given. TCP+TLS is a listener transport, same shape as quinn and iroh. ADR-010 Amendment 2 supersedes Am. 1's struct-level exclusion. dispatch stays public — but for genuinely external shapes (SSH channels, future WebTransport streams), which are connection-internal multiplexing, not listener transports. The listener-vs-multiplexing distinction is now explicit. OQ-60 resolved: the TCP+TLS loop lives in alknet-core behind a tcp feature (owned by the endpoint); builder functions are inlined by the assembly layer. A alknet-transport crate was rejected — it would contain only trivial builders; the real component (the loop) is in core. Hub- specific composition lives in the hub crate; transport runtimes that any node might need live in core. Updated: ADR-010 (Amendment 2), ADR-082 (TCP+TLS loop location), ADR-083 (revised), core/endpoint.md (struct + dispatch + shutdown), hub/README.md (transport table + assembly example + stale sibling references), tls/README.md (endpoint section + TCP+TLS loop location + references), open-questions.md (OQ-60 resolved, OQ-61 dissolved). Review: zero critical issues, five warnings fixed (stale hub README prose, stale core endpoint.md struct/dispatch listings, stale ADR-082 TCP+TLS loop location, stale TLS README reference entry, hub front-matter date).
This commit is contained in:
1 parent
81bde6f28f
commit
2abe8f1872
10 files changed
+524
-308
No files matched your search
@@ -2,49 +2,69 @@
|
||||
|
||||
- **Origin**: `docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md`
|
||||
(the endpoint refactor commits to the boundary — construction is not
|
||||
in the endpoint — but not the location of `build_iroh_endpoint`).
|
||||
- **Status**: open
|
||||
- **Door type**: one-way (where `build_iroh_endpoint` lives determines
|
||||
who depends on `iroh` for transport construction; moving it later
|
||||
churns the dep graph and every binary's assembly code)
|
||||
in the endpoint — but not the location of transport construction).
|
||||
- **Status**: resolved
|
||||
- **Door type**: one-way (where `build_iroh_endpoint` and the TCP+TLS
|
||||
loop live determines who depends on `iroh` / `tokio-rustls` for
|
||||
transport construction; the dep-graph shape is structural)
|
||||
- **Priority**: high (the hub is the first multi-transport consumer;
|
||||
its assembly code sets the pattern)
|
||||
- **Blocked on**: nothing structural. The three options are clear; the
|
||||
decision is a trade-off, not a missing capability.
|
||||
- **Scope**: This OQ covers **`build_iroh_endpoint`** — the function
|
||||
that reads `StaticConfig` and builds an `iroh::Endpoint`.
|
||||
`build_quinn_server_config_from_rustls` is **decided** (it moves to
|
||||
`alknet-tls` as `TlsServerConfig::for_quinn()` per ADR-082 — it's a
|
||||
thin wrapper over a `rustls::ServerConfig`, which `alknet-tls` owns).
|
||||
Only `build_iroh_endpoint` is genuinely undecided: it reads
|
||||
`StaticConfig` (not a rustls config), builds an `iroh::Endpoint` from
|
||||
an `Ed25519SecretKey` + relay URL, and doesn't fit the `alknet-tls`
|
||||
cert-provider boundary.
|
||||
- **Resolution**: Not yet decided. The options:
|
||||
- **Resolution**: Split answer (resolved by ADR-083 revision, 2026-07-14):
|
||||
|
||||
**Option A: In the assembly layer (the binary).** Each binary reads
|
||||
`StaticConfig` and hand-assembles quinn/iroh/TCP+TLS. Pro: maximal
|
||||
flexibility, core stays lean. Con: every binary duplicates the
|
||||
"build an iroh endpoint from an `Ed25519SecretKey` + relay URL"
|
||||
boilerplate; a 10-step procedure is copy-pasted per binary.
|
||||
**TCP+TLS accept loop → `alknet-core` behind a `tcp` feature (owned
|
||||
by the endpoint).** The endpoint takes a `TcpListener` +
|
||||
`TlsAcceptor` via `with_tcp_tls(listener, acceptor)` and runs the
|
||||
accept loop inside `run()` alongside the quinn and iroh loops. TCP+TLS
|
||||
is a listener transport — same shape as quinn and iroh (accept →
|
||||
extract ALPN + fingerprint → `Connection::from_bidi` → `dispatch`).
|
||||
Making it owned gives the endpoint a single uniform ownership model:
|
||||
it owns all its accept loops, `shutdown()` stops them all. The
|
||||
multi-owner shutdown problem (OQ-61) does not arise. Any node that
|
||||
wants TCP+TLS enables the `tcp` feature and calls `with_tcp_tls` — no
|
||||
hub dependency. This reverses ADR-010's "TCP is not an endpoint struct
|
||||
concern": the reason TCP was excluded (the endpoint built transports
|
||||
internally, TCP+TLS couldn't fit) is gone; the endpoint is now a
|
||||
multi-transport accept-loop runner and TCP+TLS fits the same shape.
|
||||
|
||||
**Option B: In `alknet-tls` as convenience helpers.** `alknet-tls`
|
||||
gains a `build_iroh_endpoint` helper. Pro: one place. Con: `alknet-tls`
|
||||
then depends on `iroh`, bloats a crate whose stated job is "TLS setup,
|
||||
not transport endpoint construction" (ADR-082 scopes `alknet-tls` to
|
||||
cert config and its accessors — `build_iroh_endpoint` doesn't touch
|
||||
certs, it touches iroh's relay/secret-key APIs).
|
||||
**Builder functions (`build_iroh_endpoint`, `build_quinn_endpoint`,
|
||||
`build_tcp_tls`) → inlined by the assembly layer.** These are trivial
|
||||
API calls (2-15 lines each, pure configuration, no shared logic). The
|
||||
assembly layer (the deployment binary — today primarily the hub
|
||||
crate's composition code) inlines them. No helper crate or module —
|
||||
20 lines total across all three. A `alknet-transport` crate was
|
||||
considered and rejected: it would contain only trivial builders (the
|
||||
real component, the TCP+TLS loop, is in core). Hub-specific transport
|
||||
helpers, if they accumulate, live in the hub crate, not a generic
|
||||
transport crate.
|
||||
|
||||
**Option C: A new crate or module — `alknet-transport` / an
|
||||
`alknet-core::transport` module — that owns transport construction.**
|
||||
Pro: clean separation (TLS = certs, transport = endpoints, endpoint =
|
||||
dispatch). Con: another layer in the dep graph.
|
||||
**`dispatch` stays public** — but for genuinely external shapes (SSH
|
||||
channels, future WebTransport streams), not for TCP+TLS. The
|
||||
distinction: listener transports (quinn, iroh, TCP+TLS) produce
|
||||
connections from an accept loop the endpoint owns; multiplexing
|
||||
transports (SSH, WT) produce connections from within an existing
|
||||
connection, and the endpoint can't own their accept loop.
|
||||
|
||||
The ADR-082 boundary ("`alknet-tls` is the cert provider, not the
|
||||
transport constructor") is a strong argument against Option B. Option C
|
||||
is the cleanest separation but adds a layer. Option A is simplest but
|
||||
risks per-binary duplication that drifts.
|
||||
- **Cross-references**: ADR-083 (endpoint refactor), ADR-082
|
||||
(`alknet-tls` — the cert provider boundary; `for_quinn()` is in scope,
|
||||
`build_iroh_endpoint` is not), ADR-010 (original endpoint design,
|
||||
where construction was welded to the endpoint)
|
||||
**Why not the hub crate for the TCP+TLS loop:** "the assembly layer"
|
||||
is, in practice, usually the hub. But a hub-worker serving TCP+TLS
|
||||
shouldn't depend on the hub crate for a transport loop. The loop
|
||||
belongs in core (behind a feature), where any node can use it. The hub
|
||||
crate owns hub-specific *composition* (wiring adapters, relay, peer
|
||||
lifecycle), not transport runtimes.
|
||||
|
||||
**Why not `alknet-tls`:** `alknet-tls`'s stated job is TLS setup —
|
||||
`rustls::ServerConfig`, cert resolvers, ACME (ADR-082). The TCP+TLS
|
||||
accept loop is transport runtime, not TLS setup. It calls
|
||||
`endpoint.dispatch()`, which is core's API. Adding it to `alknet-tls`
|
||||
would make a cert-provider crate depend on the endpoint's dispatch API
|
||||
and own a transport runtime — a category error.
|
||||
|
||||
**Why not `alknet-transport` (new crate):** The real component (the
|
||||
TCP+TLS loop) is in core. What's left for a transport crate is trivial
|
||||
builder functions. A crate for 20 lines of API calls doesn't earn its
|
||||
existence. If future transport runtimes accumulate that don't fit
|
||||
core, a transport crate can be created then — but not speculatively.
|
||||
- **Cross-references**: ADR-083 (endpoint refactor — the revision that
|
||||
resolved this), ADR-082 (`alknet-tls` — the cert provider boundary),
|
||||
ADR-010 (original endpoint design — "TCP is not an endpoint struct
|
||||
concern" is revised), OQ-61 (dissolved — the multi-owner shutdown
|
||||
problem does not arise with TCP+TLS owned by the endpoint)
|
||||
@@ -2,41 +2,28 @@
|
||||
|
||||
- **Origin**: `docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md`
|
||||
(the endpoint owns dispatched handlers; the assembly layer owns
|
||||
spawned accept loops — coordination between them on shutdown is
|
||||
spawned accept loops — coordination between them on shutdown was
|
||||
unspecified).
|
||||
- **Status**: open
|
||||
- **Door type**: two-way (the coordination mechanism is an
|
||||
implementation detail; the ownership boundary — endpoint owns
|
||||
dispatched handlers, assembly layer owns spawned accept loops — is
|
||||
the one-way part, committed in ADR-083)
|
||||
- **Priority**: medium (matters for clean hub shutdown; doesn't block
|
||||
the endpoint-shape decision or the TLS extraction)
|
||||
- **Blocked on**: nothing structural. The question is which
|
||||
coordination primitive (shared `shutdown_sender`, separate channel,
|
||||
drain semantics) — a design choice, not a missing capability.
|
||||
- **Resolution**: Not yet decided. The boundary is clear:
|
||||
- **Status**: dissolved
|
||||
- **Door type**: two-way (the coordination mechanism was an
|
||||
implementation detail)
|
||||
- **Priority**: medium
|
||||
- **Resolution**: Dissolved by ADR-083 revision (2026-07-14). The
|
||||
problem this OQ tracked — coordinating shutdown between the endpoint's
|
||||
owned loops and external TCP+TLS accept loops — does not arise. The
|
||||
TCP+TLS accept loop is now an owned transport (via `with_tcp_tls`),
|
||||
not an external sibling. The endpoint owns all its accept loops
|
||||
(quinn, iroh, TCP+TLS); `shutdown()` stops them all and drains all
|
||||
dispatched handlers. One owner, one shutdown. The `dispatch` method
|
||||
stays public for SSH channels and future WebTransport streams, but
|
||||
those are connection-internal multiplexing callers, not listener
|
||||
loops with independent shutdown needs — they call `dispatch` on
|
||||
connections the endpoint already accepted and dispatched a handler
|
||||
for.
|
||||
|
||||
- The endpoint owns shutdown of **dispatched handlers** (it spawned
|
||||
them via `tokio::spawn` in `dispatch`).
|
||||
- The assembly layer owns shutdown of the **accept loops it spawned**
|
||||
(the TCP+TLS listeners).
|
||||
|
||||
The open question is the **coordination mechanism**:
|
||||
|
||||
- Does the assembly layer use the endpoint's `shutdown_sender()` to
|
||||
signal the TCP+TLS loops, or a separate channel?
|
||||
- Does `endpoint.shutdown()` drain in-flight dispatched handlers
|
||||
(including those from TCP+TLS loops), or only the quinn/iroh ones
|
||||
it spawned via `run()`?
|
||||
- What happens to in-flight `dispatch` calls after the endpoint is
|
||||
shut down — are they rejected, or do they complete?
|
||||
|
||||
A likely shape: the assembly layer uses the endpoint's
|
||||
`shutdown_sender()` for all its accept loops (one signal, all loops
|
||||
stop accepting); the endpoint's `shutdown()` drains all dispatched
|
||||
handlers regardless of which transport spawned them (the endpoint
|
||||
owns them all once `dispatch` is called). But this needs to be
|
||||
written down and verified against the drain semantics.
|
||||
- **Cross-references**: ADR-083 (endpoint refactor — ownership
|
||||
boundary), ADR-010 (original shutdown design — single-owner, the
|
||||
simpler case being generalized)
|
||||
The premise of this OQ (external TCP+TLS loop + endpoint-owned
|
||||
dispatch = multi-owner shutdown) was a consequence of TCP+TLS being
|
||||
external. ADR-083's revision made TCP+TLS internal, retiring the
|
||||
premise.
|
||||
- **Cross-references**: ADR-083 (endpoint refactor — TCP+TLS is now
|
||||
owned), OQ-60 (resolved — the TCP+TLS loop lives in core)
|
||||
Reference in new issue
Block a user