async_inspect/task/
mod.rs

1//! Task tracking and monitoring
2//!
3//! This module provides the core data structures for tracking async tasks,
4//! including task IDs, states, and metadata.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::time::{Duration, Instant};
10
11/// Unique identifier for a task
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct TaskId(u64);
14
15impl TaskId {
16    /// Create a new unique task ID
17    pub fn new() -> Self {
18        static COUNTER: AtomicU64 = AtomicU64::new(1);
19        Self(COUNTER.fetch_add(1, Ordering::Relaxed))
20    }
21
22    /// Get the raw ID value
23    #[must_use]
24    pub fn as_u64(&self) -> u64 {
25        self.0
26    }
27
28    /// Create a `TaskId` from a raw u64 value (for testing/examples)
29    #[must_use]
30    pub fn from_u64(id: u64) -> Self {
31        Self(id)
32    }
33}
34
35impl Default for TaskId {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl fmt::Display for TaskId {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        write!(f, "#{}", self.0)
44    }
45}
46
47/// Current state of a task
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub enum TaskState {
50    /// Task has been spawned but not yet polled
51    Pending,
52    /// Task is currently being polled
53    Running,
54    /// Task is waiting on an async operation
55    Blocked {
56        /// Name of the await point
57        await_point: String,
58    },
59    /// Task has completed successfully
60    Completed,
61    /// Task was cancelled or panicked
62    Failed,
63}
64
65impl fmt::Display for TaskState {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Self::Pending => write!(f, "PENDING"),
69            Self::Running => write!(f, "RUNNING"),
70            Self::Blocked { await_point } => write!(f, "BLOCKED({await_point})"),
71            Self::Completed => write!(f, "COMPLETED"),
72            Self::Failed => write!(f, "FAILED"),
73        }
74    }
75}
76
77/// Information about a task
78#[derive(Debug, Clone)]
79pub struct TaskInfo {
80    /// Unique task identifier
81    pub id: TaskId,
82
83    /// Human-readable task name
84    pub name: String,
85
86    /// Current state of the task
87    pub state: TaskState,
88
89    /// When the task was created
90    pub created_at: Instant,
91
92    /// When the task last changed state
93    pub last_updated: Instant,
94
95    /// Number of times the task has been polled
96    pub poll_count: u64,
97
98    /// Total time spent in running state
99    pub total_run_time: Duration,
100
101    /// Parent task ID, if any
102    pub parent: Option<TaskId>,
103
104    /// Source location (<file:line>)
105    pub location: Option<String>,
106}
107
108impl TaskInfo {
109    /// Create a new task info
110    #[must_use]
111    pub fn new(name: String) -> Self {
112        let now = Instant::now();
113        Self {
114            id: TaskId::new(),
115            name,
116            state: TaskState::Pending,
117            created_at: now,
118            last_updated: now,
119            poll_count: 0,
120            total_run_time: Duration::ZERO,
121            parent: None,
122            location: None,
123        }
124    }
125
126    /// Update the task state
127    pub fn update_state(&mut self, new_state: TaskState) {
128        self.state = new_state;
129        self.last_updated = Instant::now();
130    }
131
132    /// Record a poll
133    pub fn record_poll(&mut self, duration: Duration) {
134        self.poll_count += 1;
135        self.total_run_time += duration;
136        self.last_updated = Instant::now();
137    }
138
139    /// Get the age of the task
140    #[must_use]
141    pub fn age(&self) -> Duration {
142        self.created_at.elapsed()
143    }
144
145    /// Get time since last update
146    #[must_use]
147    pub fn time_since_update(&self) -> Duration {
148        self.last_updated.elapsed()
149    }
150
151    /// Set the parent task
152    #[must_use]
153    pub fn with_parent(mut self, parent: TaskId) -> Self {
154        self.parent = Some(parent);
155        self
156    }
157
158    /// Set the source location
159    #[must_use]
160    pub fn with_location(mut self, location: String) -> Self {
161        self.location = Some(location);
162        self
163    }
164}
165
166impl fmt::Display for TaskInfo {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(
169            f,
170            "Task {} [{}]: {} (polls: {}, runtime: {:.2}s, age: {:.2}s)",
171            self.id,
172            self.name,
173            self.state,
174            self.poll_count,
175            self.total_run_time.as_secs_f64(),
176            self.age().as_secs_f64()
177        )
178    }
179}
180
181/// Filter for querying tasks
182///
183/// Use the builder pattern to construct filters:
184///
185/// ```rust
186/// use async_inspect::task::{TaskFilter, TaskState};
187/// use std::time::Duration;
188///
189/// let filter = TaskFilter::new()
190///     .with_state(TaskState::Running)
191///     .with_name_pattern("fetch")
192///     .with_min_duration(Duration::from_secs(1));
193/// ```
194#[derive(Debug, Clone, Default)]
195pub struct TaskFilter {
196    /// Filter by task state
197    pub state: Option<TaskState>,
198    /// Filter by name pattern (substring match)
199    pub name_pattern: Option<String>,
200    /// Filter by minimum age/duration
201    pub min_duration: Option<Duration>,
202    /// Filter by maximum age/duration
203    pub max_duration: Option<Duration>,
204    /// Filter by minimum poll count
205    pub min_polls: Option<u64>,
206    /// Filter by maximum poll count
207    pub max_polls: Option<u64>,
208    /// Filter by parent task
209    pub parent: Option<TaskId>,
210    /// Only show tasks without a parent (root tasks)
211    pub root_only: bool,
212}
213
214impl TaskFilter {
215    /// Create a new empty filter (matches all tasks)
216    #[must_use]
217    pub fn new() -> Self {
218        Self::default()
219    }
220
221    /// Filter by task state
222    #[must_use]
223    pub fn with_state(mut self, state: TaskState) -> Self {
224        self.state = Some(state);
225        self
226    }
227
228    /// Filter by name pattern (case-insensitive substring match)
229    #[must_use]
230    pub fn with_name_pattern(mut self, pattern: impl Into<String>) -> Self {
231        self.name_pattern = Some(pattern.into());
232        self
233    }
234
235    /// Filter by minimum age/duration
236    #[must_use]
237    pub fn with_min_duration(mut self, duration: Duration) -> Self {
238        self.min_duration = Some(duration);
239        self
240    }
241
242    /// Filter by maximum age/duration
243    #[must_use]
244    pub fn with_max_duration(mut self, duration: Duration) -> Self {
245        self.max_duration = Some(duration);
246        self
247    }
248
249    /// Filter by minimum poll count
250    #[must_use]
251    pub fn with_min_polls(mut self, count: u64) -> Self {
252        self.min_polls = Some(count);
253        self
254    }
255
256    /// Filter by maximum poll count
257    #[must_use]
258    pub fn with_max_polls(mut self, count: u64) -> Self {
259        self.max_polls = Some(count);
260        self
261    }
262
263    /// Filter by parent task
264    #[must_use]
265    pub fn with_parent(mut self, parent: TaskId) -> Self {
266        self.parent = Some(parent);
267        self
268    }
269
270    /// Only show root tasks (no parent)
271    #[must_use]
272    pub fn root_only(mut self) -> Self {
273        self.root_only = true;
274        self
275    }
276
277    /// Check if a task matches this filter
278    #[must_use]
279    pub fn matches(&self, task: &TaskInfo) -> bool {
280        // Check state
281        if let Some(ref state) = self.state {
282            if !self.state_matches(&task.state, state) {
283                return false;
284            }
285        }
286
287        // Check name pattern (case-insensitive)
288        if let Some(ref pattern) = self.name_pattern {
289            if !task.name.to_lowercase().contains(&pattern.to_lowercase()) {
290                return false;
291            }
292        }
293
294        // Check min duration
295        if let Some(min) = self.min_duration {
296            if task.age() < min {
297                return false;
298            }
299        }
300
301        // Check max duration
302        if let Some(max) = self.max_duration {
303            if task.age() > max {
304                return false;
305            }
306        }
307
308        // Check min polls
309        if let Some(min) = self.min_polls {
310            if task.poll_count < min {
311                return false;
312            }
313        }
314
315        // Check max polls
316        if let Some(max) = self.max_polls {
317            if task.poll_count > max {
318                return false;
319            }
320        }
321
322        // Check parent
323        if let Some(parent) = self.parent {
324            if task.parent != Some(parent) {
325                return false;
326            }
327        }
328
329        // Check root only
330        if self.root_only && task.parent.is_some() {
331            return false;
332        }
333
334        true
335    }
336
337    /// Check if task state matches the filter state
338    fn state_matches(&self, task_state: &TaskState, filter_state: &TaskState) -> bool {
339        match (task_state, filter_state) {
340            (TaskState::Pending, TaskState::Pending) => true,
341            (TaskState::Running, TaskState::Running) => true,
342            (TaskState::Blocked { .. }, TaskState::Blocked { .. }) => true,
343            (TaskState::Completed, TaskState::Completed) => true,
344            (TaskState::Failed, TaskState::Failed) => true,
345            _ => false,
346        }
347    }
348
349    /// Filter a collection of tasks
350    pub fn filter<'a>(&self, tasks: impl IntoIterator<Item = &'a TaskInfo>) -> Vec<&'a TaskInfo> {
351        tasks.into_iter().filter(|t| self.matches(t)).collect()
352    }
353
354    /// Filter and clone a collection of tasks
355    pub fn filter_cloned(&self, tasks: impl IntoIterator<Item = TaskInfo>) -> Vec<TaskInfo> {
356        tasks.into_iter().filter(|t| self.matches(t)).collect()
357    }
358}
359
360/// Sorting options for tasks
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
362pub enum TaskSortBy {
363    /// Sort by task ID (creation order)
364    #[default]
365    Id,
366    /// Sort by task name alphabetically
367    Name,
368    /// Sort by task age (oldest first)
369    Age,
370    /// Sort by poll count
371    Polls,
372    /// Sort by total run time
373    RunTime,
374    /// Sort by state
375    State,
376}
377
378/// Sort direction
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
380pub enum SortDirection {
381    /// Ascending order
382    #[default]
383    Ascending,
384    /// Descending order
385    Descending,
386}
387
388/// Sort tasks by the given criteria
389pub fn sort_tasks(tasks: &mut [TaskInfo], sort_by: TaskSortBy, direction: SortDirection) {
390    tasks.sort_by(|a, b| {
391        let cmp = match sort_by {
392            TaskSortBy::Id => a.id.as_u64().cmp(&b.id.as_u64()),
393            TaskSortBy::Name => a.name.cmp(&b.name),
394            TaskSortBy::Age => a.created_at.cmp(&b.created_at),
395            TaskSortBy::Polls => a.poll_count.cmp(&b.poll_count),
396            TaskSortBy::RunTime => a.total_run_time.cmp(&b.total_run_time),
397            TaskSortBy::State => state_order(&a.state).cmp(&state_order(&b.state)),
398        };
399
400        match direction {
401            SortDirection::Ascending => cmp,
402            SortDirection::Descending => cmp.reverse(),
403        }
404    });
405}
406
407/// Get sort order for task states
408fn state_order(state: &TaskState) -> u8 {
409    match state {
410        TaskState::Running => 0,
411        TaskState::Blocked { .. } => 1,
412        TaskState::Pending => 2,
413        TaskState::Completed => 3,
414        TaskState::Failed => 4,
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    #[test]
423    fn test_task_id_uniqueness() {
424        let id1 = TaskId::new();
425        let id2 = TaskId::new();
426        assert_ne!(id1, id2);
427    }
428
429    #[test]
430    fn test_task_info_creation() {
431        let task = TaskInfo::new("test_task".to_string());
432        assert_eq!(task.name, "test_task");
433        assert_eq!(task.state, TaskState::Pending);
434        assert_eq!(task.poll_count, 0);
435    }
436
437    #[test]
438    fn test_task_state_update() {
439        let mut task = TaskInfo::new("test".to_string());
440        task.update_state(TaskState::Running);
441        assert_eq!(task.state, TaskState::Running);
442    }
443
444    #[test]
445    fn test_task_poll_recording() {
446        let mut task = TaskInfo::new("test".to_string());
447        task.record_poll(Duration::from_millis(100));
448        assert_eq!(task.poll_count, 1);
449        assert_eq!(task.total_run_time, Duration::from_millis(100));
450    }
451}