Skip to main content

polyoxide_ws/
request.rs

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