|
1 | | -use axum::{extract::Extension, routing::get}; |
| 1 | +use axum::routing::get; |
2 | 2 |
|
3 | 3 | #[cfg(all( |
4 | 4 | tokio_unstable, |
5 | 5 | target_os = "linux", |
6 | 6 | any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64") |
7 | 7 | ))] |
8 | 8 | mod imp { |
9 | | - use std::{collections::BTreeMap, fmt::Write as _, sync::Arc, time::Duration}; |
| 9 | + use std::{collections::BTreeMap, fmt::Write as _, num::NonZeroU64, sync::Arc, time::Duration}; |
10 | 10 |
|
11 | 11 | use axum::{ |
12 | 12 | extract::{Extension, Query}, |
13 | 13 | response::Response, |
14 | 14 | }; |
15 | 15 | use http::{header::CONTENT_TYPE, StatusCode}; |
16 | 16 | use serde::Deserialize; |
17 | | - use tokio::{runtime::Handle, sync::Mutex}; |
18 | | - |
19 | | - const MAX_TIMEOUT_MS: u64 = 30_000; |
| 17 | + use tokio::runtime::Handle; |
20 | 18 |
|
21 | | - #[derive(Clone)] |
22 | | - struct Runtime { |
23 | | - handle: Handle, |
24 | | - dump_lock: Arc<Mutex<()>>, |
25 | | - } |
| 19 | + const DEFAULT_TIMEOUT_MS: u64 = 2_000; |
26 | 20 |
|
27 | 21 | /// The Tokio runtimes which can be inspected by the internal task dump endpoint. |
28 | 22 | #[derive(Clone, Default)] |
29 | 23 | pub struct TaskDumpRegistry { |
30 | | - runtimes: Arc<BTreeMap<&'static str, Runtime>>, |
| 24 | + runtimes: Arc<BTreeMap<&'static str, Handle>>, |
31 | 25 | } |
32 | 26 |
|
33 | 27 | impl TaskDumpRegistry { |
34 | 28 | pub fn new(runtimes: impl IntoIterator<Item = (&'static str, Handle)>) -> Self { |
35 | | - let runtimes = runtimes |
36 | | - .into_iter() |
37 | | - .map(|(name, handle)| { |
38 | | - ( |
39 | | - name, |
40 | | - Runtime { |
41 | | - handle, |
42 | | - dump_lock: Arc::new(Mutex::new(())), |
43 | | - }, |
44 | | - ) |
45 | | - }) |
46 | | - .collect(); |
47 | 29 | Self { |
48 | | - runtimes: Arc::new(runtimes), |
| 30 | + runtimes: Arc::new(runtimes.into_iter().collect()), |
49 | 31 | } |
50 | 32 | } |
51 | | - |
52 | | - fn get(&self, name: &str) -> Option<&Runtime> { |
53 | | - self.runtimes.get(name) |
54 | | - } |
55 | | - |
56 | | - fn names(&self) -> impl Iterator<Item = &'static str> + '_ { |
57 | | - self.runtimes.keys().copied() |
58 | | - } |
59 | 33 | } |
60 | 34 |
|
61 | 35 | #[derive(Deserialize)] |
62 | 36 | pub(super) struct TaskDumpQuery { |
63 | 37 | runtime: String, |
64 | | - timeout_ms: u64, |
65 | | - } |
66 | | - |
67 | | - impl TaskDumpQuery { |
68 | | - fn validate(&self) -> Result<(), (StatusCode, String)> { |
69 | | - if !(1..=MAX_TIMEOUT_MS).contains(&self.timeout_ms) { |
70 | | - return Err(( |
71 | | - StatusCode::BAD_REQUEST, |
72 | | - format!("timeout_ms must be between 1 and {MAX_TIMEOUT_MS}"), |
73 | | - )); |
74 | | - } |
75 | | - Ok(()) |
76 | | - } |
| 38 | + timeout_ms: Option<NonZeroU64>, |
77 | 39 | } |
78 | 40 |
|
79 | 41 | pub(super) async fn handle_get_task_dump( |
80 | | - Extension(registry): Extension<TaskDumpRegistry>, |
| 42 | + registry: Option<Extension<TaskDumpRegistry>>, |
81 | 43 | Query(query): Query<TaskDumpQuery>, |
82 | 44 | ) -> Result<Response, (StatusCode, String)> { |
83 | | - query.validate()?; |
| 45 | + let Some(Extension(registry)) = registry else { |
| 46 | + return Err(( |
| 47 | + StatusCode::NOT_IMPLEMENTED, |
| 48 | + "Tokio task dumps are not configured for this server".into(), |
| 49 | + )); |
| 50 | + }; |
84 | 51 |
|
85 | | - let Some(runtime) = registry.get(&query.runtime).cloned() else { |
86 | | - let valid = registry.names().collect::<Vec<_>>().join(", "); |
| 52 | + let Some(runtime) = registry.runtimes.get(query.runtime.as_str()).cloned() else { |
| 53 | + let valid = registry.runtimes.keys().copied().collect::<Vec<_>>().join(", "); |
87 | 54 | return Err(( |
88 | 55 | StatusCode::BAD_REQUEST, |
89 | 56 | format!("unknown Tokio runtime {:?}; valid runtimes: {valid}", query.runtime), |
90 | 57 | )); |
91 | 58 | }; |
92 | 59 |
|
93 | | - let permit = runtime.dump_lock.try_lock_owned().map_err(|_| { |
94 | | - ( |
95 | | - StatusCode::CONFLICT, |
96 | | - format!("a task dump is already in progress for runtime {:?}", query.runtime), |
97 | | - ) |
98 | | - })?; |
99 | | - |
100 | | - // Keep the permit in the spawned task so that a timed-out dump continues |
101 | | - // to exclude new requests until Tokio's dump future actually finishes. |
102 | | - let dump_task = tokio::spawn(async move { |
103 | | - let dump = runtime.handle.dump().await; |
104 | | - (permit, dump) |
105 | | - }); |
106 | | - let (_permit, dump) = tokio::time::timeout(Duration::from_millis(query.timeout_ms), dump_task) |
| 60 | + let timeout_ms = query.timeout_ms.map_or(DEFAULT_TIMEOUT_MS, NonZeroU64::get); |
| 61 | + let dump = tokio::time::timeout(Duration::from_millis(timeout_ms), runtime.dump()) |
107 | 62 | .await |
108 | 63 | .map_err(|_| { |
109 | 64 | ( |
110 | 65 | StatusCode::GATEWAY_TIMEOUT, |
111 | 66 | format!( |
112 | | - "timed out after {}ms while dumping Tokio runtime {:?}", |
113 | | - query.timeout_ms, query.runtime |
| 67 | + "timed out after {timeout_ms}ms while dumping Tokio runtime {:?}", |
| 68 | + query.runtime |
114 | 69 | ), |
115 | 70 | ) |
116 | | - })? |
117 | | - .map_err(|err| { |
118 | | - ( |
119 | | - StatusCode::INTERNAL_SERVER_ERROR, |
120 | | - format!("task dump worker failed for runtime {:?}: {err}", query.runtime), |
121 | | - ) |
122 | 71 | })?; |
123 | 72 |
|
124 | 73 | let runtime_name = query.runtime; |
@@ -155,44 +104,16 @@ mod imp { |
155 | 104 | mod tests { |
156 | 105 | use super::*; |
157 | 106 | use http_body_util::BodyExt as _; |
158 | | - |
159 | | - #[tokio::test] |
160 | | - async fn registry_names_are_sorted() { |
161 | | - let handle = Handle::current(); |
162 | | - let registry = TaskDumpRegistry::new([("replication", handle.clone()), ("main", handle)]); |
163 | | - |
164 | | - assert_eq!(registry.names().collect::<Vec<_>>(), ["main", "replication"]); |
165 | | - } |
166 | | - |
167 | | - #[test] |
168 | | - fn timeout_must_be_in_range() { |
169 | | - for timeout_ms in [1, MAX_TIMEOUT_MS] { |
170 | | - assert!(TaskDumpQuery { |
171 | | - runtime: "main".into(), |
172 | | - timeout_ms, |
173 | | - } |
174 | | - .validate() |
175 | | - .is_ok()); |
176 | | - } |
177 | | - |
178 | | - for timeout_ms in [0, MAX_TIMEOUT_MS + 1] { |
179 | | - assert!(TaskDumpQuery { |
180 | | - runtime: "main".into(), |
181 | | - timeout_ms, |
182 | | - } |
183 | | - .validate() |
184 | | - .is_err()); |
185 | | - } |
186 | | - } |
| 107 | + use tokio::runtime::Handle; |
187 | 108 |
|
188 | 109 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] |
189 | | - async fn dumps_registered_runtime() { |
| 110 | + async fn dumps_registered_runtime_with_default_timeout() { |
190 | 111 | let registry = TaskDumpRegistry::new([("main", Handle::current())]); |
191 | 112 | let response = handle_get_task_dump( |
192 | | - Extension(registry), |
| 113 | + Some(Extension(registry)), |
193 | 114 | Query(TaskDumpQuery { |
194 | 115 | runtime: "main".into(), |
195 | | - timeout_ms: 10_000, |
| 116 | + timeout_ms: None, |
196 | 117 | }), |
197 | 118 | ) |
198 | 119 | .await |
@@ -248,8 +169,6 @@ mod imp { |
248 | 169 | use imp::handle_get_task_dump; |
249 | 170 | pub use imp::TaskDumpRegistry; |
250 | 171 |
|
251 | | -pub fn router<S: Clone + Send + Sync + 'static>(registry: TaskDumpRegistry) -> axum::Router<S> { |
252 | | - axum::Router::new() |
253 | | - .route("/", get(handle_get_task_dump)) |
254 | | - .layer(Extension(registry)) |
| 172 | +pub fn router<S: Clone + Send + Sync + 'static>() -> axum::Router<S> { |
| 173 | + axum::Router::new().route("/", get(handle_get_task_dump)) |
255 | 174 | } |
0 commit comments