pub trait BrokerCore {
type Frame;
// Required methods
fn on_frame(
&mut self,
frame: Self::Frame,
now: Instant,
out: &mut EventSink,
);
fn poll_transmit(&mut self) -> Option<Outbound>;
fn poll_timeout(&self) -> Option<Instant>;
fn on_timeout(&mut self, now: Instant, out: &mut EventSink);
fn apply(&mut self, desired: &SubscriptionSet);
}Expand description
Protocol logic of one broker stream, written without sockets, timers or an async runtime.
This is the “sans-io” pattern. A core is a plain state machine. The code that owns the socket
feeds it inbound frames and the current time, and collects what it wants to say: events for the
consumer (EventSink), messages to put on the wire (poll_transmit)
and the next time it wants to be woken (poll_timeout). Because time is
an argument and there is no I/O, a core can be tested with recorded frames and virtual time, see
Harness.
How Harness drives a core, and what a real driver should do the same way:
- call
on_framefor each inbound frame, with the time it arrived; - after every call into the core, drain
poll_transmituntil it returnsNoneand send the messages; - sleep until
poll_timeout, then callon_timeout; - call
applywhen the set of wanted subscriptions changes.
A core must never block, sleep or read the clock itself. Use the now it is given.
§Examples
A core that answers every frame and reports a resync after 10 seconds of silence, driven with virtual time:
use std::time::{Duration, Instant};
use brokapi_core::{Event, InstrumentId, InstrumentKey, NativeId, ResyncReason, SubscriptionId};
use brokapi_sansio::{BrokerCore, EventSink, Harness, Outbound, SubscriptionSet};
/// Answers every frame with a pong. If the line is silent for 10 seconds it reports a resync.
struct WatchdogCore {
silent_since: Instant,
armed: bool,
pongs: Vec<String>,
}
impl BrokerCore for WatchdogCore {
type Frame = String;
fn on_frame(&mut self, frame: String, now: Instant, _out: &mut EventSink) {
self.silent_since = now;
self.armed = true;
self.pongs.push(format!("pong {frame}"));
}
fn poll_transmit(&mut self) -> Option<Outbound> {
if self.pongs.is_empty() { None } else { Some(Outbound::WsText(self.pongs.remove(0))) }
}
fn poll_timeout(&self) -> Option<Instant> {
self.armed.then(|| self.silent_since + Duration::from_secs(10))
}
fn on_timeout(&mut self, _now: Instant, out: &mut EventSink) {
// Disarm first: a core that keeps the same deadline would be called again for ever.
self.armed = false;
let instrument = InstrumentId::new(
InstrumentKey::new("SBER", "TQBR"),
NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
);
out.push_payload(
Some(SubscriptionId::new(1)),
Event::Resync { sub: SubscriptionId::new(1), instrument, reason: ResyncReason::SubscriptionReset },
);
}
fn apply(&mut self, _desired: &SubscriptionSet) {}
}
let t0 = Instant::now();
let core = WatchdogCore { silent_since: t0, armed: false, pongs: Vec::new() };
let mut harness = Harness::new(core, t0);
harness.feed_frame("ping".to_string());
assert_eq!(harness.transmitted(), [Outbound::WsText("pong ping".to_string())]);
harness.advance_by(Duration::from_secs(9));
assert!(harness.events().is_empty());
harness.advance_by(Duration::from_secs(2));
assert_eq!(harness.events().len(), 1);Required Associated Types§
Required Methods§
Sourcefn on_frame(&mut self, frame: Self::Frame, now: Instant, out: &mut EventSink)
fn on_frame(&mut self, frame: Self::Frame, now: Instant, out: &mut EventSink)
Handles one inbound transport frame received at now.
Events for the consumer are pushed into out. Messages for the broker are queued inside the
core and picked up by poll_transmit. The method does not return an
error: a frame the core cannot use is its own business (skip it, or emit an event).
Sourcefn poll_transmit(&mut self) -> Option<Outbound>
fn poll_transmit(&mut self) -> Option<Outbound>
Takes the next outbound wire message, if any. Call it until it returns None.
Sourcefn poll_timeout(&self) -> Option<Instant>
fn poll_timeout(&self) -> Option<Instant>
When the core wants on_timeout to be called next, or None if it has
no timer pending.
After on_timeout the core must move this deadline forward or clear it. A core that
returns the same past deadline again makes a driver loop for ever.
Sourcefn on_timeout(&mut self, now: Instant, out: &mut EventSink)
fn on_timeout(&mut self, now: Instant, out: &mut EventSink)
Handles a timer that fired. now is the time the timer was due.
Sourcefn apply(&mut self, desired: &SubscriptionSet)
fn apply(&mut self, desired: &SubscriptionSet)
Tells the core the full set of subscriptions the consumer wants now.
The argument is the whole target state, not a change. The core works out what to subscribe
and unsubscribe and queues the messages for poll_transmit.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".