-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Async: on_attach fairings #1242
Conversation
This is really excellent work, @jebrosen! Please see my inline comments. The two biggest suggestions are:
|
core/lib/src/rocket.rs
Outdated
use std::net::ToSocketAddrs; | ||
|
||
use crate::error::Error::Launch; | ||
|
||
let full_addr = format!("{}:{}", self.config.address, self.config.port); | ||
self.finish().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.
Lots of vertical space here...
core/lib/src/rocket.rs
Outdated
/// assert!(result.is_ok()); | ||
/// # } | ||
/// } | ||
/// ``` | ||
pub async fn serve(self) -> Result<(), crate::error::Error> { | ||
pub async fn launch(mut self) -> Result<(), crate::error::Error> { |
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 know most the contents of this function were introduced in some other commits, but it has become very difficult to follow. The combination of macros and cfg
s, in particular, really help to obfuscate the functionality. We really must improve this before we release 0.5.
….toml. This is not supported and is the same as putting the contents in [dependencies] anyway. It became a warning in rust-lang/cargo#7660.
observed. This is a prerequisite for async on_attach fairings. `Rocket` is now a builder wrapper around the private type `RocketInner`, with operations being run the next time it is necessary: `launch()`, `Client::new()`, or `inspect()`. `inspect()` returns an `Inspector<'_>`, which is analogous to and has the methods that could be called on an `&Rocket`.
bbc3ed4
to
6c165fa
Compare
I've added commits resolving several comments, but I am not quite finished yet / have a few follow-up questions. |
…lled afterwards anyway.
This transitively requires that `Rocket::inspect`, `Client::new`, and `Client::untracked` also become async.
`#[rocket::main]` works like `#[rocket::async_test]`, but it uses tokio's multithreaded scheduler.
…ions to a Kind type.
6c165fa
to
70b9c38
Compare
This is ready for another review. |
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.
No major requests this time around. Excellent work!
A few notes:
- Is it possible to have
async fn state()
andasync fn config()
onRocket
itself? It would be nice not to have to chain throughmanifest()
when possible. - In general, many functions now take a
Manifest
, and I now wonder: should they take aRocket
(or&mut Rocket
, or whatever it need be) and internal retrieve the manifest as necessary? This would make the functionsasync
, but in some cases (sayDbConn::get_one(rocket).await
), this API seems cleaner and more logical. What do you think? - I recall us discussing allowing the minimal Rocket application to look like:
Perhaps without the
#[rocket::main] async fn main() { rocket::ignite().mount("/", routes![index]).launch() }
async
onfn
, but the idea being you don't need toawait
the return value oflaunch()
in the common case. What is the status of that?
core/lib/src/fairing/ad_hoc.rs
Outdated
let mut opt = mutex.lock().expect("AdHoc::Attach lock"); | ||
let f = opt.take().expect("internal error: `on_attach` one-call invariant broken"); | ||
f(rocket) | ||
let f = mutex |
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 can simply move the .lock()
to the same line as mutex
.
/// let state = State::from(&rocket).expect("managing `MyManagedState`"); | ||
/// # rocket::async_test(async { | ||
/// let mut rocket = rocket::ignite().manage(MyManagedState(127)); | ||
/// let state = State::from(rocket.inspect().await).expect("managing `MyManagedState`"); |
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.
Don't we have a method on Rocket
to get State
directly? I imagine it would be cleaner to use that here.
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.
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.
That's fair. I think the idea here was to use that the two were equivalent. The addition of .inspect()
and .await
makes this feel overly verbose, however.
`Rocket::spawn()`. | ||
Rocket 0.5 uses the tokio (0.2) runtime. The runtime is started for you | ||
if you use `#[rocket::main]`, but you can still `launch()` a rocket | ||
instance on a custom-built `Runtime`. |
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.
This requires some pointers on how to do so. Is that material available?
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 necessarily all in one place - meaningfully using a custom-built Runtime
means using Builder
and block_on
, which are documented separately in the API. Something like this would be such an example:
let runtime = tokio::runtime::Builder::new()
.basic_scheduler() // use the single-threaded runtime, for illustration purposes
.build()
.unwrap();
runtime.block_on(rocket::ignite().mount("/hello", routes![world]).launch());
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.
We should perhaps open up an issue, if one doesn't already exist, to document all of the things that need to be documented before we release async
.
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.
- Is it possible to have
async fn state()
andasync fn config()
on Rocket itself? It would be nice not to have to chain through manifest() when possible.
I think so; I will try to add those.
- In general, many functions now take a Manifest, and I now wonder: should they take a Rocket (or &mut Rocket, or whatever it need be) and internal retrieve the manifest as necessary? This would make the functions async, but in some cases (say DbConn::get_one(rocket).await), this API seems cleaner and more logical. What do you think?
At a quick glance, DbConn::get_one
looks like the only place that happens (were there others?) and it should be possible to do so. In general though, &mut
is a bit annoying since it can make it more difficult to get all of config, managed state, and a database connection simultaneously - that's why Manifest
takes &self
.
- I recall us discussing allowing the minimal Rocket application to look like (thing without a
.await
)
I think this risks too much confusion. These are what I'm aiming for here:
- Have a single function to document and explain - i.e. no split between
async fn serve()
andfn launch()
. - Have it be
async
, to work better next to other async code (like setting up another server) and for better composability likeselect!(rocket.launch(), timeout)
orjoin!(rocket.launch(), websocket_server.run())
- Be consistent with other functions that return futures, and with usage inside an
async fn
that is notmain
or decorated with an attribute. I think it would be confusing to need to.await
or not.await
launch()
for different reasons than for other futures.
/// let state = State::from(&rocket).expect("managing `MyManagedState`"); | ||
/// # rocket::async_test(async { | ||
/// let mut rocket = rocket::ignite().manage(MyManagedState(127)); | ||
/// let state = State::from(rocket.inspect().await).expect("managing `MyManagedState`"); |
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.
core/lib/src/rocket.rs
Outdated
impl<Fut> hyper::Executor<Fut> for TokioExecutor where Fut: Future + Send + 'static, Fut::Output: Send { | ||
fn execute(&self, fut: Fut) { | ||
tokio::spawn(fut); | ||
pub(crate) fn finish(&mut self) -> BoxFuture<'_, ()> { |
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 recursion was already there, since on_attach
fairings can themselves call attach
- it has just moved. Switching to an async fn
/ Future
changes this to a recursion in the type system instead of in the call stack, which is why Box
ing is a solution.
As for a name change, I think I don't like process
because it's undescriptive and process_manifest
sounds like the manifest is the input - maybe one of process_pending
, apply_pending
, prepare_manifest
, update_manifest
?
`Rocket::spawn()`. | ||
Rocket 0.5 uses the tokio (0.2) runtime. The runtime is started for you | ||
if you use `#[rocket::main]`, but you can still `launch()` a rocket | ||
instance on a custom-built `Runtime`. |
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 necessarily all in one place - meaningfully using a custom-built Runtime
means using Builder
and block_on
, which are documented separately in the API. Something like this would be such an example:
let runtime = tokio::runtime::Builder::new()
.basic_scheduler() // use the single-threaded runtime, for illustration purposes
.build()
.unwrap();
runtime.block_on(rocket::ignite().mount("/hello", routes![world]).launch());
} | ||
|
||
pub fn manage<T: Send + Sync + 'static>(mut self, state: T) -> Self { | ||
self.pending.push(BuildOperation::Manage(Box::new(|rocket| { |
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 didn't do anything yet. Here's where it stands:
mount
panics immediately ifbase
is invalid. It will raise a panic infinish
if a route URI is found to be invalid.manage
panics infinish
if it's duplicated state.attach
says it's immediate, but it actually happens infinish
.
I think we could change mount to panic immediately, if we're okay reordering some log output.
Do you have any ideas for rewording the documentation? I find this a bit awkward: "If the fairing is an attach fairing, it will be attached when necessary (e.g. when calling inspect
or launch
)"
…nightly. This partially (re-)applies f4bb8bb
Your last point actually convinces me that we should allow a non- The challenge, of course, is that |
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.
Awesome. Just need to get the tests passing now.
@@ -153,7 +153,7 @@ macro_rules! default_catchers { | |||
let mut map = HashMap::new(); | |||
|
|||
$( | |||
fn $fn_name<'r>(req: &'r Request<'_>) -> futures_util::future::BoxFuture<'r, response::Result<'r>> { | |||
fn $fn_name<'r>(req: &'r Request<'_>) -> futures::future::BoxFuture<'r, response::Result<'r>> { |
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.
Should this be $crate::futures::
, to be safe?
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.
Since this macro isn't usable in any other module(s), the only conflict would be with another valid path futures::future::BoxFuture
reachable from mod defaults
below. IMO $crate
will look verbose/out of place here with no benefit.
core/lib/src/rocket.rs
Outdated
impl<Fut> hyper::Executor<Fut> for TokioExecutor where Fut: Future + Send + 'static, Fut::Output: Send { | ||
fn execute(&self, fut: Fut) { | ||
tokio::spawn(fut); | ||
pub(crate) fn finish(&mut self) -> BoxFuture<'_, ()> { |
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.
actualize_manifest
, execute_manifest
, consume_manifest
?
Is this set of examples in line with that proposal? // Pulled out of main() for illustrative purposes.
fn rocket() -> Rocket {
rocket::ignite()
.attach(AdHoc::on_attach(|rocket| async {
info!("Look! async!");
Ok(rocket)
}))
.mount("/", rocket::routes![a, b, c])
}
// No setup helpers. launch() is an async fn, so it returns a Future that can be passed to block_on.
// Also notice the semicolon - this example assumes we fix launch() to return
// a type that is easily convertible to Result, is *not* must_use, and automatically logs
// an error if not inspected (like LaunchError in 0.4).
fn main() {
create_a_runtime().block_on(rocket().launch());
}
// Using tokio::main
#[tokio::main]
async fn main() {
async_setup().await;
rocket().launch().await;
}
// This (roughly) expands to:
fn main() {
create_runtime().block_on(async {
async_setup().await;
rocket().launch().await;
})
}
// Using rocket::main with an async fn
#[rocket::main]
async fn main() -> Rocket {
async_setup().await;
rocket()
}
// Using rocket::main with a non-async fn.
#[rocket::main]
fn main() -> Rocket {
rocket()
}
// These could expand to (roughly):
fn main() {
create_runtime().block_on(async {
[async] fn __inner_main() -> Rocket { /* original contents of main() */ }
let rocket: ::rocket::Rocket = __inner_main()[.await];
rocket.launch().await;
})
} The last example is the closest to what you described, and the desugaring is very similar to what is already done for routes to be either
|
I think I have addressed or replied to all comments at this point and this is ready for another review. I am finding GitHub's PR interface more difficult to navigate than I remember it, so I might have missed something. |
Doing a final review now. I really like your proposal for a
If the function doesn't return a value, I think we should have Edit: to be clear, we should do this in another |
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.
Some minor requests/questions, but I'm also okay merging this as is! Exciting!
pub fn get_one(rocket: &::rocket::Rocket) -> Option<Self> { | ||
rocket.state::<#pool_type>() | ||
pub fn get_one(manifest: &::rocket::Manifest) -> Option<Self> { | ||
manifest.state::<#pool_type>() |
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.
How did we feel about making this pub async fn get_one()
and taking in a Rocket
or &Rocket
or &mut Rocket
?
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.
Actually, taking an &mut Rocket
should be (mostly) fine:
let mut rocket = make_rocket();
let conn = rocket.get_one().await;
let config = rocket.config().await; // <-- The code I was afraid would not work, but is actually fine.
Since get_one()
returns owned data, there is no conflict between calling get_one
and other methods.
However, I see at least one place we would "lose" something: on_launch
will no longer be able to call get_one
. And on_launch
shouldn't change to take &mut Rocket
, because by that point it is too late to make many of the modifications that &mut Rocket
allows.
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.
That's a good point.
I suppose one idea, perhaps a bad one, is to use some form of interior mutability - say a reentrant lock - to make inspect()
take an &Rocket
instead of an &mut
. I believe if we did that, however, we wouldn't need to expose Manifest
at all, which is kind of interesting. In some ways, this feels right as .inspect()
is logically read-only, and only takes an &mut
due to its implementation.
What do you think about this? Are we gaining anything by separating Rocket
and Manifest
?
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.
say a reentrant lock
I think I can see it in principle, but it has some really awkward consequences:
let conf = rocket.config().await; // so far so good
let rocket = rocket.attach(AdHoc::on_attach(|rocket|
Ok(rocket::ignite().manage(X))
)); // this attach fairing is "queued"
let state = rocket.state::<X>().await; // this must run the attach fairing
conf.get_something(); // oops
Effectively, inspect()
or functions that call it would now have to panic if outstanding borrows exist - this also includes calls made from on_launch()
.
contrib/lib/src/databases.rs
Outdated
/// # { | ||
/// let config = database_config("my_db", rocket.config()).unwrap(); | ||
/// let manifest = rocket.inspect().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.
Can all of these use rocket.config().await
now?
@@ -109,7 +109,7 @@ mod benches { | |||
|
|||
// Hold all of the requests we're going to make during the benchmark. | |||
let mut requests = vec![]; | |||
for route in client.rocket().routes() { | |||
for route in client.manifest().routes() { |
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 apparently can't comment on lines that haven't been modified, but note that this no longer compiles due to a missing .await
on Client::new()
. Can we even do async benches?
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.
Ah right, I keep forgetting about the benches (and the test script doesn't include them). In one sense the answer is "yes, of course" - just write block_on
somewhere. Doing it in such a way that the benchmarks actually measure what is supposed to be measured is the hard part - as I believe we have discussed, I expect these style of benchmarks will amplify the per-request overhead of async
and fail to demonstrate the overall advantage provided by work-stealing.
How do you feel about deferring these fixes until an upcoming PR for a blocking API for Client
, so we can compare some approaches?
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.
Sounds good to me!
This PR makes
on_attach
fairings asynchronous. It transitively requires several other functions to become asynchronous, including the "entry point" (usuallylaunch()
orClient::new()
). This specific implementation is a balance between needing to do asynchronous work inon_attach
, such as database setup, versus the verbosity of making too many builder-type functions require.await
s.Some other similarly sweeping changes that I have not made yet, and can be added to this or made as a followup PR:
Client
(with an internalRuntime
, that wouldblock_on
each operation)..await
s internal - especially in e.g. https://github.com/SergioBenitez/Rocket/compare/async...jebrosen:async-on-attach?expand=1#diff-048d6496580899aa2aec5fc6a244faf0launch()
so that the most common usage does not requirelet _ =
to suppress an unusedResult
warning.Some other things we may want, off the top of my head:
#[rocket::main]
and#[rocket::async_test]
exist (short version: an attribute is worth it, and reexports oftokio::main
andtokio::test
don't work)