Skip to content
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

More generally useful doc example for .with_graceful shutdown() #2820

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

bittrance
Copy link
Contributor

Motivation

The current doc example for .with_graceful_shutdown() does not quite show how shutdown/cancellation can be implemented. I think most readers will have a piece of code that looks like this and now wonders how to do shutdown.

let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;

In this scenario, the main issue is how to handle the fact that ::serve() blocks.

Solution

The easiest and most generally applicable way I can come up with to perform shutdown is to use tokio::sync::Notify to signal shutdown, like so:

let listener = tokio::net::TcpListener::bind("0.0.0.0:8082").await?;
let blocker = Arc::new(Notify::new());
let cancel = Arc::clone(&blocker);
tokio::spawn(
    axum::serve(listener, app)
        .with_graceful_shutdown(async move { blocker.notified().await })
        .into_future(),
);
// ...
cancel.notify_one();

I think this more generally useful pattern since the notify can be passed around and triggered where needed. In contrast, the select! strategy implied by the current doc is difficult to use in e.g. integration tests and where the api is a small component of a larger system.

The current doc for with_graceful_shutdown() implies a select! block
somewhere else that races the serve and the cancellation futures. I
think using a `tokio::sync::Notify` makes for a more generally useful
pattern since it can be passed around and triggered as needed. For
example, the select! strategy is difficult to use in integration tests.
/// let cancel = Arc::clone(&blocker);
/// tokio::spawn(
/// axum::serve(listener, router)
/// .with_graceful_shutdown(async move { blocker.notified().await })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple blocker.notified() doesn't work here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lifetime issue?

@@ -121,20 +121,23 @@ impl<M, S> Serve<M, S> {
///
/// ```
/// use axum::{Router, routing::get};
/// use std::{future::IntoFuture, sync::Arc};
/// use tokio::sync::Notify;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, can't we use just a oneshot channel for the example? As long as users don't need to see how to cancel from multiple places the whole Arc is unnecessary and it might be cleaner to have the semantics of being able to shutdown only once and having a dedicated side for producing and consuming the notification.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The oneshot channel is fallible, so it has some noise too, but I still agree that it becomes a bit clearer that way. Updated.

Since rx yields Result<(), _> it would seem we can't just pass rx directly. Is there a way we could skip the async move {...} block and pass a future directly? It would seem we need to import futures::future::FutureExt to "eat" the Result? Would that still be preferable to using an async block? Are Axum users likely to have it in their Cargo.toml already?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants