1pub use crate::subscriber::shared::REATTACH_GRACE;
55
56use super::MassiveCredentials;
57use crate::{
58 exchange::ExchangeSub,
59 subscriber::{
60 shared::{
61 self, AttachId, Attachment, Connections, Frame, Handshake, Protocol, Registry,
62 elements::{self, Element, Elements, excerpt},
63 },
64 shared_stream::{SharedTransport, sealed},
65 },
66};
67use futures::{SinkExt, Stream, StreamExt};
68use rustrade_instrument::exchange::ExchangeId;
69use rustrade_integration::{
70 error::SocketError,
71 protocol::websocket::{WebSocket, WsError, WsMessage, connect},
72};
73use serde::{Deserialize, Deserializer};
74use serde_json::json;
75use smol_str::SmolStr;
76use std::{
77 fmt,
78 pin::Pin,
79 task::{Context, Poll},
80 time::Duration,
81};
82use tracing::{debug, warn};
83use url::Url;
84
85pub const KEEPALIVE: Duration = Duration::from_secs(20);
87
88const AUTH_TIMEOUT: Duration = Duration::from_secs(10);
90
91const SUBSCRIBED: &str = "subscribed to: ";
93
94const UNSUBSCRIBED: &str = "unsubscribed to: ";
96
97#[derive(Clone, PartialEq, Eq, Hash, Debug)]
99pub(super) struct Slot {
100 pub(super) event: SmolStr,
102 pub(super) market: SmolStr,
103}
104
105impl Slot {
106 fn parse(param: &str) -> Option<Self> {
109 let (event, market) = param.split_once('.')?;
110 (!event.is_empty() && !market.is_empty()).then(|| Self {
111 event: SmolStr::new(event),
112 market: SmolStr::new(market),
113 })
114 }
115}
116
117impl<Channel, Market> From<&ExchangeSub<Channel, Market>> for Slot
119where
120 Channel: AsRef<str>,
121 Market: AsRef<str>,
122{
123 fn from(sub: &ExchangeSub<Channel, Market>) -> Self {
124 Self {
125 event: SmolStr::new(sub.channel.as_ref()),
126 market: SmolStr::new(sub.market.as_ref()),
127 }
128 }
129}
130
131impl fmt::Display for Slot {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(f, "{}.{}", self.event, self.market)
134 }
135}
136
137pub(super) fn action(action: &str, slots: &[Slot]) -> WsMessage {
139 let params = slots
140 .iter()
141 .map(Slot::to_string)
142 .collect::<Vec<_>>()
143 .join(",");
144
145 WsMessage::text(json!({ "action": action, "params": params }).to_string())
146}
147
148#[derive(Debug)]
150pub(super) struct Massive;
151
152#[derive(Debug)]
155pub(super) struct MassiveConnections(Connections<Massive>);
156
157impl MassiveConnections {
158 pub(super) fn new(credentials: MassiveCredentials) -> Self {
159 Self(Connections::new(credentials))
160 }
161
162 pub(super) async fn attach(
165 &self,
166 request: AttachRequest,
167 ) -> Result<MassiveAttachment, SocketError> {
168 let AttachRequest {
169 exchange,
170 url,
171 kind,
172 slots,
173 timeout,
174 } = request;
175
176 self.0
177 .attach(shared::AttachRequest {
178 exchange,
179 url,
180 kind,
181 slots,
182 timeout,
183 batch: (),
184 })
185 .await
186 .map(MassiveAttachment)
187 }
188}
189
190#[derive(Debug)]
192pub(super) struct AttachRequest {
193 pub(super) exchange: ExchangeId,
194 pub(super) url: Url,
195 pub(super) kind: &'static str,
197 pub(super) slots: Vec<Slot>,
199 pub(super) timeout: Duration,
200}
201
202#[derive(Debug)]
204pub(super) struct Confirmations {
205 outstanding: Vec<Slot>,
207}
208
209impl Confirmations {
210 fn outstanding(&self) -> String {
211 self.outstanding
212 .iter()
213 .map(Slot::to_string)
214 .collect::<Vec<_>>()
215 .join(", ")
216 }
217}
218
219impl Handshake<Slot> for Confirmations {
220 fn is_settled(&self) -> bool {
221 self.outstanding.is_empty()
222 }
223
224 fn observe(&mut self, confirmed: Slot) {
225 let before = self.outstanding.len();
226 self.outstanding.retain(|slot| *slot != confirmed);
227
228 if self.outstanding.len() == before {
229 debug!(%confirmed, "Massive confirmed a subscription this subscribe did not send");
230 }
231 }
232
233 fn timed_out(&self, timeout: Duration) -> String {
234 format!(
235 "subscription validation timeout reached: {timeout:?}; Massive never confirmed: {}. \
236 Massive does not answer a subscription to a channel it does not publish on the \
237 cluster, so one that waits for ever is usually a channel the cluster does not offer",
238 self.outstanding()
239 )
240 }
241
242 fn refusal(&self, error: SocketError) -> SocketError {
245 match error {
246 SocketError::Subscribe(message) if !self.outstanding.is_empty() => {
247 SocketError::Subscribe(format!(
248 "{message}; Massive refused a subscription among those it did not confirm: {}",
249 self.outstanding()
250 ))
251 }
252 other => other,
253 }
254 }
255}
256
257impl Protocol for Massive {
258 const NAME: &'static str = "Massive";
259 const CONNECTION_PER_ENDPOINT: bool = true;
261 const REFUSAL_IS_ATOMIC: bool = false;
263 const KEEPALIVE: Option<Duration> = Some(KEEPALIVE);
264
265 type Credentials = MassiveCredentials;
266 type Slot = Slot;
267 type Batch = ();
268 type Session = ();
269 type Answer = Slot;
270 type Handshake = Confirmations;
271
272 async fn connect(
273 credentials: &MassiveCredentials,
274 url: &Url,
275 ) -> Result<(WebSocket, ()), SocketError> {
276 let mut websocket = connect(url.clone()).await?;
277 authenticate(&mut websocket, credentials).await?;
278 debug!(%url, "authenticated to Massive WebSocket");
279
280 Ok((websocket, ()))
281 }
282
283 fn subscribe(
284 _registry: &mut Registry<Self>,
285 _id: AttachId,
286 sending: &[Slot],
287 ) -> (Vec<WsMessage>, Confirmations) {
288 let messages = if sending.is_empty() {
289 Vec::new()
290 } else {
291 vec![action("subscribe", sending)]
292 };
293
294 (
295 messages,
296 Confirmations {
297 outstanding: sending.to_vec(),
298 },
299 )
300 }
301
302 fn release(slots: &[Slot]) -> (Vec<WsMessage>, Option<Confirmations>) {
305 (vec![action("unsubscribe", slots)], None)
306 }
307
308 fn read(registry: &mut Registry<Self>, message: &WsMessage) -> Frame<Slot> {
309 let raws = match elements::elements::<Self>(message) {
310 Elements::Array(raws) => raws,
311 Elements::Closed(cause) => return Frame::Closed(cause),
312 Elements::Ignored => return Frame::Answers(Vec::new()),
313 };
314
315 let total = raws.len();
316 let mut data = Vec::with_capacity(total);
317 let mut answers = Vec::new();
318
319 for raw in raws {
320 let envelope = match serde_json::from_str::<Envelope>(raw.get()) {
321 Ok(envelope) => envelope,
322 Err(error) => {
323 warn!(
324 %error,
325 message = %excerpt(raw.get()),
326 "Massive sent a message that could not be read; ignored",
327 );
328 continue;
329 }
330 };
331
332 if envelope.event == "status" {
333 match status(envelope.status.as_deref(), envelope.message.as_deref()) {
334 Status::Answer(answer) => answers.push(answer),
335 Status::Evicted(cause) => return Frame::Closed(cause),
336 Status::Other => {}
337 }
338 continue;
339 }
340
341 let Some(market) = envelope.market() else {
342 warn!(
343 message = %excerpt(raw.get()),
344 "Massive sent a market data message naming no market; ignored",
345 );
346 continue;
347 };
348
349 data.push(Element {
350 slot: Slot {
351 event: envelope.event,
352 market,
353 },
354 raw,
355 });
356 }
357
358 elements::route(registry, message, total, &data);
359 Frame::Answers(answers)
360 }
361
362 fn refused(error: SocketError) -> SocketError {
364 match error {
365 SocketError::Subscribe(message) => SocketError::Subscribe(format!(
366 "{message} (the connection is shared by every stream this subscriber and its \
367 clones open on the cluster)"
368 )),
369 other => other,
370 }
371 }
372}
373
374#[derive(Debug)]
391pub struct MassiveAttachment(Attachment<Massive>);
392
393impl Stream for MassiveAttachment {
394 type Item = Result<WsMessage, WsError>;
395
396 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
397 Pin::new(&mut self.0).poll_next(cx)
398 }
399}
400
401impl sealed::Sealed for MassiveAttachment {}
402
403impl SharedTransport for MassiveAttachment {
405 type Attached = ();
406
407 fn take_attached(&mut self) {
408 self.0.take_attached()
409 }
410}
411
412#[derive(Deserialize)]
417struct Envelope {
418 #[serde(rename = "ev")]
419 event: SmolStr,
420 #[serde(default)]
422 sym: Option<SmolStr>,
423 #[serde(default)]
425 pair: Option<SmolStr>,
426 #[serde(default, deserialize_with = "text_only")]
428 p: Option<SmolStr>,
429 #[serde(default)]
431 status: Option<String>,
432 #[serde(default)]
434 message: Option<String>,
435}
436
437impl Envelope {
438 fn market(&self) -> Option<SmolStr> {
439 self.sym
440 .as_ref()
441 .or(self.pair.as_ref())
442 .or(self.p.as_ref())
443 .cloned()
444 }
445}
446
447fn text_only<'de, D>(deserializer: D) -> Result<Option<SmolStr>, D::Error>
449where
450 D: Deserializer<'de>,
451{
452 #[derive(Deserialize)]
453 #[serde(untagged)]
454 enum TextOrOther {
455 Text(SmolStr),
456 Other(serde::de::IgnoredAny),
457 }
458
459 Ok(match Option::<TextOrOther>::deserialize(deserializer)? {
460 Some(TextOrOther::Text(text)) => Some(text),
461 Some(TextOrOther::Other(_)) | None => None,
462 })
463}
464
465enum Status {
467 Answer(Result<Slot, SocketError>),
469 Evicted(String),
471 Other,
473}
474
475fn status(status: Option<&str>, message: Option<&str>) -> Status {
476 let message = message.unwrap_or_default();
477
478 match status {
479 Some("success") => {
480 if let Some(param) = message.strip_prefix(SUBSCRIBED) {
481 match Slot::parse(param) {
482 Some(slot) => Status::Answer(Ok(slot)),
483 None => {
484 warn!(message, "Massive confirmed a subscription it did not name");
485 Status::Other
486 }
487 }
488 } else if message.starts_with(UNSUBSCRIBED) {
489 debug!(message, "Massive confirmed an unsubscribe");
490 Status::Other
491 } else {
492 warn!(
493 message,
494 "Massive reported a success this client does not recognise"
495 );
496 Status::Other
497 }
498 }
499 Some("error") => Status::Answer(Err(SocketError::Subscribe(format!(
500 "Massive refused a subscription: {message}"
501 )))),
502 Some("max_connections") => Status::Evicted(format!(
503 "Massive closed the connection: max_connections: {message} (another connection opened \
504 with this key on this cluster, by a subscriber built separately or another process, \
505 took its place)"
506 )),
507 Some("connected" | "auth_success") => {
508 debug!(message, "Massive status");
509 Status::Other
510 }
511 other => {
512 warn!(
513 status = other.unwrap_or("<none>"),
514 message, "Massive reported a status this client does not recognise"
515 );
516 Status::Other
517 }
518 }
519}
520
521async fn authenticate(
523 websocket: &mut WebSocket,
524 credentials: &MassiveCredentials,
525) -> Result<(), SocketError> {
526 let connected = next_status(websocket, "connected").await?;
527 if connected.0.as_deref() != Some("connected") {
528 debug!(status = ?connected, "Massive's first frame was not the expected connected status");
530 }
531
532 websocket
533 .send(WsMessage::text(
534 json!({ "action": "auth", "params": credentials.api_key() }).to_string(),
535 ))
536 .await
537 .map_err(|error| SocketError::WebSocket(Box::new(error)))?;
538
539 match next_status(websocket, "auth_success").await? {
540 (Some(status), _) if status == "auth_success" => Ok(()),
541 (status, message) => Err(SocketError::Subscribe(format!(
542 "Massive refused authentication: {}: {}",
543 status.as_deref().unwrap_or("<no status>"),
544 message.as_deref().unwrap_or_default(),
545 ))),
546 }
547}
548
549async fn next_status(
553 websocket: &mut WebSocket,
554 awaiting: &str,
555) -> Result<(Option<String>, Option<String>), SocketError> {
556 let deadline = tokio::time::Instant::now() + AUTH_TIMEOUT;
557 let text = loop {
558 let frame = tokio::time::timeout_at(deadline, websocket.next())
559 .await
560 .map_err(|_| {
561 SocketError::Subscribe(format!(
562 "Massive sent nothing within {AUTH_TIMEOUT:?} while awaiting {awaiting}"
563 ))
564 })?;
565
566 match frame {
567 Some(Ok(WsMessage::Text(text))) => break text,
568 Some(Ok(WsMessage::Ping(_) | WsMessage::Pong(_))) => continue,
569 Some(Ok(WsMessage::Close(close))) => {
570 return Err(SocketError::Subscribe(format!(
571 "Massive closed the connection while awaiting {awaiting}: {close:?}"
572 )));
573 }
574 Some(Ok(other)) => {
575 return Err(SocketError::Subscribe(format!(
576 "Massive sent {other:?} while awaiting {awaiting}"
577 )));
578 }
579 Some(Err(error)) => return Err(SocketError::WebSocket(Box::new(error))),
580 None => {
581 return Err(SocketError::Subscribe(format!(
582 "Massive ended the connection while awaiting {awaiting}"
583 )));
584 }
585 }
586 };
587
588 let statuses = serde_json::from_str::<Vec<Envelope>>(text.as_str()).map_err(|error| {
589 SocketError::Deserialise {
590 error,
591 payload: text.as_str().to_owned(),
592 }
593 })?;
594
595 Ok(statuses
596 .into_iter()
597 .find(|envelope| envelope.event == "status")
598 .map(|envelope| (envelope.status, envelope.message))
599 .unwrap_or_default())
600}
601
602#[cfg(test)]
603#[allow(clippy::unwrap_used)] mod tests {
605 use super::*;
606 use serde_json::{Value, json};
607 use tokio::sync::mpsc;
608
609 fn slot(event: &str, market: &str) -> Slot {
610 Slot {
611 event: SmolStr::new(event),
612 market: SmolStr::new(market),
613 }
614 }
615
616 fn attach(
617 registry: &mut Registry<Massive>,
618 slots: &[(&str, &str)],
619 ) -> (AttachId, mpsc::UnboundedReceiver<WsMessage>) {
620 let (tx, rx) = mpsc::unbounded_channel();
621 let request = shared::AttachRequest {
622 exchange: ExchangeId::MassiveCrypto,
623 url: Url::parse("ws://127.0.0.1:1").unwrap(),
624 kind: "public_trades",
625 slots: slots
626 .iter()
627 .map(|(event, market)| slot(event, market))
628 .collect(),
629 timeout: Duration::from_secs(1),
630 batch: (),
631 };
632
633 (registry.insert_live(request, tx), rx)
634 }
635
636 fn frame(elements: &[Value]) -> WsMessage {
637 WsMessage::text(Value::Array(elements.to_vec()).to_string())
638 }
639
640 fn read(
641 registry: &mut Registry<Massive>,
642 message: &WsMessage,
643 ) -> Vec<Result<Slot, SocketError>> {
644 match Massive::read(registry, message) {
645 Frame::Answers(answers) => answers,
646 Frame::Closed(cause) => panic!("expected a frame of messages, got a close: {cause}"),
647 }
648 }
649
650 fn received(rx: &mut mpsc::UnboundedReceiver<WsMessage>) -> Vec<Vec<Value>> {
651 std::iter::from_fn(|| rx.try_recv().ok())
652 .map(|message| match message {
653 WsMessage::Text(text) => serde_json::from_str(text.as_str()).unwrap(),
654 other => panic!("expected a text frame, got {other:?}"),
655 })
656 .collect()
657 }
658
659 fn trade(pair: &str) -> Value {
660 json!({"ev": "XT", "pair": pair, "p": 82857.53, "s": 0.01, "t": 1, "c": [2], "i": "1"})
661 }
662
663 fn quote(pair: &str) -> Value {
664 json!({"ev": "XQ", "pair": pair, "bp": 1.0, "bs": 1.0, "ap": 2.0, "as": 1.0, "t": 1})
665 }
666
667 fn status(status: &str, message: &str) -> Value {
668 json!({"ev": "status", "status": status, "message": message})
669 }
670
671 #[test]
672 fn a_mixed_frame_is_split_per_stream_and_its_status_answers() {
673 let mut registry = Registry::default();
674 let (_, mut trades) = attach(&mut registry, &[("XT", "BTC-USD")]);
675 let (_, mut quotes) = attach(&mut registry, &[("XQ", "BTC-USD")]);
676
677 let answers = read(
678 &mut registry,
679 &frame(&[
680 trade("BTC-USD"),
681 status("success", "subscribed to: XQ.BTC-USD"),
682 quote("BTC-USD"),
683 trade("ETH-USD"),
684 ]),
685 );
686
687 assert_eq!(answers.len(), 1);
688 assert_eq!(answers[0].as_ref().unwrap(), &slot("XQ", "BTC-USD"));
689 assert_eq!(received(&mut trades), vec![vec![trade("BTC-USD")]]);
690 assert_eq!(received(&mut quotes), vec![vec![quote("BTC-USD")]]);
691 }
692
693 #[test]
694 fn a_frame_wholly_for_one_stream_is_forwarded_as_sent() {
695 let mut registry = Registry::default();
696 let (_, mut trades) = attach(&mut registry, &[("XT", "BTC-USD"), ("XT", "ETH-USD")]);
697
698 let sent = frame(&[trade("BTC-USD"), trade("ETH-USD")]);
699 read(&mut registry, &sent);
700
701 assert_eq!(trades.try_recv().unwrap(), sent);
702 }
703
704 #[test]
705 fn each_cluster_names_its_market_in_its_own_field() {
706 let mut registry = Registry::default();
707 let (_, mut stocks) = attach(&mut registry, &[("T", "BRK.B")]);
708 let (_, mut forex) = attach(&mut registry, &[("C", "EUR/USD")]);
709 let (_, mut aggregates) = attach(&mut registry, &[("CAS", "EUR/USD")]);
710
711 let stock = json!({"ev": "T", "sym": "BRK.B", "p": 1.0, "s": 1, "t": 1});
712 let fx = json!({"ev": "C", "p": "EUR/USD", "a": 1.1, "b": 1.0, "t": 1});
714 let bar = json!({"ev": "CAS", "pair": "EUR/USD", "o": 1, "h": 1, "l": 1, "c": 1, "v": 3,
715 "s": 0, "e": 1000});
716 read(
717 &mut registry,
718 &frame(&[stock.clone(), fx.clone(), bar.clone()]),
719 );
720
721 assert_eq!(received(&mut stocks), vec![vec![stock]]);
722 assert_eq!(received(&mut forex), vec![vec![fx]]);
723 assert_eq!(received(&mut aggregates), vec![vec![bar]]);
724 }
725
726 #[test]
727 fn a_refusal_names_nothing_and_an_unsubscribe_confirmation_answers_nothing() {
728 let answers = read(
729 &mut Registry::default(),
730 &frame(&[
731 status("error", "not authorized"),
732 status("success", "subscribed to: XT.ETH-USD"),
733 status("success", "unsubscribed to: XT.BTC-USD"),
734 ]),
735 );
736
737 assert_eq!(answers.len(), 2);
738 assert!(
739 matches!(&answers[0], Err(SocketError::Subscribe(message)) if message.contains("not authorized"))
740 );
741 assert_eq!(answers[1].as_ref().unwrap(), &slot("XT", "ETH-USD"));
742 }
743
744 #[test]
745 fn an_eviction_closes_the_connection_quoting_massive() {
746 let evicted = frame(&[status(
747 "max_connections",
748 "Maximum number of websocket connections exceeded.",
749 )]);
750
751 match Massive::read(&mut Registry::default(), &evicted) {
752 Frame::Closed(cause) => {
753 assert!(cause.contains("max_connections"), "{cause}");
754 assert!(
755 cause.contains("Maximum number of websocket connections"),
756 "{cause}"
757 );
758 }
759 Frame::Answers(_) => panic!("an eviction must close the connection"),
760 }
761 }
762
763 #[test]
764 fn a_confirmation_names_its_subscription_even_when_the_market_holds_a_dot() {
765 assert_eq!(Slot::parse("T.BRK.B"), Some(slot("T", "BRK.B")));
766 assert_eq!(Slot::parse("C.EUR/USD"), Some(slot("C", "EUR/USD")));
767 assert_eq!(Slot::parse("XT"), None);
768 assert_eq!(Slot::parse(".BTC-USD"), None);
769 }
770
771 #[test]
772 fn a_handshake_settles_on_every_confirmation_and_names_what_is_missing() {
773 let (messages, mut handshake) = Massive::subscribe(
774 &mut Registry::default(),
775 0,
776 &[slot("XT", "BTC-USD"), slot("XQ", "BTC-USD")],
777 );
778
779 assert_eq!(
780 messages,
781 vec![WsMessage::text(
782 json!({"action": "subscribe", "params": "XT.BTC-USD,XQ.BTC-USD"}).to_string()
783 )]
784 );
785
786 handshake.observe(slot("XQ", "BTC-USD"));
787 assert!(!handshake.is_settled());
788
789 let timed_out = handshake.timed_out(Duration::from_secs(10));
790 assert!(
791 timed_out.contains("never confirmed: XT.BTC-USD"),
792 "{timed_out}"
793 );
794
795 let refused = handshake
796 .refusal(SocketError::Subscribe("not authorized".to_owned()))
797 .to_string();
798 assert!(refused.contains("not authorized"), "{refused}");
799 assert!(refused.contains("XT.BTC-USD"), "{refused}");
800
801 handshake.observe(slot("XT", "BTC-USD"));
802 assert!(handshake.is_settled());
803 }
804
805 #[test]
806 fn an_attach_sending_nothing_waits_for_nothing() {
807 let (messages, handshake) = Massive::subscribe(&mut Registry::default(), 0, &[]);
808
809 assert!(messages.is_empty());
810 assert!(handshake.is_settled());
811 }
812
813 #[test]
814 fn a_release_is_one_unsubscribe_and_waits_for_nothing() {
815 let (messages, handshake) =
816 Massive::release(&[slot("XT", "BTC-USD"), slot("C", "EUR/USD")]);
817
818 assert_eq!(
819 messages,
820 vec![WsMessage::text(
821 json!({"action": "unsubscribe", "params": "XT.BTC-USD,C.EUR/USD"}).to_string()
822 )]
823 );
824 assert!(handshake.is_none());
825 }
826}