Skip to main content

brokapi_finam/
rpc.rs

1//! Unary call helper: deadline, one re-auth retry for reads.
2//!
3//! Internal plumbing of the connectors, public so that tests and tools can use it. Application code
4//! calls the broker through the connector traits or [`FinamRaw`](crate::FinamRaw).
5//!
6//! # Examples
7//!
8//! The difference between a read and a command, with a call that always answers `UNAUTHENTICATED`
9//! the first time:
10//!
11//! ```
12//! use std::sync::Arc;
13//! use std::sync::atomic::{AtomicU32, Ordering};
14//! use std::time::Duration;
15//! use brokapi_finam::rpc::unary;
16//! use brokapi_finam_protocol::{ApiEndpoint, start_token_manager};
17//! # use brokapi_mocks::finam::{MockServer, SECRET};
18//!
19//! # #[tokio::main(flavor = "current_thread")]
20//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
21//! # let server = MockServer::start().await;
22//! # let endpoint = ApiEndpoint::new(server.url.clone());
23//! let tokens = start_token_manager(&endpoint, SECRET, "").await?;
24//! let calls = Arc::new(AtomicU32::new(0));
25//! let call = || {
26//!     let calls = Arc::clone(&calls);
27//!     async move {
28//!         match calls.fetch_add(1, Ordering::SeqCst) {
29//!             0 => Err(tonic::Status::unauthenticated("token expired")),
30//!             _ => Ok(tonic::Response::new("ok")),
31//!         }
32//!     }
33//! };
34//!
35//! // A read renews the token and repeats once.
36//! let value = unary(&tokens, Duration::from_secs(5), true, call).await?;
37//! assert_eq!(value, "ok");
38//! assert_eq!(calls.load(Ordering::SeqCst), 2);
39//! assert_eq!(tokens.generation(), 2);
40//!
41//! // A command is sent once, whatever the answer.
42//! calls.store(0, Ordering::SeqCst);
43//! let err = unary(&tokens, Duration::from_secs(5), false, call).await.unwrap_err();
44//! assert!(err.is_unauthenticated());
45//! assert_eq!(calls.load(Ordering::SeqCst), 1);
46//! # Ok(())
47//! # }
48//! ```
49
50use std::future::Future;
51use std::sync::Arc;
52use std::time::Duration;
53
54use brokapi_finam_protocol::{FinamError, TokenManager};
55use tonic::Code;
56
57/// Runs `f` with a deadline. On `UNAUTHENTICATED` and `retry_unauth` renews the JWT and calls
58/// once more. Never set `retry_unauth` for commands (no automatic retry of trading
59/// commands).
60///
61/// A call that does not answer within `timeout` ends as status `DEADLINE_EXCEEDED`. The request may
62/// still be running at the broker, which is why a command with this result is `Unknown`.
63///
64/// # Errors
65///
66/// [`FinamError::Status`] with the code of the server, or `DEADLINE_EXCEEDED` on a timeout. When the
67/// renewal itself fails, its error is returned instead of the second call.
68///
69/// # Examples
70///
71/// See the [module docs](self).
72pub async fn unary<T, F, Fut>(
73    tokens: &Arc<TokenManager>,
74    timeout: Duration,
75    retry_unauth: bool,
76    f: F,
77) -> Result<T, FinamError>
78where
79    F: Fn() -> Fut,
80    Fut: Future<Output = Result<tonic::Response<T>, tonic::Status>>,
81{
82    match once(timeout, &f).await {
83        Err(e) if retry_unauth && e.is_unauthenticated() => {
84            tokens.refresh().await?;
85            once(timeout, &f).await
86        }
87        // A command is never repeated, but the next call must not meet the same expired token: the
88        // renewal runs in the background.
89        Err(e) if e.is_unauthenticated() => {
90            tokens.request_refresh();
91            Err(e)
92        }
93        other => other,
94    }
95}
96
97async fn once<T, F, Fut>(timeout: Duration, f: &F) -> Result<T, FinamError>
98where
99    F: Fn() -> Fut,
100    Fut: Future<Output = Result<tonic::Response<T>, tonic::Status>>,
101{
102    match tokio::time::timeout(timeout, f()).await {
103        Ok(Ok(resp)) => Ok(resp.into_inner()),
104        Ok(Err(status)) => Err(status.into()),
105        Err(_) => Err(FinamError::Status {
106            code: Code::DeadlineExceeded,
107            message: format!("no response within {timeout:?}"),
108        }),
109    }
110}
111
112/// Everything a connector needs to make calls: clients, quota, deadline.
113///
114/// Cheap to clone. [`FinamRaw`](crate::FinamRaw) wraps it for application code.
115#[derive(Clone)]
116pub struct Rpc {
117    /// Service clients with the shared token manager.
118    pub api: brokapi_finam_protocol::FinamApi,
119    /// Rate limiter shared by all connectors of one Finam user.
120    pub limiter: Arc<brokapi_runtime::BrokerLimiter>,
121    /// Deadline of one unary call.
122    pub timeout: Duration,
123}
124
125impl Rpc {
126    /// Takes the quota for the proto method `method`.
127    ///
128    /// # Errors
129    ///
130    /// [`brokapi_core::Error::RateLimited`] when a bucket on the route of `method` is empty. Nothing
131    /// is consumed in that case and the request must not be sent.
132    pub fn acquire(&self, method: &str) -> Result<(), brokapi_core::Error> {
133        self.limiter.acquire(method, brokapi_runtime::now())
134    }
135
136    /// A read-only call: quota, deadline, one re-auth retry, public error mapping.
137    ///
138    /// # Errors
139    ///
140    /// [`brokapi_core::Error::RateLimited`] (local quota or `RESOURCE_EXHAUSTED`),
141    /// [`brokapi_core::Error::Rejected`] (the broker said no), [`brokapi_core::Error::Transport`]
142    /// (connection failure or timeout). See [`crate::error::map_protocol_err`].
143    pub async fn read<T, F, Fut>(&self, method: &str, f: F) -> Result<T, brokapi_core::Error>
144    where
145        F: Fn(brokapi_finam_protocol::FinamApi) -> Fut,
146        Fut: Future<Output = Result<tonic::Response<T>, tonic::Status>>,
147    {
148        self.acquire(method)?;
149        unary(self.api.tokens(), self.timeout, true, || {
150            f(self.api.clone())
151        })
152        .await
153        .map_err(|e| crate::error::map_protocol_err(&e))
154    }
155
156    /// A trading command: quota, deadline, NO retry. The raw protocol error is returned so the
157    /// caller can tell "definitely rejected" from "unknown outcome".
158    ///
159    /// Quota is taken by the caller beforehand (`acquire`) so that a refusal happens before the
160    /// command is registered as in flight.
161    ///
162    /// # Errors
163    ///
164    /// The raw [`FinamError`]. Map it with [`crate::error::map_command_err`].
165    pub async fn send_command<T, F, Fut>(&self, f: F) -> Result<T, FinamError>
166    where
167        F: Fn(brokapi_finam_protocol::FinamApi) -> Fut,
168        Fut: Future<Output = Result<tonic::Response<T>, tonic::Status>>,
169    {
170        unary(self.api.tokens(), self.timeout, false, || {
171            f(self.api.clone())
172        })
173        .await
174    }
175}