Skip to content

Commit ba0eb69

Browse files
authored
feat: task management (#20)
* feat: task management * fix: get a handle * cleanup: docs and pub * doc: expand slightly * refactor: clean up all tasks before returning * refactor: use a future instead of the token * nit: remove newline * doc: readme update * fix: remove unnecessary generic * chore: more docs and bubble up more API to shutdown * fix: prevent deadlock of task waiting on itself * chore: bump version
1 parent 2dfffec commit ba0eb69

File tree

13 files changed

+698
-103
lines changed

13 files changed

+698
-103
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "Simple, modern, ergonomic JSON-RPC 2.0 router built with tower an
55
keywords = ["json-rpc", "jsonrpc", "json"]
66
categories = ["web-programming::http-server", "web-programming::websocket"]
77

8-
version = "0.2.0"
8+
version = "0.3.0"
99
edition = "2021"
1010
rust-version = "1.81"
1111
authors = ["init4", "James Prestwich"]
@@ -31,7 +31,7 @@ tokio-stream = { version = "0.1.17", optional = true }
3131

3232
# ipc
3333
interprocess = { version = "2.2.2", features = ["async", "tokio"], optional = true }
34-
tokio-util = { version = "0.7.13", optional = true, features = ["io"] }
34+
tokio-util = { version = "0.7.13", optional = true, features = ["io", "rt"] }
3535

3636
# ws
3737
tokio-tungstenite = { version = "0.26.1", features = ["rustls-tls-webpki-roots"], optional = true }

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ ajj aims to provide simple, flexible, and ergonomic routing for JSON-RPC.
1212
- Support for pubsub-style notifications.
1313
- Built-in support for axum, and tower's middleware and service ecosystem.
1414
- Basic built-in pubsub server implementations for WS and IPC.
15+
- Connection-oriented task management automatically cancels tasks on client
16+
disconnect.
1517

1618
## Concepts
1719

src/axum.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,50 @@
1-
use crate::types::{InboundData, Response};
1+
use crate::{
2+
types::{InboundData, Response},
3+
HandlerCtx, TaskSet,
4+
};
25
use axum::{extract::FromRequest, response::IntoResponse};
36
use bytes::Bytes;
47
use std::{future::Future, pin::Pin};
8+
use tokio::runtime::Handle;
59

6-
impl<S> axum::handler::Handler<Bytes, S> for crate::Router<S>
10+
/// A wrapper around an [`Router`] that implements the
11+
/// [`axum::handler::Handler`] trait. This struct is an implementation detail
12+
/// of the [`Router::into_axum`] and [`Router::into_axum_with_handle`] methods.
13+
///
14+
/// [`Router`]: crate::Router
15+
/// [`Router::into_axum`]: crate::Router::into_axum
16+
/// [`Router::into_axum_with_handle`]: crate::Router::into_axum_with_handle
17+
#[derive(Debug, Clone)]
18+
pub(crate) struct IntoAxum<S> {
19+
pub(crate) router: crate::Router<S>,
20+
pub(crate) task_set: TaskSet,
21+
}
22+
23+
impl<S> From<crate::Router<S>> for IntoAxum<S> {
24+
fn from(router: crate::Router<S>) -> Self {
25+
Self {
26+
router,
27+
task_set: Default::default(),
28+
}
29+
}
30+
}
31+
32+
impl<S> IntoAxum<S> {
33+
/// Create a new `IntoAxum` from a router and task set.
34+
pub(crate) fn new(router: crate::Router<S>, handle: Handle) -> Self {
35+
Self {
36+
router,
37+
task_set: handle.into(),
38+
}
39+
}
40+
41+
/// Get a new context, built from the task set.
42+
fn ctx(&self) -> HandlerCtx {
43+
self.task_set.clone().into()
44+
}
45+
}
46+
47+
impl<S> axum::handler::Handler<Bytes, S> for IntoAxum<S>
748
where
849
S: Clone + Send + Sync + 'static,
950
{
@@ -21,7 +62,8 @@ where
2162
let req = InboundData::try_from(bytes).unwrap_or_default();
2263

2364
if let Some(response) = self
24-
.call_batch_with_state(Default::default(), req, state)
65+
.router
66+
.call_batch_with_state(self.ctx(), req, state)
2567
.await
2668
{
2769
Box::<str>::from(response).into_response()

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ pub(crate) use routes::{BoxedIntoRoute, ErasedIntoRoute, Method, Route};
152152
mod router;
153153
pub use router::Router;
154154

155+
mod tasks;
156+
pub(crate) use tasks::TaskSet;
157+
155158
mod types;
156159
pub use types::{ErrorPayload, ResponsePayload};
157160

src/pubsub/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ mod ipc;
9595
pub use ipc::ReadJsonStream;
9696

9797
mod shared;
98-
pub use shared::{ConnectionId, ServerShutdown, DEFAULT_NOTIFICATION_BUFFER_PER_CLIENT};
98+
pub use shared::{ConnectionId, DEFAULT_NOTIFICATION_BUFFER_PER_CLIENT};
99+
100+
mod shutdown;
101+
pub use shutdown::ServerShutdown;
99102

100103
mod r#trait;
101104
pub use r#trait::{Connect, In, JsonReqStream, JsonSink, Listener, Out};

src/pubsub/shared.rs

Lines changed: 49 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
use core::fmt;
2-
31
use crate::{
42
pubsub::{In, JsonSink, Listener, Out},
53
types::InboundData,
4+
HandlerCtx, TaskSet,
65
};
6+
use core::fmt;
77
use serde_json::value::RawValue;
8-
use tokio::{
9-
select,
10-
sync::{mpsc, oneshot, watch},
11-
task::JoinHandle,
12-
};
8+
use tokio::{pin, select, sync::mpsc, task::JoinHandle};
139
use tokio_stream::StreamExt;
10+
use tokio_util::sync::WaitForCancellationFutureOwned;
1411
use tracing::{debug, debug_span, error, instrument, trace, Instrument};
1512

1613
/// Default notification buffer size per task.
@@ -19,18 +16,6 @@ pub const DEFAULT_NOTIFICATION_BUFFER_PER_CLIENT: usize = 16;
1916
/// Type alias for identifying connections.
2017
pub type ConnectionId = u64;
2118

22-
/// Holds the shutdown signal for some server.
23-
#[derive(Debug)]
24-
pub struct ServerShutdown {
25-
pub(crate) _shutdown: watch::Sender<()>,
26-
}
27-
28-
impl From<watch::Sender<()>> for ServerShutdown {
29-
fn from(sender: watch::Sender<()>) -> Self {
30-
Self { _shutdown: sender }
31-
}
32-
}
33-
3419
/// The `ListenerTask` listens for new connections, and spawns `RouteTask`s for
3520
/// each.
3621
pub(crate) struct ListenerTask<T: Listener> {
@@ -67,16 +52,17 @@ where
6752
}
6853

6954
/// Spawn the future produced by [`Self::task_future`].
70-
pub(crate) fn spawn(self) -> JoinHandle<()> {
55+
pub(crate) fn spawn(self) -> JoinHandle<Option<()>> {
56+
let tasks = self.manager.root_tasks.clone();
7157
let future = self.task_future();
72-
tokio::spawn(future)
58+
tasks.spawn_cancellable(future)
7359
}
7460
}
7561

7662
/// The `ConnectionManager` provides connections with IDs, and handles spawning
7763
/// the [`RouteTask`] for each connection.
7864
pub(crate) struct ConnectionManager {
79-
pub(crate) shutdown: watch::Receiver<()>,
65+
pub(crate) root_tasks: TaskSet,
8066

8167
pub(crate) next_id: ConnectionId,
8268

@@ -107,19 +93,18 @@ impl ConnectionManager {
10793
) -> (RouteTask<T>, WriteTask<T>) {
10894
let (tx, rx) = mpsc::channel(self.notification_buffer_per_task);
10995

110-
let (gone_tx, gone_rx) = oneshot::channel();
96+
let tasks = self.root_tasks.child();
11197

11298
let rt = RouteTask {
11399
router: self.router(),
114100
conn_id,
115101
write_task: tx,
116102
requests,
117-
gone: gone_tx,
103+
tasks: tasks.clone(),
118104
};
119105

120106
let wt = WriteTask {
121-
shutdown: self.shutdown.clone(),
122-
gone: gone_rx,
107+
tasks,
123108
conn_id,
124109
json: rx,
125110
connection,
@@ -156,8 +141,8 @@ struct RouteTask<T: crate::pubsub::Listener> {
156141
pub(crate) write_task: mpsc::Sender<Box<RawValue>>,
157142
/// Stream of requests.
158143
pub(crate) requests: In<T>,
159-
/// Sender to the [`WriteTask`], to notify it that this task is done.
160-
pub(crate) gone: oneshot::Sender<()>,
144+
/// The task set for this connection
145+
pub(crate) tasks: TaskSet,
161146
}
162147

163148
impl<T: crate::pubsub::Listener> fmt::Debug for RouteTask<T> {
@@ -179,18 +164,27 @@ where
179164
/// to handle the request, and given a sender to the [`WriteTask`]. This
180165
/// ensures that requests can be handled concurrently.
181166
#[instrument(name = "RouteTask", skip(self), fields(conn_id = self.conn_id))]
182-
pub async fn task_future(self) {
167+
pub async fn task_future(self, cancel: WaitForCancellationFutureOwned) {
183168
let RouteTask {
184169
router,
185170
mut requests,
186171
write_task,
187-
gone,
172+
tasks,
188173
..
189174
} = self;
190175

176+
// The write task is responsible for waiting for its children
177+
let children = tasks.child();
178+
179+
pin!(cancel);
180+
191181
loop {
192182
select! {
193183
biased;
184+
_ = &mut cancel => {
185+
debug!("RouteTask cancelled");
186+
break;
187+
}
194188
_ = write_task.closed() => {
195189
debug!("WriteTask has gone away");
196190
break;
@@ -208,7 +202,11 @@ where
208202

209203
let span = debug_span!("pubsub request handling", reqs = reqs.len());
210204

211-
let ctx = write_task.clone().into();
205+
let ctx =
206+
HandlerCtx::new(
207+
Some(write_task.clone()),
208+
children.clone(),
209+
);
212210

213211
let fut = router.handle_request_batch(ctx, reqs);
214212
let write_task = write_task.clone();
@@ -223,7 +221,7 @@ where
223221
};
224222

225223
// Run the future in a new task.
226-
tokio::spawn(
224+
children.spawn_cancellable(
227225
async move {
228226
// Send the response to the write task.
229227
// we don't care if the receiver has gone away,
@@ -239,27 +237,23 @@ where
239237
}
240238
}
241239
}
242-
// No funny business. Drop the gone signal.
243-
drop(gone);
240+
children.shutdown().await;
244241
}
245242

246243
/// Spawn the future produced by [`Self::task_future`].
247244
pub(crate) fn spawn(self) -> tokio::task::JoinHandle<()> {
248-
let future = self.task_future();
249-
tokio::spawn(future)
245+
let tasks = self.tasks.clone();
246+
247+
let future = move |cancel| self.task_future(cancel);
248+
249+
tasks.spawn_graceful(future)
250250
}
251251
}
252252

253253
/// The Write Task is responsible for writing JSON to the outbound connection.
254254
struct WriteTask<T: Listener> {
255-
/// Shutdown signal.
256-
///
257-
/// Shutdowns bubble back up to [`RouteTask`] when the write task is
258-
/// dropped, via the closed `json` channel.
259-
pub(crate) shutdown: watch::Receiver<()>,
260-
261-
/// Signal that the connection has gone away.
262-
pub(crate) gone: oneshot::Receiver<()>,
255+
/// Task set
256+
pub(crate) tasks: TaskSet,
263257

264258
/// ID of the connection.
265259
pub(crate) conn_id: ConnectionId,
@@ -281,25 +275,23 @@ impl<T: Listener> WriteTask<T> {
281275
/// channel, and acts on them. It handles JSON messages, and going away
282276
/// instructions. It also listens for the global shutdown signal from the
283277
/// [`ServerShutdown`] struct.
278+
///
279+
/// [`ServerShutdown`]: crate::pubsub::ServerShutdown
284280
#[instrument(skip(self), fields(conn_id = self.conn_id))]
285281
pub(crate) async fn task_future(self) {
286282
let WriteTask {
287-
mut shutdown,
288-
mut gone,
283+
tasks,
289284
mut json,
290285
mut connection,
291286
..
292287
} = self;
293-
shutdown.mark_unchanged();
288+
294289
loop {
295290
select! {
296291
biased;
297-
_ = &mut gone => {
298-
debug!("Connection has gone away");
299-
break;
300-
}
301-
_ = shutdown.changed() => {
302-
debug!("shutdown signal received");
292+
293+
_ = tasks.cancelled() => {
294+
debug!("Shutdown signal received");
303295
break;
304296
}
305297
json = json.recv() => {
@@ -317,7 +309,9 @@ impl<T: Listener> WriteTask<T> {
317309
}
318310

319311
/// Spawn the future produced by [`Self::task_future`].
320-
pub(crate) fn spawn(self) -> JoinHandle<()> {
321-
tokio::spawn(self.task_future())
312+
pub(crate) fn spawn(self) -> tokio::task::JoinHandle<Option<()>> {
313+
let tasks = self.tasks.clone();
314+
let future = self.task_future();
315+
tasks.spawn_cancellable(future)
322316
}
323317
}

0 commit comments

Comments
 (0)