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

Handlers can be async #68

Closed
wants to merge 12 commits into from
Closed

Conversation

illicitonion
Copy link

Fixes #14

Handlers now return an IntoFuture (which Result implements), so that async handlers can be written. In particular, this is useful for using rusoto, because it means that its Futures will be executed on the tokio runtime.

This is mostly backwards compatible, because Result implements IntoFuture, but it adds a Send + 'static bound to a few types (notably Handler, Event) so should probably be considered a breaking change.

It also stops us calling wait() everywhere except the top-level, but it tries to make minimally invasive changes. At some point I'd be tempted to change all of the inline panic! calls to be Future error returns, and handle them at the top-level, rather than panic!ing all over the place.

Each commit is useful to separately review, as they are separated out building blocks. Sorry that the example I added is so contrived...!

By submitting this pull request

  • [✔] I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • [✔] I confirm that I've made a best effort attempt to update all relevant documentation.

@sapessi
Copy link
Contributor

sapessi commented Jan 7, 2019

Hi @illicitonion, thanks for the PR. We also planned to make the whole runtime async eventually. We were holding off for now because we have a pretty significant refactor in progress in #63. I'll @davidbarsky chime in here since he owns the async RFC.

@davidbarsky
Copy link
Contributor

Thanks for cutting this CR! I think this is an excellent start from my cursory overview. I was also looking at replacing the panics with a top-level panic handler, but fell down a rabbit hole with Tower integration.

