threadpool: drop incomplete tasks on shutdown (#722)

## Motivation

When the thread pool shuts down, futures that have been polled at least once but not completed yet are simply leaked. We should drop them instead.

## Solution

Multiple changes are introduced:

* Tasks are assigned a home worker the first time they are polled.

* Each worker contains a set of tasks (`Arc<Task>`) it is home to. When a task is assigned a home worker, it is registered in that worker's set of tasks. When the task is completed, it is unregistered from the set.

* When the thread pool shuts down and after all worker threads stop, the remaining tasks in workers' sets are aborted, i.e. they are switched to the `Aborted` state and their `Future`s are dropped.

* The thread pool shutdown process is refactored to make it more robust. We don't  track the number of active threads manually anymore. Instead, there's  `Arc<ShutdownTrigger>` that aborts remaining tasks and completes the `Shutdown` future once it gets destroyed (when all `Worker`s and `ThreadPool` get dropped because they're the only ones to contain strong references to the `ShutdownTrigger`).

Closes #424 
Closes #428
This commit is contained in:
Stjepan Glavina
2019-01-17 22:12:25 +01:00
committed by GitHub
parent c980837581
commit 4c8f274db9
9 changed files with 310 additions and 34 deletions
+113
View File
@@ -3,7 +3,10 @@ extern crate tokio_executor;
extern crate futures;
extern crate env_logger;
use tokio_executor::park::{Park, Unpark};
use tokio_threadpool::*;
use tokio_threadpool::park::{DefaultPark, DefaultUnpark};
use futures::{Poll, Sink, Stream, Async, Future};
use futures::future::lazy;
@@ -420,3 +423,113 @@ fn multi_threadpool() {
done_rx.recv().unwrap();
}
#[test]
fn eagerly_drops_futures() {
use futures::future::{Future, lazy, empty};
use futures::task;
use std::sync::mpsc;
struct NotifyOnDrop(mpsc::Sender<()>);
impl Drop for NotifyOnDrop {
fn drop(&mut self) {
self.0.send(()).unwrap();
}
}
struct MyPark {
inner: DefaultPark,
#[allow(dead_code)]
park_tx: mpsc::SyncSender<()>,
unpark_tx: mpsc::SyncSender<()>,
}
impl Park for MyPark {
type Unpark = MyUnpark;
type Error = <DefaultPark as Park>::Error;
fn unpark(&self) -> Self::Unpark {
MyUnpark {
inner: self.inner.unpark(),
unpark_tx: self.unpark_tx.clone(),
}
}
fn park(&mut self) -> Result<(), Self::Error> {
self.inner.park()
}
fn park_timeout(&mut self, duration: Duration) -> Result<(), Self::Error> {
self.inner.park_timeout(duration)
}
}
struct MyUnpark {
inner: DefaultUnpark,
#[allow(dead_code)]
unpark_tx: mpsc::SyncSender<()>,
}
impl Unpark for MyUnpark {
fn unpark(&self) {
self.inner.unpark()
}
}
let (task_tx, task_rx) = mpsc::channel();
let (drop_tx, drop_rx) = mpsc::channel();
let (park_tx, park_rx) = mpsc::sync_channel(0);
let (unpark_tx, unpark_rx) = mpsc::sync_channel(0);
// Get the signal that the handler dropped.
let notify_on_drop = NotifyOnDrop(drop_tx);
let pool = tokio_threadpool::Builder::new()
.custom_park(move |_| {
MyPark {
inner: DefaultPark::new(),
park_tx: park_tx.clone(),
unpark_tx: unpark_tx.clone(),
}
})
.build();
pool.spawn(lazy(move || {
// Get a handle to the current task.
let task = task::current();
// Send it to the main thread to hold on to.
task_tx.send(task).unwrap();
// This future will never resolve, it is only used to hold on to thee
// `notify_on_drop` handle.
empty::<(), ()>().then(move |_| {
// This code path should never be reached.
if true { panic!() }
// Explicitly drop `notify_on_drop` here, this is mostly to ensure
// that the `notify_on_drop` handle gets moved into the task. It
// will actually get dropped when the runtime is dropped.
drop(notify_on_drop);
Ok(())
})
}));
// Wait until we get the task handle.
let task = task_rx.recv().unwrap();
// Drop the pool, this should result in futures being forcefully dropped.
drop(pool);
// Make sure `MyPark` and `MyUnpark` were dropped during shutdown.
assert_eq!(park_rx.try_recv(), Err(mpsc::TryRecvError::Disconnected));
assert_eq!(unpark_rx.try_recv(), Err(mpsc::TryRecvError::Disconnected));
// If the future is forcefully dropped, then we will get a signal here.
drop_rx.recv().unwrap();
// Ensure `task` lives until after the test completes.
drop(task);
}