1use crate::statsig_global::StatsigGlobal;
2use crate::statsig_options::{
3 RuntimeThreadStartCallback, DEFAULT_SDK_RUNTIME_THREAD_COUNT, SDK_RUNTIME_THREAD_COUNT_ENV_VAR,
4};
5use crate::StatsigErr;
6use crate::{log_d, log_e};
7use futures::future::join_all;
8use parking_lot::Mutex;
9use std::collections::HashMap;
10use std::future::Future;
11use std::sync::atomic::AtomicBool;
12use std::sync::Arc;
13use std::time::Duration;
14use tokio::runtime::{Builder, Handle, Runtime};
15use tokio::sync::Notify;
16use tokio::task::JoinHandle;
17
18const TAG: &str = stringify!(StatsigRuntime);
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21struct TaskId {
22 tag: String,
23 tokio_id: tokio::task::Id,
24}
25
26pub struct StatsigRuntime {
27 spawned_tasks: Arc<Mutex<HashMap<TaskId, JoinHandle<()>>>>,
28 shutdown_notify: Arc<Notify>,
29 is_shutdown: Arc<AtomicBool>,
30 sdk_runtime_thread_count: Option<usize>,
31 runtime_thread_start_callback: Option<RuntimeThreadStartCallback>,
32 prefer_owned_runtime: bool,
33}
34
35impl StatsigRuntime {
36 #[must_use]
37 pub fn get_runtime() -> Arc<StatsigRuntime> {
38 Self::get_runtime_with_thread_count(None)
39 }
40
41 #[must_use]
42 pub fn get_runtime_with_thread_count(
43 sdk_runtime_thread_count: Option<usize>,
44 ) -> Arc<StatsigRuntime> {
45 Self::get_runtime_with_options(sdk_runtime_thread_count, None)
46 }
47
48 #[must_use]
49 pub fn get_runtime_with_options(
50 sdk_runtime_thread_count: Option<usize>,
51 runtime_thread_start_callback: Option<RuntimeThreadStartCallback>,
52 ) -> Arc<StatsigRuntime> {
53 create_runtime_if_required(
54 sdk_runtime_thread_count,
55 runtime_thread_start_callback.clone(),
56 );
57
58 let prefer_owned_runtime =
62 sdk_runtime_thread_count.is_some() || runtime_thread_start_callback.is_some();
63
64 Arc::new(StatsigRuntime {
65 spawned_tasks: Arc::new(Mutex::new(HashMap::new())),
66 shutdown_notify: Arc::new(Notify::new()),
67 is_shutdown: Arc::new(AtomicBool::new(false)),
68 sdk_runtime_thread_count,
69 runtime_thread_start_callback,
70 prefer_owned_runtime,
71 })
72 }
73
74 pub fn get_handle(&self) -> Result<Handle, StatsigErr> {
75 if !self.prefer_owned_runtime {
76 if let Ok(handle) = Handle::try_current() {
77 return Ok(handle);
78 }
79 }
80
81 if let Some(handle) = self.get_owned_runtime_handle()? {
82 return Ok(handle);
83 }
84
85 Err(StatsigErr::ThreadFailure(
86 "No tokio runtime found".to_string(),
87 ))
88 }
89
90 fn get_owned_runtime_handle(&self) -> Result<Option<Handle>, StatsigErr> {
91 let global = StatsigGlobal::get();
92 let mut rt = global
93 .tokio_runtime
94 .try_lock_for(Duration::from_secs(5))
95 .ok_or_else(|| StatsigErr::LockFailure("Failed to lock tokio runtime".to_string()))?;
96 if rt.is_none() {
97 *rt = Some(Arc::new(create_new_runtime_with_options(
98 self.sdk_runtime_thread_count,
99 self.runtime_thread_start_callback.clone(),
100 )));
101 }
102 if let Some(rt) = rt.as_ref() {
103 return Ok(Some(rt.handle().clone()));
104 }
105
106 Ok(None)
107 }
108
109 pub fn get_num_active_tasks(&self) -> usize {
110 match self.spawned_tasks.try_lock_for(Duration::from_secs(5)) {
111 Some(lock) => lock.len(),
112 None => {
113 log_e!(TAG, "Failed to lock spawned tasks for get_num_active_tasks");
114 0
115 }
116 }
117 }
118
119 pub fn shutdown(&self) {
120 self.shutdown_notify.notify_waiters();
121
122 match self.spawned_tasks.try_lock_for(Duration::from_secs(5)) {
123 Some(mut lock) => {
124 for (_, task) in lock.drain() {
125 task.abort();
126 }
127 }
128 None => {
129 log_e!(TAG, "Failed to lock spawned tasks for shutdown");
130 }
131 }
132 }
133
134 pub fn spawn<F, Fut>(&self, tag: &str, task: F) -> Result<tokio::task::Id, StatsigErr>
135 where
136 F: FnOnce(Arc<Notify>) -> Fut + Send + 'static,
137 Fut: Future<Output = ()> + Send + 'static,
138 {
139 let tag_string = tag.to_string();
140 let shutdown_notify = self.shutdown_notify.clone();
141 let spawned_tasks = self.spawned_tasks.clone();
142 let is_shutdown = self.is_shutdown.clone();
143
144 log_d!(TAG, "Spawning task {}", tag);
145
146 let handle = self.get_handle()?.spawn(async move {
147 if is_shutdown.load(std::sync::atomic::Ordering::Relaxed) {
148 return;
149 }
150
151 let task_id = tokio::task::id();
152 log_d!(TAG, "Executing task {}.{}", tag_string, task_id);
153 task(shutdown_notify).await;
154 remove_join_handle_with_id(spawned_tasks, tag_string, &task_id);
155 });
156
157 Ok(self.insert_join_handle(tag, handle))
158 }
159
160 pub async fn await_tasks_with_tag(&self, tag: &str) {
161 let mut handles = Vec::new();
162
163 match self.spawned_tasks.try_lock_for(Duration::from_secs(5)) {
164 Some(mut lock) => {
165 let keys: Vec<TaskId> = lock.keys().cloned().collect();
166 for key in &keys {
167 if key.tag == tag {
168 let removed = if let Some(handle) = lock.remove(key) {
169 handle
170 } else {
171 log_e!(TAG, "No running task found for tag {}", tag);
172 continue;
173 };
174
175 handles.push(removed);
176 }
177 }
178 }
179 None => {
180 log_e!(TAG, "Failed to lock spawned tasks for await_tasks_with_tag");
181 return;
182 }
183 };
184
185 join_all(handles).await;
186 }
187
188 pub async fn await_join_handle(
189 &self,
190 tag: &str,
191 handle_id: &tokio::task::Id,
192 ) -> Result<(), StatsigErr> {
193 let task_id = TaskId {
194 tag: tag.to_string(),
195 tokio_id: *handle_id,
196 };
197
198 let handle = match self.spawned_tasks.try_lock_for(Duration::from_secs(5)) {
199 Some(mut lock) => match lock.remove(&task_id) {
200 Some(handle) => handle,
201 None => {
202 return Err(StatsigErr::ThreadFailure(
203 "No running task found".to_string(),
204 ));
205 }
206 },
207 None => {
208 log_e!(TAG, "Failed to lock spawned tasks for await_join_handle");
209 return Err(StatsigErr::ThreadFailure(
210 "Failed to lock spawned tasks".to_string(),
211 ));
212 }
213 };
214
215 handle
216 .await
217 .map_err(|e| StatsigErr::ThreadFailure(e.to_string()))?;
218
219 Ok(())
220 }
221
222 pub fn get_running_task_ids(&self) -> Vec<(String, String)> {
223 let tasks = match self.spawned_tasks.try_lock_for(Duration::from_secs(5)) {
224 Some(lock) => lock,
225 None => {
226 log_e!(TAG, "Failed to lock spawned tasks for get_running_task_ids");
227 return Vec::new();
228 }
229 };
230
231 tasks
232 .keys()
233 .map(|key| (key.tag.clone(), key.tokio_id.to_string()))
234 .collect()
235 }
236
237 fn insert_join_handle(&self, tag: &str, handle: JoinHandle<()>) -> tokio::task::Id {
238 let handle_id = handle.id();
239 let task_id = TaskId {
240 tag: tag.to_string(),
241 tokio_id: handle_id,
242 };
243
244 match self.spawned_tasks.try_lock_for(Duration::from_secs(5)) {
245 Some(mut lock) => {
246 lock.insert(task_id, handle);
247 }
248 None => {
249 log_e!(TAG, "Failed to lock spawned tasks for insert_join_handle");
250 }
251 }
252
253 handle_id
254 }
255}
256
257pub fn create_new_runtime() -> Runtime {
258 create_new_runtime_with_thread_count(None)
259}
260
261pub fn create_new_runtime_with_thread_count(sdk_runtime_thread_count: Option<usize>) -> Runtime {
262 create_new_runtime_with_options(sdk_runtime_thread_count, None)
263}
264
265fn create_new_runtime_with_options(
266 sdk_runtime_thread_count: Option<usize>,
267 runtime_thread_start_callback: Option<RuntimeThreadStartCallback>,
268) -> Runtime {
269 let worker_threads = get_runtime_thread_count(sdk_runtime_thread_count);
270
271 #[cfg(not(target_family = "wasm"))]
272 {
273 let mut builder = Builder::new_multi_thread();
274 builder
275 .worker_threads(worker_threads)
276 .thread_name("statsig")
277 .enable_all();
278 if let Some(callback) = runtime_thread_start_callback {
279 builder.on_thread_start(move || callback());
280 }
281 builder.build().expect("Failed to create a tokio Runtime")
282 }
283
284 #[cfg(target_family = "wasm")]
285 return Builder::new_current_thread()
286 .thread_name("statsig")
287 .enable_all()
288 .build()
289 .expect("Failed to create a tokio Runtime (single-threaded for wasm");
290}
291
292fn get_runtime_thread_count(sdk_runtime_thread_count: Option<usize>) -> usize {
293 match sdk_runtime_thread_count {
294 Some(count) if count > 0 => count,
295 _ => std::env::var(SDK_RUNTIME_THREAD_COUNT_ENV_VAR)
296 .ok()
297 .and_then(|value| value.parse::<usize>().ok())
298 .filter(|count| *count > 0)
299 .unwrap_or(DEFAULT_SDK_RUNTIME_THREAD_COUNT),
300 }
301}
302
303fn remove_join_handle_with_id(
304 spawned_tasks: Arc<Mutex<HashMap<TaskId, JoinHandle<()>>>>,
305 tag: String,
306 handle_id: &tokio::task::Id,
307) {
308 let task_id = TaskId {
309 tag,
310 tokio_id: *handle_id,
311 };
312
313 match spawned_tasks.try_lock_for(Duration::from_secs(5)) {
314 Some(mut lock) => {
315 lock.remove(&task_id);
316 }
317 None => {
318 log_e!(
319 TAG,
320 "Failed to lock spawned tasks for remove_join_handle_with_id"
321 );
322 }
323 }
324}
325
326fn create_runtime_if_required(
327 sdk_runtime_thread_count: Option<usize>,
328 runtime_thread_start_callback: Option<RuntimeThreadStartCallback>,
329) {
330 if sdk_runtime_thread_count.is_none()
331 && runtime_thread_start_callback.is_none()
332 && Handle::try_current().is_ok()
333 {
334 log_d!(TAG, "External tokio runtime found");
335 return;
336 }
337
338 let global = StatsigGlobal::get();
339 let mut lock = global
340 .tokio_runtime
341 .try_lock_for(Duration::from_secs(5))
342 .expect("Failed to lock owned tokio runtime");
343
344 match lock.as_ref() {
345 Some(_) => {
346 log_d!(TAG, "Existing StatsigGlobal tokio runtime found");
347 }
348 None => {
349 log_d!(TAG, "Creating new tokio runtime for StatsigGlobal");
350 let rt = Arc::new(create_new_runtime_with_options(
351 sdk_runtime_thread_count,
352 runtime_thread_start_callback,
353 ));
354
355 lock.replace(rt);
356 }
357 };
358}
359
360impl Drop for StatsigRuntime {
361 fn drop(&mut self) {
362 self.shutdown();
363
364 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 #[cfg(not(target_family = "wasm"))]
407 fn creates_runtime_with_configured_worker_count() {
408 let runtime = create_new_runtime_with_thread_count(Some(2));
409
410 assert_eq!(runtime.metrics().num_workers(), 2);
411 }
412
413 #[test]
414 #[cfg(not(target_family = "wasm"))]
415 fn calls_thread_start_callback_for_each_worker() {
416 use std::sync::mpsc;
417
418 let (sender, receiver) = mpsc::channel();
419 let callback = Arc::new(move || {
420 sender
421 .send(std::thread::current().name().map(str::to_owned))
422 .expect("thread start callback receiver should still exist");
423 });
424
425 let runtime = create_new_runtime_with_options(Some(2), Some(callback));
426 let thread_names = (0..2)
427 .map(|_| {
428 receiver
429 .recv_timeout(Duration::from_secs(5))
430 .expect("thread start callback should run for every worker")
431 })
432 .collect::<Vec<_>>();
433
434 assert_eq!(thread_names, vec![Some("statsig".to_string()); 2]);
435 drop(runtime);
436 }
437}