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

[Do not merge] Refactor to address RFCs #63

Merged
merged 45 commits into from Jan 24, 2019
Merged

[Do not merge] Refactor to address RFCs #63

merged 45 commits into from Jan 24, 2019

Conversation

sapessi
Copy link
Contributor

@sapessi sapessi commented Dec 27, 2018

This is a major refactor of the crate structure to address the comments in RFCs #53. While the changes are major, they should not be breaking for existing code.

Description of changes: Moved all of the "main loop" logic in a runtime-core crate that defines a Vec<u8>-based handler. The runtime crate now simply wraps the bytes handler into a typed handler method using serde.

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.

@@ -135,6 +153,8 @@ impl Error for ApiError {
None
}
}
unsafe impl Send for ApiError {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, why's this is necessary? would be useful to add a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will go away, with the next iteration all errors will switch to failure. I have quite a bit of legacy error handling code in here. Just wanted to make one big change at a time.

///
/// # Returns
/// A new `RuntimeError` instance with the `recoverable` property set to `false`.
pub(crate) fn unrecoverable(msg: &str) -> RuntimeError {
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be late, but a potential option would be to have a struct named RuntimeError, several convenience methods, and an internal enum called RuntimeErrorKind that has several variants that include whether or not this an irrecoverable error.

While I'm questioning assumptions in this crate, what do we consider to be an irrevocable error? Is it just an error that wasn't caught and handled?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same comment as above.

Copy link
Contributor

Choose a reason for hiding this comment

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

See my comment above.

/// The `RuntimeError` object is returned by the custom runtime as it polls
/// for new events and tries to execute the handler function. The error
/// is primarily used by other methods within this crate and should not be relevant
/// to developers building Lambda functions. Handlers are expected to return
Copy link
Contributor

Choose a reason for hiding this comment

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

should not be relevant to developers building Lambda functions.

I'd maybe phrase this along the lines of:

This error is primarily used by the Lambda runtime itself. It not expected that Lambda function implementations will need to construct or handle this error.

As I wrote that, I realized that maybe RuntimeError can be pub(crate). Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree, no need to expose this. I will make the change as I work on the errors refactor.

Copy link
Contributor

Choose a reason for hiding this comment

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

Wanted to followup—should these remain public?

lambda-runtime-core/src/handler.rs Outdated Show resolved Hide resolved
@@ -252,7 +194,7 @@ where
"Error for {} is not recoverable, sending fail_init signal and panicking",
request_id
);
self.runtime_client.fail_init(&e);
self.runtime_client.fail_init(&ErrorResponse::from(Box::new(e)));
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps I'm missing something, but why does this need to be boxed first?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as above - haven't touched errors at all

lambda-runtime-core/src/runtime.rs Show resolved Hide resolved
lambda-runtime/Cargo.toml Outdated Show resolved Hide resolved
lambda-runtime/examples/custom_error.rs Outdated Show resolved Hide resolved
lambda-runtime/examples/custom_error_failure.rs Outdated Show resolved Hide resolved
/// defined in the `lambda_runtime_core` crate. The closure simply uses `serde_json`
/// to serialize and deserialize the incoming event from a `Vec<u8>` and the output
/// to a `Vec<u8>`.
fn wrap<E, O>(mut h: impl Handler<E, O>) -> impl FnMut(Vec<u8>, Context) -> Result<Vec<u8>, error::HandlerError>
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this!

Copy link
Contributor

@davidbarsky davidbarsky left a comment

Choose a reason for hiding this comment

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

These changes look good to me.

@sapessi sapessi mentioned this pull request Jan 7, 2019
@sapessi sapessi mentioned this pull request Jan 10, 2019
lambda-runtime-client/Cargo.toml Outdated Show resolved Hide resolved
runtime_agent: String,
host: String,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the addition of host and change from endpoint to next_endpoint?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

next_endpoint is the pre-parsed uri for the /next endpoint, which remains stable throughout the lifetime of the client. The host is used to re-generate the Uri for the response endpoints for each request. I'm caching the next_endpoint Uri value to avoid parsing it each time.

lambda-runtime-client/src/error.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@softprops softprops left a comment

Choose a reason for hiding this comment

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

Posted a handful of comments. Notably I'm wondering if this change could been broken down into digestable chunks (pulls). This felt like a lot to absorb in one pull.


err
}
/*#[test] skip this tests as it's broken right now
Copy link
Contributor

Choose a reason for hiding this comment

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

I aways feel better about using git branches to manage unfinished code. Does this test provide ant value in it's commented out form? If it's a signal I'd suggesting using #[ignore] https://doc.rust-lang.org/1.5.0/book/testing.html#the-ignore-attribute

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed this as per @davidbarsky's suggestion. We will re-introduce this once we finally figure out what is going on with backtraces in failure... #!@?!!&

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, it might've been an older commit.

lambda-runtime-client/Cargo.toml Outdated Show resolved Hide resolved
}

/// Initializes the Lambda runtime with the given handler. Optionally this macro can
/// also receive an instance of Tokio runtime for the Hyper HTTP client. If the Tokio
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like it would be more useful for readers of these api docs to see something like "receive a customized instance of Tokio runtime to drive internal lambda operations to completion". The mention of Hyper HTTP client is kind of an implementation detail that's not useful for readers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Like it. Made the change

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this might've been addressed partially in #63 (comment).

impl<Function, Event, Output, EventError> Handler<Event, Output, EventError> for Function
where
Function: FnMut(Event, Context) -> Result<Output, EventError>,
EventError: Fail + LambdaErrorExt + Display + Send + Sync,
Copy link
Contributor

Choose a reason for hiding this comment

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

Few notes here.

Because Fail requires Display + Send + Sync. Adding Display + Send + Sync on top of Fail is redundant.

I'll have to read through a bit more to understand the purpose of LambdaErrorExt but my initial impression is this. The core interface should make the trivial easy and the complex possible. If a dependency on Fail wasn't enough implementing LambdaErrorExt may seem like a tall order for making the simple trivial. Thoughts?

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 reason why we have Display, Send, and Sync there is that I don't trust the failure crate to maintain those in the Fail trait. This library has burned me more than once recently (scorched me to death is more appropriate). Since in the next iteration of the library we plan to make the core async we wanted to introduce a constraint of our own in this, even if failure removes it.

Lambda gives us the ability to return an error type field which we plan to use to specify the error's root cause. The LambdaErrorExt trait defines the error_type(&self) -> &str to provide this. In the crate we have included implementations of the trait for almost all errors in the standard library as well as failure types. With these default implementations, I'm hoping to still make the trivial easy (+1 on this turn of phrase by the way, very nicely put).

Copy link
Contributor

Choose a reason for hiding this comment

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

The reason why we have Display, Send, and Sync there is that I don't trust the failure crate to maintain those in the Fail trait. This library has burned me more than once recently (scorched me to death is more appropriate). Since in the next iteration of the library we plan to make the core async we wanted to introduce a constraint of our own in this, even if failure removes it.

You're not wrong to distrust Failure given your experience, but the way the Failure is evolving is to just provide a derive for std::error::Error + Display + Send + Sync bounds. Additionally, the standard library encourages implementing Debug + Display on errors, and I can think of a single error in the standard library (std::sync::PoisonError) that doesn't implement Send/Sync. Most errors I've seen implement Send/Sync, and if they don't, that's often grounds for opening a PR/issue.

(With regard to std::sync::PoisonError—I believe that most people just panic on a poisoned mutex. I'll be shocked if a Lambda customer has to worry about poisoned mutexes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Im thinking then may it's a good idea to put the Failure dependency behind a feature flag to maintain a more stable API. Scorch free!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Responded in another comment too (perhaps we should switch to the main PR thread for this conversation). With the current implementation, we don't force the failure crate or the Fail trait on developers building Lambda functions, their errors become Fail for us automatically because we include failure. This gives us the ability to switch away from failure in the future without making a breaking change.

lambda-runtime-client/src/error.rs Outdated Show resolved Hide resolved
lambda-runtime-errors-derive/Cargo.toml Outdated Show resolved Hide resolved
lambda-runtime-errors/errorgen.py Show resolved Hide resolved
lambda-runtime-errors/src/error_ext_impl.rs Show resolved Hide resolved
lambda-runtime-errors/src/lib.rs Outdated Show resolved Hide resolved
}
// the value return by the error_type function is included as the
// `errorType` in the AWS Lambda response
impl LambdaErrorExt for CustomError {
Copy link
Contributor

Choose a reason for hiding this comment

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

I can foresee this getting very onerous. Not only for those familiar with rust but for those new to rust dabbling through experimenting transitioning a lambda to rust. Is there any thing the runtime could provide that could provide a default impl that allows for specialization when needed? This goes back to making the trivial easy and the complex possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

Something I'm debating—not really brought up with Stefano—is to point people to just use Failure and be done with it, but I'm not convinced that's the right approach.

Copy link
Contributor

Choose a reason for hiding this comment

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

Failure sounds like a same middle ground as it plays nice with the ecosystem what already exists. The complexity seems to come from an implementation detail but if feels like too much weight to on users which won't benefit as much/appreciate the feature of the named errors inside lambda responses. It's likely user logging errors to cloudwatch logs will provide the same debugging benefit at no extra interface cost.

That said do we gain anything from failure over Box beside the back trace? Std Errors are really the least common denominator and work well with ? Chaining

Copy link
Contributor

Choose a reason for hiding this comment

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

Failure sounds like a same middle ground as it plays nice with the ecosystem what already exists. The complexity seems to come from an implementation detail but if feels like too much weight to on users which won't benefit as much/appreciate the feature of the named errors inside lambda responses. It's likely user logging errors to cloudwatch logs will provide the same debugging benefit at no extra interface cost.

I keep going back and forth on this, but the InvocationError endpoint does expect an ErrorType. I wonder if we should have a top-level error handler—required that a customer implement this—that handles such reporting. The issue with a Display impl, iirc, is that the Display won't give an accurate error report.

The other thing is that I have opinions as to how things should be, but Stefano works with customers who use Lambda, and he sees how people use.

That said do we gain anything from failure over Box beside the back trace? Std Errors are really the least common denominator and work well with ? Chaining

Easier derives + From impls. It's really a nicer experience, IMO.

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 a customer as well!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's why I had included the error derive crate, to allow developers to just:

#[derive(LambdaErrorExt)]
struct BasicCustomError;

Do you feel we need more? What should our generic be for the trait implementation be, any Fail?

Copy link
Contributor

Choose a reason for hiding this comment

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

@softprops

I'm a customer as well!

That's true, sorry! I just remember him telling me that people are creating Dashboards on specific error strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One more thing. We implemented errors this way to avoid forcing the failure crate on users of the runtime - it's purely a utility for us. When they take the runtime as a dependency their errors automatically become fails because we bring the failure crate. Once the standard library errors include back traces we can remove failure from the runtime implementation without it being a breaking change

lambda-runtime-client/src/error.rs Outdated Show resolved Hide resolved
lambda-runtime-client/Cargo.toml Outdated Show resolved Hide resolved

#[test]
fn does_not_produce_stack_trace() {
env::remove_var("RUST_BACKTRACE");
Copy link
Contributor

Choose a reason for hiding this comment

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

Backtraces... are kinda tricky to test. I'm not sure of a better way, though.

lambda-runtime-core/build.rs Show resolved Hide resolved
/// The `RuntimeError` object is returned by the custom runtime as it polls
/// for new events and tries to execute the handler function. The error
/// is primarily used by other methods within this crate and should not be relevant
/// to developers building Lambda functions. Handlers are expected to return
Copy link
Contributor

Choose a reason for hiding this comment

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

Wanted to followup—should these remain public?

lambda-runtime-errors/src/lib.rs Show resolved Hide resolved
lambda-runtime-errors/src/lib.rs Show resolved Hide resolved

pub use crate::error_ext_impl::*;

use failure::{format_err, Compat, Error, Fail};
Copy link
Contributor

Choose a reason for hiding this comment

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

I think Failure can be placed behind a feature flag that's on by default. Do you disagree?

Copy link
Contributor

Choose a reason for hiding this comment

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

Crowbar did that with error_chain. It's actually a nice feature of rust that fits the 0 cost tagline. Only pay for what you use, and make it possible to opt out of what you dont

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See my answer in the other comments - why put it behind a feature flag if it has no impact on developers using the library? They do not have to use failure, it's just a utility for us.

Copy link
Contributor

Choose a reason for hiding this comment

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

The feature flag allows you conditionally disable compilation for libraries that you're not using.

I think we should use Failure internally, but customers shouldn't need to pay the compilation cost if they're not using Failure in their application. The conditional compilation/feature flag might make some the conversions easier.

}
// the value return by the error_type function is included as the
// `errorType` in the AWS Lambda response
impl LambdaErrorExt for CustomError {
Copy link
Contributor

Choose a reason for hiding this comment

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

Something I'm debating—not really brought up with Stefano—is to point people to just use Failure and be done with it, but I'm not convinced that's the right approach.

@sapessi
Copy link
Contributor Author

sapessi commented Jan 22, 2019

Posted a handful of comments. Notably I'm wondering if this change could been broken down into digestable chunks (pulls). This felt like a lot to absorb in one pull.

Apologies for the huge number of changes. We worked on bite-sized chunks with @davidbarsky as he reviewed each commit during development. The nature of the changes (refactor) made them hard to publish them in separate PRs without potentially breaking stuff for anyone depending on master. Hopefully it won't (need to) happen again.

}*/

