Skip to content

Commit 8b5dbe4

Browse files
committed
test(subscriber): add test for tasks being kept open
In the Tokio instrumentation, a tracing span is created for each task which is spawned. Since the new span is created within the context of where `tokio::spawn()` (or similar) is called from, it gets a contextual parent attached. In tracing, when a span has a child span (either because the child was created in the context of the parent, or because the parent was set explicitly) then that span will not be closed until the child has closed. The result in the console subscriber is that a task which spawns another task won't have a `dropped_at` time set until the spawned task exits, even if the parent task exits much earlier. This causes Tokio Console to show an incorrect lost waker warning (#345). It also affects other spans that are entered when a task is spawned (#412). The solution is to modify the instrumentation in Tokio so that task spans are explicit roots (`parent: None`). This will be done as part of enriching the Tokio instrumentation (tokio-rs/tokio#5792). This change adds functionality to the test framework within `console-subscriber` so that the state of a task can be set as an expectation. The state is calculated based on 4 values: * `console_api::tasks::Stats::dropped_at` * `console_api::tasks::Stats::last_wake` * `console_api::PollStats::last_poll_started` * `console_api::PollStats::last_poll_ended` It can then be tested that a task that spawns another task and then ends actually goes to the `Completed` state, even if the spawned task is still running. As of Tokio 1.33.0, this test fails, but the PR FIXME:TBD fixes this and the test should pass from Tokio 1.34 onwards.
1 parent 8269b5f commit 8b5dbe4

File tree

4 files changed

+168
-3
lines changed

4 files changed

+168
-3
lines changed

console-subscriber/tests/framework.rs

+49-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use futures::future;
1111
use tokio::{task, time::sleep};
1212

1313
mod support;
14-
use support::{assert_task, assert_tasks, ExpectedTask};
14+
use support::{assert_task, assert_tasks, ExpectedTask, TaskState};
1515

