Skip to main content

polyoxide_ws/
request.rs

1//! Request/answer correlation on one connection (W12).
2
3use std::{future::Future, time::Duration};
4
5use futures_util::StreamExt;
6use tokio::time::Instant;
7use tokio_tungstenite::tungstenite::Message;
8
9use crate::{Connection, WsError};
10
11/// What a frame read while a request awaits its answer is, as the caller's
12/// matcher reads it.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Correlated<A> {
15    /// The answer awaited, which ends the wait.
16    Answer(A),
17    /// An answer to another request, which nobody awaits any more, such as
18    /// one that arrived after its request timed out. Dropped.
19    Uncorrelated,
20    /// Not an answer: kept, and yielded by the stream afterwards, if its
21    /// [`Mode`](crate::Mode) yields it.
22    Push,
23}
24
25/// A connection's request ids and pacing.
26#[derive(Debug)]
27pub(crate) struct Requests {
28    next_id: u64,
29    min_interval: Option<Duration>,
30    answer_timeout: Option<Duration>,
31    last_request: Option<Instant>,
32}
33
34impl Default for Requests {
35    fn default() -> Self {
36        Self {
37            next_id: 1,
38            min_interval: None,
39            answer_timeout: None,
40            last_request: None,
41        }
42    }
43}
44
45/// Requests and their answers.
46///
47/// Ids count from 1. A request is sent no sooner than its minimum interval
48/// after the previous one, when one is set, and then reads until the caller's
49/// matcher recognises its answer. Every other frame read meanwhile is kept and
50/// yielded by the stream afterwards, in the order it arrived; an answer to
51/// another request is dropped. With an answer timeout set, a request whose
52/// send and wait together outlast it fails as
53/// [`Kind::NoAnswer`](crate::Kind::NoAnswer), so a peer that stops reading
54/// cannot hang it; without one, it waits as long as the connection stays
55/// open. A server close while waiting fails the request with the close, as
56/// does any request after it, and a request after a failed read fails as
57/// [`Kind::AlreadyClosed`](crate::Kind::AlreadyClosed) without sending.
58///
59/// The envelope is the caller's: a builder writes the request for its id, and
60/// a matcher reads each frame. Neither request nor its wait is cancel-safe,
61/// and after a [`NoAnswer`](crate::Kind::NoAnswer) the server's state is
62/// unknown: replace the connection rather than reuse it.
63///
64/// The basis is one socket crate's former `request` and `await_answer`
65/// (its bare tier's, on this correlation since Story 4.7), with its 10 s
66/// answer timeout and 200 ms pacing, and another socket crate's former
67/// `take_id` and `await_response` (`ws/client.rs`, Story 4.6), which set
68/// neither.
69impl Connection {
70    /// Send requests no sooner than `interval` apart. Only
71    /// [`request`](Self::request) is paced: a frame sent with
72    /// [`send`](Self::send), under an id from [`take_id`](Self::take_id) or
73    /// not, neither waits nor counts.
74    pub fn min_request_interval(mut self, interval: Duration) -> Self {
75        self.requests.min_interval = Some(interval);
76        self
77    }
78
79    /// Give up on a request after `timeout`, its send and its wait for the
80    /// answer together, and on [`await_answer`](Self::await_answer)'s wait.
81    pub fn answer_timeout(mut self, timeout: Duration) -> Self {
82        self.requests.answer_timeout = Some(timeout);
83        self
84    }
85
86    /// The id the next request will take.
87    pub fn next_id(&self) -> u64 {
88        self.requests.next_id
89    }
90
91    /// Take the next id, for a request sent another way. A request sent that
92    /// way is not paced by [`min_request_interval`](Self::min_request_interval),
93    /// and does not delay the next paced one.
94    pub fn take_id(&mut self) -> u64 {
95        let id = self.requests.next_id;
96        self.requests.next_id += 1;
97        id
98    }
99
100    /// Send the request `build` writes for the next id, and read until
101    /// `answer` recognises its answer.
102    pub async fn request<A>(
103        &mut self,
104        build: impl FnOnce(u64) -> Message,
105        mut answer: impl FnMut(u64, &Message) -> Correlated<A>,
106    ) -> Result<A, WsError> {
107        // A connection the server closed reports its close, not a send error,
108        // and one whose read failed sends nothing.
109        if let Some(closed) = self.closed() {
110            return Err(closed);
111        }
112        if self.is_finished() {
113            return Err(already_closed());
114        }
115        if let (Some(last), Some(interval)) =
116            (self.requests.last_request, self.requests.min_interval)
117        {
118            // An interval too long to add is one never reached.
119            if let Some(at) = last.checked_add(interval) {
120                tokio::time::sleep_until(at).await;
121            }
122        }
123        let id = self.take_id();
124        let timeout = self.requests.answer_timeout;
125        within(timeout, async {
126            self.send(build(id)).await?;
127            self.requests.last_request = Some(Instant::now());
128            self.read_until(&mut |message: &Message| answer(id, message))
129                .await
130        })
131        .await
132    }
133
134    /// Read until `answer` recognises a frame as the answer awaited, keeping
135    /// pushes for the stream, within the answer timeout when one is set.
136    ///
137    /// For an answer to something [`request`](Self::request) did not send,
138    /// such as a protocol ping's pong.
139    pub async fn await_answer<A>(
140        &mut self,
141        mut answer: impl FnMut(&Message) -> Correlated<A>,
142    ) -> Result<A, WsError> {
143        let timeout = self.requests.answer_timeout;
144        within(timeout, self.read_until(&mut answer)).await
145    }
146
147    async fn read_until<A>(
148        &mut self,
149        answer: &mut impl FnMut(&Message) -> Correlated<A>,
150    ) -> Result<A, WsError> {
151        loop {
152            if let Some(closed) = self.closed() {
153                return Err(closed);
154            }
155            if self.is_finished() {
156                return Err(already_closed());
157            }
158            let message = match self.socket().next().await {
159                Some(Ok(message)) => message,
160                Some(Err(err)) => {
161                    self.fail();
162                    return Err(err.into());
163                }
164                None => {
165                    self.record_close(None);
166                    continue;
167                }
168            };
169            self.touch();
170            if let Message::Close(_) = message {
171                self.record_close(Some(&message));
172                self.keep(message);
173                self.flush_reply().await;
174                continue;
175            }
176            match answer(&message) {
177                Correlated::Answer(answer) => return Ok(answer),
178                Correlated::Uncorrelated => {
179                    tracing::debug!(?message, "dropped an answer nobody awaits");
180                }
181                Correlated::Push => self.keep(message),
182            }
183        }
184    }
185}
186
187/// Run `exchange` within `timeout`, if one is set, failing as `NoAnswer`.
188async fn within<A>(
189    timeout: Option<Duration>,
190    exchange: impl Future<Output = Result<A, WsError>>,
191) -> Result<A, WsError> {
192    match timeout {
193        Some(timeout) => tokio::time::timeout(timeout, exchange)
194            .await
195            .map_err(|_| WsError::no_answer(timeout))?,
196        None => exchange.await,
197    }
198}
199
200/// The error for a connection used after its read failed.
201fn already_closed() -> WsError {
202    WsError::from(tokio_tungstenite::tungstenite::Error::AlreadyClosed)
203}
204
205#[cfg(test)]
206mod tests {
207    use serde_json::{json, Value};
208    use tokio_tungstenite::tungstenite::protocol::{frame::coding::CloseCode, CloseFrame};
209
210    use super::*;
211    use crate::{
212        connect,
213        test_server::{next_item, Script, ScriptedServer},
214        Kind, Mode, DEFAULT_CONNECT_TIMEOUT,
215    };
216
217    fn text(value: Value) -> Message {
218        Message::Text(value.to_string().into())
219    }
220
221    fn parse(message: &Message) -> Option<Value> {
222        match message {
223            Message::Text(text) => serde_json::from_str(text).ok(),
224            _ => None,
225        }
226    }
227
228    /// A server that holds each connection and answers with `answer`, the
229    /// same stateless answerer on every connection.
230    async fn answering<A>(answer: A) -> ScriptedServer
231    where
232        A: FnMut(&str) -> Vec<Message> + Clone + Send + 'static,
233    {
234        ScriptedServer::start_answering(vec![Script::hold()], move |_| answer.clone()).await
235    }
236
237    async fn open(server: &ScriptedServer) -> Connection {
238        let socket = connect(server.url.as_str(), DEFAULT_CONNECT_TIMEOUT)
239            .await
240            .expect("connect");
241        Connection::new(socket, Mode::Data)
242    }
243
244    async fn next_text(connection: &mut Connection) -> Value {
245        let message = tokio::time::timeout(Duration::from_secs(2), connection.next())
246            .await
247            .expect("a frame within 2 s")
248            .expect("the stream is open")
249            .expect("not an error");
250        parse(&message).expect("a JSON text frame")
251    }
252
253    /// One venue's envelope: `{"id", "req", "chs"}` answered by
254    /// `{"id", "data"}`, with pushes `{"ch", "sq", "data"}`.
255    mod perps {
256        use super::*;
257
258        pub(super) fn subscribe(channels: &'static [&'static str]) -> impl FnOnce(u64) -> Message {
259            move |id| text(json!({"id": id, "req": "sub", "chs": channels}))
260        }
261
262        pub(super) fn answer(id: u64, message: &Message) -> Correlated<Value> {
263            match parse(message) {
264                Some(frame) if frame.get("ch").is_some() => Correlated::Push,
265                Some(frame) if frame["id"] == id => Correlated::Answer(frame["data"].clone()),
266                Some(frame) if frame.get("id").is_some() => Correlated::Uncorrelated,
267                _ => Correlated::Push,
268            }
269        }
270
271        pub(super) fn push(sq: u64) -> Message {
272            text(json!({"ch": "bbo::1", "ts": 1, "sq": sq, "data": {}}))
273        }
274
275        pub(super) fn statuses(request: &str) -> Message {
276            let request: Value = serde_json::from_str(request).unwrap();
277            let ok: Vec<Value> = request["chs"]
278                .as_array()
279                .unwrap()
280                .iter()
281                .map(|_| json!({"status": "ok"}))
282                .collect();
283            text(json!({"id": request["id"], "data": ok}))
284        }
285    }
286
287    /// Another venue's envelope: `{"method", "params", "id"}` answered by
288    /// `{"result", "id"}` or `{"error", "id"}`, with pushes `{"stream", "data"}`.
289    mod binance {
290        use super::*;
291
292        pub(super) fn subscribe(name: &'static str) -> impl FnOnce(u64) -> Message {
293            move |id| text(json!({"method": "SUBSCRIBE", "params": [name], "id": id}))
294        }
295
296        pub(super) fn answer(id: u64, message: &Message) -> Correlated<Result<Value, i64>> {
297            match parse(message) {
298                Some(frame) if frame.get("stream").is_some() => Correlated::Push,
299                Some(frame) if frame["id"] == id => Correlated::Answer(match frame.get("error") {
300                    Some(error) => Err(error["code"].as_i64().unwrap()),
301                    None => Ok(frame["result"].clone()),
302                }),
303                Some(_) => Correlated::Uncorrelated,
304                None => Correlated::Push,
305            }
306        }
307
308        pub(super) fn push(n: u64) -> Message {
309            text(json!({"stream": "btcusdt@aggTrade", "data": {"a": n}}))
310        }
311
312        pub(super) fn result(request: &str) -> Message {
313            let request: Value = serde_json::from_str(request).unwrap();
314            text(json!({"result": null, "id": request["id"]}))
315        }
316    }
317
318    #[tokio::test]
319    async fn ids_count_from_one() {
320        let server = ScriptedServer::start(vec![Script::hold()]).await;
321        let mut connection = open(&server).await;
322        assert_eq!(connection.next_id(), 1);
323        assert_eq!(connection.take_id(), 1);
324        assert_eq!(connection.take_id(), 2);
325        assert_eq!(connection.next_id(), 3);
326    }
327
328    #[tokio::test]
329    async fn a_request_after_a_failed_read_sends_nothing() {
330        let server = ScriptedServer::start(vec![Script::hold().then_cut()]).await;
331        let mut connection = open(&server).await;
332        assert!(next_item(&mut connection).await.is_err());
333        let err = connection
334            .request(binance::subscribe("a@aggTrade"), binance::answer)
335            .await
336            .expect_err("the connection is gone");
337        assert_eq!(err.kind(), Kind::AlreadyClosed);
338        assert_eq!(connection.next_id(), 1, "no id was taken");
339    }
340
341    #[tokio::test]
342    async fn an_interval_too_long_to_add_is_never_waited() {
343        let server = answering(|request: &str| vec![binance::result(request)]).await;
344        let mut connection = open(&server)
345            .await
346            .min_request_interval(Duration::MAX)
347            .answer_timeout(Duration::from_secs(2));
348        for name in ["a@aggTrade", "b@aggTrade"] {
349            let result = connection
350                .request(binance::subscribe(name), binance::answer)
351                .await
352                .expect("an answer, not a panic or a wait");
353            assert_eq!(result, Ok(Value::Null));
354        }
355    }
356
357    #[tokio::test]
358    async fn a_request_refreshes_last_inbound() {
359        let server = answering(|request: &str| vec![binance::result(request)]).await;
360        let mut connection = open(&server).await;
361        let before = connection.last_inbound();
362        tokio::time::sleep(Duration::from_millis(10)).await;
363        let result = connection
364            .request(binance::subscribe("a@aggTrade"), binance::answer)
365            .await
366            .expect("an answer");
367        assert_eq!(result, Ok(Value::Null));
368        assert!(
369            connection.last_inbound() > before,
370            "the answer, read by the request, must count as inbound"
371        );
372    }
373
374    #[tokio::test]
375    async fn all_mode_yields_a_close_read_while_awaiting() {
376        let server = answering(|_: &str| {
377            vec![Message::Close(Some(CloseFrame {
378                code: CloseCode::Policy,
379                reason: "Too many requests".into(),
380            }))]
381        })
382        .await;
383        let socket = connect(server.url.as_str(), DEFAULT_CONNECT_TIMEOUT)
384            .await
385            .expect("connect");
386        let mut connection = Connection::new(socket, Mode::All);
387        let err = connection
388            .request(binance::subscribe("a@aggTrade"), binance::answer)
389            .await
390            .expect_err("closed");
391        assert!(
392            matches!(
393                err.kind(),
394                Kind::Closed {
395                    code: Some(1008),
396                    ..
397                }
398            ),
399            "{err:?}"
400        );
401        assert!(matches!(
402            next_item(&mut connection).await,
403            Ok(Message::Close(Some(_)))
404        ));
405        assert!(connection.next().await.is_none());
406    }
407
408    #[tokio::test]
409    async fn perps_pushes_read_before_the_answer_are_kept_and_yielded_in_order() {
410        let server = answering(|request: &str| {
411            vec![perps::push(10), perps::push(11), perps::statuses(request)]
412        })
413        .await;
414        let mut connection = open(&server).await;
415        let data = connection
416            .request(perps::subscribe(&["bbo::1", "trades::1"]), perps::answer)
417            .await
418            .expect("an answer");
419        assert_eq!(data, json!([{"status": "ok"}, {"status": "ok"}]));
420        assert_eq!(next_text(&mut connection).await["sq"], 10);
421        assert_eq!(next_text(&mut connection).await["sq"], 11);
422    }
423
424    #[tokio::test]
425    async fn perps_an_answer_to_another_request_is_dropped() {
426        let server = answering(|request: &str| {
427            vec![
428                text(json!({"id": 99, "data": {"status": "ok"}})),
429                perps::statuses(request),
430                perps::push(12),
431            ]
432        })
433        .await;
434        let mut connection = open(&server).await;
435        connection
436            .request(perps::subscribe(&["bbo::1"]), perps::answer)
437            .await
438            .expect("its own answer");
439        assert_eq!(
440            next_text(&mut connection).await["sq"],
441            12,
442            "the stray answer must not be yielded"
443        );
444    }
445
446    #[tokio::test]
447    async fn binance_requests_are_paced() {
448        let interval = Duration::from_millis(50);
449        let server = answering(|request: &str| vec![binance::result(request)]).await;
450        let mut connection = open(&server).await.min_request_interval(interval);
451        let start = Instant::now();
452        for name in ["a@aggTrade", "b@aggTrade", "c@aggTrade"] {
453            let result = connection
454                .request(binance::subscribe(name), binance::answer)
455                .await
456                .expect("an answer");
457            assert_eq!(result, Ok(Value::Null));
458        }
459        assert!(
460            start.elapsed() >= interval * 2,
461            "three requests in {:?}",
462            start.elapsed()
463        );
464    }
465
466    #[tokio::test]
467    async fn binance_an_unanswered_request_is_no_answer_and_pushes_meanwhile_are_kept() {
468        let after = Duration::from_millis(200);
469        let server = answering(|_: &str| vec![binance::push(1), binance::push(2)]).await;
470        let mut connection = open(&server).await.answer_timeout(after);
471        let err = connection
472            .request(binance::subscribe("a@aggTrade"), binance::answer)
473            .await
474            .expect_err("nothing answers it");
475        assert_eq!(err.kind(), Kind::NoAnswer { after });
476        assert_eq!(next_text(&mut connection).await["data"]["a"], 1);
477        assert_eq!(next_text(&mut connection).await["data"]["a"], 2);
478    }
479
480    #[tokio::test]
481    async fn binance_a_late_answer_is_not_taken_for_the_next() {
482        // Request 2's answer is held until request 3 arrives, by when request
483        // 2 has timed out. Request 3 must skip it and take its own, and the
484        // push sent ahead of both is kept.
485        let server = ScriptedServer::start_answering(vec![Script::hold()], |_| {
486            let mut held: Option<Message> = None;
487            move |request: &str| {
488                let value: Value = serde_json::from_str(request).unwrap();
489                match value["id"].as_u64() {
490                    Some(2) => {
491                        held = Some(text(
492                            json!({"error": {"code": 2, "msg": "Invalid request"}, "id": 2}),
493                        ));
494                        Vec::new()
495                    }
496                    Some(3) => vec![
497                        binance::push(7),
498                        held.take().expect("request 2's answer"),
499                        binance::result(request),
500                    ],
501                    _ => vec![binance::result(request)],
502                }
503            }
504        })
505        .await;
506        let mut connection = open(&server)
507            .await
508            .answer_timeout(Duration::from_millis(200));
509        for (name, expected) in [
510            ("a@aggTrade", Ok(Ok(Value::Null))),
511            ("b@aggTrade", Err(())),
512            ("c@aggTrade", Ok(Ok(Value::Null))),
513        ] {
514            let result = connection
515                .request(binance::subscribe(name), binance::answer)
516                .await
517                .map_err(|err| assert!(matches!(err.kind(), Kind::NoAnswer { .. }), "{err:?}"));
518            assert_eq!(result, expected, "{name}");
519        }
520        let push = tokio::time::timeout(Duration::from_secs(2), connection.next())
521            .await
522            .expect("the push read during request 3 was dropped");
523        assert_eq!(parse(&push.unwrap().unwrap()).unwrap()["data"]["a"], 7);
524    }
525
526    #[tokio::test]
527    async fn binance_an_error_answer_is_the_callers_to_read() {
528        let server = answering(|request: &str| {
529            let value: Value = serde_json::from_str(request).unwrap();
530            vec![text(
531                json!({"error": {"code": 2, "msg": "Invalid request"}, "id": value["id"]}),
532            )]
533        })
534        .await;
535        let mut connection = open(&server).await;
536        let result = connection
537            .request(binance::subscribe("a@aggTrade"), binance::answer)
538            .await
539            .expect("an answer");
540        assert_eq!(result, Err(2));
541    }
542
543    #[tokio::test]
544    async fn a_protocol_ping_awaits_its_pong_without_taking_an_id() {
545        let server = ScriptedServer::start(vec![Script::hold()]).await;
546        let mut connection = open(&server).await.answer_timeout(Duration::from_secs(2));
547        connection
548            .send(Message::Ping(Vec::new().into()))
549            .await
550            .expect("ping");
551        connection
552            .await_answer(|message| match message {
553                Message::Pong(_) => Correlated::Answer(()),
554                _ => Correlated::Push,
555            })
556            .await
557            .expect("a pong");
558        assert_eq!(connection.next_id(), 1);
559    }
560
561    #[tokio::test]
562    async fn a_close_while_awaiting_is_answered() {
563        // DRIFT R3 on the request path: the reply goes out though nothing
564        // reads the connection again.
565        let server = ScriptedServer::start(vec![Script::hold()
566            .after_first_frame()
567            .then_close()
568            .close_with(1008, "Too many requests")])
569        .await;
570        let mut connection = open(&server).await;
571        let err = connection
572            .request(binance::subscribe("a@aggTrade"), binance::answer)
573            .await
574            .expect_err("closed");
575        assert!(
576            matches!(
577                err.kind(),
578                Kind::Closed {
579                    code: Some(1008),
580                    ..
581                }
582            ),
583            "{err:?}"
584        );
585        server
586            .wait_for("the close reply", |s| s.close_reply_count() == 1)
587            .await;
588    }
589
590    #[tokio::test]
591    async fn a_close_while_awaiting_fails_the_request_and_every_later_one() {
592        let server = answering(|_: &str| {
593            vec![Message::Close(Some(CloseFrame {
594                code: CloseCode::Policy,
595                reason: "Too many requests".into(),
596            }))]
597        })
598        .await;
599        let mut connection = open(&server).await;
600        let closed = Kind::Closed {
601            code: Some(1008),
602            reason: "Too many requests",
603        };
604        let err = connection
605            .request(binance::subscribe("a@aggTrade"), binance::answer)
606            .await
607            .expect_err("closed");
608        assert_eq!(err.kind(), closed);
609        let again = connection
610            .request(binance::subscribe("b@aggTrade"), binance::answer)
611            .await
612            .expect_err("still closed");
613        assert_eq!(again.kind(), closed);
614        assert_eq!(connection.next_id(), 2, "nothing was sent after the close");
615        assert!(connection.next().await.is_none());
616    }
617}