for v in s.variants() {
println!("Variant: {}", v.ast().ident);
Copy link
Contributor

Choose a reason for hiding this comment

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

println?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

whoopsie - some careful debugging there on my part.

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 derive crate is very much a work in progress. Few more things I'd like to do.

@sapessi
Copy link
Contributor Author

sapessi commented Jan 23, 2019

Moving the failure conversation to a thread here to make it more manageable.

I spoke with @davidbarsky this morning. The dependency on failure is something we decided to take because it makes our life easier when accepting any kind of error and also allows us to collect backtraces. Because of how the Fail trait works, we do not need developers to include failure as a dependency in their functions. Their errors - as long as they implemented Error from the standard library - automatically implement Fail because we include failure.

Both David and I agree that this is a temporary dependency that we will remove in the future, once backtraces are available in the standard library. Since we do not require failure to be included in functions the change should be non-breaking.

Other than compilation time, and on the basis that we plan to remove failure in the future once backtraces are stable in the standard library, do you have any other concern with releasing 0.2 with failure @softprops? Unless we have a serious issue with the dependency, I'd rather not block the release, it's been a long time coming.

@softprops
Copy link
Contributor

No concerns here. I didn't realize all you need is std Error. That sounds great. I'd love a release. I've got a growing number of serverless framework templates that depend on git urls for the http lambda module. I'd love to update those to a release

@sapessi
Copy link
Contributor Author

sapessi commented Jan 23, 2019

Huzza! I'll merge and push 0.2 tomorrow. The http crate will go out as a 0.1. I'll probably hold off on the errors_derive crate until I've had time to clean it up properly.

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

3 participants