1use crate::{
7 AccountID, Client, Error, Result,
8 admission::StreamLease,
9 models::{ClientPrice, PricingHeartbeat, Transaction, TransactionHeartbeat},
10};
11use reqwest::Response;
12use serde::Serialize;
13use std::time::Duration;
14
15const MAX_RECORD_BYTES: usize = 1024 * 1024;
26const MAX_BACKLOG_BYTES: usize = 16 * 1024 * 1024;
35
36#[derive(Debug, Clone, PartialEq)]
38#[non_exhaustive]
39pub enum PriceStreamEvent {
40 Price(Box<ClientPrice>),
42 Heartbeat(PricingHeartbeat),
44}
45
46#[derive(Debug, Clone, PartialEq)]
48#[non_exhaustive]
49pub enum TransactionStreamEvent {
50 Transaction(Box<Transaction>),
52 Heartbeat(TransactionHeartbeat),
54}
55
56pub struct HttpStream<E> {
58 response: Option<Response>,
59 buffer: Vec<u8>,
60 start: usize,
61 decoder: fn(&[u8]) -> Result<E>,
62 ended: bool,
63 lease: Option<StreamLease>,
64}
65
66impl<E> HttpStream<E> {
67 fn finish(&mut self) {
68 self.ended = true;
69 self.response.take();
70 self.lease.take();
71 }
72 pub async fn next_event(&mut self) -> Option<Result<E>> {
74 if self.ended {
75 return None;
76 }
77 if self.buffer.len() - self.start > MAX_BACKLOG_BYTES {
82 self.finish();
83 return Some(Err(Error::ResponseTooLarge));
84 }
85 loop {
86 if let Some(offset) = self.buffer[self.start..]
91 .iter()
92 .position(|byte| *byte == b'\n')
93 {
94 let newline = self.start + offset;
95 let mut end = newline;
96 if end > self.start && self.buffer[end - 1] == b'\r' {
97 end -= 1;
98 }
99 if end - self.start > MAX_RECORD_BYTES {
100 self.finish();
101 return Some(Err(Error::ResponseTooLarge));
102 }
103 let line: Vec<u8> = self.buffer[self.start..end].to_vec();
104 self.start = newline + 1;
105 if self.start == self.buffer.len() {
106 self.buffer.clear();
107 self.start = 0;
108 } else if self.start > MAX_RECORD_BYTES {
109 self.buffer.drain(..self.start);
110 self.start = 0;
111 }
112 if line.is_empty() {
113 continue;
114 }
115 let result = (self.decoder)(&line);
116 if result.is_err() {
117 self.finish();
118 }
119 return Some(result);
120 }
121 if self.buffer.len() - self.start > MAX_RECORD_BYTES {
124 self.finish();
125 return Some(Err(Error::ResponseTooLarge));
126 }
127 let Some(response) = self.response.as_mut() else {
128 self.finish();
129 return None;
130 };
131 match tokio::time::timeout(Duration::from_secs(30), response.chunk()).await {
132 Err(_) => {
133 self.finish();
134 return Some(Err(Error::Transport(
135 "OANDA stream heartbeat timed out; continuity must be recovered".into(),
136 )));
137 }
138 Ok(Ok(Some(chunk))) => {
139 self.buffer.extend_from_slice(&chunk);
147 if self.buffer.len() - self.start > MAX_BACKLOG_BYTES {
148 self.finish();
149 return Some(Err(Error::ResponseTooLarge));
150 }
151 }
152 Ok(Ok(None)) => {
153 self.finish();
154 return Some(Err(Error::Transport(
155 "OANDA stream ended; continuity must be recovered".into(),
156 )));
157 }
158 Ok(Err(error)) => {
159 self.finish();
160 return Some(Err(Error::Transport(error.to_string())));
161 }
162 }
163 }
164 }
165}
166
167impl Client {
168 pub(crate) async fn open_http_stream<E, Q: Serialize>(
169 &self,
170 path: &str,
171 query: Option<&Q>,
172 decoder: fn(&[u8]) -> Result<E>,
173 ) -> Result<HttpStream<E>> {
174 let lease = self.inner.admission.lease_stream()?;
175 let url = Self::build_url(&self.inner.stream, path, query)?;
176 let response = self
177 .inner
178 .stream_http
179 .get(url)
180 .bearer_auth(&self.inner.token)
181 .header(
182 "Accept-Datetime-Format",
183 self.inner.datetime_format.as_str(),
184 )
185 .send()
186 .await
187 .map_err(|e| Error::Transport(e.to_string()))?;
188 if !response.status().is_success() {
189 let status = response.status().as_u16();
190 let body = crate::client::read_bounded(response, self.inner.max_body).await?;
191 let reason = serde_json::from_slice::<serde_json::Value>(&body).ok();
196 let code = reason
197 .as_ref()
198 .and_then(|value| value.get("errorCode"))
199 .and_then(serde_json::Value::as_str)
200 .map(str::to_owned);
201 let message = reason
202 .as_ref()
203 .and_then(|value| value.get("errorMessage"))
204 .and_then(serde_json::Value::as_str)
205 .map(str::to_owned);
206 return Err(Error::Provider {
207 status,
208 code,
209 message,
210 });
211 }
212 Ok(HttpStream {
213 response: Some(response),
214 buffer: Vec::new(),
215 start: 0,
216 decoder,
217 ended: false,
218 lease: Some(lease),
219 })
220 }
221
222 pub async fn stream_pricing(
229 &self,
230 account: &AccountID,
231 query: &crate::pricing::StreamPricingQuery,
232 ) -> Result<HttpStream<PriceStreamEvent>> {
233 if query.instruments.is_empty() {
234 return Err(Error::InvalidInput(
235 "pricing instruments must not be empty".into(),
236 ));
237 }
238 let path = format!(
239 "/v3/accounts/{}/pricing/stream",
240 crate::client::segment(account.as_str())
241 );
242 self.open_http_stream(&path, Some(query), decode_price)
243 .await
244 }
245
246 pub async fn stream_transactions(
252 &self,
253 account: &AccountID,
254 ) -> Result<HttpStream<TransactionStreamEvent>> {
255 let path = format!(
256 "/v3/accounts/{}/transactions/stream",
257 crate::client::segment(account.as_str())
258 );
259 self.open_http_stream(&path, None::<&()>, decode_transaction)
260 .await
261 }
262}
263
264fn decode_price(line: &[u8]) -> Result<PriceStreamEvent> {
265 let value: serde_json::Value =
266 serde_json::from_slice(line).map_err(|e| Error::Decode(e.to_string()))?;
267 match value.get("type").and_then(serde_json::Value::as_str) {
268 Some("PRICE") => serde_json::from_value(value)
269 .map(Box::new)
270 .map(PriceStreamEvent::Price)
271 .map_err(|e| Error::Decode(e.to_string())),
272 Some("HEARTBEAT") => serde_json::from_value(value)
273 .map(PriceStreamEvent::Heartbeat)
274 .map_err(|e| Error::Decode(e.to_string())),
275 _ => Err(Error::Decode("unknown OANDA pricing stream record".into())),
276 }
277}
278
279fn decode_transaction(line: &[u8]) -> Result<TransactionStreamEvent> {
280 let value: serde_json::Value =
281 serde_json::from_slice(line).map_err(|e| Error::Decode(e.to_string()))?;
282 if value.get("type").and_then(serde_json::Value::as_str) == Some("HEARTBEAT") {
283 serde_json::from_value(value)
284 .map(TransactionStreamEvent::Heartbeat)
285 .map_err(|e| Error::Decode(e.to_string()))
286 } else {
287 serde_json::from_value(value)
288 .map(Box::new)
289 .map(TransactionStreamEvent::Transaction)
290 .map_err(|e| Error::Decode(e.to_string()))
291 }
292}
293
294#[cfg(test)]
295#[allow(clippy::expect_used, clippy::panic)]
298mod tests {
299 use super::*;
300 use crate::Environment;
301
302 #[tokio::test]
303 async fn cancelled_stream_owner_releases_its_connection_slot() {
304 let client = Client::builder(Environment::Practice, "fixture").build();
305 assert!(client.is_ok());
306 let Ok(client) = client else { return };
307 if let Ok(mut slots) = client.inner.admission.inner.stream_slots.lock() {
308 slots.active = 1;
309 }
310 let lease = StreamLease {
311 owner: client.inner.admission.clone(),
312 };
313 let task = tokio::spawn(async move {
314 let _lease = lease;
315 std::future::pending::<()>().await;
316 });
317 tokio::task::yield_now().await;
318 task.abort();
319 let _ = task.await;
320 if let Ok(slots) = client.inner.admission.inner.stream_slots.lock() {
321 assert_eq!(slots.active, 0);
322 }
323 }
324
325 #[tokio::test]
326 async fn a_backlog_beyond_the_local_bound_ends_the_generation() {
327 let mut buffer = Vec::new();
333 for index in 0..200_000_u32 {
334 buffer.extend_from_slice(
335 format!(
336 "{{\"type\":\"HEARTBEAT\",\"lastTransactionID\":\"{index}\",\
337 \"time\":\"2024-01-02T03:04:05.000000000Z\"}}\r\n"
338 )
339 .as_bytes(),
340 );
341 }
342 assert!(
343 buffer.len() > MAX_BACKLOG_BYTES,
344 "fixture must exceed the cap"
345 );
346 let mut stream = HttpStream {
347 response: None,
348 buffer,
349 start: 0,
350 decoder: decode_transaction,
351 ended: false,
352 lease: None,
353 };
354 assert!(matches!(
355 stream.next_event().await,
356 Some(Err(Error::ResponseTooLarge))
357 ));
358 assert!(stream.next_event().await.is_none());
359 }
360
361 #[tokio::test]
362 async fn a_cancelled_stream_releases_its_slot_even_after_a_poisoned_lock() {
363 let client = Client::builder(Environment::Practice, "fixture").build();
364 assert!(client.is_ok());
365 let Ok(client) = client else { return };
366 if let Ok(mut slots) = client.inner.admission.inner.stream_slots.lock() {
367 slots.active = 1;
368 }
369 let lease = StreamLease {
370 owner: client.inner.admission.clone(),
371 };
372 let poisoned = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
373 let Ok(_held) = client.inner.admission.inner.stream_slots.lock() else {
374 return;
375 };
376 panic!("poison the lock");
377 }));
378 assert!(poisoned.is_err());
379 drop(lease);
380 let slots = client
381 .inner
382 .admission
383 .inner
384 .stream_slots
385 .lock()
386 .unwrap_or_else(std::sync::PoisonError::into_inner);
387 assert_eq!(
388 slots.active, 0,
389 "a released connection must not be reported as capacity in use"
390 );
391 }
392}