-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(jstzd): define Task trait and type JoinHandle #555
Conversation
crates/jstzd/src/task/task.rs
Outdated
|
||
pub struct JoinHandle { | ||
task: OnceCell<tokioJoinHandle<()>>, | ||
allowed_signal: SignalKind, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you've misunderstood the purpose of signals here. A signal is used to notify the task of some external action (commonly terminating).
But instead of abruptly aborting the Tokio task (which doesn't leave any room for graceful termination / cleaning up of resources), its the implementator of the Task
trait responsibility to ensure that when sent a termination signal, run_task
returns.
The intended use would be that the signal would be queued in the mpsc::Sender
, with the implementator using the mpsc::Recieve
to handle signals (including termination).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we should have an abort
on a configurable timeout?
crates/jstzd/src/task/task.rs
Outdated
} | ||
|
||
pub struct JoinHandle { | ||
task: OnceCell<tokioJoinHandle<()>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would Option
work instead? I don't see anywhere that we're relying on OnceCell::set
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. The idea was that this task should only be consumed once, e.g. accessing it after .wait
is called should lead to an error, but it's not really a big deal at this moment. This is now changed to Option
.
crates/jstzd/src/task/task.rs
Outdated
return match self.task.take() { | ||
Some(handle) => { | ||
let result = handle.await; | ||
if result.is_err() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest using an if let
here to avoid the unwrap_err
crates/jstzd/src/task/task.rs
Outdated
let waker = cx.waker().clone(); | ||
tokio::spawn(async move { | ||
tokio::time::sleep(std::time::Duration::from_secs_f32(0.3)).await; | ||
waker.wake(); | ||
}); | ||
Poll::Pending |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of creating a new thread to wakeup the waker, recommend using the .poll
method of the handle
(it also implements Future
). Something like the following should work:
let pin = Pin::new(handle);
match pin.poll(cx) {
Poll::Ready(_) => Poll::Ready(())
Poll::Pending => Poll::Pending,
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed offline, calling poll here requires a mutable reference to handle
and that makes things quite complicated here. I made a few attempts but still could not make it work.
crates/jstzd/src/task/task.rs
Outdated
} | ||
|
||
impl Future for JoinHandle { | ||
type Output = (); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we return an anyhow::Result<()>
? Since .await
is essentially .wait
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. This is now updated.
crates/jstzd/src/task/task.rs
Outdated
/// ); | ||
/// # }); | ||
/// ``` | ||
pub fn new(task: tokioJoinHandle<()>, signal: SignalKind) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really intended to be part of the API, recommend guarding with #[cfg(test)]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we will need this initialiser since all Task
implementations will also involve creating new JoinHandle
instances (in method spawn_task
.) Having this initialiser makes it easier and prevents implementations from messing up internal members.
crates/jstzd/src/task/task.rs
Outdated
// On drop, send shutdown signal and wait for the task to terminate | ||
impl Drop for JoinHandle { | ||
fn drop(&mut self) { | ||
if !self.dropped { | ||
let mut this = JoinHandle::default(); | ||
std::mem::swap(&mut this, self); | ||
this.dropped = true; | ||
tokio::spawn(async move { this.async_drop().await }); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this is rather similar to the AsyncDropper
wrapper from async_dropper_simple
. I'm more leaning towards using the derive
feature from async_drop
instead of the simple
version in which case we could use #[derive(AsyncDrop)]
. What do you think? cc @ryutamago @zcabter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially, was leaning towards async_dropper_simple
since it removed the Default requirement as it gets more difficult to define default for large structs and there is a tendency to simply derive non-meaningful defaults makes the code harder to use but it adds invasive boilerplate.
If we use derive, our structs are rather simple (✅ ) and so we just have to be careful to define a meaningful defaults where we can.
tldr; derive is fine with me, define meaningful defaults where possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the derive feature requires us to implement Eq
and PartialEq
, which is going to be even messier. Had a discussion with @johnyob and the cleaner solution would be to write a macro that implements the drop for us
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, we don't need to implement drop if the tasks don't panic. Instead, have the jstzd orchestrator registers shutdown/clean up hooks for each task it registers. The orchestrator could control shutdown of tasks based on the state of other tasks. If orchestrator itself panics, it calls shutdown in its async dropper which isolates the async dropper boilerplate. This makes the shutdowns more explicit rather than implicitly cascading, its easier to test and easier to read. wdyt?
crates/jstzd/src/task/task.rs
Outdated
)); | ||
} | ||
if let Some(handle) = self.task.get() { | ||
handle.abort(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment above re: signals
crates/jstzd/src/task/task.rs
Outdated
impl Drop for JoinHandle { | ||
fn drop(&mut self) { | ||
if !self.dropped { | ||
let mut this = JoinHandle::default(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q:
shouldn't we set self.dropped = true
before and after the swap to prevent double drop?
if !self.dropped {
self.dropped = true
let mut this = ...
std::mem::swap(&mut this, self);
self.dropped = true;
tokio::spwan(
crates/jstzd/src/task/task.rs
Outdated
let mut this = JoinHandle::default(); | ||
std::mem::swap(&mut this, self); | ||
this.dropped = true; | ||
tokio::spawn(async move { this.async_drop().await }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the main thread exits before the async_drop completes, the drop will not get executed. the async-dropper-simple library uses the scope_and_block function instead to block the current thread until the task ends. perhaps it is a better option ? cc @johnyob @zcabter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think calling tokio::spawn
here causes race conditions. Drop
is synchronous so we should only allow synchronous calls. I suggest something like tokio::block_in_place
?
I've managed to fix everything. This implementation is a bit annoying, but I think we need it to be like this. Here is the explanation. We want to enable the following things:
To achieve these, some new members are added to the
Signalling is now achieved through channels. Task creators are supposed to somehow include the receiver in the task definition (and implement signal handling) and pass the sender to the handle. Calling One main challenge here is moving handle instances. A possible use case of the handles is as follows: let handle = JoinHandle::new(...);
// Do something and send signals to the task
tokio::spawn(async move {
...
handle.signal(...);
});
// Abort the task should anything fatal happen
tokio::spawn(async move {
...
handle.abort().await;
});
let result = handle.wait().await;
Also moved |
e386ff8
to
8491325
Compare
Codecov ReportAttention: Patch coverage is
... and 14 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
|
Context
Define the
Task
trait and implement the future wrapper typeJoinHandle
.Task
represents any task to be run with backends, e.g. docker engine, shell, jstz node.JSTZ-53
Description
Changes in
Cargo.toml
will be updated accordingly after #553 is merged.Manually testing the PR
cargo test