1use std::{
2 borrow::Cow,
3 collections::{BTreeMap, VecDeque},
4 future::Future,
5 time::Duration,
6};
7
8use futures::{SinkExt, StreamExt, stream};
9use simulator_api::{
10 AccountData, AccountModifications, AgentStatsReport, BacktestError, BacktestRequest,
11 BacktestResponse, BacktestStatus, ContinueParams, CreateBacktestSessionRequest,
12 CreateBacktestSessionRequestV1, SequencedResponse, SessionSummary,
13};
14use solana_address::Address;
15use solana_client::{
16 nonblocking::rpc_client::RpcClient,
17 rpc_response::{Response, RpcLogsResponse},
18};
19use solana_commitment_config::CommitmentConfig;
20use thiserror::Error;
21use tokio::net::TcpStream;
22use tokio_tungstenite::{
23 MaybeTlsStream, WebSocketStream,
24 tungstenite::{
25 Error as WsError, Message,
26 error::ProtocolError,
27 protocol::{CloseFrame, frame::coding::CloseCode},
28 },
29};
30
31use crate::{
32 BacktestClientError, BacktestClientResult, Continue,
33 injection::ProgramModError,
34 subscriptions::{
35 AccountDiffNotification, AccountDiffSubscriptionHandle, LogSubscriptionHandle,
36 SubscriptionError,
37 },
38};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ReadyOutcome {
43 Ready,
45 Completed,
47}
48
49#[derive(Debug, Default)]
51pub struct ContinueResult {
52 pub slot_notifications: u64,
54 pub last_slot: Option<u64>,
56 pub statuses: Vec<BacktestStatus>,
58 pub ready_for_continue: bool,
60 pub completed: bool,
62}
63
64#[derive(Debug)]
66pub struct AdvanceState {
67 pub expected_slots: u64,
69 pub slot_notifications: u64,
71 pub last_slot: Option<u64>,
73 pub statuses: Vec<BacktestStatus>,
75 pub ready_for_continue: bool,
77 pub completed: bool,
79 pub summary: Option<SessionSummary>,
81 pub agent_stats: Option<Vec<AgentStatsReport>>,
83}
84
85impl AdvanceState {
86 pub fn new(expected_slots: u64) -> Self {
88 Self {
89 expected_slots,
90 slot_notifications: 0,
91 last_slot: None,
92 statuses: Vec::new(),
93 ready_for_continue: false,
94 completed: false,
95 summary: None,
96 agent_stats: None,
97 }
98 }
99
100 pub fn is_done(&self, wait_for_slots: bool) -> bool {
102 if self.completed {
103 return true;
104 }
105
106 if !self.ready_for_continue {
107 return false;
108 }
109
110 !wait_for_slots || self.slot_notifications >= self.expected_slots
111 }
112}
113
114#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
116pub struct SessionCoverage {
117 completed: bool,
118 highest_slot_seen: Option<u64>,
119}
120
121impl SessionCoverage {
122 pub fn observe_slot(&mut self, slot: u64) {
124 self.highest_slot_seen = Some(
125 self.highest_slot_seen
126 .map_or(slot, |current| current.max(slot)),
127 );
128 }
129
130 pub fn mark_completed(&mut self) {
132 self.completed = true;
133 }
134
135 pub fn observe_response(&mut self, response: &BacktestResponse) {
137 match response {
138 BacktestResponse::SlotNotification(slot) => self.observe_slot(*slot),
139 BacktestResponse::Completed { .. } => self.mark_completed(),
140 _ => {}
141 }
142 }
143
144 pub fn is_completed(&self) -> bool {
146 self.completed
147 }
148
149 pub fn highest_slot_seen(&self) -> Option<u64> {
151 self.highest_slot_seen
152 }
153
154 pub fn validate_end_slot(&self, expected_end_slot: u64) -> Result<(), CoverageError> {
156 if !self.completed {
157 return Err(CoverageError::NotCompleted);
158 }
159
160 let Some(actual_end_slot) = self.highest_slot_seen else {
161 return Err(CoverageError::NoSlotsObserved);
162 };
163
164 if actual_end_slot < expected_end_slot {
165 return Err(CoverageError::RangeNotReached {
166 actual_end_slot,
167 expected_end_slot,
168 });
169 }
170
171 Ok(())
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
177pub enum CoverageError {
178 #[error("ended before completion")]
179 NotCompleted,
180 #[error("completed without slot notifications")]
181 NoSlotsObserved,
182 #[error("completed at slot {actual_end_slot} but expected at least {expected_end_slot}")]
183 RangeNotReached {
184 actual_end_slot: u64,
185 expected_end_slot: u64,
186 },
187}
188
189pub struct BacktestSession {
194 ws: Option<WebSocketStream<MaybeTlsStream<TcpStream>>>,
195 session_id: Option<String>,
196 rpc_endpoint: Option<String>,
197 task_id: Option<String>,
198 rpc: Option<RpcClient>,
199 last_sequence: Option<u64>,
200 pub(crate) ready_for_continue: bool,
201 request_timeout: Option<Duration>,
202 log_raw: bool,
203 backlog: VecDeque<(Option<u64>, BacktestResponse)>,
204}
205
206impl BacktestSession {
207 pub(crate) fn new(
208 ws: WebSocketStream<MaybeTlsStream<TcpStream>>,
209 request_timeout: Option<Duration>,
210 log_raw: bool,
211 ) -> Self {
212 Self {
213 ws: Some(ws),
214 session_id: None,
215 rpc_endpoint: None,
216 task_id: None,
217 rpc: None,
218 last_sequence: None,
219 ready_for_continue: false,
220 request_timeout,
221 log_raw,
222 backlog: VecDeque::new(),
223 }
224 }
225
226 pub fn session_id(&self) -> Option<&str> {
228 self.session_id.as_deref()
229 }
230
231 pub fn rpc_endpoint(&self) -> Option<&str> {
233 self.rpc_endpoint.as_deref()
234 }
235
236 pub fn task_id(&self) -> Option<&str> {
239 self.task_id.as_deref()
240 }
241
242 pub fn last_sequence(&self) -> Option<u64> {
244 self.last_sequence
245 }
246
247 pub fn rpc(&self) -> &RpcClient {
251 self.rpc
252 .as_ref()
253 .expect("rpc is set during session creation")
254 }
255
256 pub fn is_ready_for_continue(&self) -> bool {
258 self.ready_for_continue
259 }
260
261 pub fn apply_response(&mut self, response: &BacktestResponse) {
263 match response {
264 BacktestResponse::ReadyForContinue | BacktestResponse::Paused(_) => {
265 self.ready_for_continue = true;
266 }
267 BacktestResponse::Completed { .. } => {
268 self.ready_for_continue = false;
269 }
270 _ => {}
271 }
272 }
273
274 fn ws_mut(&mut self) -> BacktestClientResult<&mut WebSocketStream<MaybeTlsStream<TcpStream>>> {
275 self.ws.as_mut().ok_or_else(|| BacktestClientError::Closed {
276 reason: "websocket closed".to_string(),
277 })
278 }
279
280 pub(crate) async fn create_with_request(
281 &mut self,
282 request: CreateBacktestSessionRequest,
283 rpc_base_url: String,
284 mut on_parallel_session_created: Option<&mut (dyn FnMut(String) + Send)>,
285 ) -> BacktestClientResult<CreateRequestResult> {
286 let expect_parallel = matches!(
287 &request,
288 CreateBacktestSessionRequest::V1(CreateBacktestSessionRequestV1 { parallel: true, .. })
289 );
290 self.send(&BacktestRequest::CreateBacktestSession(request), None)
291 .await?;
292 let mut streamed_parallel_session_ids = Vec::new();
293 let mut streamed_parallel_task_ids: Vec<Option<String>> = Vec::new();
294 let mut pending: Vec<(Option<u64>, BacktestResponse)> = Vec::new();
298
299 loop {
300 let response =
301 self.next_response(None)
302 .await?
303 .ok_or_else(|| BacktestClientError::Closed {
304 reason: "websocket ended before SessionCreated".to_string(),
305 })?;
306
307 match response {
308 BacktestResponse::SessionCreated {
309 session_id,
310 rpc_endpoint,
311 task_id,
312 } => {
313 if expect_parallel {
314 if let Some(callback) = on_parallel_session_created.as_mut() {
315 (**callback)(session_id.clone());
316 }
317 streamed_parallel_session_ids.push(session_id);
318 streamed_parallel_task_ids.push(task_id);
319 continue;
320 }
321 let created_session_id = session_id.clone();
322 let created_task_id = task_id.clone();
323 self.session_id = Some(session_id);
324 self.task_id = task_id;
325 let resolved = resolve_rpc_url(&rpc_base_url, &rpc_endpoint);
326 self.rpc = Some(RpcClient::new_with_commitment(
327 resolved.clone(),
328 CommitmentConfig::confirmed(),
329 ));
330 self.rpc_endpoint = Some(resolved);
331 self.backlog.extend(pending);
332 return Ok(CreateRequestResult::Single {
333 session_id: created_session_id,
334 task_id: created_task_id,
335 });
336 }
337 BacktestResponse::SessionsCreatedV2 {
338 session_ids,
339 task_ids,
340 ..
341 } => {
342 if expect_parallel && session_ids.is_empty() {
343 self.backlog.extend(pending);
344 return Ok(CreateRequestResult::Parallel {
345 session_ids: streamed_parallel_session_ids,
346 task_ids: streamed_parallel_task_ids,
347 });
348 }
349 let task_ids = align_task_ids(task_ids, session_ids.len());
350 self.backlog.extend(pending);
351 return Ok(CreateRequestResult::Parallel {
352 session_ids,
353 task_ids,
354 });
355 }
356 BacktestResponse::ReadyForContinue => {
357 self.ready_for_continue = true;
358 }
359 BacktestResponse::Error(err) => return Err(BacktestClientError::Remote(err)),
360 other => {
361 pending.push((self.last_sequence, other));
362 }
363 }
364 }
365 }
366
367 pub(crate) async fn attach(
368 &mut self,
369 session_id: String,
370 last_sequence: Option<u64>,
371 rpc_base_url: String,
372 ) -> BacktestClientResult<()> {
373 self.send(
374 &BacktestRequest::AttachBacktestSession {
375 session_id,
376 last_sequence,
377 },
378 None,
379 )
380 .await?;
381
382 self.wait_for_response(
383 || BacktestClientError::Closed {
384 reason: "websocket ended before SessionAttached".to_string(),
385 },
386 move |session, response| match response {
387 BacktestResponse::SessionAttached {
388 session_id,
389 rpc_endpoint,
390 task_id,
391 } => {
392 session.session_id = Some(session_id);
393 session.task_id = task_id;
394 let resolved = resolve_rpc_url(&rpc_base_url, &rpc_endpoint);
395 session.rpc = Some(RpcClient::new_with_commitment(
396 resolved.clone(),
397 CommitmentConfig::confirmed(),
398 ));
399 session.rpc_endpoint = Some(resolved);
400 Ok(Some(()))
401 }
402 BacktestResponse::ReadyForContinue => {
403 session.ready_for_continue = true;
404 Ok(None)
405 }
406 BacktestResponse::Error(err) => Err(BacktestClientError::Remote(err)),
407 other => {
408 session.backlog.push_back((session.last_sequence, other));
409 Ok(None)
410 }
411 },
412 )
413 .await
414 }
415
416 pub async fn resume_attached_session(&mut self) -> BacktestClientResult<()> {
419 self.send(&BacktestRequest::ResumeAttachedSession, None)
420 .await?;
421
422 self.wait_for_response(
423 || BacktestClientError::Closed {
424 reason: "websocket ended before ResumeAttachedSession acknowledgement".to_string(),
425 },
426 |session, response| match response {
427 BacktestResponse::Success => Ok(Some(())),
428 BacktestResponse::Error(err) => Err(BacktestClientError::Remote(err)),
429 other => {
430 session.backlog.push_back((session.last_sequence, other));
431 Ok(None)
432 }
433 },
434 )
435 .await
436 }
437
438 async fn wait_for_response<T, E, F>(
439 &mut self,
440 closed_error: E,
441 mut handle_response: F,
442 ) -> BacktestClientResult<T>
443 where
444 E: FnOnce() -> BacktestClientError,
445 F: FnMut(&mut Self, BacktestResponse) -> BacktestClientResult<Option<T>>,
446 {
447 let mut closed_error = Some(closed_error);
448
449 loop {
450 let response = self
451 .next_response(None)
452 .await?
453 .ok_or_else(|| closed_error.take().expect("closed error set")())?;
454
455 if let Some(result) = handle_response(self, response)? {
456 return Ok(result);
457 }
458 }
459 }
460
461 pub async fn send(
463 &mut self,
464 request: &BacktestRequest,
465 timeout: Option<Duration>,
466 ) -> BacktestClientResult<()> {
467 let text = serde_json::to_string(request)
468 .map_err(|source| BacktestClientError::SerializeRequest { source })?;
469
470 let request_timeout = self.request_timeout;
471 let timeout = timeout.or(request_timeout);
472
473 let send_fut = self.ws_mut()?.send(Message::Text(text));
474 let send_result = match timeout {
475 Some(duration) => tokio::time::timeout(duration, send_fut)
476 .await
477 .map_err(|_| BacktestClientError::Timeout {
478 action: "sending",
479 duration,
480 })?,
481 None => send_fut.await,
482 };
483
484 send_result.map_err(|source| BacktestClientError::WebSocket {
485 action: "sending",
486 source: Box::new(source),
487 })?;
488
489 Ok(())
490 }
491
492 pub async fn next_response(
494 &mut self,
495 timeout: Option<Duration>,
496 ) -> BacktestClientResult<Option<BacktestResponse>> {
497 if let Some((sequence, response)) = self.backlog.pop_front() {
498 self.last_sequence = sequence.or(self.last_sequence);
499 return Ok(Some(response));
500 }
501
502 let text = match self.next_text(timeout).await? {
503 Some(text) => text,
504 None => return Ok(None),
505 };
506
507 let (sequence, response) = match serde_json::from_str::<SequencedResponse>(&text) {
508 Ok(sequenced) => (Some(sequenced.seq_id), sequenced.response),
509 Err(_) => {
510 let response =
511 serde_json::from_str::<BacktestResponse>(&text).map_err(|source| {
512 BacktestClientError::DeserializeResponse {
513 raw: text.clone(),
514 source,
515 }
516 })?;
517 (None, response)
518 }
519 };
520 self.last_sequence = sequence.or(self.last_sequence);
521
522 Ok(Some(response))
523 }
524
525 pub async fn next_event(
527 &mut self,
528 timeout: Option<Duration>,
529 ) -> BacktestClientResult<Option<BacktestResponse>> {
530 let response = self.next_response(timeout).await?;
531 if let Some(ref response) = response {
532 self.apply_response(response);
533 }
534 Ok(response)
535 }
536
537 pub fn responses(
541 self,
542 timeout: Option<Duration>,
543 ) -> impl futures::Stream<Item = BacktestClientResult<BacktestResponse>> {
544 stream::unfold(Some(self), move |state| async move {
545 let mut session = match state {
546 Some(session) => session,
547 None => return None,
548 };
549
550 match session.next_response(timeout).await {
551 Ok(Some(response)) => {
552 session.apply_response(&response);
553 Some((Ok(response), Some(session)))
554 }
555 Ok(None) => None,
556 Err(err) => Some((Err(err), None)),
557 }
558 })
559 }
560
561 pub async fn ensure_ready(
563 &mut self,
564 timeout: Option<Duration>,
565 ) -> BacktestClientResult<ReadyOutcome> {
566 if self.ready_for_continue {
567 return Ok(ReadyOutcome::Ready);
568 }
569
570 loop {
571 let response =
572 self.next_response(timeout)
573 .await?
574 .ok_or_else(|| BacktestClientError::Closed {
575 reason: "websocket ended while waiting for ReadyForContinue".to_string(),
576 })?;
577
578 match response {
579 BacktestResponse::ReadyForContinue => {
580 self.ready_for_continue = true;
581 return Ok(ReadyOutcome::Ready);
582 }
583 BacktestResponse::Completed { .. } => return Ok(ReadyOutcome::Completed),
584 BacktestResponse::Error(err) => return Err(BacktestClientError::Remote(err)),
585 _ => {}
586 }
587 }
588 }
589
590 pub async fn wait_for_status(
592 &mut self,
593 desired: BacktestStatus,
594 timeout: Option<Duration>,
595 ) -> BacktestClientResult<()> {
596 let desired = std::mem::discriminant(&desired);
597
598 loop {
599 let response =
600 self.next_response(timeout)
601 .await?
602 .ok_or_else(|| BacktestClientError::Closed {
603 reason: "websocket ended while waiting for status".to_string(),
604 })?;
605
606 match response {
607 BacktestResponse::Status { status }
608 if std::mem::discriminant(&status) == desired =>
609 {
610 return Ok(());
611 }
612 BacktestResponse::Error(err) => return Err(BacktestClientError::Remote(err)),
613 BacktestResponse::Completed {
614 summary,
615 agent_stats,
616 } => {
617 return Err(BacktestClientError::UnexpectedResponse {
618 context: "waiting for status",
619 response: Box::new(BacktestResponse::Completed {
620 summary,
621 agent_stats,
622 }),
623 });
624 }
625 _ => {}
626 }
627 }
628 }
629
630 pub(crate) fn push_backlog(&mut self, response: BacktestResponse) {
632 self.backlog.push_back((None, response));
633 }
634
635 pub async fn send_continue(
637 &mut self,
638 params: ContinueParams,
639 timeout: Option<Duration>,
640 ) -> BacktestClientResult<()> {
641 self.ready_for_continue = false;
642 self.send(&BacktestRequest::Continue(params), timeout).await
643 }
644
645 pub async fn advance_step<F>(
647 &mut self,
648 state: &mut AdvanceState,
649 wait_for_slots: bool,
650 timeout: Option<Duration>,
651 on_event: &mut F,
652 ) -> BacktestClientResult<()>
653 where
654 F: FnMut(&BacktestResponse),
655 {
656 let Some(response) = self.next_response(timeout).await? else {
657 return Err(BacktestClientError::Closed {
658 reason: "websocket ended while awaiting continue responses".to_string(),
659 });
660 };
661
662 if self.log_raw {
663 tracing::debug!("<- {response:?}");
664 }
665
666 on_event(&response);
667
668 match response {
669 BacktestResponse::ReadyForContinue => {
670 self.ready_for_continue = true;
671 state.ready_for_continue = true;
672 }
673 BacktestResponse::SlotNotification(slot) => {
674 state.slot_notifications += 1;
675 state.last_slot = Some(slot);
676 }
677 BacktestResponse::Status { status } => {
678 state.statuses.push(status);
679 }
680 BacktestResponse::Success => {}
681 BacktestResponse::Completed {
682 summary,
683 agent_stats,
684 } => {
685 state.completed = true;
686 state.summary = summary;
687 state.agent_stats = agent_stats;
688 }
689 BacktestResponse::Error(err @ BacktestError::SimulationError { .. }) => {
690 tracing::warn!(error = %crate::error::err_chain(&err), "simulation error");
691 }
692 BacktestResponse::Error(err) => return Err(BacktestClientError::Remote(err)),
693 BacktestResponse::SessionCreated { .. }
694 | BacktestResponse::SessionAttached { .. }
695 | BacktestResponse::SessionsCreatedV2 { .. }
696 | BacktestResponse::ParallelSessionAttachedV2 { .. }
697 | BacktestResponse::SessionEventV2 { .. }
698 | BacktestResponse::Paused(_)
699 | BacktestResponse::DiscoveryBatch(_) => {
700 return Err(BacktestClientError::UnexpectedResponse {
701 context: "continuing",
702 response: Box::new(response),
703 });
704 }
705 }
706
707 if wait_for_slots && state.slot_notifications > state.expected_slots {
708 tracing::warn!(
709 "received {} slot notifications (expected {})",
710 state.slot_notifications,
711 state.expected_slots
712 );
713 }
714
715 Ok(())
716 }
717
718 pub async fn continue_until_ready<F>(
720 &mut self,
721 cont: Continue,
722 timeout: Option<Duration>,
723 mut on_event: F,
724 ) -> BacktestClientResult<ContinueResult>
725 where
726 F: FnMut(&BacktestResponse),
727 {
728 let expected_slots = cont.advance_count;
729 self.advance_internal(
730 cont.into_params(),
731 expected_slots,
732 false,
733 timeout,
734 &mut on_event,
735 )
736 .await
737 }
738
739 pub async fn advance<F>(
741 &mut self,
742 cont: Continue,
743 timeout: Option<Duration>,
744 mut on_event: F,
745 ) -> BacktestClientResult<ContinueResult>
746 where
747 F: FnMut(&BacktestResponse),
748 {
749 let expected_slots = cont.advance_count;
750 self.advance_internal(
751 cont.into_params(),
752 expected_slots,
753 true,
754 timeout,
755 &mut on_event,
756 )
757 .await
758 }
759
760 async fn advance_internal<F>(
761 &mut self,
762 params: ContinueParams,
763 expected_slots: u64,
764 wait_for_slots: bool,
765 timeout: Option<Duration>,
766 on_event: &mut F,
767 ) -> BacktestClientResult<ContinueResult>
768 where
769 F: FnMut(&BacktestResponse),
770 {
771 self.send_continue(params, timeout).await?;
772
773 let mut state = AdvanceState::new(expected_slots);
774 while !state.is_done(wait_for_slots) {
775 self.advance_step(&mut state, wait_for_slots, timeout, on_event)
776 .await?;
777 }
778
779 Ok(ContinueResult {
780 slot_notifications: state.slot_notifications,
781 last_slot: state.last_slot,
782 statuses: state.statuses,
783 ready_for_continue: state.ready_for_continue,
784 completed: state.completed,
785 })
786 }
787
788 pub async fn modify_program(
797 &self,
798 program_id: &str,
799 elf: &[u8],
800 ) -> Result<BTreeMap<Address, AccountData>, ProgramModError> {
801 let rpc = self.rpc.as_ref().ok_or(ProgramModError::NoRpcEndpoint)?;
802 crate::injection::modify_program_via_rpc(rpc, program_id, elf).await
803 }
804
805 pub async fn modify_accounts(
809 &self,
810 modifications: &AccountModifications,
811 ) -> BacktestClientResult<usize> {
812 let rpc_endpoint =
813 self.rpc_endpoint
814 .as_deref()
815 .ok_or_else(|| BacktestClientError::Closed {
816 reason: "no RPC endpoint available".to_string(),
817 })?;
818
819 crate::rpc::modify_accounts(rpc_endpoint, modifications).await
820 }
821
822 pub async fn subscribe_program_logs<F, Fut>(
828 &self,
829 program_id: &str,
830 commitment: CommitmentConfig,
831 on_notification: F,
832 ) -> Result<LogSubscriptionHandle, SubscriptionError>
833 where
834 F: Fn(Response<RpcLogsResponse>) -> Fut + Send + Sync + 'static,
835 Fut: Future<Output = ()> + Send + 'static,
836 {
837 let rpc_endpoint = self
838 .rpc_endpoint
839 .as_deref()
840 .ok_or(SubscriptionError::NoRpcEndpoint)?;
841 crate::subscriptions::subscribe_program_logs(
842 rpc_endpoint,
843 program_id,
844 commitment,
845 on_notification,
846 )
847 .await
848 }
849
850 pub async fn subscribe_account_diffs<F, Fut>(
856 &self,
857 account: &str,
858 on_notification: F,
859 ) -> Result<AccountDiffSubscriptionHandle, SubscriptionError>
860 where
861 F: Fn(AccountDiffNotification) -> Fut + Send + Sync + 'static,
862 Fut: Future<Output = ()> + Send + 'static,
863 {
864 let rpc_endpoint = self
865 .rpc_endpoint
866 .as_deref()
867 .ok_or(SubscriptionError::NoRpcEndpoint)?;
868 crate::subscriptions::subscribe_account_diffs(rpc_endpoint, account, on_notification).await
869 }
870
871 pub async fn close(&mut self, timeout: Option<Duration>) -> BacktestClientResult<()> {
875 self.close_with_frame(timeout, None).await
876 }
877
878 pub async fn close_with_frame(
880 &mut self,
881 timeout: Option<Duration>,
882 frame: Option<CloseFrame<'static>>,
883 ) -> BacktestClientResult<()> {
884 if self.ws.is_none() {
885 return Ok(());
886 }
887
888 let mut sent = false;
889 match self
890 .send(&BacktestRequest::CloseBacktestSession, timeout)
891 .await
892 {
893 Ok(()) => sent = true,
894 Err(err) if is_close_ok(&err) => {}
895 Err(err) => return Err(err),
896 }
897
898 if sent {
899 let response = match self.next_response(timeout).await {
900 Ok(Some(r)) => r,
901 Ok(None) => {
902 self.ws.take();
903 return Ok(());
904 }
905 Err(BacktestClientError::Closed { .. }) => {
906 self.ws.take();
907 return Ok(());
908 }
909 Err(BacktestClientError::WebSocket {
910 action: "receiving",
911 source,
912 }) if is_reset_without_close(&source) => {
913 self.ws.take();
914 return Ok(());
915 }
916 Err(err) => return Err(err),
917 };
918
919 match response {
920 BacktestResponse::Success | BacktestResponse::Completed { .. } => {}
921 BacktestResponse::Error(err) => return Err(BacktestClientError::Remote(err)),
922 other => {
923 return Err(BacktestClientError::UnexpectedResponse {
924 context: "closing session",
925 response: Box::new(other),
926 });
927 }
928 }
929 }
930
931 if let Some(mut ws) = self.ws.take()
934 && let Err(source) = ws.close(frame).await
935 && !is_ws_closed_error(&source)
936 {
937 return Err(BacktestClientError::WebSocket {
938 action: "closing",
939 source: Box::new(source),
940 });
941 }
942 Ok(())
943 }
944
945 pub async fn close_with_reason(
947 &mut self,
948 timeout: Option<Duration>,
949 code: CloseCode,
950 reason: impl Into<String>,
951 ) -> BacktestClientResult<()> {
952 let frame = CloseFrame {
953 code,
954 reason: Cow::Owned(reason.into()),
955 };
956 self.close_with_frame(timeout, Some(frame)).await
957 }
958
959 pub fn abort(mut self) {
966 let _ws = self.ws.take();
967 }
968
969 async fn next_text(
970 &mut self,
971 timeout: Option<Duration>,
972 ) -> BacktestClientResult<Option<String>> {
973 loop {
974 let request_timeout = self.request_timeout;
975 let timeout = timeout.or(request_timeout);
976
977 let next_fut = self.ws_mut()?.next();
978 let msg = match timeout {
979 Some(duration) => tokio::time::timeout(duration, next_fut)
980 .await
981 .map_err(|_| BacktestClientError::Timeout {
982 action: "receiving",
983 duration,
984 })?,
985 None => next_fut.await,
986 };
987
988 let Some(msg) = msg else {
989 return Ok(None);
990 };
991
992 let msg = match msg {
993 Ok(msg) => msg,
994 Err(source) => {
995 return Err(BacktestClientError::WebSocket {
996 action: "receiving",
997 source: Box::new(source),
998 });
999 }
1000 };
1001
1002 match msg {
1003 Message::Text(text) => {
1004 if self.log_raw {
1005 tracing::debug!("<- raw: {text}");
1006 }
1007 return Ok(Some(text));
1008 }
1009 Message::Binary(bin) => match String::from_utf8(bin) {
1010 Ok(text) => {
1011 if self.log_raw {
1012 tracing::debug!("<- raw(bin): {text}");
1013 }
1014 return Ok(Some(text));
1015 }
1016 Err(err) => {
1017 tracing::warn!("discarding non-utf8 binary message: {err}");
1018 continue;
1019 }
1020 },
1021 Message::Close(frame) => {
1022 let reason = close_reason(frame);
1023 return Err(BacktestClientError::Closed { reason });
1024 }
1025 Message::Ping(_) | Message::Pong(_) => continue,
1026 Message::Frame(_) => continue,
1027 }
1028 }
1029 }
1030}
1031
1032impl std::fmt::Debug for BacktestSession {
1033 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1034 f.debug_struct("BacktestSession")
1035 .field("session_id", &self.session_id)
1036 .field("rpc_endpoint", &self.rpc_endpoint)
1037 .field(
1038 "rpc",
1039 &self
1040 .rpc
1041 .as_ref()
1042 .map(|_| "<RpcClient>")
1043 .unwrap_or("<not set>"),
1044 )
1045 .field("ready_for_continue", &self.ready_for_continue)
1046 .field("request_timeout", &self.request_timeout)
1047 .finish_non_exhaustive()
1048 }
1049}
1050
1051#[derive(Debug)]
1052pub(crate) enum CreateRequestResult {
1053 Single {
1054 session_id: String,
1055 task_id: Option<String>,
1056 },
1057 Parallel {
1058 session_ids: Vec<String>,
1059 task_ids: Vec<Option<String>>,
1060 },
1061}
1062
1063fn align_task_ids(mut task_ids: Vec<Option<String>>, expected_len: usize) -> Vec<Option<String>> {
1066 if task_ids.len() < expected_len {
1067 task_ids.resize(expected_len, None);
1068 } else if task_ids.len() > expected_len {
1069 task_ids.truncate(expected_len);
1070 }
1071 task_ids
1072}
1073
1074impl Drop for BacktestSession {
1075 fn drop(&mut self) {
1076 let Some(ws) = self.ws.take() else {
1077 return;
1078 };
1079
1080 if let Ok(handle) = tokio::runtime::Handle::try_current() {
1081 handle.spawn(async move {
1082 let mut ws = ws;
1083 let _ = ws.close(None).await;
1084 });
1085 }
1086 }
1087}
1088
1089fn resolve_rpc_url(base: &str, endpoint: &str) -> String {
1090 if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
1091 endpoint.to_string()
1092 } else {
1093 format!("{}/{}", base, endpoint.trim_start_matches('/'))
1094 }
1095}
1096
1097fn close_reason(frame: Option<CloseFrame<'static>>) -> String {
1098 match frame {
1099 Some(frame) => format!("{:?}: {}", frame.code, frame.reason),
1100 None => "no close frame".to_string(),
1101 }
1102}
1103
1104fn is_reset_without_close(err: &WsError) -> bool {
1105 matches!(
1106 err,
1107 WsError::Protocol(ProtocolError::ResetWithoutClosingHandshake)
1108 )
1109}
1110
1111fn is_ws_closed_error(err: &WsError) -> bool {
1112 matches!(
1113 err,
1114 WsError::ConnectionClosed
1115 | WsError::AlreadyClosed
1116 | WsError::Protocol(ProtocolError::ResetWithoutClosingHandshake)
1117 )
1118}
1119
1120fn is_close_ok(err: &BacktestClientError) -> bool {
1121 match err {
1122 BacktestClientError::Closed { .. } => true,
1123 BacktestClientError::WebSocket { source, .. } => is_ws_closed_error(source),
1124 _ => false,
1125 }
1126}
1127
1128#[cfg(test)]
1129mod tests {
1130 use super::*;
1131
1132 #[test]
1133 fn coverage_tracks_slot_and_completion_from_responses() {
1134 let mut coverage = SessionCoverage::default();
1135 coverage.observe_response(&BacktestResponse::SlotNotification(10));
1136 coverage.observe_response(&BacktestResponse::SlotNotification(12));
1137 coverage.observe_response(&BacktestResponse::Completed {
1138 summary: None,
1139 agent_stats: None,
1140 });
1141
1142 assert!(coverage.is_completed());
1143 assert_eq!(coverage.highest_slot_seen(), Some(12));
1144 }
1145
1146 #[test]
1147 fn coverage_validate_end_slot_checks_completion_and_range() {
1148 let mut coverage = SessionCoverage::default();
1149 assert_eq!(
1150 coverage.validate_end_slot(5),
1151 Err(CoverageError::NotCompleted)
1152 );
1153
1154 coverage.mark_completed();
1155 assert_eq!(
1156 coverage.validate_end_slot(5),
1157 Err(CoverageError::NoSlotsObserved)
1158 );
1159
1160 coverage.observe_slot(4);
1161 assert_eq!(
1162 coverage.validate_end_slot(5),
1163 Err(CoverageError::RangeNotReached {
1164 actual_end_slot: 4,
1165 expected_end_slot: 5,
1166 })
1167 );
1168
1169 coverage.observe_slot(6);
1170 assert_eq!(coverage.validate_end_slot(5), Ok(()));
1171 }
1172}