1use std::{
27 sync::{
28 Arc,
29 atomic::{AtomicBool, AtomicU64, Ordering},
30 },
31 time::Duration,
32};
33
34use ahash::AHashMap;
35use nautilus_core::string::secret::SecretString;
36use nautilus_live::task::{SharedTaskSlot, TaskJoinOutcome};
37use nautilus_network::{
38 RECONNECTED,
39 websocket::{AuthTracker, WebSocketClient},
40};
41use serde_json::Value;
42use tokio_tungstenite::tungstenite::Message;
43
44use super::{
45 client::UNAUTHENTICATED_CONNECTION_EPOCH,
46 error::DeriveWsError,
47 messages::{DeriveWsChannel, DeriveWsFrame, WsSubscribeParams, WsSubscriptionPayload},
48};
49use crate::http::models::JsonRpcRequest;
50
51#[derive(Debug)]
53pub(super) enum HandlerCommand {
54 SetClient(WebSocketClient),
56 Request {
60 method: &'static str,
61 params: SecretString,
62 connection_epoch: Option<u64>,
63 response_tx: tokio::sync::oneshot::Sender<Result<Value, DeriveWsError>>,
64 },
65 Disconnect,
67}
68
69#[derive(Debug, Clone)]
71pub enum DeriveWsMessage {
72 Authenticated,
75 Reconnected,
78 SessionRecoveryFailed(String),
80 Subscription(WsSubscriptionPayload),
82}
83
84#[derive(Debug)]
85struct SendCommand {
86 id: u64,
87 token: u64,
88 connection_epoch: u64,
89 payload: SecretString,
90}
91
92#[derive(Debug)]
93struct SendFailure {
94 id: u64,
95 token: u64,
96 reason: String,
97}
98
99#[derive(Debug)]
100struct PendingRequest {
101 token: u64,
102 response_tx: tokio::sync::oneshot::Sender<Result<Value, DeriveWsError>>,
103}
104
105pub(super) struct FeedHandler {
108 signal: Arc<AtomicBool>,
109 client: Option<Arc<WebSocketClient>>,
110 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
111 cmd_closed: bool,
112 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
113 raw_closed: bool,
114 next_id: Arc<AtomicU64>,
115 next_send_token: u64,
116 pending: AHashMap<u64, PendingRequest>,
117 send_tx: Option<tokio::sync::mpsc::UnboundedSender<SendCommand>>,
118 send_failure_rx: tokio::sync::mpsc::UnboundedReceiver<SendFailure>,
119 send_task: Arc<SharedTaskSlot<()>>,
120 auth_tracker: AuthTracker,
121 authenticated_epoch: Arc<AtomicU64>,
122}
123
124impl FeedHandler {
125 pub(super) fn new_with_send_task(
126 signal: Arc<AtomicBool>,
127 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
128 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
129 next_id: Arc<AtomicU64>,
130 auth_tracker: AuthTracker,
131 authenticated_epoch: Arc<AtomicU64>,
132 send_task: Arc<SharedTaskSlot<()>>,
133 ) -> Self {
134 let (_, send_failure_rx) = tokio::sync::mpsc::unbounded_channel();
135 Self {
136 signal,
137 client: None,
138 cmd_rx,
139 cmd_closed: false,
140 raw_rx,
141 raw_closed: false,
142 next_id,
143 next_send_token: 0,
144 pending: AHashMap::new(),
145 send_tx: None,
146 send_failure_rx,
147 send_task,
148 auth_tracker,
149 authenticated_epoch,
150 }
151 }
152
153 pub(super) async fn next(&mut self) -> Option<DeriveWsMessage> {
157 loop {
158 tokio::select! {
159 cmd = self.cmd_rx.recv(), if !self.cmd_closed => {
160 match cmd {
161 None => {
162 self.cmd_closed = true;
163
164 if self.raw_closed {
165 self.shutdown_send_path(
166 "WebSocket handler stopped before response was received",
167 )
168 .await;
169 return None;
170 }
171 }
172 Some(HandlerCommand::SetClient(client)) => {
173 log::debug!("Setting WebSocket client in Derive handler");
174 let client = Arc::new(client);
175 self.stop_send_worker().await;
176 self.start_send_worker(Arc::clone(&client));
177 self.client = Some(client);
178 }
179 Some(HandlerCommand::Request {
180 method,
181 params,
182 connection_epoch,
183 response_tx,
184 }) => {
185 self.dispatch_request(method, params, connection_epoch, response_tx);
186 }
187 Some(HandlerCommand::Disconnect) => {
188 log::debug!("Derive handler received disconnect command");
189 self.shutdown_send_path(
190 "WebSocket disconnected before response was received",
191 )
192 .await;
193
194 if let Some(ref client) = self.client {
195 client.disconnect().await;
196 }
197 self.signal.store(true, Ordering::SeqCst);
198 return None;
199 }
200 }
201 }
202
203 raw = self.raw_rx.recv(), if !self.raw_closed => {
204 match raw {
205 None => {
206 self.raw_closed = true;
207
208 if self.cmd_closed {
209 self.shutdown_send_path(
210 "WebSocket handler stopped before response was received",
211 )
212 .await;
213 return None;
214 }
215 }
216 Some(Message::Text(text)) => {
217 if text.as_str() == RECONNECTED {
218 log::info!("Derive WebSocket reconnected sentinel received");
219 self.auth_tracker.invalidate();
220 self.authenticated_epoch.store(
221 UNAUTHENTICATED_CONNECTION_EPOCH,
222 Ordering::Release,
223 );
224 self.restart_send_worker(
225 "WebSocket reconnected before response was received",
226 )
227 .await;
228 return Some(DeriveWsMessage::Reconnected);
229 }
230
231 match DeriveWsFrame::parse(&text) {
232 Ok(DeriveWsFrame::Response { id, result, error }) => {
233 if let Some(pending) = self.pending.remove(&id) {
234 let outcome = match (result, error) {
235 (_, Some(err)) => Err(DeriveWsError::JsonRpc {
236 code: err.code,
237 message: err.message,
238 data: err.data,
239 }),
240 (Some(value), None) => Ok(value),
241 (None, None) => Ok(Value::Null),
242 };
243 let _ = pending.response_tx.send(outcome);
244 } else {
245 log::debug!(
246 "Derive WebSocket response with unknown id={id} dropped",
247 );
248 }
249 }
250 Ok(DeriveWsFrame::Subscription(payload)) => {
251 return Some(DeriveWsMessage::Subscription(payload));
252 }
253 Ok(DeriveWsFrame::UncorrelatedError(error)) => {
254 self.fail_uncorrelated_error(error);
255 }
256 Ok(DeriveWsFrame::Unknown(value)) => {
257 log::debug!("Derive WebSocket unknown frame: {value}");
258 }
259 Err(e) => {
260 log::error!(
261 "Derive WebSocket frame parse error: {e}, text: {text}",
262 );
263 }
264 }
265 }
266 Some(Message::Ping(data)) => {
267 if let Some(ref client) = self.client
268 && let Err(e) = client.send_pong(data.to_vec()).await {
269 log::error!("Derive WebSocket send_pong failed: {e}");
270 }
271 }
272 Some(Message::Close(_)) => {
273 log::debug!("Derive WebSocket close frame received");
274 self.shutdown_send_path(
275 "WebSocket closed before response was received",
276 )
277 .await;
278 return None;
279 }
280 Some(_) => {}
281 }
282 }
283
284 Some(failure) = self.send_failure_rx.recv() => {
285 self.handle_send_failure(failure);
286 }
287
288 else => {
289 log::debug!("Derive handler shutting down: channels closed");
290 self.shutdown_send_path(
291 "WebSocket handler stopped before response was received",
292 )
293 .await;
294 return None;
295 }
296 }
297 }
298 }
299
300 fn dispatch_request(
301 &mut self,
302 method: &'static str,
303 params: SecretString,
304 connection_epoch: Option<u64>,
305 response_tx: tokio::sync::oneshot::Sender<Result<Value, DeriveWsError>>,
306 ) {
307 let Some(client) = self.client.as_ref() else {
308 let _ = response_tx.send(Err(DeriveWsError::NotConnected));
309 return;
310 };
311 let current_epoch = client.connection_epoch();
312 let connection_epoch = if method.starts_with("private/") {
313 let authenticated_epoch = self.authenticated_epoch.load(Ordering::Acquire);
314 if !client.connection_mode().is_active() || authenticated_epoch != current_epoch {
315 let _ = response_tx.send(Err(DeriveWsError::Authentication {
316 operation: method.to_string(),
317 reason: "WebSocket session is not authenticated".to_string(),
318 }));
319 return;
320 }
321 authenticated_epoch
322 } else {
323 connection_epoch.unwrap_or(current_epoch)
324 };
325
326 if !client.connection_mode().is_active() || connection_epoch != current_epoch {
327 let _ = response_tx.send(Err(DeriveWsError::transport(format!(
328 "connection changed before `{method}` was sent",
329 ))));
330 return;
331 }
332
333 self.enqueue_request(method, params, connection_epoch, response_tx);
334 }
335
336 fn enqueue_request(
337 &mut self,
338 method: &'static str,
339 params_json: SecretString,
340 connection_epoch: u64,
341 response_tx: tokio::sync::oneshot::Sender<Result<Value, DeriveWsError>>,
342 ) {
343 let Some(send_tx) = self.send_tx.clone() else {
344 let _ = response_tx.send(Err(DeriveWsError::NotConnected));
345 return;
346 };
347 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
348 let token = self.next_send_token;
349 self.next_send_token = self.next_send_token.wrapping_add(1);
350 let params: Value = match serde_json::from_str(params_json.expose_secret()) {
351 Ok(params) => params,
352 Err(e) => {
353 let _ = response_tx.send(Err(DeriveWsError::Serde(e)));
354 return;
355 }
356 };
357 drop(params_json);
358 let request = JsonRpcRequest::new(id, method, params);
359 let payload = match serde_json::to_string(&request) {
360 Ok(payload) => SecretString::from(payload),
361 Err(e) => {
362 let _ = response_tx.send(Err(DeriveWsError::Serde(e)));
363 return;
364 }
365 };
366 self.pending
367 .insert(id, PendingRequest { token, response_tx });
368 log::debug!("Derive WebSocket sending `{method}` id={id}");
369 if let Err(e) = send_tx.send(SendCommand {
370 id,
371 token,
372 connection_epoch,
373 payload,
374 }) && self
375 .pending
376 .get(&id)
377 .is_some_and(|pending| pending.token == token)
378 && let Some(pending) = self.pending.remove(&id)
379 {
380 let _ = pending
381 .response_tx
382 .send(Err(DeriveWsError::transport(format!(
383 "failed to queue WebSocket request: {e}",
384 ))));
385 }
386 }
387
388 fn start_send_worker(&mut self, client: Arc<WebSocketClient>) {
389 if !self.send_task.is_empty() {
390 log::error!("Cannot start Derive WebSocket send worker while the prior task is owned");
391 return;
392 }
393
394 let (send_tx, send_rx) = tokio::sync::mpsc::unbounded_channel();
395 let (failure_tx, failure_rx) = tokio::sync::mpsc::unbounded_channel();
396 self.send_tx = Some(send_tx);
397 self.send_failure_rx = failure_rx;
398
399 if let Err(e) = self
400 .send_task
401 .spawn(run_send_worker(client, send_rx, failure_tx))
402 {
403 self.send_tx.take();
404 log::error!("Failed to start Derive WebSocket send worker: {e}");
405 }
406 }
407
408 async fn stop_send_worker(&mut self) {
409 self.send_tx.take();
410
411 if let Some(outcome) = self
412 .send_task
413 .finish(Duration::from_secs(1), Duration::from_secs(2))
414 .await
415 {
416 match outcome {
417 TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted => {}
418 TaskJoinOutcome::Failed(e) => {
419 log::error!("Derive WebSocket send worker failed: {e}");
420 }
421 TaskJoinOutcome::Incomplete => {
422 log::error!("Derive WebSocket send worker did not stop after abort");
423 }
424 }
425 }
426 }
427
428 async fn restart_send_worker(&mut self, reason: &str) {
429 self.stop_send_worker().await;
430 self.fail_pending(reason);
431
432 if let Some(client) = self.client.clone() {
433 self.start_send_worker(client);
434 }
435 }
436
437 async fn shutdown_send_path(&mut self, reason: &str) {
438 self.stop_send_worker().await;
439 self.fail_pending(reason);
440 }
441
442 fn handle_send_failure(&mut self, failure: SendFailure) {
443 if !self
444 .pending
445 .get(&failure.id)
446 .is_some_and(|pending| pending.token == failure.token)
447 {
448 return;
449 }
450
451 if let Some(pending) = self.pending.remove(&failure.id) {
452 let _ = pending
453 .response_tx
454 .send(Err(DeriveWsError::transport(failure.reason)));
455 }
456 }
457
458 fn fail_uncorrelated_error(&mut self, error: crate::http::models::JsonRpcError) {
459 if self.pending.len() != 1 {
460 log::warn!(
461 "Derive WebSocket uncorrelated JSON-RPC error with {} pending requests: code={}, message={}",
462 self.pending.len(),
463 error.code,
464 error.message,
465 );
466 return;
467 }
468
469 if let Some((_, pending)) = self.pending.drain().next() {
470 let _ = pending.response_tx.send(Err(DeriveWsError::JsonRpc {
471 code: error.code,
472 message: error.message,
473 data: error.data,
474 }));
475 }
476 }
477
478 fn fail_pending(&mut self, reason: &str) {
479 if self.pending.is_empty() {
480 return;
481 }
482 log::debug!(
483 "Failing {} pending Derive WebSocket request(s): {reason}",
484 self.pending.len(),
485 );
486
487 for (_, pending) in self.pending.drain() {
488 let _ = pending
489 .response_tx
490 .send(Err(DeriveWsError::transport(reason.to_string())));
491 }
492 }
493}
494
495impl Drop for FeedHandler {
496 fn drop(&mut self) {
497 self.send_tx.take();
498 self.send_task.abort();
499 }
500}
501
502async fn run_send_worker(
503 client: Arc<WebSocketClient>,
504 mut send_rx: tokio::sync::mpsc::UnboundedReceiver<SendCommand>,
505 failure_tx: tokio::sync::mpsc::UnboundedSender<SendFailure>,
506) {
507 while let Some(command) = send_rx.recv().await {
508 if let Err(e) = client
509 .send_text_on_connection(
510 command.payload.expose_secret().to_owned(),
511 None,
512 command.connection_epoch,
513 )
514 .await
515 && failure_tx
516 .send(SendFailure {
517 id: command.id,
518 token: command.token,
519 reason: e.to_string(),
520 })
521 .is_err()
522 {
523 return;
524 }
525 }
526}
527
528#[must_use]
530pub(super) fn subscribe_params(channel: DeriveWsChannel) -> WsSubscribeParams {
531 WsSubscribeParams {
532 channels: vec![channel],
533 }
534}
535
536#[must_use]
539pub(super) fn ticker_subscribe_params(instrument_name: &str, interval: &str) -> WsSubscribeParams {
540 subscribe_params(DeriveWsChannel::ticker_slim(instrument_name, interval))
541}
542
543#[must_use]
546pub(super) fn orderbook_subscribe_params(
547 instrument_name: &str,
548 group: &str,
549 depth: &str,
550) -> WsSubscribeParams {
551 subscribe_params(DeriveWsChannel::orderbook(instrument_name, group, depth))
552}
553
554#[must_use]
557pub(super) fn trades_subscribe_params(instrument_type: &str, currency: &str) -> WsSubscribeParams {
558 subscribe_params(DeriveWsChannel::trades(instrument_type, currency))
559}
560
561#[cfg(test)]
562mod tests {
563 use nautilus_core::string::secret::zeroize_json_value;
564 use rstest::rstest;
565 use serde_json::json;
566
567 use super::*;
568
569 fn unauthenticated_epoch() -> Arc<AtomicU64> {
570 Arc::new(AtomicU64::new(u64::MAX))
571 }
572
573 fn secret_json(mut value: Value) -> SecretString {
574 let serialized = SecretString::from(value.to_string());
575 zeroize_json_value(&mut value);
576 serialized
577 }
578
579 #[rstest]
580 fn test_queued_request_debug_redacts_payload() {
581 let secret = "signature-secret";
582 let (response_tx, _response_rx) = tokio::sync::oneshot::channel();
583 let command = HandlerCommand::Request {
584 method: "public/login",
585 params: SecretString::from(format!(r#"{{"signature":"{secret}"}}"#)),
586 connection_epoch: None,
587 response_tx,
588 };
589 let send = SendCommand {
590 id: 1,
591 token: 1,
592 connection_epoch: 1,
593 payload: SecretString::from(format!(r#"{{"signature":"{secret}"}}"#)),
594 };
595
596 let debug = format!("{command:?} {send:?}");
597
598 assert!(!debug.contains(secret));
599 }
600
601 fn feed_handler(
602 signal: Arc<AtomicBool>,
603 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
604 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
605 next_id: Arc<AtomicU64>,
606 auth_tracker: AuthTracker,
607 authenticated_epoch: Arc<AtomicU64>,
608 ) -> FeedHandler {
609 FeedHandler::new_with_send_task(
610 signal,
611 cmd_rx,
612 raw_rx,
613 next_id,
614 auth_tracker,
615 authenticated_epoch,
616 Arc::new(SharedTaskSlot::new()),
617 )
618 }
619
620 #[rstest]
621 fn test_subscribe_params_carries_single_channel() {
622 let params = subscribe_params(DeriveWsChannel::ticker_slim("ETH-PERP", "1000"));
623 assert_eq!(
624 params.channels,
625 vec![DeriveWsChannel::ticker_slim("ETH-PERP", "1000")],
626 );
627 }
628
629 #[rstest]
630 fn test_ticker_subscribe_params_formats_topic() {
631 let params = ticker_subscribe_params("ETH-PERP", "1000");
632 assert_eq!(
633 params.channels,
634 vec![DeriveWsChannel::ticker_slim("ETH-PERP", "1000")],
635 );
636 }
637
638 #[rstest]
639 fn test_orderbook_subscribe_params_formats_topic() {
640 let params = orderbook_subscribe_params("ETH-PERP", "1", "10");
641 assert_eq!(
642 params.channels,
643 vec![DeriveWsChannel::orderbook("ETH-PERP", "1", "10")],
644 );
645 }
646
647 #[rstest]
648 fn test_trades_subscribe_params_formats_topic() {
649 let params = trades_subscribe_params("perp", "ETH");
650 assert_eq!(
651 params.channels,
652 vec![DeriveWsChannel::trades("perp", "ETH")],
653 );
654 }
655
656 #[rstest]
657 #[tokio::test]
658 async fn test_dispatch_request_without_client_returns_not_connected() {
659 let signal = Arc::new(AtomicBool::new(false));
661 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
662 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
663 let next_id = Arc::new(AtomicU64::new(1));
664 let auth_tracker = AuthTracker::new();
665 let mut handler = feed_handler(
666 signal,
667 cmd_rx,
668 raw_rx,
669 next_id,
670 auth_tracker,
671 unauthenticated_epoch(),
672 );
673
674 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
675 let params =
676 secret_json(serde_json::to_value(WsSubscribeParams { channels: vec![] }).unwrap());
677 handler.dispatch_request("public/login", params, None, response_tx);
678
679 let outcome = response_rx.await.expect("oneshot resolved");
680 match outcome {
681 Err(DeriveWsError::NotConnected) => {}
682 other => panic!("expected NotConnected, was {other:?}"),
683 }
684 }
685
686 #[rstest]
687 #[tokio::test]
688 async fn test_dispatch_registers_pending_requests_before_ordered_queueing() {
689 let signal = Arc::new(AtomicBool::new(false));
690 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
691 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
692 let next_id = Arc::new(AtomicU64::new(1));
693 let auth_tracker = AuthTracker::new();
694 let mut handler = feed_handler(
695 signal,
696 cmd_rx,
697 raw_rx,
698 next_id,
699 auth_tracker,
700 unauthenticated_epoch(),
701 );
702 let (send_tx, mut send_rx) = tokio::sync::mpsc::unbounded_channel();
703 handler.send_tx = Some(send_tx);
704
705 let (first_tx, _first_rx) = tokio::sync::oneshot::channel();
706 let (second_tx, _second_rx) = tokio::sync::oneshot::channel();
707 handler.enqueue_request("first", secret_json(json!({"sequence": 1})), 0, first_tx);
708 handler.enqueue_request("second", secret_json(json!({"sequence": 2})), 0, second_tx);
709
710 let first = send_rx.recv().await.expect("first queued send");
711 let second = send_rx.recv().await.expect("second queued send");
712 let first_payload: Value = serde_json::from_str(first.payload.expose_secret()).unwrap();
713 let second_payload: Value = serde_json::from_str(second.payload.expose_secret()).unwrap();
714
715 assert_eq!(first.id, 1);
716 assert_eq!(second.id, 2);
717 assert_eq!(first_payload["method"], "first");
718 assert_eq!(second_payload["method"], "second");
719 assert_eq!(handler.pending.get(&first.id).unwrap().token, first.token);
720 assert_eq!(handler.pending.get(&second.id).unwrap().token, second.token);
721 }
722
723 #[rstest]
724 #[tokio::test]
725 async fn test_late_send_failure_does_not_remove_reused_request_id() {
726 let signal = Arc::new(AtomicBool::new(false));
727 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
728 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
729 let next_id = Arc::new(AtomicU64::new(1));
730 let auth_tracker = AuthTracker::new();
731 let mut handler = feed_handler(
732 signal,
733 cmd_rx,
734 raw_rx,
735 Arc::clone(&next_id),
736 auth_tracker,
737 unauthenticated_epoch(),
738 );
739 let (send_tx, mut send_rx) = tokio::sync::mpsc::unbounded_channel();
740 handler.send_tx = Some(send_tx);
741
742 let (old_tx, old_rx) = tokio::sync::oneshot::channel();
743 handler.enqueue_request("first", secret_json(json!({})), 0, old_tx);
744 let old_send = send_rx.recv().await.expect("old queued send");
745 let old_pending = handler.pending.remove(&old_send.id).unwrap();
746 old_pending.response_tx.send(Ok(Value::Null)).unwrap();
747 old_rx.await.unwrap().unwrap();
748
749 next_id.store(old_send.id, Ordering::Relaxed);
750 let (new_tx, new_rx) = tokio::sync::oneshot::channel();
751 handler.enqueue_request("second", secret_json(json!({})), 0, new_tx);
752 let new_send = send_rx.recv().await.expect("new queued send");
753 handler.handle_send_failure(SendFailure {
754 id: old_send.id,
755 token: old_send.token,
756 reason: "late failure".to_string(),
757 });
758
759 assert_eq!(old_send.id, new_send.id);
760 assert_ne!(old_send.token, new_send.token);
761 assert_eq!(
762 handler.pending.get(&new_send.id).unwrap().token,
763 new_send.token,
764 );
765
766 handler.handle_send_failure(SendFailure {
767 id: new_send.id,
768 token: new_send.token,
769 reason: "current failure".to_string(),
770 });
771 let error = new_rx.await.unwrap().expect_err("current request failed");
772 assert!(error.to_string().contains("current failure"));
773 }
774
775 #[rstest]
776 #[tokio::test]
777 async fn test_shutdown_aborts_send_worker_and_drains_pending_requests() {
778 let signal = Arc::new(AtomicBool::new(false));
779 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
780 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
781 let next_id = Arc::new(AtomicU64::new(1));
782 let auth_tracker = AuthTracker::new();
783 let mut handler = feed_handler(
784 signal,
785 cmd_rx,
786 raw_rx,
787 next_id,
788 auth_tracker,
789 unauthenticated_epoch(),
790 );
791 let (send_tx, _send_rx) = tokio::sync::mpsc::unbounded_channel();
792 handler.send_tx = Some(send_tx);
793
794 let task = tokio::spawn(std::future::pending::<()>());
795 let abort_handle = task.abort_handle();
796 handler.send_task.insert(task);
797
798 let (first_tx, first_rx) = tokio::sync::oneshot::channel();
799 let (second_tx, second_rx) = tokio::sync::oneshot::channel();
800 handler.enqueue_request("first", secret_json(json!({})), 0, first_tx);
801 handler.enqueue_request("second", secret_json(json!({})), 0, second_tx);
802 handler.shutdown_send_path("disconnect requested").await;
803 tokio::task::yield_now().await;
804
805 let first_error = first_rx.await.unwrap().expect_err("first request failed");
806 let second_error = second_rx.await.unwrap().expect_err("second request failed");
807 assert!(abort_handle.is_finished());
808 assert!(handler.send_tx.is_none());
809 assert!(handler.send_task.is_empty());
810 assert!(handler.pending.is_empty());
811 assert!(first_error.to_string().contains("disconnect requested"));
812 assert!(second_error.to_string().contains("disconnect requested"));
813 }
814
815 #[rstest]
816 #[tokio::test]
817 async fn test_next_stops_when_input_channels_close_with_failure_channel_open() {
818 let signal = Arc::new(AtomicBool::new(false));
819 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
820 let (raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
821 let next_id = Arc::new(AtomicU64::new(1));
822 let auth_tracker = AuthTracker::new();
823 let mut handler = feed_handler(
824 signal,
825 cmd_rx,
826 raw_rx,
827 next_id,
828 auth_tracker,
829 unauthenticated_epoch(),
830 );
831 let (_failure_tx, failure_rx) = tokio::sync::mpsc::unbounded_channel();
832 handler.send_failure_rx = failure_rx;
833 drop(cmd_tx);
834 drop(raw_tx);
835
836 let outcome = tokio::time::timeout(std::time::Duration::from_millis(100), handler.next())
837 .await
838 .expect("handler stopped after both input channels closed");
839
840 assert!(outcome.is_none());
841 }
842
843 #[rstest]
844 #[tokio::test]
845 async fn test_uncorrelated_error_fails_only_pending_request() {
846 let signal = Arc::new(AtomicBool::new(false));
847 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
848 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
849 let next_id = Arc::new(AtomicU64::new(1));
850 let auth_tracker = AuthTracker::new();
851 let mut handler = feed_handler(
852 signal,
853 cmd_rx,
854 raw_rx,
855 next_id,
856 auth_tracker,
857 unauthenticated_epoch(),
858 );
859 let (send_tx, _send_rx) = tokio::sync::mpsc::unbounded_channel();
860 handler.send_tx = Some(send_tx);
861 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
862 handler.enqueue_request("request", secret_json(json!({})), 0, response_tx);
863
864 handler.fail_uncorrelated_error(crate::http::models::JsonRpcError {
865 code: -32700,
866 message: "Parse error".to_string(),
867 data: Some(json!("invalid JSON")),
868 });
869 let error = response_rx.await.unwrap().expect_err("request failed");
870
871 match error {
872 DeriveWsError::JsonRpc {
873 code,
874 message,
875 data,
876 } => {
877 assert_eq!(code, -32700);
878 assert_eq!(message, "Parse error");
879 assert_eq!(data, Some(json!("invalid JSON")));
880 }
881 other => panic!("expected JsonRpc, was {other:?}"),
882 }
883 assert!(handler.pending.is_empty());
884 }
885
886 #[rstest]
887 #[tokio::test]
888 async fn test_uncorrelated_error_does_not_guess_between_pending_requests() {
889 let signal = Arc::new(AtomicBool::new(false));
890 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
891 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
892 let next_id = Arc::new(AtomicU64::new(1));
893 let auth_tracker = AuthTracker::new();
894 let mut handler = feed_handler(
895 signal,
896 cmd_rx,
897 raw_rx,
898 next_id,
899 auth_tracker,
900 unauthenticated_epoch(),
901 );
902 let (send_tx, _send_rx) = tokio::sync::mpsc::unbounded_channel();
903 handler.send_tx = Some(send_tx);
904 let (first_tx, first_rx) = tokio::sync::oneshot::channel();
905 let (second_tx, second_rx) = tokio::sync::oneshot::channel();
906 handler.enqueue_request("first", secret_json(json!({})), 0, first_tx);
907 handler.enqueue_request("second", secret_json(json!({})), 0, second_tx);
908
909 handler.fail_uncorrelated_error(crate::http::models::JsonRpcError {
910 code: -32600,
911 message: "Invalid Request".to_string(),
912 data: None,
913 });
914
915 assert_eq!(handler.pending.len(), 2);
916 handler.fail_pending("test cleanup");
917 assert!(first_rx.await.unwrap().is_err());
918 assert!(second_rx.await.unwrap().is_err());
919 }
920}