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