dynamo_runtime/
runtime.rs1use super::utils::GracefulShutdownTracker;
17use crate::{
18 compute,
19 config::{self, RuntimeConfig},
20};
21
22use futures::Future;
23use once_cell::sync::OnceCell;
24use std::{
25 mem::ManuallyDrop,
26 sync::{Arc, atomic::Ordering},
27 time::Duration,
28};
29use tokio::{signal, sync::Mutex, task::JoinHandle};
30
31pub use tokio_util::sync::CancellationToken;
32
33const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_SECS: u64 = 15 * 60;
34
35pub(crate) fn graceful_shutdown_timeout() -> Duration {
36 let timeout_secs = std::env::var(
37 config::environment_names::runtime::DYN_RUNTIME_GRACEFUL_SHUTDOWN_TIMEOUT_SECS,
38 )
39 .ok()
40 .and_then(|s| s.parse::<u64>().ok())
41 .unwrap_or(DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_SECS);
42
43 Duration::from_secs(timeout_secs)
44}
45
46#[derive(Clone, Debug)]
48enum RuntimeType {
49 Shared(Arc<ManuallyDrop<tokio::runtime::Runtime>>),
50 External(tokio::runtime::Handle),
51}
52
53#[derive(Debug, Clone)]
55pub struct Runtime {
56 id: Arc<String>,
57 primary: RuntimeType,
58 secondary: RuntimeType,
59 cancellation_token: CancellationToken,
60 endpoint_shutdown_token: CancellationToken,
61 graceful_shutdown_tracker: Arc<GracefulShutdownTracker>,
62 compute_pool: Option<Arc<compute::ComputePool>>,
63 block_in_place_permits: Option<Arc<tokio::sync::Semaphore>>,
64}
65
66impl Runtime {
67 fn new(runtime: RuntimeType, secondary: Option<RuntimeType>) -> anyhow::Result<Runtime> {
68 crate::nvtx::init();
70
71 let id = Arc::new(uuid::Uuid::new_v4().to_string());
73
74 let cancellation_token = CancellationToken::new();
76
77 let endpoint_shutdown_token = cancellation_token.child_token();
79
80 let secondary = match secondary {
82 Some(secondary) => secondary,
83 None => {
84 tracing::debug!("Created secondary runtime with single thread");
85 RuntimeType::Shared(Arc::new(ManuallyDrop::new(
86 RuntimeConfig::single_threaded().create_runtime()?,
87 )))
88 }
89 };
90
91 let compute_pool = None;
94 let block_in_place_permits = None;
95
96 Ok(Runtime {
97 id,
98 primary: runtime,
99 secondary,
100 cancellation_token,
101 endpoint_shutdown_token,
102 graceful_shutdown_tracker: Arc::new(GracefulShutdownTracker::new()),
103 compute_pool,
104 block_in_place_permits,
105 })
106 }
107
108 fn new_with_config(
109 runtime: RuntimeType,
110 secondary: Option<RuntimeType>,
111 config: &RuntimeConfig,
112 ) -> anyhow::Result<Runtime> {
113 let mut rt = Self::new(runtime, secondary)?;
114
115 let compute_config = crate::compute::ComputeConfig {
117 num_threads: config.compute_threads,
118 stack_size: config.compute_stack_size,
119 thread_prefix: config.compute_thread_prefix.clone(),
120 pin_threads: false,
121 };
122
123 if config.compute_threads == Some(0) {
125 tracing::info!("Compute pool disabled (compute_threads = 0)");
126 } else {
127 match crate::compute::ComputePool::new(compute_config) {
128 Ok(pool) => {
129 rt.compute_pool = Some(Arc::new(pool));
130 tracing::debug!(
131 "Initialized compute pool with {} threads",
132 rt.compute_pool.as_ref().unwrap().num_threads()
133 );
134 }
135 Err(e) => {
136 tracing::warn!(
137 "Failed to create compute pool: {}. CPU-intensive operations will use spawn_blocking",
138 e
139 );
140 }
141 }
142 }
143
144 let num_workers = config
146 .num_worker_threads
147 .unwrap_or_else(|| std::thread::available_parallelism().unwrap().get());
148 let permits = num_workers.saturating_sub(1).max(1);
150 rt.block_in_place_permits = Some(Arc::new(tokio::sync::Semaphore::new(permits)));
151 tracing::debug!(
152 "Initialized block_in_place permits: {} (from {} worker threads)",
153 permits,
154 num_workers
155 );
156
157 Ok(rt)
158 }
159
160 pub fn initialize_thread_local(&self) {
163 if let (Some(pool), Some(permits)) = (&self.compute_pool, &self.block_in_place_permits) {
164 crate::compute::thread_local::initialize_context(Arc::clone(pool), Arc::clone(permits));
165 }
166 let thread_name = std::thread::current()
168 .name()
169 .map(|n| n.to_string())
170 .unwrap_or_else(|| format!("tokio-worker-{:?}", std::thread::current().id()));
171 crate::nvtx::name_current_thread_impl(&thread_name);
172 }
173
174 pub async fn initialize_all_thread_locals(&self) -> anyhow::Result<()> {
177 if let (Some(pool), Some(permits)) = (&self.compute_pool, &self.block_in_place_permits) {
178 let num_workers = self.detect_worker_thread_count().await;
180
181 if num_workers == 0 {
182 return Err(anyhow::anyhow!("No worker threads detected"));
183 }
184
185 let barrier = Arc::new(std::sync::Barrier::new(num_workers));
187 let init_pool = Arc::clone(pool);
188 let init_permits = Arc::clone(permits);
189
190 let mut handles = Vec::new();
192 for i in 0..num_workers {
193 let barrier_clone = Arc::clone(&barrier);
194 let pool_clone = Arc::clone(&init_pool);
195 let permits_clone = Arc::clone(&init_permits);
196
197 let handle = tokio::task::spawn_blocking(move || {
198 barrier_clone.wait();
200
201 crate::compute::thread_local::initialize_context(pool_clone, permits_clone);
203
204 let thread_id = std::thread::current().id();
206 tracing::trace!(
207 "Initialized thread-local compute context on thread {:?} (worker {})",
208 thread_id,
209 i
210 );
211 });
212 handles.push(handle);
213 }
214
215 for handle in handles {
217 handle.await?;
218 }
219
220 tracing::info!(
221 "Successfully initialized thread-local compute context on {} worker threads",
222 num_workers
223 );
224 } else {
225 tracing::debug!("No compute pool configured, skipping thread-local initialization");
226 }
227 Ok(())
228 }
229
230 async fn detect_worker_thread_count(&self) -> usize {
232 use parking_lot::Mutex;
233 use std::collections::HashSet;
234
235 let thread_ids = Arc::new(Mutex::new(HashSet::new()));
236 let mut handles = Vec::new();
237
238 let num_probes = 100;
241 for _ in 0..num_probes {
242 let ids = Arc::clone(&thread_ids);
243 let handle = tokio::task::spawn_blocking(move || {
244 let thread_id = std::thread::current().id();
245 ids.lock().insert(thread_id);
246 });
247 handles.push(handle);
248 }
249
250 for handle in handles {
252 let _ = handle.await;
253 }
254
255 let count = thread_ids.lock().len();
256 tracing::debug!("Detected {count} worker threads in runtime");
257 count
258 }
259
260 pub fn from_current() -> anyhow::Result<Runtime> {
261 Runtime::from_handle(tokio::runtime::Handle::current())
262 }
263
264 pub fn from_handle(handle: tokio::runtime::Handle) -> anyhow::Result<Runtime> {
265 let primary = RuntimeType::External(handle.clone());
266 let secondary = RuntimeType::External(handle);
267 Runtime::new(primary, Some(secondary))
268 }
269
270 pub fn from_handle_with_config(
276 handle: tokio::runtime::Handle,
277 config: &RuntimeConfig,
278 ) -> anyhow::Result<Runtime> {
279 let primary = RuntimeType::External(handle.clone());
280 let secondary = RuntimeType::External(handle);
281 Runtime::new_with_config(primary, Some(secondary), config)
282 }
283
284 pub fn from_settings() -> anyhow::Result<Runtime> {
287 let config = config::RuntimeConfig::from_settings()?;
288 let runtime = Arc::new(ManuallyDrop::new(config.create_runtime()?));
289 let primary = RuntimeType::Shared(runtime.clone());
290 let secondary = RuntimeType::External(runtime.handle().clone());
291 Runtime::new_with_config(primary, Some(secondary), &config)
292 }
293
294 pub fn single_threaded() -> anyhow::Result<Runtime> {
296 let config = config::RuntimeConfig::single_threaded();
297 let owned = RuntimeType::Shared(Arc::new(ManuallyDrop::new(config.create_runtime()?)));
298 Runtime::new(owned, None)
299 }
300
301 pub fn id(&self) -> &str {
303 &self.id
304 }
305
306 pub fn primary(&self) -> tokio::runtime::Handle {
308 self.primary.handle()
309 }
310
311 pub fn secondary(&self) -> tokio::runtime::Handle {
313 self.secondary.handle()
314 }
315
316 pub fn primary_token(&self) -> CancellationToken {
318 self.cancellation_token.clone()
319 }
320
321 pub fn child_token(&self) -> CancellationToken {
323 self.endpoint_shutdown_token.child_token()
324 }
325
326 pub(crate) fn graceful_shutdown_tracker(&self) -> Arc<GracefulShutdownTracker> {
328 self.graceful_shutdown_tracker.clone()
329 }
330
331 pub fn compute_pool(&self) -> Option<&Arc<crate::compute::ComputePool>> {
335 self.compute_pool.as_ref()
336 }
337
338 pub fn shutdown(&self) {
340 tracing::info!("Runtime shutdown initiated");
341
342 let tracker = self.graceful_shutdown_tracker.clone();
344 let main_token = self.cancellation_token.clone();
345 let endpoint_token = self.endpoint_shutdown_token.clone();
346
347 let handle = self.primary();
349 handle.spawn(async move {
350 tracing::info!("Phase 1: Cancelling endpoint shutdown token");
352 endpoint_token.cancel();
353
354 tracing::info!("Phase 2: Waiting for graceful endpoints to complete");
356
357 let count = tracker.get_count();
358 tracing::info!("Active graceful endpoints: {count}");
359
360 if count != 0 {
361 let timeout = graceful_shutdown_timeout();
362 if tokio::time::timeout(timeout, tracker.wait_for_completion())
363 .await
364 .is_err()
365 {
366 let remaining = tracker.get_count();
367 tracing::error!(
368 timeout_secs = timeout.as_secs(),
369 remaining_endpoints = remaining,
370 "Graceful endpoint shutdown timed out; proceeding with runtime teardown"
371 );
372 }
373 }
374
375 tracing::info!("Phase 3: Connections to backend services will now be disconnected");
377 main_token.cancel();
378 });
379 }
380}
381
382impl RuntimeType {
383 pub fn handle(&self) -> tokio::runtime::Handle {
385 match self {
386 RuntimeType::External(rt) => rt.clone(),
387 RuntimeType::Shared(rt) => rt.handle().clone(),
388 }
389 }
390}
391
392impl Drop for RuntimeType {
405 fn drop(&mut self) {
406 match self {
407 RuntimeType::External(_) => {}
408 RuntimeType::Shared(arc) => {
409 let Some(md_runtime) = Arc::get_mut(arc) else {
410 return;
413 };
414 if tokio::runtime::Handle::try_current().is_ok() {
415 let tokio_runtime = unsafe { ManuallyDrop::take(md_runtime) };
417 tokio_runtime.shutdown_background();
418 } else {
419 unsafe { ManuallyDrop::drop(md_runtime) };
426 }
427 }
428 }
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435 use crate::config::environment_names::runtime as env_runtime;
436
437 #[tokio::test(start_paused = true)]
438 async fn shutdown_cancels_main_token_after_graceful_timeout() {
439 temp_env::async_with_vars(
440 [(
441 env_runtime::DYN_RUNTIME_GRACEFUL_SHUTDOWN_TIMEOUT_SECS,
442 Some("5"),
443 )],
444 async {
445 let runtime = Runtime::from_current().unwrap();
446 let tracker = runtime.graceful_shutdown_tracker();
447 let _guard = tracker.register_task();
448 let main_token = runtime.primary_token();
449 let endpoint_token = runtime.child_token();
450
451 runtime.shutdown();
452 tokio::task::yield_now().await;
453
454 assert!(endpoint_token.is_cancelled());
455 assert!(!main_token.is_cancelled());
456 assert_eq!(tracker.get_count(), 1);
457
458 tokio::time::advance(Duration::from_secs(4)).await;
459 tokio::task::yield_now().await;
460
461 assert!(!main_token.is_cancelled());
462
463 tokio::time::advance(Duration::from_secs(1)).await;
464 tokio::task::yield_now().await;
465
466 assert!(main_token.is_cancelled());
467 assert_eq!(tracker.get_count(), 1);
468 },
469 )
470 .await;
471 }
472}