nautilus_binance/common/
execution.rs1use std::{
19 future::Future,
20 sync::Mutex,
21 time::{Duration, Instant},
22};
23
24use nautilus_common::live::get_runtime;
25use nautilus_core::MUTEX_POISONED;
26use nautilus_live::ExecutionClientCore;
27use nautilus_model::identifiers::AccountId;
28use tokio::task::JoinHandle;
29
30pub fn spawn_task<F>(pending_tasks: &Mutex<Vec<JoinHandle<()>>>, description: &'static str, fut: F)
38where
39 F: Future<Output = anyhow::Result<()>> + Send + 'static,
40{
41 let runtime = get_runtime();
42 let handle = runtime.spawn(async move {
43 if let Err(e) = fut.await {
44 log::warn!("{description} failed: {e}");
45 }
46 });
47
48 let mut tasks = pending_tasks.lock().expect(MUTEX_POISONED);
49 tasks.retain(|handle| !handle.is_finished());
50 tasks.push(handle);
51}
52
53pub fn abort_pending_tasks(pending_tasks: &Mutex<Vec<JoinHandle<()>>>) {
59 let mut tasks = pending_tasks.lock().expect(MUTEX_POISONED);
60 for handle in tasks.drain(..) {
61 handle.abort();
62 }
63}
64
65pub async fn await_account_registered(
75 core: &ExecutionClientCore,
76 account_id: AccountId,
77 timeout_secs: f64,
78) -> anyhow::Result<()> {
79 if core.cache().account(&account_id).is_some() {
80 log::info!("Account {account_id} registered");
81 return Ok(());
82 }
83
84 let start = Instant::now();
85 let timeout = Duration::from_secs_f64(timeout_secs);
86 let interval = Duration::from_millis(10);
87
88 loop {
89 tokio::time::sleep(interval).await;
90
91 if core.cache().account(&account_id).is_some() {
92 log::info!("Account {account_id} registered");
93 return Ok(());
94 }
95
96 if start.elapsed() >= timeout {
97 anyhow::bail!(
98 "Timeout waiting for account {account_id} to be registered after {timeout_secs}s"
99 );
100 }
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use std::{cell::RefCell, rc::Rc};
107
108 use nautilus_common::cache::Cache;
109 use nautilus_live::ExecutionClientCore;
110 use nautilus_model::{
111 accounts::{AccountAny, CashAccount},
112 enums::{AccountType, OmsType},
113 events::AccountState,
114 identifiers::{AccountId, TraderId},
115 types::{AccountBalance, Money},
116 };
117 use rstest::rstest;
118
119 use super::*;
120 use crate::common::consts::{BINANCE_CLIENT_ID, BINANCE_VENUE};
121
122 #[rstest]
123 fn test_spawn_task_prunes_finished_handles() {
124 let finished = get_runtime().spawn(async {});
125 get_runtime().block_on(async {
126 tokio::time::timeout(Duration::from_secs(1), async {
127 while !finished.is_finished() {
128 tokio::task::yield_now().await;
129 }
130 })
131 .await
132 .expect("Finished task should complete");
133 });
134
135 let pending_tasks = Mutex::new(vec![finished]);
136
137 spawn_task(&pending_tasks, "test task", async { Ok(()) });
138
139 assert_eq!(
140 pending_tasks.lock().expect(MUTEX_POISONED).len(),
141 1,
142 "spawn_task should drop finished handles before storing the new one",
143 );
144
145 abort_pending_tasks(&pending_tasks);
146 }
147
148 #[rstest]
149 #[tokio::test]
150 async fn test_abort_pending_tasks_aborts_running_tasks() {
151 let (drop_tx, drop_rx) = tokio::sync::oneshot::channel();
152 let guard = AbortDropSignal { tx: Some(drop_tx) };
153
154 let handle = get_runtime().spawn(async move {
155 let _guard = guard;
156 tokio::time::sleep(Duration::from_secs(60)).await;
157 });
158 let pending_tasks = Mutex::new(vec![handle]);
159
160 abort_pending_tasks(&pending_tasks);
161
162 assert!(pending_tasks.lock().expect(MUTEX_POISONED).is_empty());
163 tokio::time::timeout(Duration::from_secs(1), drop_rx)
164 .await
165 .expect("Aborted task should drop its future")
166 .expect("Drop signal should be sent");
167 }
168
169 #[rstest]
170 #[tokio::test]
171 async fn test_await_account_registered_returns_when_account_is_added() {
172 let account_id = AccountId::from("BINANCE-001");
173 let cache = Rc::new(RefCell::new(Cache::default()));
174 let core = create_test_core(cache.clone(), account_id);
175
176 let wait_fut = await_account_registered(&core, account_id, 0.5);
177 let register_fut = async move {
178 tokio::time::sleep(Duration::from_millis(20)).await;
179 add_test_account_to_cache(&cache, account_id);
180 };
181
182 let (result, ()) = tokio::join!(wait_fut, register_fut);
183 result.unwrap();
184 }
185
186 #[rstest]
187 #[tokio::test]
188 async fn test_await_account_registered_times_out() {
189 let account_id = AccountId::from("BINANCE-001");
190 let cache = Rc::new(RefCell::new(Cache::default()));
191 let core = create_test_core(cache, account_id);
192
193 let error = await_account_registered(&core, account_id, 0.02)
194 .await
195 .expect_err("Missing account should time out");
196
197 assert!(error.to_string().contains("BINANCE-001"));
198 }
199
200 struct AbortDropSignal {
201 tx: Option<tokio::sync::oneshot::Sender<()>>,
202 }
203
204 impl Drop for AbortDropSignal {
205 fn drop(&mut self) {
206 if let Some(tx) = self.tx.take() {
207 let _ = tx.send(());
208 }
209 }
210 }
211
212 fn create_test_core(cache: Rc<RefCell<Cache>>, account_id: AccountId) -> ExecutionClientCore {
213 ExecutionClientCore::new(
214 TraderId::from("TESTER-001"),
215 *BINANCE_CLIENT_ID,
216 *BINANCE_VENUE,
217 OmsType::Hedging,
218 account_id,
219 AccountType::Cash,
220 None,
221 cache,
222 )
223 }
224
225 fn add_test_account_to_cache(cache: &Rc<RefCell<Cache>>, account_id: AccountId) {
226 let state = AccountState::new(
227 account_id,
228 AccountType::Cash,
229 vec![AccountBalance::new(
230 Money::from("1.0 BTC"),
231 Money::from("0 BTC"),
232 Money::from("1.0 BTC"),
233 )],
234 vec![],
235 true,
236 nautilus_core::UUID4::new(),
237 nautilus_core::UnixNanos::default(),
238 nautilus_core::UnixNanos::default(),
239 None,
240 );
241
242 let account = AccountAny::Cash(CashAccount::new(state, true, false));
243 cache.borrow_mut().add_account(account).unwrap();
244 }
245}