1use crate::{
7 AccountID, Client, Error, Result,
8 admission::StreamLease,
9 models::{ClientPrice, PricingHeartbeat, Transaction, TransactionHeartbeat},
10 validation::Validate,
11};
12use reqwest::Response;
13use serde::Serialize;
14use std::time::Duration;
15
16const MAX_RECORD_BYTES: usize = 1024 * 1024;
26const MAX_BACKLOG_BYTES: usize = 16 * 1024 * 1024;
35const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(30);
37
38#[derive(Debug, Clone, PartialEq)]
40#[non_exhaustive]
41pub enum PriceStreamEvent {
42 Price(Box<ClientPrice>),
44 Heartbeat(PricingHeartbeat),
46}
47
48#[derive(Debug, Clone, PartialEq)]
50#[non_exhaustive]
51pub enum TransactionStreamEvent {
52 Transaction(Box<Transaction>),
54 Heartbeat(TransactionHeartbeat),
56}
57
58struct Buffered<E> {
59 bytes: Vec<u8>,
60 start: usize,
61 decoder: fn(&[u8]) -> Result<E>,
62}
63
64impl<E> Buffered<E> {
65 fn remaining(&self) -> Result<usize> {
66 self.bytes
67 .len()
68 .checked_sub(self.start)
69 .ok_or(Error::Invariant("stream buffer cursor exceeds its length"))
70 }
71
72 fn next(&mut self) -> Option<Result<E>> {
73 let remaining = match self.remaining() {
74 Ok(value) => value,
75 Err(error) => return Some(Err(error)),
76 };
77 if remaining > MAX_BACKLOG_BYTES {
78 return Some(Err(Error::ResponseTooLarge));
79 }
80 loop {
81 let Some(offset) = self.bytes[self.start..]
82 .iter()
83 .position(|byte| *byte == b'\n')
84 else {
85 return (self.bytes.len() - self.start > MAX_RECORD_BYTES)
86 .then_some(Err(Error::ResponseTooLarge));
87 };
88 let newline = self.start + offset;
89 let end = if newline > self.start && self.bytes[newline - 1] == b'\r' {
90 newline - 1
91 } else {
92 newline
93 };
94 if end - self.start > MAX_RECORD_BYTES {
95 return Some(Err(Error::ResponseTooLarge));
96 }
97 let event = (end > self.start).then(|| (self.decoder)(&self.bytes[self.start..end]));
99 self.start = newline + 1;
100 if self.start == self.bytes.len() {
101 self.bytes.clear();
102 self.start = 0;
103 } else if self.start > MAX_RECORD_BYTES {
104 self.bytes.drain(..self.start);
105 self.start = 0;
106 }
107 if let Some(event) = event {
108 return Some(event);
109 }
110 }
111 }
112}
113
114#[must_use = "consume the finite prefix and handle errors before claiming it was drained"]
123pub struct BufferedEvents<E> {
124 buffered: Buffered<E>,
125 initial_error: Option<Error>,
126 finished: bool,
127}
128
129impl<E> std::fmt::Debug for BufferedEvents<E> {
130 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 formatter
132 .debug_struct("BufferedEvents")
133 .field(
134 "remaining_bytes",
135 &self.buffered.bytes.len().checked_sub(self.buffered.start),
136 )
137 .field("finished", &self.finished)
138 .finish_non_exhaustive()
139 }
140}
141
142impl<E> Iterator for BufferedEvents<E> {
143 type Item = Result<E>;
144
145 fn next(&mut self) -> Option<Self::Item> {
146 if self.finished {
147 return None;
148 }
149 if let Some(error) = self.initial_error.take() {
150 self.finished = true;
151 return Some(Err(error));
152 }
153 if let Some(event) = self.buffered.next() {
154 self.finished = event.is_err();
155 return Some(event);
156 }
157 self.finished = true;
158 match self.buffered.remaining() {
159 Ok(0) => None,
160 Ok(bytes) => Some(Err(Error::IncompleteStreamRecord { bytes })),
161 Err(error) => Some(Err(error)),
162 }
163 }
164}
165
166impl<E> std::iter::FusedIterator for BufferedEvents<E> {}
167
168pub struct HttpStream<E> {
170 response: Option<Response>,
171 buffered: Buffered<E>,
172 ended: bool,
173 lease: Option<StreamLease>,
174}
175
176impl<E> HttpStream<E> {
177 fn finish(&mut self) {
178 self.ended = true;
179 self.response.take();
180 self.lease.take();
181 }
182
183 pub fn close(mut self) -> BufferedEvents<E> {
196 let initial_error = self.ended.then_some(Error::StreamAlreadyEnded);
197 self.finish();
198 BufferedEvents {
199 buffered: self.buffered,
200 initial_error,
201 finished: false,
202 }
203 }
204
205 pub async fn next_event(&mut self) -> Option<Result<E>> {
207 if self.ended {
208 return None;
209 }
210 loop {
211 if let Some(result) = self.buffered.next() {
212 if result.is_err() {
213 self.finish();
214 }
215 return Some(result);
216 }
217 let Some(response) = self.response.as_mut() else {
218 self.finish();
219 return None;
220 };
221 match tokio::time::timeout(HEARTBEAT_TIMEOUT, response.chunk()).await {
222 Err(_) => {
223 self.finish();
224 return Some(Err(Error::Transport(
225 "OANDA stream heartbeat timed out; continuity must be recovered".into(),
226 )));
227 }
228 Ok(Ok(Some(chunk))) => {
229 self.buffered.bytes.extend_from_slice(&chunk);
230 match self.buffered.remaining() {
231 Ok(bytes) if bytes <= MAX_BACKLOG_BYTES => {}
232 Ok(_) => {
233 self.finish();
234 return Some(Err(Error::ResponseTooLarge));
235 }
236 Err(error) => {
237 self.finish();
238 return Some(Err(error));
239 }
240 }
241 }
242 Ok(Ok(None)) => {
243 self.finish();
244 return Some(Err(Error::Transport(
245 "OANDA stream ended; continuity must be recovered".into(),
246 )));
247 }
248 Ok(Err(error)) => {
249 self.finish();
250 return Some(Err(Error::Transport(error.to_string())));
251 }
252 }
253 }
254 }
255}
256
257impl Client {
258 pub(crate) async fn open_http_stream<E, Q: Validate + Serialize>(
259 &self,
260 path: &str,
261 query: Option<&Q>,
262 decoder: fn(&[u8]) -> Result<E>,
263 ) -> Result<HttpStream<E>> {
264 let query = query
265 .map(|query| {
266 query.validate()?;
267 serde_json::to_value(query).map_err(|error| Error::InvalidInput(error.to_string()))
268 })
269 .transpose()?;
270 let url = Self::build_url(&self.inner.stream, path, query.as_ref())?;
271 let lease = self.inner.admission.lease_stream()?;
272 let response = self
273 .inner
274 .stream_http
275 .get(url)
276 .bearer_auth(self.inner.token.as_str())
277 .header(
278 "Accept-Datetime-Format",
279 self.inner.datetime_format.as_str(),
280 )
281 .send()
282 .await
283 .map_err(|e| Error::Transport(e.to_string()))?;
284 if !response.status().is_success() {
285 let status = response.status().as_u16();
286 let body = crate::client::read_bounded(response, self.inner.max_body).await?;
287 let reason = serde_json::from_slice::<serde_json::Value>(&body).ok();
292 let code = reason
293 .as_ref()
294 .and_then(|value| value.get("errorCode"))
295 .and_then(serde_json::Value::as_str)
296 .map(str::to_owned);
297 let message = reason
298 .as_ref()
299 .and_then(|value| value.get("errorMessage"))
300 .and_then(serde_json::Value::as_str)
301 .map(str::to_owned);
302 return Err(Error::Provider {
303 status,
304 code,
305 message,
306 });
307 }
308 Ok(HttpStream {
309 response: Some(response),
310 buffered: Buffered {
311 bytes: Vec::new(),
312 start: 0,
313 decoder,
314 },
315 ended: false,
316 lease: Some(lease),
317 })
318 }
319
320 pub async fn stream_pricing(
327 &self,
328 account: &AccountID,
329 query: &crate::pricing::StreamPricingQuery,
330 ) -> Result<HttpStream<PriceStreamEvent>> {
331 let path = format!(
332 "/v3/accounts/{}/pricing/stream",
333 crate::client::segment(account.as_str())
334 );
335 self.open_http_stream(&path, Some(query), decode_price)
336 .await
337 }
338
339 pub async fn stream_transactions(
345 &self,
346 account: &AccountID,
347 ) -> Result<HttpStream<TransactionStreamEvent>> {
348 let path = format!(
349 "/v3/accounts/{}/transactions/stream",
350 crate::client::segment(account.as_str())
351 );
352 self.open_http_stream(&path, None::<&()>, decode_transaction)
353 .await
354 }
355}
356
357fn decode_price(line: &[u8]) -> Result<PriceStreamEvent> {
358 let value: serde_json::Value =
359 serde_json::from_slice(line).map_err(|e| Error::Decode(e.to_string()))?;
360 match value.get("type").and_then(serde_json::Value::as_str) {
361 Some("PRICE") => serde_json::from_value(value)
362 .map(Box::new)
363 .map(PriceStreamEvent::Price)
364 .map_err(|e| Error::Decode(e.to_string())),
365 Some("HEARTBEAT") => serde_json::from_value(value)
366 .map(PriceStreamEvent::Heartbeat)
367 .map_err(|e| Error::Decode(e.to_string())),
368 _ => Err(Error::Decode("unknown OANDA pricing stream record".into())),
369 }
370}
371
372fn decode_transaction(line: &[u8]) -> Result<TransactionStreamEvent> {
373 let value: serde_json::Value =
374 serde_json::from_slice(line).map_err(|e| Error::Decode(e.to_string()))?;
375 if value.get("type").and_then(serde_json::Value::as_str) == Some("HEARTBEAT") {
376 serde_json::from_value(value)
377 .map(TransactionStreamEvent::Heartbeat)
378 .map_err(|e| Error::Decode(e.to_string()))
379 } else {
380 serde_json::from_value(value)
381 .map(Box::new)
382 .map(TransactionStreamEvent::Transaction)
383 .map_err(|e| Error::Decode(e.to_string()))
384 }
385}
386
387#[cfg(test)]
388#[allow(clippy::expect_used, clippy::panic)]
391mod tests {
392 use super::*;
393 use crate::Environment;
394
395 fn from_buffer<E>(bytes: Vec<u8>, decoder: fn(&[u8]) -> Result<E>) -> HttpStream<E> {
396 HttpStream {
397 response: None,
398 buffered: Buffered {
399 bytes,
400 start: 0,
401 decoder,
402 },
403 ended: false,
404 lease: None,
405 }
406 }
407
408 fn heartbeat(id: u32) -> String {
409 format!(
410 "{{\"type\":\"HEARTBEAT\",\"lastTransactionID\":\"{id}\",\"time\":\"2024-01-02T03:04:05.000000001Z\"}}\r\n"
411 )
412 }
413
414 fn transaction_id(event: Result<TransactionStreamEvent>) -> String {
415 match event.expect("valid synthetic heartbeat") {
416 TransactionStreamEvent::Heartbeat(value) => {
417 value.last_transaction_id.expect("fixture id").to_string()
418 }
419 TransactionStreamEvent::Transaction(_) => panic!("fixture contains no transaction"),
420 }
421 }
422
423 #[tokio::test]
424 async fn closing_preserves_the_exact_remaining_complete_prefix_and_original_decoder() {
425 let bytes = format!("\r\n{}\n{}{}", heartbeat(10), heartbeat(11), heartbeat(12));
426 let mut stream = from_buffer(bytes.into_bytes(), decode_transaction);
427 assert_eq!(
428 transaction_id(stream.next_event().await.expect("first record")),
429 "10"
430 );
431 let mut buffered = stream.close();
432 assert_eq!(
433 transaction_id(buffered.next().expect("second record")),
434 "11"
435 );
436 assert_eq!(transaction_id(buffered.next().expect("third record")), "12");
437 assert!(buffered.next().is_none());
438 assert!(buffered.next().is_none());
439 let price = br#"{"type":"PRICE","instrument":"EUR_USD","closeoutBid":"1.2300","bids":[{"price":"1.229900","liquidity":17}]}"#;
440 let mut bytes = price.to_vec();
441 bytes.extend_from_slice(b"\r\n");
442 let mut buffered = from_buffer(bytes, decode_price).close();
443 match buffered
444 .next()
445 .expect("price")
446 .expect("typed pricing decode")
447 {
448 PriceStreamEvent::Price(value) => {
449 let bid = value.closeout_bid.expect("fixture bid");
450 assert_eq!(bid.to_string(), "1.2300");
451 assert_eq!(bid.scale(), 4);
452 assert_eq!(
453 value.bids.expect("fixture buckets")[0]
454 .price
455 .expect("bucket price")
456 .to_string(),
457 "1.229900"
458 );
459 }
460 PriceStreamEvent::Heartbeat(_) => panic!("fixture is a price"),
461 }
462 assert!(buffered.next().is_none());
463 }
464
465 #[tokio::test]
466 async fn a_large_buffered_burst_drains_in_order_without_a_record_count_limit() {
467 let mut bytes = Vec::new();
468 for id in 0..30_000 {
469 bytes.extend_from_slice(heartbeat(id).as_bytes());
470 }
471 assert!(bytes.len() > MAX_RECORD_BYTES && bytes.len() < MAX_BACKLOG_BYTES);
472 let server = crate::test_support::held_stream(&bytes).await;
473 let client = crate::test_support::fixture_client(server.url.clone());
474 let account = AccountID::new("101-001-1-001").expect("fixture account");
475 let mut stream = client
476 .stream_transactions(&account)
477 .await
478 .expect("real stream");
479 tokio::time::timeout(Duration::from_secs(5), preload(&mut stream, bytes.len()))
480 .await
481 .expect("finite coalesced HTTP prefix");
482 let mut buffered = stream.close();
483 for id in 0..30_000 {
484 assert_eq!(
485 transaction_id(buffered.next().expect("retained complete record")),
486 id.to_string()
487 );
488 }
489 assert!(buffered.next().is_none());
490 peer_closed(server).await;
491 }
492
493 #[test]
494 fn a_partial_tail_is_explicit_after_the_complete_prefix_and_then_fused() {
495 for tail in [b"{\"type\":".as_slice(), b"\r", b" "] {
496 let mut bytes = heartbeat(21).into_bytes();
497 bytes.extend_from_slice(tail);
498 let mut buffered = from_buffer(bytes, decode_transaction).close();
499 assert_eq!(
500 transaction_id(buffered.next().expect("complete record")),
501 "21"
502 );
503 assert!(
504 matches!(buffered.next(), Some(Err(Error::IncompleteStreamRecord { bytes })) if bytes == tail.len())
505 );
506 assert!(buffered.next().is_none());
507 assert!(buffered.next().is_none());
508 }
509 for empty in [b"".as_slice(), b"\n\r\n"] {
510 assert!(
511 from_buffer(empty.to_vec(), decode_transaction)
512 .close()
513 .next()
514 .is_none()
515 );
516 }
517 }
518
519 #[test]
520 fn malformed_and_oversized_prefixes_end_explicitly_without_skipping_to_later_records() {
521 let mut malformed = b"not-json\n".to_vec();
522 malformed.extend_from_slice(heartbeat(31).as_bytes());
523 let mut buffered = from_buffer(malformed, decode_transaction).close();
524 assert!(matches!(buffered.next(), Some(Err(Error::Decode(_)))));
525 assert!(buffered.next().is_none());
526 for mut oversized in [
527 vec![b'x'; MAX_RECORD_BYTES + 1],
528 vec![b'x'; MAX_BACKLOG_BYTES + 1],
529 ] {
530 let mut partial = from_buffer(oversized.clone(), decode_transaction).close();
531 assert!(matches!(partial.next(), Some(Err(Error::ResponseTooLarge))));
532 assert!(partial.next().is_none());
533 oversized.extend_from_slice(b"\n");
534 oversized.extend_from_slice(heartbeat(32).as_bytes());
535 let mut complete = from_buffer(oversized, decode_transaction).close();
536 assert!(matches!(
537 complete.next(),
538 Some(Err(Error::ResponseTooLarge))
539 ));
540 assert!(complete.next().is_none());
541 }
542 }
543
544 #[tokio::test]
545 async fn an_already_ended_generation_cannot_report_a_clean_buffered_cut() {
546 let mut stream = from_buffer(b"not-json\n".to_vec(), decode_transaction);
547 assert!(matches!(
548 stream.next_event().await,
549 Some(Err(Error::Decode(_)))
550 ));
551 let mut buffered = stream.close();
552 assert!(matches!(
553 buffered.next(),
554 Some(Err(Error::StreamAlreadyEnded))
555 ));
556 assert!(buffered.next().is_none());
557 }
558
559 #[test]
560 fn buffered_debug_keeps_raw_data_private_and_invalid_cursors_explicit() {
561 let mut stream = from_buffer(b"sensitive-fixture-body\n".to_vec(), decode_transaction);
562 assert!(!format!("{:?}", stream.close()).contains("sensitive-fixture-body"));
563 stream = from_buffer(Vec::new(), decode_transaction);
564 stream.buffered.start = 1;
565 let mut buffered = stream.close();
566 assert!(matches!(buffered.next(), Some(Err(Error::Invariant(_)))));
567 assert!(buffered.next().is_none());
568 }
569
570 async fn preload(stream: &mut HttpStream<TransactionStreamEvent>, bytes: usize) {
573 while stream.buffered.remaining().expect("valid buffer cursor") < bytes {
574 let chunk = stream
575 .response
576 .as_mut()
577 .expect("open response")
578 .chunk()
579 .await
580 .expect("real HTTP chunk")
581 .expect("held body remains open");
582 stream.buffered.bytes.extend_from_slice(&chunk);
583 }
584 assert_eq!(stream.buffered.remaining().expect("buffer length"), bytes);
585 }
586
587 async fn peer_closed(server: crate::test_support::HeldStream) {
588 tokio::time::timeout(Duration::from_secs(5), server.closed)
589 .await
590 .expect("owned HTTP peer did not close")
591 .expect("loopback server task");
592 }
593
594 #[tokio::test]
595 async fn cancelling_a_real_http_read_then_closing_retains_partial_bytes_and_releases_the_peer()
596 {
597 use futures_util::FutureExt;
598 for prefix in [b"".as_slice(), b"{\"type\":"] {
599 let server = crate::test_support::held_stream(prefix).await;
600 let client = crate::test_support::fixture_client(server.url.clone());
601 let account = AccountID::new("101-001-1-001").expect("fixture account");
602 let mut stream = client
603 .stream_transactions(&account)
604 .await
605 .expect("real stream");
606 tokio::time::timeout(Duration::from_secs(5), preload(&mut stream, prefix.len()))
607 .await
608 .expect("finite HTTP prefix");
609 let mut reading = Box::pin(stream.next_event());
610 assert!(reading.as_mut().now_or_never().is_none());
611 drop(reading);
612 let mut buffered = stream.close();
613 if prefix.is_empty() {
614 assert!(buffered.next().is_none());
615 } else {
616 assert!(
617 matches!(buffered.next(), Some(Err(Error::IncompleteStreamRecord { bytes })) if bytes == prefix.len())
618 );
619 }
620 assert!(buffered.next().is_none());
621 peer_closed(server).await;
622 }
623 }
624
625 #[tokio::test]
626 async fn real_http_close_releases_capacity_before_drain_and_preserves_sibling_stream_and_rest()
627 {
628 let owner = crate::Admission::with_exit_reserve(0).expect("admission owner");
629 let mut leases = Vec::new();
630 for _ in 0..18 {
631 leases.push(owner.lease_stream().expect("occupied sibling lease"));
632 }
633 let prefix = format!("{}{}", heartbeat(41), heartbeat(42));
634 let first = crate::test_support::held_stream(prefix.as_bytes()).await;
635 let sibling = crate::test_support::held_stream(heartbeat(43).as_bytes()).await;
636 let (rest, rest_server) = crate::test_support::server(crate::test_support::http(
637 "200 OK",
638 "application/json",
639 r#"{"accounts":[]}"#,
640 ))
641 .await;
642 let client = Client::builder(Environment::Practice, "fixture-secret")
643 .endpoints(rest.clone(), first.url.clone())
644 .admission(owner.clone())
645 .build()
646 .expect("client");
647 let other = Client::builder(Environment::Practice, "another-fixture-secret")
648 .endpoints(rest, sibling.url.clone())
649 .admission(owner.clone())
650 .build()
651 .expect("sibling client");
652 let account = AccountID::new("101-001-1-001").expect("fixture account");
653 let mut stream = client
654 .stream_transactions(&account)
655 .await
656 .expect("first real stream");
657 let mut sibling_stream = other
658 .stream_transactions(&account)
659 .await
660 .expect("sibling real stream");
661 assert!(matches!(owner.lease_stream(), Err(Error::RateLimited)));
662 assert_eq!(
663 transaction_id(stream.next_event().await.expect("first record")),
664 "41"
665 );
666 tokio::time::timeout(
667 Duration::from_secs(5),
668 preload(&mut stream, heartbeat(42).len()),
669 )
670 .await
671 .expect("remaining finite prefix");
672 let mut buffered = stream.close();
673 let reused = owner
674 .lease_stream()
675 .expect("closed stream slot is reusable before iterator consumption");
676 peer_closed(first).await;
677 assert_eq!(
678 transaction_id(
679 sibling_stream
680 .next_event()
681 .await
682 .expect("sibling still live")
683 ),
684 "43"
685 );
686 assert!(
687 client
688 .list_accounts()
689 .await
690 .expect("REST remains available")
691 .body
692 .accounts
693 .expect("explicit account list")
694 .is_empty()
695 );
696 rest_server.await.expect("REST server");
697 assert_eq!(
698 transaction_id(buffered.next().expect("undelivered record")),
699 "42"
700 );
701 assert!(buffered.next().is_none());
702 drop(reused);
703 drop(leases);
704 drop(sibling_stream.close());
705 peer_closed(sibling).await;
706 }
707
708 #[tokio::test]
709 async fn real_http_buffered_malformed_oversized_and_unterminated_records_are_not_clean_drains()
710 {
711 let mut malformed = b"not-json\n".to_vec();
712 malformed.extend_from_slice(heartbeat(51).as_bytes());
713 let mut oversized = vec![b'x'; MAX_RECORD_BYTES + 1];
714 oversized.push(b'\n');
715 for (index, prefix) in [malformed, oversized, b"{\"type\":".to_vec()]
716 .into_iter()
717 .enumerate()
718 {
719 let server = crate::test_support::held_stream(&prefix).await;
720 let client = crate::test_support::fixture_client(server.url.clone());
721 let account = AccountID::new("101-001-1-001").expect("fixture account");
722 let mut stream = client
723 .stream_transactions(&account)
724 .await
725 .expect("real stream");
726 tokio::time::timeout(Duration::from_secs(5), preload(&mut stream, prefix.len()))
727 .await
728 .expect("finite HTTP prefix");
729 let mut buffered = stream.close();
730 match (index, buffered.next()) {
731 (0, Some(Err(Error::Decode(_)))) | (1, Some(Err(Error::ResponseTooLarge))) => {}
732 (2, Some(Err(Error::IncompleteStreamRecord { bytes }))) => {
733 assert_eq!(bytes, prefix.len());
734 }
735 (_, observed) => panic!("buffered error was lost or changed: {observed:?}"),
736 }
737 assert!(buffered.next().is_none());
738 peer_closed(server).await;
739 }
740 }
741
742 #[tokio::test]
743 async fn an_errored_real_http_generation_cannot_be_closed_as_a_clean_prefix() {
744 let (url, server) = crate::test_support::server(crate::test_support::http(
745 "200 OK",
746 "application/octet-stream",
747 "not-json\n",
748 ))
749 .await;
750 let client = crate::test_support::fixture_client(url);
751 let account = AccountID::new("101-001-1-001").expect("fixture account");
752 let mut stream = client
753 .stream_transactions(&account)
754 .await
755 .expect("real stream");
756 assert!(matches!(
757 stream.next_event().await,
758 Some(Err(Error::Decode(_)))
759 ));
760 let mut buffered = stream.close();
761 assert!(matches!(
762 buffered.next(),
763 Some(Err(Error::StreamAlreadyEnded))
764 ));
765 assert!(buffered.next().is_none());
766 server.await.expect("real HTTP server");
767 }
768
769 #[tokio::test]
770 async fn cancelled_stream_owner_releases_its_connection_slot() {
771 let client = Client::builder(Environment::Practice, "fixture").build();
772 assert!(client.is_ok());
773 let Ok(client) = client else { return };
774 if let Ok(mut slots) = client.inner.admission.inner.stream_slots.lock() {
775 slots.active = 1;
776 }
777 let lease = StreamLease {
778 owner: client.inner.admission.clone(),
779 };
780 let task = tokio::spawn(async move {
781 let _lease = lease;
782 std::future::pending::<()>().await;
783 });
784 tokio::task::yield_now().await;
785 task.abort();
786 let _ = task.await;
787 if let Ok(slots) = client.inner.admission.inner.stream_slots.lock() {
788 assert_eq!(slots.active, 0);
789 }
790 }
791
792 #[tokio::test]
793 async fn a_backlog_beyond_the_local_bound_ends_the_generation() {
794 let mut buffer = Vec::new();
800 for index in 0..200_000_u32 {
801 buffer.extend_from_slice(
802 format!(
803 "{{\"type\":\"HEARTBEAT\",\"lastTransactionID\":\"{index}\",\
804 \"time\":\"2024-01-02T03:04:05.000000000Z\"}}\r\n"
805 )
806 .as_bytes(),
807 );
808 }
809 assert!(
810 buffer.len() > MAX_BACKLOG_BYTES,
811 "fixture must exceed the cap"
812 );
813 let mut stream = HttpStream {
814 response: None,
815 buffered: Buffered {
816 bytes: buffer,
817 start: 0,
818 decoder: decode_transaction,
819 },
820 ended: false,
821 lease: None,
822 };
823 assert!(matches!(
824 stream.next_event().await,
825 Some(Err(Error::ResponseTooLarge))
826 ));
827 assert!(stream.next_event().await.is_none());
828 }
829
830 #[tokio::test]
831 async fn a_cancelled_stream_releases_its_slot_even_after_a_poisoned_lock() {
832 let client = Client::builder(Environment::Practice, "fixture").build();
833 assert!(client.is_ok());
834 let Ok(client) = client else { return };
835 if let Ok(mut slots) = client.inner.admission.inner.stream_slots.lock() {
836 slots.active = 1;
837 }
838 let lease = StreamLease {
839 owner: client.inner.admission.clone(),
840 };
841 let poisoned = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
842 let Ok(_held) = client.inner.admission.inner.stream_slots.lock() else {
843 return;
844 };
845 panic!("poison the lock");
846 }));
847 assert!(poisoned.is_err());
848 drop(lease);
849 let slots = client
850 .inner
851 .admission
852 .inner
853 .stream_slots
854 .lock()
855 .unwrap_or_else(std::sync::PoisonError::into_inner);
856 assert_eq!(
857 slots.active, 0,
858 "a released connection must not be reported as capacity in use"
859 );
860 }
861}