- endpoint/crate-init: initialize crate, Cargo.toml, module skeleton - endpoint/registry: extract HandlerRegistry (~50 lines) - endpoint/endpoint-core: AlknetEndpoint fresh build against ADR-083 shape - endpoint/dispatch: public dispatch, build_auth_context, ACME guard - endpoint/accept-quinn: quinn accept loop + extractors (extracted) - endpoint/accept-iroh: iroh accept loop + extractors (extracted) - endpoint/accept-tcp-tls: TCP+TLS accept loop (new code) - endpoint/tests: move + adapt 17 tests - endpoint/review-endpoint: review checkpoint 7 generations, 3 parallel tasks (accept loops), no cycles. Depends on tls/review-tls (Phase 1 complete).
7.9 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| endpoint/endpoint-core | Implement AlknetEndpoint struct, new, builder methods, run, and shutdown | pending |
|
moderate | medium | component | implementation |
Description
Phase 2, Task 3 of the crate extraction. Implement the AlknetEndpoint struct and its
core methods (new, with_quinn, with_iroh, with_tcp_tls, run, shutdown,
shutdown_sender) in crates/alknet-endpoint/src/endpoint.rs.
This is a fresh build against the ADR-083 shape, not a direct copy of the old
endpoint.rs. The old AlknetEndpoint::new() took a StaticConfig and built transports
internally. The new new() takes no StaticConfig and no TLS config — the assembly layer
builds transports and hands them to the endpoint via builder methods.
Target shape (per ADR-083 / architecture spec)
pub struct AlknetEndpoint {
#[cfg(feature = "quinn")]
quinn: Option<quinn::Endpoint>,
#[cfg(feature = "iroh")]
iroh: Option<iroh::Endpoint>,
#[cfg(feature = "tcp")]
tcp_tls: Option<TcpTlsListener>, // (TcpListener, TlsAcceptor)
handlers: Arc<HandlerRegistry>,
dynamic: Arc<ArcSwap<DynamicConfig>>,
identity_provider: Arc<dyn IdentityProvider>,
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;
#[cfg(feature = "quinn")]
pub fn with_quinn(mut self, endpoint: quinn::Endpoint) -> Self;
#[cfg(feature = "iroh")]
pub fn with_iroh(mut self, endpoint: iroh::Endpoint) -> Self;
#[cfg(feature = "tcp")]
pub fn with_tcp_tls(
mut self,
listener: tokio::net::TcpListener,
acceptor: tokio_rustls::TlsAcceptor,
) -> Self;
pub fn shutdown_sender(&self) -> watch::Sender<bool>;
pub async fn run(self: Arc<Self>);
/// Infallible — signals all owned accept loops to stop, waits for
/// in-flight handlers with drain_timeout, then forcefully closes.
pub async fn shutdown(&self);
}
Key differences from the old endpoint.rs
-
new()takes noStaticConfig: The oldnew()readlisten_addr,tls_identity,iroh_relayfromStaticConfigand built transports internally. The newnew()takes onlyHandlerRegistry,DynamicConfig,IdentityProvider, anddrain_timeout— no transport construction. The assembly layer readsStaticConfigand builds transports. -
Builder methods instead of internal construction:
with_quinn(endpoint),with_iroh(endpoint),with_tcp_tls(listener, acceptor)replace the internalTlsSetup::new()+build_quinn_server_config_from_rustls()+build_iroh_endpoint()chain. The endpoint receives pre-built, pre-bound transports. -
TcpTlsListenertype: A new type alias for the TCP+TLS transport pair:#[cfg(feature = "tcp")] pub(crate) type TcpTlsListener = (tokio::net::TcpListener, tokio_rustls::TlsAcceptor); -
shutdown()is infallible: Returns()notResult<(), EndpointError>. The oldshutdown()returnedResultbut could never actually fail (the?was oniroh.close().awaitwhich is infallible). The newshutdown()isasync fn shutdown(&self)with noResult. -
No
EndpointError: The error type is removed entirely.BindFailedis vestigial (the endpoint doesn't bind).HandlerNotFoundis swallowed bydispatch(close + log).TlsConfigwas already removed by ADR-083. -
No
acme_state_handlefield: ACME state lives onTlsServerConfiginalknet-tlsnow. The endpoint doesn't see it. -
run()spawns accept loops for each active transport: Quinn, iroh, and TCP+TLS each get their owntokio::spawn'd accept loop. The oldrun()only handled quinn and iroh; the new one adds TCP+TLS.
run() implementation
pub async fn run(self: Arc<Self>) {
let mut tasks: Vec<tokio::task::JoinHandle<()>> = Vec::new();
#[cfg(feature = "quinn")]
if let Some(quinn) = &self.quinn {
let quinn = quinn.clone();
let handlers = self.handlers.clone();
let identity_provider = self.identity_provider.clone();
let mut shutdown_rx = self.shutdown_rx.clone();
tasks.push(tokio::spawn(async move {
crate::accept::quinn::run_accept_loop(quinn, handlers, identity_provider, &mut shutdown_rx).await;
}));
}
#[cfg(feature = "iroh")]
if let Some(iroh) = &self.iroh {
// ... same pattern for iroh
}
#[cfg(feature = "tcp")]
if let Some((listener, acceptor)) = self.tcp_tls.take() {
// ... same pattern for TCP+TLS
}
for task in tasks {
let _ = task.await;
}
}
shutdown() implementation
pub async fn shutdown(&self) {
let _ = self.shutdown_tx.send(true);
#[cfg(feature = "quinn")]
if let Some(quinn) = &self.quinn {
quinn.close(0u32.into(), b"shutdown");
}
#[cfg(feature = "iroh")]
if let Some(iroh) = &self.iroh {
iroh.close().await;
}
#[cfg(feature = "tcp")]
// TCP+TLS: the accept loop watches shutdown_rx; no explicit close needed
// (the listener is dropped when the endpoint is dropped)
tokio::time::sleep(self.drain_timeout).await;
#[cfg(feature = "quinn")]
if let Some(quinn) = &self.quinn {
quinn.wait_idle().await;
}
}
What stays in core
The old AlknetEndpoint in endpoint.rs lines 118-277 is not deleted — it stays as a
duplicate. The prune happens in Phase 4. This task only adds code to alknet-endpoint.
Acceptance Criteria
AlknetEndpointstruct defined with all fields (quinn, iroh, tcp_tls, handlers, dynamic, identity_provider, shutdown_tx/rx, drain_timeout)AlknetEndpoint::new()takesHandlerRegistry,Arc<ArcSwap<DynamicConfig>>,Arc<dyn IdentityProvider>,Duration— noStaticConfig, no TLS configwith_quinn(endpoint)builder method (feature-gated onquinn)with_iroh(endpoint)builder method (feature-gated oniroh)with_tcp_tls(listener, acceptor)builder method (feature-gated ontcp)TcpTlsListenertype alias defined (feature-gated ontcp)shutdown_sender()returns a clone of the shutdown watch senderrun()spawns accept loops for each active transportshutdown()is infallible (async fn shutdown(&self), noResult)Debugimpl forAlknetEndpoint(lists handlers, drain_timeout; no transport internals)- No
EndpointErrortype (removed) - No
acme_state_handlefield (ACME lives inalknet-tls) - No
has_iroh_identityfunction (transport-building decision moved to assembly layer) - Feature gates correct:
quinn,iroh,tcpeach gate their respective fields/methods cargo check -p alknet-endpointsucceeds (all feature combos)cargo clippy -p alknet-endpointsucceeds with no warningscargo test -p alknet-corestill passes (old code untouched)
References
- docs/research/alknet-crate-extraction/findings.md — Phase 2, endpoint module
- docs/architecture/crates/endpoint/README.md — AlknetEndpoint spec (lines 48-103)
- docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md — ADR-083
- crates/alknet-core/src/endpoint.rs — lines 118-277 (old code, reference only)
Notes
This is the core structural task of Phase 2. The
AlknetEndpointis built fresh against the ADR-083 shape — it's not a copy-paste of the old code. The key difference: the oldnew()built transports internally fromStaticConfig; the newnew()takes no transport config and receives pre-built transports via builder methods.EndpointErroris removed entirely.shutdown()is infallible. The old code inendpoint.rsis NOT deleted — that's Phase 4.
Summary
To be filled on completion