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