docs(arch): endpoint as pure accept-loop runner + acme-tls/1 guard relocation (ADR-083, OQ-60/61)
ADR-083: AlknetEndpoint becomes a pure accept-loop runner with a public dispatch method. Transport construction moves out of the endpoint — the assembly layer reads StaticConfig, builds transports from TlsServerConfig(s), and hands pre-built quinn/iroh endpoints to the endpoint via with_quinn/with_iroh. TCP+TLS dispatch is first-class (same dispatch path as quinn/iroh); ADR-010 Amendment 1's duplicated-dispatch workaround is retired. StaticConfig stays in core as the assembly-layer config; the endpoint takes only drain_timeout. The acme-tls/1 guard moves from dispatch_quinn to the shared dispatch method — ACME TLS-ALPN-01 challenges arrive over TCP (CAs validate via TCP to port 443, not QUIC), so the guard's quinn-specific location was a latent bug once TCP+TLS exists. The guard is transport-agnostic; the rationale (no handler, silent close) is unchanged. ADR-027 §5 amended. OQ-60: where build_iroh_endpoint lives (assembly layer / alknet-tls helper / transport module). build_quinn_server_config_from_rustls is decided — it moves to alknet-tls as for_quinn() per ADR-082; only build_iroh_endpoint is genuinely undecided. OQ-61: multi-owner shutdown coordination. Boundary committed (endpoint owns dispatched handlers; assembly layer owns spawned accept loops); mechanism open. ADR-082 amended: drops the Arc<TlsServerConfig> endpoint signature (superseded by ADR-083); keeps its scope as the alknet-tls crate's TlsServerConfig + accessors. Review: zero critical issues, five warnings fixed (build_iroh_endpoint destination contradiction, undeclared shutdown_sender, missing ADR-027 amendment marker, underspecified dispatch no-match behavior, stale door-type timing clause).
This commit is contained in:
1 parent
9823c6e4ab
commit
81bde6f28f
8 files changed
+564
-50
No files matched your search
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-12
|
||||
last_updated: 2026-07-14
|
||||
---
|
||||
|
||||
# Alknet Architecture
|
||||
@@ -194,7 +194,7 @@ adapter location map is now consistent: all HTTP-backed adapters
|
||||
| [007](decisions/007-bistream-type-definition.md) | BiStream Type Definition | Accepted |
|
||||
| [008](decisions/008-secret-service-integration.md) | Vault Integration Point | Accepted |
|
||||
| [009](decisions/009-one-way-door-decision-framework.md) | One-Way Door Decision Framework | Accepted |
|
||||
| [010](decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Accepted |
|
||||
| [010](decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Accepted (Amendment 1 superseded by ADR-083 — TCP+TLS dispatch is first-class via public `dispatch`) |
|
||||
| [011](decisions/011-authcontext-structure.md) | AuthContext Structure and Resolution Flow | Accepted |
|
||||
| [012](decisions/012-call-protocol-stream-model.md) | Call Protocol Stream Model | Accepted |
|
||||
| [013](decisions/013-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Accepted |
|
||||
@@ -211,7 +211,7 @@ adapter location map is now consistent: all HTTP-backed adapters
|
||||
| [024](decisions/024-operation-registry-layering.md) | Operation Registry Layering | Accepted |
|
||||
| [025](decisions/025-vault-local-only-dispatch.md) | Vault Local-Only Dispatch | Accepted |
|
||||
| [026](decisions/026-vault-key-model-hd-derivation.md) | Vault Key Model — HD Derivation | Accepted |
|
||||
| [027](decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | TLS Identity Redesign — ACME + RawKey Decoupling | Accepted |
|
||||
| [027](decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | TLS Identity Redesign — ACME + RawKey Decoupling | Accepted (§5 amended by ADR-083 — guard moves to shared `dispatch`) |
|
||||
| [028](decisions/028-callclient-peer-scoped-registry-filtering.md) | Peer-Scoped Registry Filtering for CallClient Inbound Dispatch | ~~Accepted~~ → **Superseded** by ADR-029 |
|
||||
| [029](decisions/029-peer-graph-routing-model.md) | Peer-Graph Routing Model for alknet-call Composition | Accepted (Assumption 1's `PeerId` source superseded by ADR-030) |
|
||||
| [030](decisions/030-peerentry-and-identity-id-decoupling.md) | PeerEntry and Identity.id Decoupling | Accepted (supersedes ADR-029 Assumption 1's UUID source) |
|
||||
@@ -266,7 +266,8 @@ adapter location map is now consistent: all HTTP-backed adapters
|
||||
| [079](decisions/079-hub-relay-translate-not-forward.md) | Hub Relay — Translate, Not Transparently Forward | Accepted |
|
||||
| [080](decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | Accepted |
|
||||
| [081](decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | Accepted |
|
||||
| [082](decisions/082-alknet-tls-extraction.md) | alknet-tls Crate Extraction | Proposed |
|
||||
| [082](decisions/082-alknet-tls-extraction.md) | alknet-tls Crate Extraction | Proposed (amended — endpoint signature superseded by ADR-083) |
|
||||
| [083](decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as Pure Accept-Loop Runner with Public Dispatch | Proposed |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-13
|
||||
last_updated: 2026-07-14
|
||||
---
|
||||
|
||||
# alknet-tls
|
||||
@@ -267,26 +267,47 @@ production and `rustls::sign` in the test helper `build_ed25519_spki_der`
|
||||
### What `AlknetEndpoint` does after the refactor
|
||||
|
||||
`AlknetEndpoint::new()` currently builds `TlsSetup` internally. After
|
||||
the refactor, it takes an `Arc<TlsServerConfig>`:
|
||||
the refactor (see [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md)),
|
||||
the endpoint takes **no TLS config at all** — it is a pure accept-loop
|
||||
runner with a public `dispatch` method:
|
||||
|
||||
```rust
|
||||
impl AlknetEndpoint {
|
||||
pub async fn new(
|
||||
static_config: &StaticConfig,
|
||||
tls_config: Arc<TlsServerConfig>, // ← new param
|
||||
pub fn new(
|
||||
handlers: HandlerRegistry,
|
||||
dynamic: Arc<ArcSwap<DynamicConfig>>,
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
) -> Result<Self, EndpointError>;
|
||||
drain_timeout: Duration,
|
||||
) -> Self;
|
||||
|
||||
pub fn with_quinn(mut self, endpoint: quinn::Endpoint) -> Self;
|
||||
pub fn with_iroh(mut self, endpoint: iroh::Endpoint) -> Self;
|
||||
|
||||
pub fn dispatch(
|
||||
&self,
|
||||
connection: Connection,
|
||||
alpn: Vec<u8>,
|
||||
fingerprint: Option<String>,
|
||||
remote_addr: Option<SocketAddr>,
|
||||
);
|
||||
|
||||
pub async fn run(self: Arc<Self>);
|
||||
}
|
||||
```
|
||||
|
||||
The endpoint calls `tls_config.for_quinn()` to get its quinn server
|
||||
config. The ACME handle is on the `TlsServerConfig`, not the endpoint —
|
||||
the endpoint doesn't own the ACME task. The assembly layer builds the
|
||||
`TlsServerConfig` once, passes `Arc::clone()` to the endpoint, and
|
||||
passes another `Arc::clone()` to the TCP+TLS accept loop (which lives in
|
||||
the hub or a future `TcpTlsAcceptor`).
|
||||
The assembly layer builds the `TlsServerConfig`(s), builds the
|
||||
transports (`for_quinn()` → `quinn::Endpoint::server()`,
|
||||
`for_tcp_tls()` → `TlsAcceptor`, the `Ed25519SecretKey` → iroh), and
|
||||
hands the pre-built quinn/iroh endpoints to `AlknetEndpoint` via
|
||||
builder methods. A hub serving native clients and browsers holds two
|
||||
`TlsServerConfig`s (raw key + X.509/ACME); the endpoint takes neither —
|
||||
it takes the already-built transport endpoints. The TCP+TLS accept
|
||||
loops (one per config) call `endpoint.dispatch(...)` — the same
|
||||
dispatch path quinn and iroh use. The ACME handle lives on the
|
||||
`TlsServerConfig`, not the endpoint.
|
||||
|
||||
This resolves the single-`Arc<TlsServerConfig>` problem: the endpoint
|
||||
has no "the TLS config" to take because a hub has two.
|
||||
|
||||
### The TCP+TLS accept loop (out of scope for this crate)
|
||||
|
||||
@@ -336,6 +357,7 @@ All design decisions are documented as ADRs in
|
||||
| ADR | Decision | Summary |
|
||||
|-----|----------|---------|
|
||||
| [082](../../decisions/082-alknet-tls-extraction.md) | alknet-tls crate extraction | Extract TLS setup from alknet-core/endpoint.rs; `TlsServerConfig` shareable across quinn + TCP+TLS + iroh; one ACME state machine |
|
||||
| [083](../../decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as accept-loop runner | `AlknetEndpoint` takes no TLS config; assembly layer builds transports from `TlsServerConfig`s; public `dispatch` method; `acme-tls/1` guard moves to shared `dispatch` |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -348,6 +370,17 @@ See [open-questions.md](../../open-questions.md) for full details.
|
||||
on `alknet-tls` — a new dep edge. If it stays, core keeps a narrow
|
||||
`rustls` dep. Decision-ready — the answer depends on whether we want
|
||||
core to be `rustls`-free.
|
||||
- **OQ-60** (open): Where does transport construction live? The
|
||||
endpoint-as-accept-loop-runner boundary (ADR-083) commits to
|
||||
construction being outside the endpoint, but the destination —
|
||||
assembly layer, `alknet-tls` convenience helpers, or a transport
|
||||
module/crate — is open. `alknet-tls`'s stated job is "TLS setup, not
|
||||
transport accept logic," which is an argument against the helper
|
||||
option.
|
||||
- **OQ-61** (open): Multi-owner shutdown coordination. The endpoint owns
|
||||
dispatched handlers (spawned in `dispatch`); the assembly layer owns
|
||||
spawned accept loops (TCP+TLS). The coordination mechanism (shared
|
||||
`shutdown_sender`, drain semantics) is unspecified.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
Accepted (§5 amended by ADR-083 — the `acme-tls/1` guard moves from
|
||||
`dispatch_quinn` to the shared `dispatch` method, since ACME challenges
|
||||
arrive over TCP+TLS, not QUIC; the rationale holds, only the location
|
||||
changes)
|
||||
|
||||
## Context
|
||||
|
||||
@@ -171,11 +174,13 @@ if let Some(TlsIdentity::RawKey(key)) = static_config.tls_identity.as_ref() {
|
||||
`iroh::SecretKey::from_bytes(&[u8; 32])` accepts raw Ed25519 key bytes —
|
||||
no information loss. This conversion is `#[cfg(feature = "iroh")]` only.
|
||||
|
||||
### 5. ACME ALPN challenge handling in `dispatch_quinn`
|
||||
### 5. ACME ALPN challenge handling in `dispatch` (moved from `dispatch_quinn` by ADR-083)
|
||||
|
||||
Add an early-return guard in `dispatch_quinn` before the handler lookup:
|
||||
Add an early-return guard in `dispatch` (the shared dispatch path,
|
||||
moved from `dispatch_quinn` by ADR-083) before the handler lookup:
|
||||
|
||||
```rust
|
||||
// In the shared `dispatch` method (moved from `dispatch_quinn` by ADR-083):
|
||||
if alpn == b"acme-tls/1" {
|
||||
debug!("acme-tls/1 challenge connection completed at TLS layer; closing");
|
||||
connection.close(0u32.into(), b"acme done");
|
||||
@@ -185,7 +190,13 @@ if alpn == b"acme-tls/1" {
|
||||
|
||||
This avoids the misleading "no handler for ALPN" warning. The challenge
|
||||
is already answered at the TLS layer; the application just closes
|
||||
gracefully. No `ProtocolHandler` registration for `acme-tls/1`.
|
||||
gracefully. No `ProtocolHandler` registration for `acme-tls/1`. The
|
||||
guard is transport-agnostic — it fires for any connection whose TLS
|
||||
handshake negotiated `acme-tls/1`, regardless of which transport
|
||||
delivered it. In practice ACME TLS-ALPN-01 challenges arrive over
|
||||
TCP+TLS (CAs validate via TCP to port 443, not QUIC); advertising
|
||||
`acme-tls/1` on a QUIC listener that shares the ACME config is
|
||||
harmless. See ADR-083 for the full rationale.
|
||||
|
||||
### 6. Feature-gate ACME behind a new `acme` feature
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
Proposed (amended 2026-07-14: the `AlknetEndpoint::new` signature
|
||||
referenced here was superseded by ADR-083 — the endpoint takes no TLS
|
||||
config; the assembly layer builds transports from `TlsServerConfig`s)
|
||||
|
||||
## Context
|
||||
|
||||
@@ -127,7 +129,7 @@ impl TlsServerConfig {
|
||||
| `Ed25519SecretKey` | `alknet-core/config.rs` | Config type — iroh reads it directly |
|
||||
| `AcmeDirectory` | `alknet-core/config.rs` | Config type |
|
||||
| `fingerprint.rs` | `alknet-core` | Shared by server (endpoint) and client (`alknet-call`'s `FingerprintPinVerifier`) — moving it would create a dep edge from `alknet-call` to `alknet-tls`. Production code uses `sha2` + manual DER only; `rustls` is test-only. See OQ-59. |
|
||||
| `AlknetEndpoint` | `alknet-core` | The endpoint struct stays; it takes `Arc<TlsServerConfig>` instead of building it internally |
|
||||
| `AlknetEndpoint` | `alknet-core` | The endpoint struct stays; it takes no TLS config (see ADR-083) |
|
||||
|
||||
### Feature gates
|
||||
|
||||
@@ -148,25 +150,25 @@ when `acme` is enabled. `rustls-pki-types` is available via `rustls`'s
|
||||
re-export (core lists it directly; the new crate can rely on the
|
||||
re-export or list it directly — implementation detail).
|
||||
|
||||
### `AlknetEndpoint` takes `Arc<TlsServerConfig>`
|
||||
### `AlknetEndpoint` takes no TLS config (see ADR-083)
|
||||
|
||||
```rust
|
||||
impl AlknetEndpoint {
|
||||
pub async fn new(
|
||||
static_config: &StaticConfig,
|
||||
tls_config: Arc<TlsServerConfig>, // ← new param
|
||||
handlers: HandlerRegistry,
|
||||
dynamic: Arc<ArcSwap<DynamicConfig>>,
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
) -> Result<Self, EndpointError>;
|
||||
}
|
||||
```
|
||||
ADR-082's original proposal was that `AlknetEndpoint::new` would take
|
||||
`Arc<TlsServerConfig>`. That does not hold: a hub serving both native
|
||||
clients (raw key) and browsers (X.509/ACME) holds **two**
|
||||
`TlsServerConfig`s, and the endpoint has no single "the TLS config" to
|
||||
take. The endpoint takes no TLS config at all — it is a pure
|
||||
accept-loop runner with a public `dispatch` method. The assembly layer
|
||||
builds the `TlsServerConfig`s and the transports, and hands the
|
||||
pre-built quinn/iroh endpoints to `AlknetEndpoint` via builder methods.
|
||||
See [ADR-083](083-endpoint-as-accept-loop-runner.md) for the endpoint's
|
||||
new shape and signature.
|
||||
|
||||
The endpoint calls `tls_config.for_quinn()` to get its quinn server
|
||||
config. The ACME handle lives on the `TlsServerConfig`, not the
|
||||
endpoint — the endpoint doesn't own the ACME task. The assembly layer
|
||||
builds the `TlsServerConfig` once, passes `Arc::clone()` to the
|
||||
endpoint, and passes another `Arc::clone()` to the TCP+TLS accept loop.
|
||||
`alknet-tls`'s job is to make the cert available to whichever transports
|
||||
the deployment runs. The endpoint's job is to dispatch. The two are
|
||||
decoupled — `alknet-tls` provides `TlsServerConfig` and its accessors
|
||||
(`for_quinn`, `for_tcp_tls`, `rustls_config`); the assembly layer wires
|
||||
them to transports; the endpoint dispatches connections from those
|
||||
transports.
|
||||
|
||||
### The TCP+TLS accept loop lives outside `alknet-tls`
|
||||
|
||||
@@ -228,11 +230,12 @@ that compiles and passes type-checks but silently changes TLS behavior:
|
||||
(requires X.509) is the exception, not the constraint.
|
||||
|
||||
**Negative:**
|
||||
- `AlknetEndpoint::new` gains a new parameter (`Arc<TlsServerConfig>`),
|
||||
which is a breaking change for existing callers. The assembly layer
|
||||
(CLI binary, hub construction) must build the `TlsServerConfig` before
|
||||
constructing the endpoint. This is expected — it's the point of the
|
||||
extraction.
|
||||
- `AlknetEndpoint::new` no longer takes a `tls_config` parameter, which
|
||||
is a breaking change for existing callers. The assembly layer builds
|
||||
the `TlsServerConfig`s and the transports and hands pre-built
|
||||
endpoints to `AlknetEndpoint` via builder methods (see
|
||||
[ADR-083](083-endpoint-as-accept-loop-runner.md)). This is expected —
|
||||
it is the point of the extraction.
|
||||
- `alknet-core` may keep a narrow `rustls` dep if `fingerprint.rs` stays
|
||||
(OQ-59). If `fingerprint.rs` moves to `alknet-tls`, `alknet-call`'s
|
||||
client-side `FingerprintPinVerifier` gains a dep on `alknet-tls`. The
|
||||
@@ -243,11 +246,12 @@ that compiles and passes type-checks but silently changes TLS behavior:
|
||||
## Door type
|
||||
|
||||
**One-way.** Extracting TLS setup into a shareable config is a
|
||||
structural change: `AlknetEndpoint::new` signature changes, the assembly
|
||||
layer must build `TlsServerConfig` separately, and the cert-sharing
|
||||
contract (one config, N transports) becomes the architecture. Reversing
|
||||
would mean re-welding TLS to the endpoint and losing the multi-transport
|
||||
cert-reuse capability — the exact capability the hub needs.
|
||||
structural change: the assembly layer must build `TlsServerConfig`
|
||||
separately and the cert-sharing contract (one config, N transports)
|
||||
becomes the architecture. Reversing would mean re-welding TLS to the
|
||||
endpoint and losing the multi-transport cert-reuse capability — the
|
||||
exact capability the hub needs. The `AlknetEndpoint::new` signature
|
||||
change is documented in ADR-083, not here.
|
||||
|
||||
The `TlsServerConfig` API surface (`new`, `for_quinn`, `for_tcp_tls`,
|
||||
`rustls_config`) is one-way — changing it after consumers exist is a
|
||||
@@ -259,6 +263,9 @@ details that can change without breaking the contract.
|
||||
|
||||
- ADR-010 Amendment 1 — TCP+TLS dispatch via `from_stream` (the accept
|
||||
loop that consumes `TlsServerConfig::for_tcp_tls()`)
|
||||
- ADR-083 — endpoint as pure accept-loop runner with public dispatch
|
||||
(the endpoint takes no TLS config; the assembly layer builds
|
||||
transports from `TlsServerConfig`s)
|
||||
- ADR-027 — `TlsIdentity` (RawKey / X509 / Acme), RFC 7250, browser
|
||||
limitation
|
||||
- ADR-030 §6 — fingerprint normalization (`ed25519:<hex>` across
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
# ADR-083: Endpoint as Pure Accept-Loop Runner with Public Dispatch
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
## Context
|
||||
|
||||
`AlknetEndpoint` (ADR-010) conflates two concerns:
|
||||
|
||||
1. **Transport construction** — building `quinn::Endpoint` and
|
||||
`iroh::Endpoint`, reading `static_config.tls_identity`, constructing TLS
|
||||
internally (`TlsSetup::new`, `build_rustls_server_config`), wrapping
|
||||
rustls into `quinn::ServerConfig`, binding.
|
||||
2. **Dispatch** — take a connection, extract its ALPN, look up the
|
||||
handler, build `AuthContext`, spawn the handler.
|
||||
|
||||
The conflation was tolerable while TLS was welded to quinn (one transport,
|
||||
one cert path). Two things surfaced it:
|
||||
|
||||
- **ADR-082 (`alknet-tls` extraction)** pulls quinn's TLS construction
|
||||
out of the endpoint into a shareable `TlsServerConfig`. Once the cert
|
||||
is built outside the endpoint, the endpoint's transport-construction
|
||||
role is the odd one out — it's the only thing still reading
|
||||
`static_config` and building transports.
|
||||
- **A hub holds two `TlsServerConfig`s**, not one (raw key for native
|
||||
clients on QUIC + TCP+TLS fallback; X.509/ACME for HTTPS on TCP+TLS).
|
||||
ADR-082's original signature `AlknetEndpoint::new(..., Arc<TlsServerConfig>)`
|
||||
cannot represent this — a hub has two configs and the endpoint has no
|
||||
single "the TLS config" to take. The TLS extraction surfaces this
|
||||
because the endpoint can no longer build the one cert it used to own.
|
||||
|
||||
### The dispatch logic is the endpoint's real value
|
||||
|
||||
Stripped of transport construction, what the endpoint provides is
|
||||
**dispatch**: take a connection, extract its ALPN, look up the handler,
|
||||
build `AuthContext`, spawn. This is identical for every transport. The
|
||||
transport-specific parts are narrow bookends:
|
||||
|
||||
- **ALPN extraction** — quinn: `handshake_data().protocol`; iroh:
|
||||
`connecting.alpn().await`; TCP+TLS: `tls_stream.alpn()`.
|
||||
- **Fingerprint extraction** — quinn: `peer_identity()` →
|
||||
`CertificateDer`; iroh: `remote_id()` → `ed25519:<hex>`; TCP+TLS:
|
||||
`tls_stream.peer_certificates()` → `CertificateDer`.
|
||||
- **Connection construction** — quinn: `from_quinn_with_alpn`; iroh:
|
||||
`from_iroh`; TCP+TLS: `from_bidi(tls_stream, alpn, remote_addr)`.
|
||||
- **No-handler close** — quinn/iroh: `connection.close()`; TCP+TLS: drop
|
||||
the stream (the `Connection::close` API handles both uniformly —
|
||||
ADR-065).
|
||||
|
||||
Everything between those bookends — the ACME ALPN guard, the handler
|
||||
lookup, the `build_auth_context` call, the `tokio::spawn` — is identical.
|
||||
Exposing a public `dispatch` method that takes the already-extracted
|
||||
ALPN, fingerprint, and `Connection` lets every transport's accept loop
|
||||
call the same dispatch path. No duplicated `build_auth_context` or
|
||||
handler-lookup logic at the assembly layer.
|
||||
|
||||
### TCP+TLS is a first-class transport, not a sibling afterthought
|
||||
|
||||
ADR-010 Amendment 1 made TCP+TLS a "sibling accept loop" outside the
|
||||
endpoint because the endpoint's dispatch logic was crate-private and
|
||||
couldn't be shared. The assembly layer had to duplicate
|
||||
`build_auth_context` and handler lookup, or call `HandlerRegistry::get`
|
||||
directly. The "sibling" framing was a workaround for the endpoint being
|
||||
welded to quinn — not a deliberate design.
|
||||
|
||||
With a public `dispatch` method, the TCP+TLS accept loop calls into the
|
||||
endpoint — the same path quinn and iroh use. Amendment 1's *ownership*
|
||||
model (the TCP+TLS listener owns its own `TcpListener` and `TlsAcceptor`,
|
||||
living outside the endpoint struct) survives; its *dispatch* workaround
|
||||
(duplicated `build_auth_context` and handler-lookup logic) is retired.
|
||||
|
||||
TCP+TLS is the most common transport after raw-key QUIC, not an edge
|
||||
case: HTTPS for browsers, worker registration over HTTP, raw-key fallback
|
||||
for native clients when UDP is blocked.
|
||||
|
||||
### The `acme-tls/1` guard
|
||||
|
||||
ADR-027 §5 places the `acme-tls/1` early-return guard in
|
||||
`dispatch_quinn`. The rationale (no handler for `acme-tls/1`; the
|
||||
challenge is answered at the TLS layer; close gracefully) is correct.
|
||||
The **location** is wrong once TCP+TLS exists.
|
||||
|
||||
ACME TLS-ALPN-01 (RFC 8737) challenges are validated by the CA
|
||||
connecting **over TCP to port 443**. Let's Encrypt's validator is a TCP
|
||||
TLS client — it does not speak QUIC. In any real hub deployment, the
|
||||
ACME challenge arrives on the **TCP+TLS** listener (443), not the QUIC
|
||||
listener (4433). Today the guard fires only because quinn happens to be
|
||||
the listener that's bound to the ACME port — an artifact of TCP+TLS not
|
||||
existing yet, not a property of QUIC.
|
||||
|
||||
The guard's job is transport-agnostic: "if the TLS handshake negotiated
|
||||
`acme-tls/1`, this is a challenge connection — the cert was already
|
||||
served at the TLS layer via `ResolvesServerCertAcme`, close it, no
|
||||
handler." `Connection::close` is transport-abstracted (ADR-065), so
|
||||
the guard works uniformly for quinn, iroh, and TCP+TLS. The right home is
|
||||
the **shared `dispatch` method**, not `dispatch_quinn`. In practice only
|
||||
the TCP+TLS listener on 443 receives challenges (CAs validate via TCP);
|
||||
advertising `acme-tls/1` on a QUIC listener that shares the ACME config
|
||||
is harmless — no QUIC client negotiates it.
|
||||
|
||||
### `StaticConfig`'s role shifts
|
||||
|
||||
`StaticConfig` today holds `listen_addr`, `tls_identity`, `iroh_relay`,
|
||||
`drain_timeout`. After this ADR, the endpoint reads only `drain_timeout`
|
||||
(passed directly to `new`); the assembly layer reads the rest to build
|
||||
transports. `StaticConfig` becomes the canonical deployment config the
|
||||
**assembly layer** reads, not "the thing the endpoint takes." It stays
|
||||
in `alknet-core` (it's a config type) and stays extensible (future
|
||||
transport config fields land here as the assembly layer needs them).
|
||||
|
||||
This is a conceptual shift from ADR-010's framing ("the CLI binary
|
||||
constructs a `HandlerRegistry` and passes it, with `StaticConfig`, to
|
||||
`AlknetEndpoint::new()`"). The endpoint no longer takes `StaticConfig`
|
||||
at all. The endpoint takes `drain_timeout` and pre-built transport
|
||||
endpoints; the assembly layer is the `StaticConfig` consumer.
|
||||
|
||||
## Decision
|
||||
|
||||
### The endpoint is a pure accept-loop runner + a public dispatch method
|
||||
|
||||
```rust
|
||||
pub struct AlknetEndpoint {
|
||||
quinn: Option<quinn::Endpoint>,
|
||||
iroh: Option<iroh::Endpoint>,
|
||||
handlers: Arc<HandlerRegistry>, // ADR-010
|
||||
dynamic: Arc<ArcSwap<DynamicConfig>>, // ADR-010, unchanged
|
||||
identity_provider: Arc<dyn IdentityProvider>, // ADR-010, unchanged
|
||||
shutdown_tx: watch::Sender<bool>,
|
||||
shutdown_rx: watch::Receiver<bool>,
|
||||
drain_timeout: Duration,
|
||||
}
|
||||
|
||||
impl AlknetEndpoint {
|
||||
pub fn new(
|
||||
handlers: HandlerRegistry,
|
||||
dynamic: Arc<ArcSwap<DynamicConfig>>,
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
drain_timeout: Duration,
|
||||
) -> Self;
|
||||
|
||||
pub fn with_quinn(mut self, endpoint: quinn::Endpoint) -> Self;
|
||||
pub fn with_iroh(mut self, endpoint: iroh::Endpoint) -> Self;
|
||||
|
||||
/// Clone of the shutdown watch sender. The assembly layer uses this
|
||||
/// to signal its own accept loops (TCP+TLS) to stop accepting — one
|
||||
/// signal, all loops stop. See OQ-61 for the full coordination model.
|
||||
pub fn shutdown_sender(&self) -> watch::Sender<bool>;
|
||||
|
||||
/// Dispatch a pre-established connection by ALPN. Called by the
|
||||
/// endpoint's own accept loops (quinn, iroh) after transport-
|
||||
/// specific extraction, and by external accept loops (TCP+TLS,
|
||||
/// future SSH, future WebTransport) after their own extraction.
|
||||
///
|
||||
/// Synchronous (non-async): performs the ACME guard, handler
|
||||
/// lookup, `build_auth_context`, and `tokio::spawn`s the handler.
|
||||
/// Returns immediately after spawning — the handler runs on its own
|
||||
/// task. The caller's accept loop is not blocked by handler
|
||||
/// execution.
|
||||
///
|
||||
/// No-handler match (ALPN not in the registry): closes the
|
||||
/// connection and logs a warning, per ADR-010's existing dispatch
|
||||
/// behavior. Does not panic, does not return an error — the
|
||||
/// connection is dropped, the accept loop continues.
|
||||
pub fn dispatch(
|
||||
&self,
|
||||
connection: Connection,
|
||||
alpn: Vec<u8>,
|
||||
fingerprint: Option<String>,
|
||||
remote_addr: Option<SocketAddr>,
|
||||
);
|
||||
|
||||
/// Run the endpoint's owned accept loops. Returns when shutdown is
|
||||
/// signaled. The caller drives shutdown via `shutdown_sender()`.
|
||||
pub async fn run(self: Arc<Self>);
|
||||
}
|
||||
```
|
||||
|
||||
The consumer pattern: the assembly layer holds `Arc<AlknetEndpoint>`,
|
||||
clones the `Arc` for `run` (which consumes one clone), and passes
|
||||
`&endpoint` to its TCP+TLS accept loops so they can call `dispatch`.
|
||||
After `run` returns, the assembly layer drops its `Arc`; in-flight
|
||||
dispatched handlers (spawned by `dispatch` via `tokio::spawn`) are
|
||||
owned by the endpoint's runtime and drain per `shutdown` / OQ-61.
|
||||
|
||||
`AlknetEndpoint::new` takes **no `StaticConfig`** and **no TLS config**.
|
||||
The assembly layer (the deployment binary that composes crates — see
|
||||
ADR-014) reads `StaticConfig`, builds the transports, and hands the
|
||||
quinn/iroh endpoints to `AlknetEndpoint` via builder methods. The
|
||||
`handlers`, `dynamic`, and `identity_provider` fields are unchanged
|
||||
passthroughs from ADR-010 — this ADR does not change auth or dynamic
|
||||
config; it changes who builds transports and where dispatch lives.
|
||||
|
||||
`build_iroh_endpoint` moves out of `alknet-core/endpoint.rs`. Its
|
||||
destination is an open question (see OQ-60) — the assembly layer, an
|
||||
`alknet-tls` convenience helper, or a transport-construction
|
||||
module/crate. This ADR commits to the **boundary** (construction is
|
||||
not in the endpoint); the *where* is tracked separately so it doesn't
|
||||
block the endpoint-shape decision. (`build_quinn_server_config_from_rustls`
|
||||
is different: it's a thin wrapper that converts a `rustls::ServerConfig`
|
||||
into a `quinn::ServerConfig`, which is exactly `TlsServerConfig::for_quinn()`
|
||||
— its destination is `alknet-tls`, decided in ADR-082. Only
|
||||
`build_iroh_endpoint`, which reads `StaticConfig` and builds an
|
||||
`iroh::Endpoint` without a rustls config, is genuinely undecided.)
|
||||
|
||||
### `dispatch` is public and transport-agnostic
|
||||
|
||||
The public `dispatch` method takes the already-extracted ALPN,
|
||||
fingerprint (if available), remote address (if available), and a
|
||||
`Connection`. It performs the ACME guard, the handler lookup, the
|
||||
`build_auth_context` call, and the `tokio::spawn`. `build_auth_context`
|
||||
becomes a private helper called by `dispatch`, not a standalone
|
||||
function — the assembly layer calls `dispatch`, which calls
|
||||
`build_auth_context` internally.
|
||||
|
||||
Transport-specific extraction (`extract_quinn_alpn`,
|
||||
`extract_quinn_client_fingerprint`, `extract_iroh_client_fingerprint`)
|
||||
stays private in the endpoint — the accept loops call them, then call
|
||||
`dispatch` with the results.
|
||||
|
||||
### The `acme-tls/1` guard moves to `dispatch`
|
||||
|
||||
```rust
|
||||
pub fn dispatch(&self, connection: Connection, alpn: Vec<u8>, ...) {
|
||||
if alpn == b"acme-tls/1" {
|
||||
debug!("acme-tls/1 challenge connection completed at TLS layer; closing");
|
||||
connection.close(0, "acme done");
|
||||
return;
|
||||
}
|
||||
// ... handler lookup, build_auth_context, spawn
|
||||
}
|
||||
```
|
||||
|
||||
The guard is **not** feature-gated on `acme`: if the `acme` feature is
|
||||
off, no transport advertises `acme-tls/1`, the ALPN never negotiates, the
|
||||
guard never fires. It is harmless dead code without the feature.
|
||||
|
||||
This supersedes ADR-027 §5's "in `dispatch_quinn`" location. The
|
||||
rationale (no handler for `acme-tls/1`, silent close) holds; only the
|
||||
location changes. The `acme-tls/1` ALPN append still happens in
|
||||
`TlsServerConfig::new`'s ACME branch (ADR-082), not per-transport — every
|
||||
transport using the ACME `TlsServerConfig` advertises it, but in
|
||||
practice only the TCP+TLS listener on 443 receives challenges.
|
||||
|
||||
### `StaticConfig` stays in core; the endpoint drops it
|
||||
|
||||
`StaticConfig` remains in `alknet-core/config.rs` as the canonical
|
||||
deployment config the assembly layer reads. The endpoint no longer takes
|
||||
`&StaticConfig` — `new()` takes `drain_timeout: Duration` directly.
|
||||
Future transport config fields (addresses, relay URLs, identity) land in
|
||||
`StaticConfig` as the assembly layer needs them; the endpoint's `new`
|
||||
signature does not change when they're added, because the endpoint
|
||||
doesn't read them.
|
||||
|
||||
### What moves out of `alknet-core/endpoint.rs`
|
||||
|
||||
| Code | Destination |
|
||||
|------|-------------|
|
||||
| `build_rustls_server_config()` | `alknet-tls` (ADR-082) |
|
||||
| `TlsSetup` / ACME state machine | `alknet-tls` (ADR-082) |
|
||||
| `RawKeyCertResolver` | `alknet-tls` (ADR-082) |
|
||||
| `Ed25519SigningKey` | `alknet-tls` (ADR-082) |
|
||||
| `AcceptAnyCertVerifier` | `alknet-tls` (ADR-082) |
|
||||
| `SelfSignedCert` / `generate_self_signed_cert()` | `alknet-tls` (ADR-082) |
|
||||
| `load_cert_chain()` / `load_private_key()` | `alknet-tls` (ADR-082) |
|
||||
| `build_quinn_server_config_from_rustls()` | `alknet-tls` (`for_quinn()`, ADR-082) |
|
||||
| `build_iroh_endpoint()` | Out of core (destination: OQ-60) |
|
||||
|
||||
### What stays in `alknet-core/endpoint.rs`
|
||||
|
||||
- `AlknetEndpoint` struct (accept-loop runner + public `dispatch`)
|
||||
- `HandlerRegistry`
|
||||
- `dispatch` (public — ACME guard, handler lookup, `build_auth_context`,
|
||||
spawn)
|
||||
- `dispatch_quinn` / `dispatch_iroh` (private — transport-specific
|
||||
extraction, then call `dispatch`)
|
||||
- `run_quinn_accept_loop` / `run_iroh_accept_loop`
|
||||
- `extract_quinn_alpn` / `extract_quinn_client_fingerprint` /
|
||||
`extract_iroh_client_fingerprint`
|
||||
- `build_auth_context` (private helper, called by `dispatch`)
|
||||
|
||||
### ADR-082 amendment
|
||||
|
||||
ADR-082's `AlknetEndpoint::new(..., Arc<TlsServerConfig>, ...)` signature
|
||||
is superseded by this ADR. The endpoint takes no TLS config; the
|
||||
assembly layer builds transports from `TlsServerConfig`s and hands them
|
||||
to the endpoint via `with_quinn` / `with_iroh`. ADR-082 should reference
|
||||
this ADR for the endpoint signature and focus on what `alknet-tls`
|
||||
provides (`TlsServerConfig` and its accessors).
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- The endpoint has one job: dispatch. Transport construction lives
|
||||
outside it, where the multi-`TlsServerConfig` hub case is natural.
|
||||
- TCP+TLS dispatch is first-class — same `dispatch` path as quinn/iroh,
|
||||
no duplicated `build_auth_context` or handler-lookup logic at the
|
||||
assembly layer. ADR-010 Amendment 1's second-class-dispatch workaround
|
||||
is retired.
|
||||
- The `acme-tls/1` guard is transport-agnostic — it works for TCP+TLS
|
||||
(where challenges actually arrive) and any future transport. ADR-027
|
||||
§5's quinn-specific location is corrected.
|
||||
- `StaticConfig`'s role is clear: it's the assembly-layer config, not
|
||||
the endpoint's. Adding transport config fields doesn't churn the
|
||||
endpoint's `new` signature.
|
||||
- The hub (the first multi-transport consumer) is unblocked: build two
|
||||
`TlsServerConfig`s, build quinn/iroh/TCP+TLS listeners, hand the
|
||||
endpoint the quinn/iroh ones, spawn the TCP+TLS loops calling
|
||||
`endpoint.dispatch`.
|
||||
|
||||
**Negative:**
|
||||
- `AlknetEndpoint::new` signature changes (breaking). Pre-1.0, in-repo
|
||||
consumers only — the assembly layer and tests must update. Expected;
|
||||
this is the point of the refactor.
|
||||
- `build_iroh_endpoint` leaves core. Its destination is an open question
|
||||
(OQ-60), not decided here. The boundary (not in the endpoint) is
|
||||
committed; the *where* is not, so that the endpoint-shape decision
|
||||
isn't blocked on the transport-construction-location decision.
|
||||
(`build_quinn_server_config_from_rustls` is decided — it moves to
|
||||
`alknet-tls` as `for_quinn()` per ADR-082; only `build_iroh_endpoint`
|
||||
is open.)
|
||||
- Multi-owner shutdown: the endpoint owns shutdown of *dispatched
|
||||
handlers* (it spawned them in `dispatch`); the assembly layer owns
|
||||
shutdown of the *accept loops it spawned* (TCP+TLS listeners). The
|
||||
coordination mechanism (shared `shutdown_sender`, drain semantics) is
|
||||
a follow-up design point, not resolved by this ADR — see OQ-61.
|
||||
|
||||
## Door type
|
||||
|
||||
**One-way.** The endpoint's `new` signature, the public `dispatch`
|
||||
contract, and the "endpoint owns dispatch, not construction" boundary
|
||||
are structural. Reversing would mean re-welding transport construction
|
||||
to the endpoint and re-privatizing `dispatch` — breaking every
|
||||
multi-transport consumer (hub, future HTTP, future SSH).
|
||||
|
||||
The `dispatch` signature (`connection, alpn, fingerprint, remote_addr`)
|
||||
is one-way — changing it after consumers exist is a rewrite. The
|
||||
internal implementation (how extraction is factored, how `run` spawns
|
||||
tasks) is two-way.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-010: ALPN router and endpoint (amended — the endpoint no longer
|
||||
constructs transports; Amendment 1's second-class-dispatch workaround
|
||||
is retired by the public `dispatch` method)
|
||||
- ADR-014: Secret material flow and capability injection (defines the
|
||||
"assembly layer" term — the deployment binary that composes crates)
|
||||
- ADR-010 Amendment 1: TCP+TLS as sibling (superseded — Amendment 1's
|
||||
*ownership* model (sibling listener outside the endpoint) survives;
|
||||
its *dispatch* workaround (duplicated logic) is retired by this ADR's
|
||||
public `dispatch`)
|
||||
- ADR-027 §5: ACME ALPN challenge handling (location amended — guard
|
||||
moves from `dispatch_quinn` to shared `dispatch`)
|
||||
- ADR-065: `Connection::from_stream`/`from_bidi` (the primitive that
|
||||
makes TCP+TLS dispatch possible; `Connection::close` is
|
||||
transport-abstracted, so the `acme-tls/1` guard works uniformly)
|
||||
- ADR-082: `alknet-tls` extraction (amended — endpoint takes no
|
||||
`Arc<TlsServerConfig>`; the assembly layer builds transports from
|
||||
`TlsServerConfig`s)
|
||||
- ADR-080: `ChannelClient::from_connection` (the transport-agnostic
|
||||
client pattern this ADR mirrors on the server side)
|
||||
- `docs/research/alknet-endpoint-refactor/findings.md` — the analysis
|
||||
that surfaced the conflation and the two-config hub case
|
||||
- `crates/alknet-core/src/endpoint.rs` — the code being refactored
|
||||
- OQ-60: Where does transport construction live? (assembly layer,
|
||||
`alknet-tls` helper, or transport module/crate)
|
||||
- OQ-61: Multi-owner shutdown coordination (endpoint owns dispatched
|
||||
handlers; assembly layer owns spawned accept loops)
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-12
|
||||
last_updated: 2026-07-14
|
||||
---
|
||||
|
||||
# Open Questions
|
||||
@@ -77,6 +77,8 @@ Door type is separate from whether a decision is made. A two-way door is a decis
|
||||
| [OQ-14](questions/014-batch-operation-semantics.md) | Batch Operation Semantics | resolved | two | low |
|
||||
| [OQ-55](questions/055-alknetclient-establishment-extraction.md) | AlknetClient / Client Establishment Extraction | deferred(scope) | two | med |
|
||||
| [OQ-59](questions/059-fingerprint-module-location.md) | Should `fingerprint.rs` Stay in Core or Move to `alknet-tls`? | open | two | med |
|
||||
| [OQ-60](questions/060-transport-construction-location.md) | Where Does Transport Construction Live? | open | one | high |
|
||||
| [OQ-61](questions/061-multi-owner-shutdown-coordination.md) | Multi-Owner Shutdown Coordination | open | two | med |
|
||||
|
||||
### alknet-call
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# OQ-60: Where Does Transport Construction Live?
|
||||
|
||||
- **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)
|
||||
- **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:
|
||||
|
||||
**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.
|
||||
|
||||
**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).
|
||||
|
||||
**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.
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,42 @@
|
||||
# OQ-61: Multi-Owner Shutdown Coordination
|
||||
|
||||
- **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
|
||||
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:
|
||||
|
||||
- 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)
|
||||
Reference in new issue
Block a user