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
35fn 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_settings() -> anyhow::Result<Runtime> {
273 let config = config::RuntimeConfig::from_settings()?;
274 let runtime = Arc::new(ManuallyDrop::new(config.create_runtime()?));
275 let primary = RuntimeType::Shared(runtime.clone());
276 let secondary = RuntimeType::External(runtime.handle().clone());
277 Runtime::new_with_config(primary, Some(secondary), &config)
278 }
279
280 pub fn single_threaded() -> anyhow::Result<Runtime> {
282 let config = config::RuntimeConfig::single_threaded();
283 let owned = RuntimeType::Shared(Arc::new(ManuallyDrop::new(config.create_runtime()?)));
284 Runtime::new(owned, None)
285 }
286
287 pub fn id(&self) -> &str {
289 &self.id
290 }
291
292 pub fn primary(&self) -> tokio::runtime::Handle {
294 self.primary.handle()
295 }
296
297 pub fn secondary(&self) -> tokio::runtime::Handle {
299 self.secondary.handle()
300 }
301
302 pub fn primary_token(&self) -> CancellationToken {
304 self.cancellation_token.clone()
305 }
306
307 pub fn child_token(&self) -> CancellationToken {
309 self.endpoint_shutdown_token.child_token()
310 }
311
312 pub(crate) fn graceful_shutdown_tracker(&self) -> Arc<GracefulShutdownTracker> {
314 self.graceful_shutdown_tracker.clone()
315 }
316
317 pub fn compute_pool(&self) -> Option<&Arc<crate::compute::ComputePool>> {
321 self.compute_pool.as_ref()
322 }
323
324 pub fn shutdown(&self) {
326 tracing::info!("Runtime shutdown initiated");
327
328 let tracker = self.graceful_shutdown_tracker.clone();
330 let main_token = self.cancellation_token.clone();
331 let endpoint_token = self.endpoint_shutdown_token.clone();
332
333 let handle = self.primary();
335 handle.spawn(async move {
336 tracing::info!("Phase 1: Cancelling endpoint shutdown token");
338 endpoint_token.cancel();
339
340 tracing::info!("Phase 2: Waiting for graceful endpoints to complete");
342
343 let count = tracker.get_count();
344 tracing::info!("Active graceful endpoints: {count}");
345
346 if count != 0 {
347 let timeout = graceful_shutdown_timeout();
348 if tokio::time::timeout(timeout, tracker.wait_for_completion())
349 .await
350 .is_err()
351 {
352 let remaining = tracker.get_count();
353 tracing::error!(
354 timeout_secs = timeout.as_secs(),
355 remaining_endpoints = remaining,
356 "Graceful endpoint shutdown timed out; proceeding with runtime teardown"
357 );
358 }
359 }
360
361 tracing::info!("Phase 3: Connections to backend services will now be disconnected");
363 main_token.cancel();
364 });
365 }
366}
367
368impl RuntimeType {
369 pub fn handle(&self) -> tokio::runtime::Handle {
371 match self {
372 RuntimeType::External(rt) => rt.clone(),
373 RuntimeType::Shared(rt) => rt.handle().clone(),
374 }
375 }
376}
377
378impl Drop for RuntimeType {
391 fn drop(&mut self) {
392 match self {
393 RuntimeType::External(_) => {}
394 RuntimeType::Shared(arc) => {
395 let Some(md_runtime) = Arc::get_mut(arc) else {
396 return;
399 };
400 if tokio::runtime::Handle::try_current().is_ok() {
401 let tokio_runtime = unsafe { ManuallyDrop::take(md_runtime) };
403 tokio_runtime.shutdown_background();
404 } else {
405 unsafe { ManuallyDrop::drop(md_runtime) };
412 }
413 }
414 }
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::config::environment_names::runtime as env_runtime;
422
423 #[tokio::test(start_paused = true)]
424 async fn shutdown_cancels_main_token_after_graceful_timeout() {
425 temp_env::async_with_vars(
426 [(
427 env_runtime::DYN_RUNTIME_GRACEFUL_SHUTDOWN_TIMEOUT_SECS,
428 Some("5"),
429 )],
430 async {
431 let runtime = Runtime::from_current().unwrap();
432 let tracker = runtime.graceful_shutdown_tracker();
433 let _guard = tracker.register_task();
434 let main_token = runtime.primary_token();
435 let endpoint_token = runtime.child_token();
436
437 runtime.shutdown();
438 tokio::task::yield_now().await;
439
440 assert!(endpoint_token.is_cancelled());
441 assert!(!main_token.is_cancelled());
442 assert_eq!(tracker.get_count(), 1);
443
444 tokio::time::advance(Duration::from_secs(4)).await;
445 tokio::task::yield_now().await;
446
447 assert!(!main_token.is_cancelled());
448
449 tokio::time::advance(Duration::from_secs(1)).await;
450 tokio::task::yield_now().await;
451
452 assert!(main_token.is_cancelled());
453 assert_eq!(tracker.get_count(), 1);
454 },
455 )
456 .await;
457 }
458}