@davidbarsky davidbarsky self-requested a review January 9, 2019 16:58
README.md Outdated
}
```

`Handler` provides a default implementation that enables you to provide a Rust closure or function pointer to the `lambda!()` macro.

If your handler is synchronous, you can just return a `Result` from it; if your handler is asynchronous, you can return a `Future` from it.
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for these docs! Small phrasing nit—I'd prefer avoiding using “just” in documentation. I'd instead emphasize that due to the IntoFuture implementation on Result, most synchronous handlers won't need to make any code changes.

Copy link
Author

Choose a reason for hiding this comment

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

Rephrased

@@ -70,14 +70,14 @@ use crate::{request::LambdaRequest, response::LambdaResponse};
pub type Request = http::Request<Body>;

/// Functions serving as ALB and API Gateway handlers must conform to this type.
pub trait Handler<R> {
pub trait Handler<R>: Send {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is Send not inferred on Handler?

Copy link
Author

Choose a reason for hiding this comment

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

Because Handler is a trait, there's no way for the type system to know that an implementation of it doesn't have some non-Send fields. We can either add the bound here to require that implementors are Send, or we can add a + Send bound in each of the places that requires Handler be Send, but doing the latter would require adding the bound to several core functions, and would lead to more confusing error messages if you had a non-Send trait.

@@ -92,14 +92,14 @@ where
///
/// # Panics
/// The function panics if the Lambda environment variables are not set.
pub fn start<R>(f: impl Handler<R>, runtime: Option<TokioRuntime>)
pub fn start<R>(f: impl Handler<R> + 'static, runtime: Option<TokioRuntime>)
Copy link
Contributor

Choose a reason for hiding this comment

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

Two things:

  • I'm not completely sure that impl Handler needs to be &'static. I wonder if start<R> can be refactored to take an impl Future instead of an impl Handler<R>.
  • If we're able to get rid of all .waits() inside the client, we can remove the optional TokioRuntime parameter.

Copy link
Author

Choose a reason for hiding this comment

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

I'll hold off on looking at this until #63 merges, if that's ok

http_client: Client<HttpConnector, Body>,
endpoint: String,
}

impl RuntimeClient {
/// Creates a new instance of the Runtime APIclient SDK. The http client has timeouts disabled and
/// will always send a `Connection: keep-alive` header.
pub fn new(endpoint: String, runtime: Option<Runtime>) -> Result<Self, ApiError> {
pub fn new(endpoint: String, task_executor: TaskExecutor) -> Result<Self, ApiError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

The signature can be changed to just return Self—we're not returning ApiError anywhere now (nice job that!)

Copy link
Author

Choose a reason for hiding this comment

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

Done

resp.status()
)));
let http_client = self.http_client.clone();
uri.into_future()
Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't know that Uri had an IntoFuture implementation. Can I ask why you opted for this?

Copy link
Contributor

@softprops softprops Jan 12, 2019

Choose a reason for hiding this comment

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

I think uri here is a Result and all Result types get a free IntoFuture impl. Since the return type changed, the ? needed to be removed above

Copy link
Author

Choose a reason for hiding this comment

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

Yeah; I could change this to future::done(uri) if that's more clear?

where
E: serde::de::DeserializeOwned,
O: serde::Serialize,
E: serde::de::DeserializeOwned + Send + 'static,
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm think I'm missing why the event needs to be static—aren't we taking ownership of it here?

Copy link
Author

Choose a reason for hiding this comment

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

We're taking ownership of the instance of E - the 'static here is stating that that instance's lifetime doesn't depend on any other (non-static) lifetimes; otherwise the E may have a reference to some other data in it which could be dropped, which would mean the E instance was no longer valid. It's a bound on whether E itself can have borrows, rather than on how long the E instance actually lives.

@@ -179,7 +205,7 @@ impl<F, E, O> Runtime<F, E, O> {
Ok(Runtime {
runtime_client: client,
settings: config,
handler: f,
handler: Arc::new(Mutex::new(f)),
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not the biggest fan of having a mutex here. Can you elaborate why it's necessary?

Copy link
Author

Choose a reason for hiding this comment

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

Because run takes a &mut self, we're only allowed to call run from one thread at a time. This Mutex is how we guarantee that multiple threads don't call run.

If we wanted to drop the Mutex, we would need to either:

  1. Change run from taking &mut self to just taking &self (this would mean we could only implement it for Fn not for FnMut like we currently do). This would be easy, but an API change.
  2. Somehow guarantee that we only have one thread which calls a Handler. This is what we were doing before (by blocking the executions); if we wanted to do this, we could probably use some kind of same thread executor, but I imagine that would be awkward for actually writing handlers, because they couldn't necessarily rely on the runtime they submit tasks to being able to do more than one thing at a time.

@@ -191,134 +217,169 @@ impl<F, E, O> Runtime<F, E, O> {
impl<F, E, O> Runtime<F, E, O>
where
F: Handler<E, O>,
E: serde::de::DeserializeOwned,
O: serde::Serialize,
E: serde::de::DeserializeOwned + Send + 'static,
Copy link
Contributor

Choose a reason for hiding this comment

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

See earlier comments about the 'static bound w.r.t. ownership.

let settings = self.settings.clone();
let handler = self.handler.clone();

loop_fn((), move |()| {
Copy link
Contributor

Choose a reason for hiding this comment

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

We might have the opportunity to refactor this chunk of code into a bunch of smaller and more methods & futures.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, this could very reasonably become a series of nicely tightly scoped maps and and_thens in a follow-up change :)

settings: FunctionSettings,
) -> impl Future<Item = (E, Context), Error = String> {
loop_fn(
(0, None),
Copy link
Contributor

Choose a reason for hiding this comment

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

I was thinking of using tower-retry for retries in a slightly less ad-hoc way (e.g., https://gist.github.com/seanmonstar/2469a8e544ada2d31d13c8ee54f17d48). I'll talk to the Tower folks about getting them on crates.io. I've been doing that locally with some success.

Copy link
Author

Choose a reason for hiding this comment

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

I'll happily give this a go when the crate is published! :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good!

@softprops
Copy link
Contributor

Futures retry exists and doesn't depend on anything but the futures crate https://mexus.gitlab.io/futures-retry/futures_retry/

@saks
Copy link
Contributor

saks commented Apr 22, 2019

Any progress on this?

@nappa85
Copy link

nappa85 commented May 10, 2019

Sorry, didn't saw this PR before making mine...

@davidbarsky
Copy link
Contributor

@nappa85 you're fine! don't worry about it.

@illicitonion
Copy link
Author

Sorry, didn't saw this PR before making mine...

No worries @nappa85! I haven't had time to fix this up, and something that's done and works is definitely better than something that's part-finished! :)

@softprops
Copy link
Contributor

I think this can be closed now. First class async support now exists in master

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.

async support
6 participants