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 shutdown_started: CancellationToken,
62 graceful_shutdown_tracker: Arc<GracefulShutdownTracker>,
63 compute_pool: Option<Arc<compute::ComputePool>>,
64 block_in_place_permits: Option<Arc<tokio::sync::Semaphore>>,
65}
66
67impl Runtime {
68 fn new(runtime: RuntimeType, secondary: Option<RuntimeType>) -> anyhow::Result<Runtime> {
69 crate::nvtx::init();
71
72 let id = Arc::new(uuid::Uuid::new_v4().to_string());
74
75 let cancellation_token = CancellationToken::new();
77
78 let endpoint_shutdown_token = cancellation_token.child_token();
80 let shutdown_started = endpoint_shutdown_token.child_token();
81
82 let secondary = match secondary {
84 Some(secondary) => secondary,
85 None => {
86 tracing::debug!("Created secondary runtime with single thread");
87 RuntimeType::Shared(Arc::new(ManuallyDrop::new(
88 RuntimeConfig::single_threaded().create_runtime()?,
89 )))
90 }
91 };
92
93 let compute_pool = None;
96 let block_in_place_permits = None;
97
98 Ok(Runtime {
99 id,
100 primary: runtime,
101 secondary,
102 cancellation_token,
103 endpoint_shutdown_token,
104 shutdown_started,
105 graceful_shutdown_tracker: Arc::new(GracefulShutdownTracker::new()),
106 compute_pool,
107 block_in_place_permits,
108 })
109 }
110
111 fn new_with_config(
112 runtime: RuntimeType,
113 secondary: Option<RuntimeType>,
114 config: &RuntimeConfig,
115 ) -> anyhow::Result<Runtime> {
116 let mut rt = Self::new(runtime, secondary)?;
117
118 let compute_config = crate::compute::ComputeConfig {
120 num_threads: config.compute_threads,
121 stack_size: config.compute_stack_size,
122 thread_prefix: config.compute_thread_prefix.clone(),
123 pin_threads: false,
124 };
125
126 if config.compute_threads == Some(0) {
128 tracing::info!("Compute pool disabled (compute_threads = 0)");
129 } else {
130 match crate::compute::ComputePool::new(compute_config) {
131 Ok(pool) => {
132 rt.compute_pool = Some(Arc::new(pool));
133 tracing::debug!(
134 "Initialized compute pool with {} threads",
135 rt.compute_pool.as_ref().unwrap().num_threads()
136 );
137 }
138 Err(e) => {
139 tracing::warn!(
140 "Failed to create compute pool: {}. CPU-intensive operations will use spawn_blocking",
141 e
142 );
143 }
144 }
145 }
146
147 let num_workers = config
149 .num_worker_threads
150 .unwrap_or_else(|| std::thread::available_parallelism().unwrap().get());
151 let permits = num_workers.saturating_sub(1).max(1);
153 rt.block_in_place_permits = Some(Arc::new(tokio::sync::Semaphore::new(permits)));
154 tracing::debug!(
155 "Initialized block_in_place permits: {} (from {} worker threads)",
156 permits,
157 num_workers
158 );
159
160 Ok(rt)
161 }
162
163 pub fn initialize_thread_local(&self) {
166 if let (Some(pool), Some(permits)) = (&self.compute_pool, &self.block_in_place_permits) {
167 crate::compute::thread_local::initialize_context(Arc::clone(pool), Arc::clone(permits));
168 }
169 let thread_name = std::thread::current()
171 .name()
172 .map(|n| n.to_string())
173 .unwrap_or_else(|| format!("tokio-worker-{:?}", std::thread::current().id()));
174 crate::nvtx::name_current_thread_impl(&thread_name);
175 }
176
177 pub async fn initialize_all_thread_locals(&self) -> anyhow::Result<()> {
180 if let (Some(pool), Some(permits)) = (&self.compute_pool, &self.block_in_place_permits) {
181 let num_workers = self.detect_worker_thread_count().await;
183
184 if num_workers == 0 {
185 return Err(anyhow::anyhow!("No worker threads detected"));
186 }
187
188 let barrier = Arc::new(std::sync::Barrier::new(num_workers));
190 let init_pool = Arc::clone(pool);
191 let init_permits = Arc::clone(permits);
192
193 let mut handles = Vec::new();
195 for i in 0..num_workers {
196 let barrier_clone = Arc::clone(&barrier);
197 let pool_clone = Arc::clone(&init_pool);
198 let permits_clone = Arc::clone(&init_permits);
199
200 let handle = tokio::task::spawn_blocking(move || {
201 barrier_clone.wait();
203
204 crate::compute::thread_local::initialize_context(pool_clone, permits_clone);
206
207 let thread_id = std::thread::current().id();
209 tracing::trace!(
210 "Initialized thread-local compute context on thread {:?} (worker {})",
211 thread_id,
212 i
213 );
214 });
215 handles.push(handle);
216 }
217
218 for handle in handles {
220 handle.await?;
221 }
222
223 tracing::info!(
224 "Successfully initialized thread-local compute context on {} worker threads",
225 num_workers
226 );
227 } else {
228 tracing::debug!("No compute pool configured, skipping thread-local initialization");
229 }
230 Ok(())
231 }
232
233 async fn detect_worker_thread_count(&self) -> usize {
235 use parking_lot::Mutex;
236 use std::collections::HashSet;
237
238 let thread_ids = Arc::new(Mutex::new(HashSet::new()));
239 let mut handles = Vec::new();
240
241 let num_probes = 100;
244 for _ in 0..num_probes {
245 let ids = Arc::clone(&thread_ids);
246 let handle = tokio::task::spawn_blocking(move || {
247 let thread_id = std::thread::current().id();
248 ids.lock().insert(thread_id);
249 });
250 handles.push(handle);
251 }
252
253 for handle in handles {
255 let _ = handle.await;
256 }
257
258 let count = thread_ids.lock().len();
259 tracing::debug!("Detected {count} worker threads in runtime");
260 count
261 }
262
263 pub fn from_current() -> anyhow::Result<Runtime> {
264 Runtime::from_handle(tokio::runtime::Handle::current())
265 }
266
267 pub fn from_handle(handle: tokio::runtime::Handle) -> anyhow::Result<Runtime> {
268 let primary = RuntimeType::External(handle.clone());
269 let secondary = RuntimeType::External(handle);
270 Runtime::new(primary, Some(secondary))
271 }
272
273 pub fn from_handle_with_config(
279 handle: tokio::runtime::Handle,
280 config: &RuntimeConfig,
281 ) -> anyhow::Result<Runtime> {
282 let primary = RuntimeType::External(handle.clone());
283 let secondary = RuntimeType::External(handle);
284 Runtime::new_with_config(primary, Some(secondary), config)
285 }
286
287 pub fn from_settings() -> anyhow::Result<Runtime> {
290 let config = config::RuntimeConfig::from_settings()?;
291 let runtime = Arc::new(ManuallyDrop::new(config.create_runtime()?));
292 let primary = RuntimeType::Shared(runtime.clone());
293 let secondary = RuntimeType::External(runtime.handle().clone());
294 Runtime::new_with_config(primary, Some(secondary), &config)
295 }
296
297 pub fn single_threaded() -> anyhow::Result<Runtime> {
299 let config = config::RuntimeConfig::single_threaded();
300 let owned = RuntimeType::Shared(Arc::new(ManuallyDrop::new(config.create_runtime()?)));
301 Runtime::new(owned, None)
302 }
303
304 pub fn id(&self) -> &str {
306 &self.id
307 }
308
309 pub fn primary(&self) -> tokio::runtime::Handle {
311 self.primary.handle()
312 }
313
314 pub fn secondary(&self) -> tokio::runtime::Handle {
316 self.secondary.handle()
317 }
318
319 pub fn primary_token(&self) -> CancellationToken {
321 self.cancellation_token.clone()
322 }
323
324 pub fn child_token(&self) -> CancellationToken {
326 self.endpoint_shutdown_token.child_token()
327 }
328
329 pub(crate) fn graceful_shutdown_tracker(&self) -> Arc<GracefulShutdownTracker> {
331 self.graceful_shutdown_tracker.clone()
332 }
333
334 pub fn compute_pool(&self) -> Option<&Arc<crate::compute::ComputePool>> {
338 self.compute_pool.as_ref()
339 }
340
341 pub fn mark_shutting_down(&self) {
344 if !self.shutdown_started.is_cancelled() {
345 tracing::info!("Runtime readiness withdrawn for shutdown");
346 self.shutdown_started.cancel();
347 }
348 }
349
350 pub fn is_shutting_down(&self) -> bool {
351 self.shutdown_started.is_cancelled()
352 }
353
354 pub fn shutdown_started_token(&self) -> CancellationToken {
356 self.shutdown_started.child_token()
357 }
358
359 pub fn shutdown(&self) {
361 self.mark_shutting_down();
362 tracing::info!("Runtime shutdown initiated");
363
364 let tracker = self.graceful_shutdown_tracker.clone();
366 let main_token = self.cancellation_token.clone();
367 let endpoint_token = self.endpoint_shutdown_token.clone();
368
369 let handle = self.primary();
371 handle.spawn(async move {
372 tracing::info!("Phase 1: Cancelling endpoint shutdown token");
374 endpoint_token.cancel();
375
376 tracing::info!("Phase 2: Waiting for graceful endpoints to complete");
378
379 let count = tracker.get_count();
380 tracing::info!("Active graceful endpoints: {count}");
381
382 if count != 0 {
383 let timeout = graceful_shutdown_timeout();
384 if tokio::time::timeout(timeout, tracker.wait_for_completion())
385 .await
386 .is_err()
387 {
388 let remaining = tracker.get_count();
389 tracing::error!(
390 timeout_secs = timeout.as_secs(),
391 remaining_endpoints = remaining,
392 "Graceful endpoint shutdown timed out; proceeding with runtime teardown"
393 );
394 }
395 }
396
397 tracing::info!("Phase 3: Connections to backend services will now be disconnected");
399 main_token.cancel();
400 });
401 }
402}
403
404impl RuntimeType {
405 pub fn handle(&self) -> tokio::runtime::Handle {
407 match self {
408 RuntimeType::External(rt) => rt.clone(),
409 RuntimeType::Shared(rt) => rt.handle().clone(),
410 }
411 }
412}
413
414impl Drop for RuntimeType {
427 fn drop(&mut self) {
428 match self {
429 RuntimeType::External(_) => {}
430 RuntimeType::Shared(arc) => {
431 let Some(md_runtime) = Arc::get_mut(arc) else {
432 return;
435 };
436 if tokio::runtime::Handle::try_current().is_ok() {
437 let tokio_runtime = unsafe { ManuallyDrop::take(md_runtime) };
439 tokio_runtime.shutdown_background();
440 } else {
441 unsafe { ManuallyDrop::drop(md_runtime) };
448 }
449 }
450 }
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use crate::config::environment_names::runtime as env_runtime;
458
459 #[tokio::test(start_paused = true)]
460 async fn shutdown_cancels_main_token_after_graceful_timeout() {
461 temp_env::async_with_vars(
462 [(
463 env_runtime::DYN_RUNTIME_GRACEFUL_SHUTDOWN_TIMEOUT_SECS,
464 Some("5"),
465 )],
466 async {
467 let runtime = Runtime::from_current().unwrap();
468 let tracker = runtime.graceful_shutdown_tracker();
469 let _guard = tracker.register_task();
470 let main_token = runtime.primary_token();
471 let endpoint_token = runtime.child_token();
472
473 runtime.shutdown();
474 tokio::task::yield_now().await;
475
476 assert!(endpoint_token.is_cancelled());
477 assert!(!main_token.is_cancelled());
478 assert_eq!(tracker.get_count(), 1);
479
480 tokio::time::advance(Duration::from_secs(4)).await;
481 tokio::task::yield_now().await;
482
483 assert!(!main_token.is_cancelled());
484
485 tokio::time::advance(Duration::from_secs(1)).await;
486 tokio::task::yield_now().await;
487
488 assert!(main_token.is_cancelled());
489 assert_eq!(tracker.get_count(), 1);
490 },
491 )
492 .await;
493 }
494}