1616
#[test]
1717
fn expect_present() {
@@ -198,6 +198,54 @@ fn fail_polls() {
198198
assert_task(expected_task, future);
199199
}
200200

201+
#[test]
202+
fn main_task_completes() {
203+
let expected_task = ExpectedTask::default()
204+
.match_default_name()
205+
.expect_state(TaskState::Completed);
206+
207+
let future = async {};
208+
209+
assert_task(expected_task, future);
210+
}
211+
212+
#[test]
213+
#[should_panic(expected = "Test failed: Task validation failed:
214+
- Task { name=task }: expected `state` to be Idle, but actual was Completed")]
215+
fn fail_completed_task_is_idle() {
216+
let expected_task = ExpectedTask::default()
217+
.match_name("task".into())
218+
.expect_state(TaskState::Idle);
219+
220+
let future = async {
221+
_ = task::Builder::new()
222+
.name("task")
223+
.spawn(futures::future::ready(()))
224+
.unwrap()
225+
.await;
226+
};
227+
228+
assert_task(expected_task, future);
229+
}
230+
231+
#[test]
232+
#[should_panic(expected = "Test failed: Task validation failed:
233+
- Task { name=task }: expected `state` to be Completed, but actual was Idle")]
234+
fn fail_idle_task_is_completed() {
235+
let expected_task = ExpectedTask::default()
236+
.match_name("task".into())
237+
.expect_state(TaskState::Completed);
238+
239+
let future = async {
240+
_ = task::Builder::new()
241+
.name("task")
242+
.spawn(futures::future::pending::<()>())
243+
.unwrap();
244+
};
245+
246+
assert_task(expected_task, future);
247+
}
248+
201249
async fn yield_to_runtime() {
202250
// There is a race condition that can occur when tests are run in parallel,
203251
// caused by tokio-rs/tracing#2743. It tends to cause test failures only

console-subscriber/tests/spawn.rs

+26-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::time::Duration;
33
use tokio::time::sleep;
44

55
mod support;
6-
use support::{assert_tasks, spawn_named, ExpectedTask};
6+
use support::{assert_tasks, spawn_named, ExpectedTask, TaskState};
77

88
/// This test asserts the behavior that was fixed in #440. Before that fix,
99
/// the polls of a child were also counted towards the parent (the task which
@@ -34,3 +34,28 @@ fn child_polls_dont_count_towards_parent_polls() {
3434

3535
assert_tasks(expected_tasks, future);
3636
}
37+
38+
/// This test asserts that the lifetime of a task is not affected by the
39+
/// lifetimes of tasks that it spawns. The test will pass when #345 is
40+
/// fixed.
41+
#[test]
42+
fn spawner_task_with_running_children_completes() {
43+
let expected_tasks = vec![
44+
ExpectedTask::default()
45+
.match_name("parent".into())
46+
.expect_state(TaskState::Completed),
47+
ExpectedTask::default()
48+
.match_name("child".into())
49+
.expect_state(TaskState::Idle),
50+
];
51+
52+
let future = async {
53+
spawn_named("parent", async {
54+
spawn_named("child", futures::future::pending::<()>());
55+
})
56+
.await
57+
.expect("joining parent failed");
58+
};
59+
60+
assert_tasks(expected_tasks, future);
61+
}

console-subscriber/tests/support/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use subscriber::run_test;
88

99
pub(crate) use subscriber::MAIN_TASK_NAME;
1010
pub(crate) use task::ExpectedTask;
11+
#[allow(unused_imports)]
12+
pub(crate) use task::TaskState;
1113
use tokio::task::JoinHandle;
1214

1315
/// Assert that an `expected_task` is recorded by a console-subscriber

console-subscriber/tests/support/task.rs

+91-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use std::{error, fmt};
1+
use std::{error, fmt, time::SystemTime};
22

33
use console_api::tasks;
4+
use prost_types::Timestamp;
45

56
use super::MAIN_TASK_NAME;
67

@@ -13,6 +14,7 @@ use super::MAIN_TASK_NAME;
1314
pub(super) struct ActualTask {
1415
pub(super) id: u64,
1516
pub(super) name: Option<String>,
17+
pub(super) state: Option<TaskState>,
1618
pub(super) wakes: u64,
1719
pub(super) self_wakes: u64,
1820
pub(super) polls: u64,
@@ -23,6 +25,7 @@ impl ActualTask {
2325
Self {
2426
id,
2527
name: None,
28+
state: None,
2629
wakes: 0,
2730
self_wakes: 0,
2831
polls: 0,
@@ -35,6 +38,59 @@ impl ActualTask {
3538
if let Some(poll_stats) = &stats.poll_stats {
3639
self.polls = poll_stats.polls;
3740
}
41+
42+
self.state = calculate_task_state(stats);
43+
}
44+
}
45+
46+
/// The state of a task.
47+
///
48+
/// The task state is an amalgamation of a various fields. It is presented in
49+
/// this way to make testing more straight forward.
50+
#[derive(Clone, Debug, Eq, PartialEq)]
51+
pub(crate) enum TaskState {
52+
/// Task has completed.
53+
///
54+
/// Indicates that [`dropped_at`] has some value.
55+
///
56+
/// [`dropped_at`]: fn@tasks::Stats::dropped_at
57+
Completed,
58+
/// Task is being polled.
59+
///
60+
/// Indicates that the task is not [`Completed`] and the
61+
/// [`last_poll_started`] time is later than [`last_poll_ended`] (or
62+
/// [`last_poll_ended`] has not been set).
63+
Running,
64+
/// Task has been scheduled.
65+
///
66+
/// Indicates that the task is not [`Completed`] and the [`last_wake`] time
67+
/// is later than [`last_poll_started`].
68+
Scheduled,
69+
/// Task is idle.
70+
///
71+
/// Indicates that the task is between polls.
72+
Idle,
73+
}
74+
75+
fn calculate_task_state(stats: &tasks::Stats) -> Option<TaskState> {
76+
if stats.dropped_at.is_some() {
77+
return Some(TaskState::Completed);
78+
}
79+
80+
fn convert(ts: &Option<Timestamp>) -> Option<SystemTime> {
81+
ts.as_ref().map(|v| v.clone().try_into().unwrap())
82+
}
83+
let poll_stats = stats.poll_stats.as_ref()?;
84+
let last_poll_started = convert(&poll_stats.last_poll_started);
85+
let last_poll_ended = convert(&poll_stats.last_poll_ended);
86+
let last_wake = convert(&stats.last_wake);
87+
88+
if last_poll_started > last_poll_ended {
89+
Some(TaskState::Running)
90+
} else if last_wake > last_poll_started {
91+
Some(TaskState::Scheduled)
92+
} else {
93+
Some(TaskState::Idle)
3894
}
3995
}
4096

@@ -88,6 +144,7 @@ impl fmt::Debug for TaskValidationFailure {
88144
pub(crate) struct ExpectedTask {
89145
match_name: Option<String>,
90146
expect_present: Option<bool>,
147+
expect_state: Option<TaskState>,
91148
expect_wakes: Option<u64>,
92149
expect_self_wakes: Option<u64>,
93150
expect_polls: Option<u64>,
@@ -98,6 +155,7 @@ impl Default for ExpectedTask {
98155
Self {
99156
match_name: None,
100157
expect_present: None,
158+
expect_state: None,
101159
expect_wakes: None,
102160
expect_self_wakes: None,
103161
expect_polls: None,
@@ -147,6 +205,28 @@ impl ExpectedTask {
147205
no_expectations = false;
148206
}
149207

208+
if let Some(expected_state) = &self.expect_state {
209+
no_expectations = false;
210+
if let Some(actual_state) = &actual_task.state {
211+
if expected_state != actual_state {
212+
return Err(TaskValidationFailure {
213+
expected: self.clone(),
214+
actual: Some(actual_task.clone()),
215+
failure: format!(
216+
"{self}: expected `state` to be \
217+
{expected_state:?}, but actual was \
218+
{actual_state}",
219+
actual_state = actual_task
220+
.state
221+
.as_ref()
222+
.map(|s| format!("{:?}", s))
223+
.unwrap_or("None".into()),
224+
),
225+
});
226+
}
227+
}
228+
}
229+
150230
if let Some(expected_wakes) = self.expect_wakes {
151231
no_expectations = false;
152232
if expected_wakes != actual_task.wakes {
@@ -239,6 +319,16 @@ impl ExpectedTask {
239319
self
240320
}
241321

322+
/// Expects that a task has a specific [`TaskState`].
323+
///
324+
/// To validate, the actual task must be in this state at the time
325+
/// the test ends and the validation is performed.
326+
#[allow(dead_code)]
327+
pub(crate) fn expect_state(mut self, state: TaskState) -> Self {
328+
self.expect_state = Some(state);
329+
self
330+
}
331+
242332
/// Expects that a task has a specific value for `wakes`.
243333
///
244334
/// To validate, the actual task matching this expected task must have

0 commit comments

Comments
 (0)