Write an out-of-tree transport
A transport, frame transport, or discovery backend in another crate depends on velo-ext only. It does not depend on velo. velo-ext has no Prometheus, no Tonic, no NATS, and no dependency on velo.
[dependencies]
velo-ext = "0.5"
Pick the trait
| Trait | What you provide |
|---|---|
velo_ext::Transport | A messenger transport |
velo_ext::FrameTransport | A frame transport for streams |
velo_ext::PeerDiscovery | A peer discovery backend |
velo_ext::ServiceDiscovery | A service discovery backend |
velo_ext::TransportObservability | Rarely needed. The runtime gives you one of these. |
Procedure for a messenger transport
- Implement
Transportfor your type. - Send each inbound
MessageType::Messageframe throughTransportAdapter::admit_message. Do not checkShutdownState::is_draining()first. See Shutdown and drain. - If
admit_messagereturnsAdmitOutcome::Drainingand your transport has a return path, send aMessageType::ShuttingDownframe with the header of the rejected request. If there is no return path, record the rejection and drop the frame. - If
admit_messagereturnsAdmitOutcome::Disconnected, the runtime has stopped. RecordTransportRejection::RouteFailedthrough the observability handle and drop the frame. - Route the other inbound types to their streams on the adapter:
Responseto the response stream,AckandEventto the event stream, andShuttingDownto the shutdown stream. - Store the handle that
set_observabilitygives you. Record each inbound frame as you route it. - On the
Admittedarm, and only there, record the frame as inboundmessage. - In
shutdown, stop your tasks and threads. The runtime calls it at most once, with its Tokio runtime handle entered, on a dedicated thread, so you may block there, for example to join a thread. Two cases run it on the caller instead. Any failure while the transports start (another transport fails to start, a duplicate key, an address that cannot be merged) or a build that is cancelled then runs it on the building task. A thread that cannot be created runs it on whatever dropped or shut down the instance. After the final owner is dropped, the runtime may already be gone, so a task thatshutdownspawns may never run. Ifshutdownpanics, the runtime still calls the other transports' hooks, and then the shutdown call panics. - If your transport keeps state for each peer, override
unregisterto drop it and close the peer's connections. It must be cheap and idempotent. See Peer routing.
Step 7 matters because the inbound queue depth is a difference between two counters. If your transport admits a frame and does not record it, the depth goes negative. See Observability.
Step 6 matters because the runtime creates a series for each direction and message type in advance. If only message moves, the dashboard reads "no responses arrived", not "responses are not instrumented".
use std::sync::{Arc, OnceLock};
use velo_ext::{Transport, TransportObservability};
struct MyTransport {
obs: OnceLock<Arc<dyn TransportObservability>>,
/* ... */
}
impl Transport for MyTransport {
// ... the required methods ...
fn set_observability(&self, obs: Arc<dyn TransportObservability>) {
let _ = self.obs.set(obs);
}
}
Stability of velo-ext
New trait methods in velo-ext always have a default implementation, so a new release does not break your implementation. A change to an existing signature is a breaking release. See Versioning.
A transport with one ordered channel to each peer keeps the defaults of lanes and send_message_on_lane. Override both only if your transport keeps several ordered channels to one peer. The two methods need velo-ext = "0.5.4" or later. unregister needs velo-ext = "0.5.6" or later.