diff --git a/.gitignore b/.gitignore index c48c05f..472eebe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target /Cargo.lock examples/file-server.rs +.DS_Store +.vscode/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f6ed591..fd8a941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,28 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to the adaptation of [Semantic Versioning](https://semver.org/spec/v2.0.0.html) utilized by [Cargo](https://doc.rust-lang.org/cargo/reference/semver.html). +## [0.0.2] - 2023-xx-xx + +### Changed + +- Replace initialization functions with a compile-time friendly API that eliminates any potential virtual calls. +- Documentation and examples. + +### Added + +- `EventHandler` interface. +- `EventContext` type which implements `LambdaContext`. + +### Removed + +- `RefLambdaContext` type which previously implemented `LambdaContext`. + ## [0.0.1] - 2022-05-22 + ### Added - Core interface traits and default implementations. @@ -12,4 +30,4 @@ and this project adheres to the adaptation of [Semantic Versioning](https://semv - Macros for easy creation of runtimes. - Documentation. - `ureq` based HTTP backend. -- Echo server example. \ No newline at end of file +- Echo server example. diff --git a/Cargo.toml b/Cargo.toml index 1c72693..74f0c23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rtlambda" -version = "0.0.1" +version = "0.0.2-remove-dd" authors = ["Guy Or "] description = "A Rust runtime for AWS Lambda and a library for creating custom runtimes." license = "MIT OR Apache-2.0" diff --git a/LICENSE.MIT b/LICENSE.MIT index e822b28..b31afb4 100644 --- a/LICENSE.MIT +++ b/LICENSE.MIT @@ -1,4 +1,4 @@ -Copyright 2022 Guy Or and the "rtlambda" authors +Copyright 2022-2023 Guy Or and the "rtlambda" authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..edc932e --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +checks: + cargo check + cargo clippy + cargo fmt diff --git a/Readme.md b/Readme.md index fe557ed..18750f3 100644 --- a/Readme.md +++ b/Readme.md @@ -3,123 +3,121 @@ `rtlambda` is a A Rust runtime for [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html) and a library for creating custom runtimes. The default runtime implementation shipped with `rtlambda` lets you write native Rust event handlers for AWS Lambda. - + Your handler code gets compiled along with the runtime into a single executable file which is deployed to the Lambda service. `rtlambda` keeps dependencies and complexity to a minimum and does not depend on `tokio`. Writing `rtlambda` functions is simple and easy. ## Usage + To get started, you may adapt the echo-server example: Add `rtlambda` and `serde` as a dependency to your `Cargo.toml` file: ```toml [dependencies] -rtlambda = "0.0.1" +rtlambda = "0.0.2" serde = { version = "1", features = ["derive"] } ``` And in your `main.rs` file: + ```rust use rtlambda::prelude::*; use serde::Serialize; // Import the [`default_runtime`] macro from rtlambda. -#[macro_use] extern crate rtlambda; +#[macro_use] +extern crate rtlambda; // Create a struct representing the lambda's response, and derive the [`serde::Serialize`] trait. #[derive(Serialize, Clone)] -struct EchoMessage { +pub struct EchoMessage { msg: String, req_id: String, } -// Define output and error types for berevity. -// The Output type must implement [`serde::Serialize`] -type OUT = EchoMessage; -// The error type must implement the `Display` trait -type ERR = String; - -// Implement an initialization function. -// The initialization function returns a Result with the Ok type resolving to a dynamically allocated -// closure that accepts the Event from Lambda (as an optional string) and the context object. -// The closure itself returns a Result with the Ok and Err types being the previously defined `OUT` and `ERR` types respectively. -// The initialization function may fail (e.g if a db connection was not succesfully opened, etc..) and in that case -// the function should return an Err variant of the same `ERR` type defined for the event handler. -fn initialize() -> Result< - Box, RefLambdaContext) -> Result>, - ERR, -> { - // Your one-time initialization logic goes here: - - // - - // Return the event handler closure - return Ok(Box::new(move |event, context| { +// Implement the event handler +pub struct EchoEventHandler { + my_int: i32, // If any of your resources are not [`std::marker::Sized`], use a Box! + // my_db_conn: Box +} + +impl EventHandler for EchoEventHandler { + // Defines the Output type which must implement [`serde::Serialize`] + type EventOutput = EchoMessage; + // The error types must implement the `Display` trait + type EventError = String; + type InitError = String; + + fn initialize() -> Result { + // Initialization logic goes here... + // Construct your EventHandler object + Ok(Self { my_int: 42 }) + // If an error occurs during initialization return Err + //Err("Something bad happened!".to_string()) + } + + fn on_event( + &mut self, + event: &str, + context: &Ctx, + ) -> Result { // Get the aws request id - let req_id = context.aws_request_id().unwrap(); - - // Unwrap the event string - let event = match event { - Some(v) => v, - None => { - return Err(format!( - "AWS should not permit empty events. Something strange must've happened." - )) - } - }; - + let req_id = context.get_aws_request_id().unwrap(); + if event == "\"\"" { return Err(format!("Empty input, nothing to echo.")); } - // rtlambda leaves use-case specific concerns such as event JSON deserialization to the handler. - // In this example we do not deserialize the event. Use serde_json or any other library to perform deserialization if needed. - // Echo the event back as a string. Ok(EchoMessage { - msg: format!("ECHO: {}", event), + msg: format!("my_int: {}, ECHO: {}", self.my_int, event), req_id: req_id.to_string(), }) - - })); + } } fn main() { // Create a runtime instance and run its loop. // This is the equivalent of: - // let mut runtime = DefaultRuntime::::new(LAMBDA_VER, initialize); - let mut runtime = default_runtime!(OUT, ERR, LAMBDA_VER, initialize); + // let mut runtime = DefaultRuntime::::new(LAMBDA_VER); + let mut runtime = default_runtime!(EchoEventHandler); runtime.run(); } ``` ## How does it work -Your main function code creates and runs the runtime, which invokes your handler on incoming events and handles errors if any. - - A typical setup consists of: -* Creating a new binary crate and including `rtlambda` as a dependency in your `Cargo.toml` file. -* Importing the prelude - `rtlambda::prelude::*` in the `main.rs` file. -* Writing an initialization function that contains one-time initialization code and returns a closure - containing the event handling logic (the business logic of your lambda). -* In your main function, creating a new `DefaultRuntime` passing the Lambda API version and a pointer to your initialization function. -* Calling the `run()` method on the runtime instance to start the runtime. + +Your main function code creates and runs the runtime, which invokes your handler on incoming events and handles errors if any. + +A typical setup consists of: + +- Creating a new binary crate and including `rtlambda` as a dependency in your `Cargo.toml` file. +- Importing the prelude - `rtlambda::prelude::*` in the `main.rs` file. +- Implementing an `EventHandler`, this can be any `Sized` struct. `rtlambda` takes care of initializing it, using the `initialize` associated function. +- In your main function, creating a new `DefaultRuntime` by using the `default_runtime!` macro with your event handler type as an argument. +- Calling the `run()` method on the runtime instance to start the runtime. ### As a framework -`rtlambda`'s API utilizes generic traits - with bounds on their type parameters - to define its interface. -This design minimizes dynamic dispatch calls while allowing the user to: -* Define their own output and error types. -* Choose their HTTP client implementation. -* Implement their own version of internal runtime concerns such as runtime logic, env var handling and context building. +`rtlambda`'s API utilizes generic traits to define its interface. + +This design eliminates dynamic dispatch while allowing the user to: -Each trait is provided with a default type implementing it. For example the default HTTP backend is based on [ureq](https://crates.io/crates/ureq). +- Define their own output and error types. +- Choose their HTTP client implementation. +- Implement their own version of internal runtime concerns such as runtime loop, env var handling and context building. + +Each trait is provided with a default type implementing it. For example the default HTTP backend is based on [ureq](https://crates.io/crates/ureq). The majority of users should be fine with the default implementation and only need to define their output and error types. -Output types should currently implement the [serde::Serialize](https://docs.serde.rs/serde/ser/trait.Serialize.html) trait. +Output types should implement the [serde::Serialize](https://docs.serde.rs/serde/ser/trait.Serialize.html) trait. Error types should implement [std::fmt::Display](https://doc.rust-lang.org/std/fmt/trait.Display.html). ## Build and Deploy + `rtlambda` is designed to be built into a single executable that contains both your function code and the runtime itself (In AWS terms the runtime "is embedded in the function deployment package"). AWS currently allows you to deploy your function on either `x86` or `aarch64` based machines with either the Amazon Linux 2 OS or the legacy Amazon Linux. @@ -129,28 +127,31 @@ Since Rust is a compiled language, it is therefore required to build your projec Two simple ways to build your code are either using Docker or spinning up an EC2 VM matching your target environment. ### Using Docker + For Amazon Linux 2 you can follow these steps: - - 1. Clone https://github.com/guyo13/amazon_linux_rust_docker.git - 2. Cd into either the `x86` or `aarch64` dirs and run `docker build .` - 3. Run a container with the built image and mount your crate's root directory to the container's `/opt/src` dir - by adding the `-v /path/to/crate:/opt/src` argument to the `docker run` command. - 4. Connect to the container and execute `cargo build --release`. - 5. **For aarch64 builds** in order to best leverage the AWS Graviton2 CPU, **before** building run `export RUSTFLAGS="-C target-cpu=neoverse-n1 -C target-feature=+lse,+outline-atomics"` **inside** the container. +1. Clone https://github.com/guyo13/amazon_linux_rust_docker.git +2. Cd into either the `x86` or `aarch64` dirs and run `docker build .` +3. Run a container with the built image and mount your crate's root directory to the container's `/opt/src` dir - by adding the `-v /path/to/crate:/opt/src` argument to the `docker run` command. +4. Connect to the container and execute `cargo build --release`. +5. **For aarch64 builds** in order to best leverage the AWS Graviton2 CPU, **before** building run `export RUSTFLAGS="-C target-cpu=neoverse-n1 -C target-feature=+lse,+outline-atomics"` **inside** the container. -**This method seems to be viable if your host architecture (eg. x86 laptop/mac) matches your target architecture. Building on an emulated container currently fails on Docker for Mac with rustc 1.60.** +**This method seems to be viable if your host architecture (eg. x86 laptop/mac) matches your target architecture. Building on an emulated container currently fails on Docker for Mac with rustc 1.60.** ### Using a VM + - Create a virtual machine on [EC2](https://aws.amazon.com/ec2/getting-started/) that matches your target platform (architecture and OS). - Install the Rust toolchain using rustup-init ([Instructions](https://rustup.rs/)). - Clone your crate into the build VM and repeat steps 4-5 above (on the VM. This time there is no container). ### Deploying + After compiling your app on either Docker or a VM, copy the executable binary file into a file named `bootstrap` and zip it into a `function.zip` archive. Create a Function using the AWS Lambda dashboard or the `aws` cli, make sure that the platform settings (CPU architecture and OS) match your compilation target. Deploy your `function.zip` archive to the newly created function using any of the [methods supported by AWS](https://docs.aws.amazon.com/lambda/latest/dg/configuration-function-zip.html#configuration-function-update). ## License + `rtlambda` is dual-licensed under MIT or Apache-2.0. -See the included license files for details. \ No newline at end of file +See the included license files for details. diff --git a/examples/echo-server.rs b/examples/echo-server.rs index 41e384e..719f78d 100644 --- a/examples/echo-server.rs +++ b/examples/echo-server.rs @@ -7,40 +7,39 @@ extern crate rtlambda; // Create a struct representing the lambda's response, and derive the [`serde::Serialize`] trait. #[derive(Serialize, Clone)] -struct EchoMessage { +pub struct EchoMessage { msg: String, req_id: String, } -// Define output and error types for berevity. -// The Output type must implement [`serde::Serialize`] -type OUT = EchoMessage; -// The error type must implement the `Display` trait -type ERR = String; +// Implement the event handler +pub struct EchoEventHandler { + my_int: i32, // If any of your resources are not [`std::marker::Sized`], use a Box! + // my_db_conn: Box +} -// Implement an initialization function. -// The initialization function returns a Result with the Ok type resolving to a dynamically allocated -// closure that accepts the Event from Lambda (as an optional string) and the context object. -// The closure itself returns a Result with the Ok and Err types being the previously defined `OUT` and `ERR` types respectively. -// The initialization function may fail (e.g if a db connection was not succesfully opened, etc..) and in that case -// the function should return an Err variant of the same `ERR` type defined for the event handler. -fn initialize() -> Result< - Box, RefLambdaContext) -> Result>, - ERR, -> { - return Ok(Box::new(move |event, context| { +impl EventHandler for EchoEventHandler { + // Defines the Output type which must implement [`serde::Serialize`] + type EventOutput = EchoMessage; + // The error types must implement the `Display` trait + type EventError = String; + type InitError = String; + + fn initialize() -> Result { + // Initialization logic goes here... + // Construct your EventHandler object + Ok(Self { my_int: 42 }) + // If an error occurs during initialization return Err + //Err("Something bad happened!".to_string()) + } + + fn on_event( + &mut self, + event: &str, + context: &Ctx, + ) -> Result { // Get the aws request id - let req_id = context.aws_request_id().unwrap(); - - // Unwrap the event string - let event = match event { - Some(v) => v, - None => { - return Err(format!( - "AWS should not permit empty events. Something strange must've happened." - )) - } - }; + let req_id = context.get_aws_request_id().unwrap(); if event == "\"\"" { return Err(format!("Empty input, nothing to echo.")); @@ -48,17 +47,17 @@ fn initialize() -> Result< // Echo the event back as a string. Ok(EchoMessage { - msg: format!("ECHO: {}", event), + msg: format!("my_int: {}, ECHO: {}", self.my_int, event), req_id: req_id.to_string(), }) - })); + } } fn main() { // Create a runtime instance and run its loop. // This is the equivalent of: - // let mut runtime = DefaultRuntime::::new(LAMBDA_VER, initialize); - let mut runtime = default_runtime!(OUT, ERR, LAMBDA_VER, initialize); + // let mut runtime = DefaultRuntime::::new(LAMBDA_VER); + let mut runtime = default_runtime!(EchoEventHandler); runtime.run(); } diff --git a/src/api/context.rs b/src/api/context.rs new file mode 100644 index 0000000..de2f876 --- /dev/null +++ b/src/api/context.rs @@ -0,0 +1,41 @@ +use crate::error::Error; +use std::time::Duration; + +use super::LambdaEnvVars; + +/// A trait that should be implemented by types representing a [Context object]([https://docs.aws.amazon.com/lambda/latest/dg/python-context.html]). +/// +/// The context object exposes constant data from the instance's environment variables, +/// as well as data - such as request id and execution deadline - that is specific to each event. +pub trait LambdaContext: LambdaEnvVars { + /// A default implementation that relies on the `get_deadline` function to calculate the remaining time. + fn get_remaining_time_ms(&self) -> Result { + let now = std::time::SystemTime::now(); + match now.duration_since(std::time::SystemTime::UNIX_EPOCH) { + Ok(now_since_epoch) => match self.get_deadline() { + Some(dur) => dur, + None => return Err(Error::new("Missing deadline info".to_string())), + } + .checked_sub(now_since_epoch) + .ok_or_else(|| Error::new("Duration error".to_string())), + Err(e) => Err(Error::new(e.to_string())), + } + } + // Per-invocation data (event-related) + fn get_deadline(&self) -> Option; + fn get_invoked_function_arn(&self) -> Option<&str>; + fn get_aws_request_id(&self) -> Option<&str>; + // Identity and Client context - see [https://docs.aws.amazon.com/lambda/latest/dg/python-context.html] + // TODO - parse these structures and return a relevant type + fn get_cognito_identity(&self) -> Option<&str>; + fn get_client_context(&self) -> Option<&str>; +} + +/// A trait defining a setter interface that are used for setting context variables that vary between lambda events. +pub trait LambdaContextSetter { + fn set_deadline(&mut self, dl: Option); + fn set_invoked_function_arn(&mut self, arn: Option<&str>); + fn set_aws_request_id(&mut self, request_id: Option<&str>); + fn set_cognito_identity(&mut self, identity: Option<&str>); + fn set_client_context(&mut self, ctx: Option<&str>); +} diff --git a/src/api/env.rs b/src/api/env.rs new file mode 100644 index 0000000..b979d5d --- /dev/null +++ b/src/api/env.rs @@ -0,0 +1,61 @@ +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. + +// `SPDX-License-Identifier: MIT OR Apache-2.0` + +/// An enum representing the `InitializationType` choices set as an env-var on the instance by AWS Lambda. +/// See [Defined runtime environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime). +#[derive(Clone, Copy, Debug)] +pub enum InitializationType { + OnDemand, + ProvisionedConcurrency, + Unknown, +} + +impl InitializationType { + /// Returns the [`InitializationType`] value corresponding to the input string. + /// itype must be lowercase. + pub fn from_string(itype: &str) -> InitializationType { + match itype { + "on-demand" => Self::OnDemand, + "provisioned-concurrency" => Self::ProvisionedConcurrency, + // Shouldn't reach here but if for some reason AWS doesn't get it right... + _ => Self::Unknown, + } + } +} + +/// An interface for reading the environment variables set by the AWS Lambda service. +/// +/// Based on - [Defined runtime environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime). +pub trait LambdaEnvVars: Default { + fn get_handler_location(&self) -> Option<&str>; + fn get_x_ray_tracing_id(&self) -> Option<&str>; + fn get_aws_default_region(&self) -> Option<&str>; + fn get_aws_region(&self) -> Option<&str>; + fn get_execution_env(&self) -> Option<&str>; + fn get_lambda_function_name(&self) -> Option<&str>; + fn get_lambda_function_memory_size(&self) -> Option; + fn get_lambda_function_version(&self) -> Option<&str>; + fn get_lambda_initialization_type(&self) -> InitializationType; + fn get_lambda_log_group_name(&self) -> Option<&str>; + fn get_lambda_log_stream_name(&self) -> Option<&str>; + fn get_access_key(&self) -> Option<&str>; + fn get_access_key_id(&self) -> Option<&str>; + fn get_secret_access_key(&self) -> Option<&str>; + fn get_session_token(&self) -> Option<&str>; + fn get_lambda_runtime_api(&self) -> Option<&str>; + fn get_lambda_task_root(&self) -> Option<&str>; + fn get_lambda_runtime_dir(&self) -> Option<&str>; + fn get_tz(&self) -> Option<&str>; + /// Returns the string value of an env-var `var_name` wrapped in an [`Option`], + /// or `None` if the env-var is not set or the [`std::env::var`] function returns an error. + fn get_var(var_name: &str) -> Option { + use std::env; + env::var(var_name).ok() + } +} + +pub trait LambdaEnvSetter { + /// Signals that the previous tracing id has changed as a result of a new incoming event. + fn set_x_ray_tracing_id(&mut self, new_id: Option<&str>); +} diff --git a/src/api/handler.rs b/src/api/handler.rs new file mode 100644 index 0000000..e5f25bf --- /dev/null +++ b/src/api/handler.rs @@ -0,0 +1,33 @@ +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. + +// `SPDX-License-Identifier: MIT OR Apache-2.0` + +use crate::api::LambdaContext; + +use serde::Serialize; +use std::fmt::Display; + +/// Defines an event handler plus any initilization logic required for implementing the lambda. +/// This is the main interface users of the library should implement. +pub trait EventHandler: Sized { + /// Defines the lambda's output type which must implement or derive [`serde::Serialize`] in order to be sent as a JSON to the RuntimeAPI. + type EventOutput: Serialize; + /// Defines the lambda's error type which must implement or derive [`Display`]. + type EventError: Display; + /// Defines the lambda's initialization error type which must implement or derive [`Display`]. + type InitError: Display; + /// Constructs the event handler object and sets up any resources that are reusable across the lifetime of the lambda instance. + /// Returns a [`Result`] with the event handler object or an error object if failed. + fn initialize() -> Result; + /// Processes each incoming lambda event and returns a [`Result`] with the lambda's output. + /// # Arguments + /// + /// * `event` - The JSON event as a string slice, should be deserialized by the implementation. + /// * `context` - A shared reference to the current event context. + /// `Ctx` Defines the context object type, typically a [`crate::data::context::EventContext`]. + fn on_event( + &mut self, + event: &str, + context: &Ctx, + ) -> Result; +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..b41f8be --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,17 @@ +mod context; +mod env; +/// Defines the event handler interface. +mod handler; +/// Defines the interface used to read a response from the Lambda API. +mod response; +/// Defines the lambda runtime interface. +mod runtime; +/// Defines the [`crate::transport::Transport`] abstraction used to support multiple HTTP backends. +mod transport; + +pub use crate::api::context::*; +pub use crate::api::env::*; +pub use crate::api::handler::*; +pub use crate::api::response::*; +pub use crate::api::runtime::*; +pub use crate::api::transport::*; diff --git a/src/api/response.rs b/src/api/response.rs new file mode 100644 index 0000000..911e16b --- /dev/null +++ b/src/api/response.rs @@ -0,0 +1,42 @@ +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. + +// `SPDX-License-Identifier: MIT OR Apache-2.0` + +use crate::error::*; + +pub static AWS_REQ_ID: &str = "Lambda-Runtime-Aws-Request-Id"; +pub static AWS_DEADLINE_MS: &str = "Lambda-Runtime-Deadline-Ms"; +pub static AWS_FUNC_ARN: &str = "Lambda-Runtime-Invoked-Function-Arn"; +pub static AWS_TRACE_ID: &str = "Lambda-Runtime-Trace-Id"; +pub static AWS_CLIENT_CTX: &str = "Lambda-Runtime-Client-Context"; +pub static AWS_COG_ID: &str = "Lambda-Runtime-Cognito-Identity"; +pub static AWS_FUNC_ERR_TYPE: &str = "Lambda-Runtime-Function-Error-Type"; + +//Based on [https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-next] +/// A trait serving as an abstraction of the response from the [AWS Lambda runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). +pub trait LambdaAPIResponse { + // TODO: find out whether lambda might send a non-UTF-8 encoded json event and change signature if needed + fn get_body(self) -> Result; + fn get_status_code(&self) -> u16; + fn get_aws_request_id(&self) -> Option<&str>; + fn get_deadline(&self) -> Option; + fn get_invoked_function_arn(&self) -> Option<&str>; + fn get_x_ray_tracing_id(&self) -> Option<&str>; + fn get_client_context(&self) -> Option<&str>; + fn get_cognito_identity(&self) -> Option<&str>; + fn is_success(&self) -> bool { + matches!(self.get_status_code(), 200..=299) + } + + fn is_client_err(&self) -> bool { + matches!(self.get_status_code(), 400..=499) + } + + fn is_server_err(&self) -> bool { + matches!(self.get_status_code(), 500..=599) + } + + fn is_err(&self) -> bool { + matches!(self.get_status_code(), 400..=499 | 500..=599) + } +} diff --git a/src/api/runtime.rs b/src/api/runtime.rs new file mode 100644 index 0000000..73f3959 --- /dev/null +++ b/src/api/runtime.rs @@ -0,0 +1,37 @@ +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. + +// `SPDX-License-Identifier: MIT OR Apache-2.0` + +use crate::api::{EventHandler, Transport}; +use crate::error::*; + +/// Defines the interface for the Lambda runtime. +pub trait LambdaRuntime { + /// Defines the type of the event handler executed by the runtime in each invocation. + type Handler: EventHandler; + /// Defines the Transport type. See `[crate::transport::Transport]`. + type Transport: crate::api::Transport; + /// Used to fetch the next event from the Lambda service. + fn next_invocation(&mut self) -> Result<::Response, Error>; + /// Sends back a JSON formatted response to the Lambda service, after processing an event. + fn invocation_response( + &self, + request_id: &str, + response: &::EventOutput, + ) -> Result<::Response, Error>; + /// Used to report an error during initialization to the Lambda service. + fn initialization_error( + &self, + error_type: Option<&str>, + error_req: Option<&str>, + ) -> Result<::Response, Error>; + /// Used to report an error during function invocation to the Lambda service. + fn invocation_error( + &self, + request_id: &str, + error_type: Option<&str>, + error_req: Option<&str>, + ) -> Result<::Response, Error>; + /// Implements the runtime loop logic. + fn run(&mut self); +} diff --git a/src/transport/mod.rs b/src/api/transport.rs similarity index 59% rename from src/transport/mod.rs rename to src/api/transport.rs index b639f0e..8845dfd 100644 --- a/src/transport/mod.rs +++ b/src/api/transport.rs @@ -1,13 +1,16 @@ -// Copyright 2022 Guy Or and the "rtlambda" authors. All rights reserved. +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. // `SPDX-License-Identifier: MIT OR Apache-2.0` -use crate::data::response::LambdaAPIResponse; +use crate::api::LambdaAPIResponse; use crate::error::Error; /// A generic trait that is used as an abstraction to the HTTP client library (AKA "Backend") -/// used to interact with the [runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html), and the response type returned by that backend. -pub trait Transport: Default { +/// Used to communicate with the [runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). +pub trait Transport: Default { + /// Defines the type returned by the Transport's methods. + type Response: LambdaAPIResponse; + // TODO - optimize the headers type /// Sends an HTTP GET request to the specified `url` with the optional `body` and `headers`. fn get( @@ -15,12 +18,12 @@ pub trait Transport: Default { url: &str, body: Option<&str>, headers: Option<(Vec<&str>, Vec<&str>)>, - ) -> Result; + ) -> Result; /// Sends an HTTP POST request to the specified `url` with the optional `body` and `headers`. fn post( &self, url: &str, body: Option<&str>, headers: Option<(Vec<&str>, Vec<&str>)>, - ) -> Result; + ) -> Result; } diff --git a/src/backends/mod.rs b/src/backends/mod.rs index aa7bac6..302a4b7 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Guy Or and the "rtlambda" authors. All rights reserved. +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. // `SPDX-License-Identifier: MIT OR Apache-2.0` diff --git a/src/backends/ureq.rs b/src/backends/ureq.rs index e698914..7afca1b 100644 --- a/src/backends/ureq.rs +++ b/src/backends/ureq.rs @@ -1,120 +1,67 @@ -// Copyright 2022 Guy Or and the "rtlambda" authors. All rights reserved. +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. // `SPDX-License-Identifier: MIT OR Apache-2.0` -use crate::data::response::*; +use crate::api::{ + LambdaAPIResponse, Transport, AWS_CLIENT_CTX, AWS_COG_ID, AWS_DEADLINE_MS, AWS_FUNC_ARN, + AWS_REQ_ID, AWS_TRACE_ID, +}; use crate::error::Error; -use crate::transport::Transport; -use ureq::Agent; -use ureq::Response; - use std::time::Duration; +use ureq::Agent; -macro_rules! copy_str_header { - ($resp:expr, $header:expr) => { - $resp.header($header).map(|v| v.to_string()) - }; -} - -/// A wrapper that processes a [ureq::Response] and implements the [`crate::data::response::LambdaAPIResponse`] trait. -pub struct UreqResponse { - body: Option, - status: u16, - _request_id: String, - _deadline: Option, - _arn: Option, - _trace_id: Option, - _cognito_id: Option, - _client_context: Option, -} - -impl UreqResponse { - /// A constructor that consumes a [ureq::Response] by copying the relevant headers and reading the request body. - fn from_response(resp: Response) -> Result { - // Copy status - let status = resp.status(); - - // Copy AWS headers - let _request_id = match resp.header(AWS_REQ_ID) { - Some(v) => v.to_string(), - None => { - return Err(Error::new( - "Missing Lambda-Runtime-Aws-Request-Id header".to_string(), - )) - } - }; - - // Parse milliseconds to Duration - let _deadline = match resp.header(AWS_DEADLINE_MS) { - Some(ms) => match ms.parse::() { - Ok(val) => Some(Duration::from_millis(val)), - Err(_) => None, - }, - None => None, - }; - let _arn = copy_str_header!(resp, AWS_FUNC_ARN); - let _trace_id = copy_str_header!(resp, AWS_TRACE_ID); - let _cognito_id = copy_str_header!(resp, AWS_COG_ID); - let _client_context = copy_str_header!(resp, AWS_CLIENT_CTX); - - // Consume the response into a string - let body = match resp.into_string() { - Ok(data) => Some(data), - Err(err) => return Err(Error::new(format!("{}", err))), - }; - - Ok(Self { - body, - status, - _request_id, - _deadline, - _arn, - _trace_id, - _cognito_id, - _client_context, - }) - } -} - -impl LambdaAPIResponse for UreqResponse { +impl LambdaAPIResponse for ureq::Response { #[inline(always)] - fn get_body(&self) -> Option<&str> { - self.body.as_deref() + fn get_body(self) -> Result { + match self.into_string() { + Ok(data) => Ok(data), + Err(err) => Err(Error::new(format!("{}", err))), + } } #[inline(always)] fn get_status_code(&self) -> u16 { - self.status + self.status() } #[inline] - fn aws_request_id(&self) -> Option<&str> { - Some(&self._request_id) + fn get_aws_request_id(&self) -> Option<&str> { + self.header(AWS_REQ_ID) } + #[inline] - fn deadline(&self) -> Option { - self._deadline + fn get_deadline(&self) -> Option { + match self.header(AWS_DEADLINE_MS) { + Some(ms) => match ms.parse::() { + Ok(val) => Some(val), + Err(_) => None, + }, + None => None, + } } + #[inline] - fn invoked_function_arn(&self) -> Option<&str> { - self._arn.as_deref() + fn get_invoked_function_arn(&self) -> Option<&str> { + self.header(AWS_FUNC_ARN) } + #[inline] - fn trace_id(&self) -> Option<&str> { - self._trace_id.as_deref() + fn get_x_ray_tracing_id(&self) -> Option<&str> { + self.header(AWS_TRACE_ID) } + #[inline] - fn client_context(&self) -> Option<&str> { - self._client_context.as_deref() + fn get_client_context(&self) -> Option<&str> { + self.header(AWS_CLIENT_CTX) } + #[inline] - fn cognito_identity(&self) -> Option<&str> { - self._cognito_id.as_deref() + fn get_cognito_identity(&self) -> Option<&str> { + self.header(AWS_COG_ID) } } /// Wraps a [`ureq::Agent`] to implement the [`crate::transport::Transport`] trait. -/// Contains a specialized implementation for [`UreqResponse`] type parameter. /// /// AWS runtime instructs the implementation to disable timeout on the next invocation call. /// This implementation achieves this by creating a [`ureq::Agent`] with 1 day in seconds of timeout. @@ -122,13 +69,15 @@ pub struct UreqTransport { agent: Agent, } -impl UreqTransport { +impl Default for UreqTransport { /// Creates a new transport objects with an underlying [ureq::Agent] that will (practically) not time out. - fn new() -> Self { + fn default() -> Self { let agent = ureq::builder().timeout(Duration::from_secs(86400)).build(); UreqTransport { agent } } +} +impl UreqTransport { /// Sends a request using the underlying agent. fn request( &self, @@ -136,7 +85,7 @@ impl UreqTransport { url: &str, body: Option<&str>, headers: Option<(Vec<&str>, Vec<&str>)>, - ) -> Result { + ) -> Result { let mut req = self.agent.request(method, url); if let Some(headers) = headers { let (keys, values) = headers; @@ -154,24 +103,16 @@ impl UreqTransport { } } -impl Default for UreqTransport { - fn default() -> Self { - Self::new() - } -} +impl Transport for UreqTransport { + type Response = ureq::Response; -impl Transport for UreqTransport { fn get( &self, url: &str, body: Option<&str>, headers: Option<(Vec<&str>, Vec<&str>)>, - ) -> Result { - let res = self.request("GET", url, body, headers); - if let Ok(res) = res { - return UreqResponse::from_response(res); - } - Err(res.unwrap_err()) + ) -> Result { + self.request("GET", url, body, headers) } fn post( @@ -179,11 +120,7 @@ impl Transport for UreqTransport { url: &str, body: Option<&str>, headers: Option<(Vec<&str>, Vec<&str>)>, - ) -> Result { - let res = self.request("POST", url, body, headers); - if let Ok(res) = res { - return UreqResponse::from_response(res); - } - Err(res.unwrap_err()) + ) -> Result { + self.request("POST", url, body, headers) } } diff --git a/src/data/context.rs b/src/data/context.rs index 512b332..e820f02 100644 --- a/src/data/context.rs +++ b/src/data/context.rs @@ -1,121 +1,257 @@ -// Copyright 2022 Guy Or and the "rtlambda" authors. All rights reserved. +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. // `SPDX-License-Identifier: MIT OR Apache-2.0` -use crate::data::env::RuntimeEnvVars; -use crate::data::response::LambdaAPIResponse; -use crate::error::Error; +use crate::api::{ + InitializationType, LambdaContext, LambdaContextSetter, LambdaEnvSetter, LambdaEnvVars, +}; +use std::env::{remove_var, set_var}; use std::time::Duration; -/// An interface trait that should be implemented by types representing a [Context object]([https://docs.aws.amazon.com/lambda/latest/dg/python-context.html]). -/// -/// The context object exposes constant data from the instance's environment variables, -/// as well as data - such as request id and execution deadline - that is specific to each event. -pub trait LambdaContext { - /// A default implementation that calculates the time difference between its time of invocation and the - /// handler execution deadline specified by AWS Lambda. - fn get_remaining_time_ms(&self) -> Result { - let now = std::time::SystemTime::now(); - match now.duration_since(std::time::SystemTime::UNIX_EPOCH) { - Ok(now_since_epoch) => match self.get_deadline() { - Some(dur) => dur, - None => return Err(Error::new("Missing deadline info".to_string())), - } - .checked_sub(now_since_epoch) - .ok_or_else(|| Error::new("Duration error".to_string())), - Err(e) => Err(Error::new(e.to_string())), +static _X_AMZN_TRACE_ID: &str = "_X_AMZN_TRACE_ID"; +static _HANDLER: &str = "_HANDLER"; +static AWS_DEFAULT_REGION: &str = "AWS_DEFAULT_REGION"; +static AWS_REGION: &str = "AWS_REGION"; +static AWS_EXECUTION_ENV: &str = "AWS_EXECUTION_ENV"; +static AWS_LAMBDA_FUNCTION_NAME: &str = "AWS_LAMBDA_FUNCTION_NAME"; +static AWS_LAMBDA_FUNCTION_MEMORY_SIZE: &str = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE"; +static AWS_LAMBDA_FUNCTION_VERSION: &str = "AWS_LAMBDA_FUNCTION_VERSION"; +static AWS_LAMBDA_INITIALIZATION_TYPE: &str = "AWS_LAMBDA_INITIALIZATION_TYPE"; +static AWS_LAMBDA_LOG_GROUP_NAME: &str = "AWS_LAMBDA_LOG_GROUP_NAME"; +static AWS_LAMBDA_LOG_STREAM_NAME: &str = "AWS_LAMBDA_LOG_STREAM_NAME"; +static AWS_ACCESS_KEY: &str = "AWS_ACCESS_KEY"; +static AWS_ACCESS_KEY_ID: &str = "AWS_ACCESS_KEY_ID"; +static AWS_SECRET_ACCESS_KEY: &str = "AWS_SECRET_ACCESS_KEY"; +static AWS_SESSION_TOKEN: &str = "AWS_SESSION_TOKEN"; +static AWS_LAMBDA_RUNTIME_API: &str = "AWS_LAMBDA_RUNTIME_API"; +static LAMBDA_TASK_ROOT: &str = "LAMBDA_TASK_ROOT"; +static LAMBDA_RUNTIME_DIR: &str = "LAMBDA_RUNTIME_DIR"; +static TZ: &str = "TZ"; + +/// An implementation of [`LambdaContext`], [`LambdaContextSetter`] and [`LambdaEnvSetter`]. +pub struct EventContext { + pub handler: Option, + pub region: Option, + pub default_region: Option, + // Custom runtimes currently don't have this value set as per AWS docs + pub execution_env: Option, + pub function_name: Option, + pub function_memory_size: Option, + pub function_version: Option, + pub initialization_type: InitializationType, + pub log_group_name: Option, + pub log_stream_name: Option, + pub access_key: Option, + pub access_key_id: Option, + pub secret_access_key: Option, + pub session_token: Option, + pub runtime_api: Option, + pub task_root: Option, + pub runtime_dir: Option, + pub tz: Option, + // These values are set by the runtime after each next invocation request + pub trace_id: Option, + pub deadline: Option, + pub function_arn: Option, + pub request_id: Option, + pub cognito_id: Option, + pub client_context: Option, +} + +impl Default for EventContext { + fn default() -> Self { + use std::env; + Self { + handler: env::var(_HANDLER).ok(), + default_region: env::var(AWS_DEFAULT_REGION).ok(), + region: env::var(AWS_REGION).ok(), + trace_id: None, + execution_env: env::var(AWS_EXECUTION_ENV).ok(), + function_name: env::var(AWS_LAMBDA_FUNCTION_NAME).ok(), + function_memory_size: match env::var(AWS_LAMBDA_FUNCTION_MEMORY_SIZE).ok() { + Some(v) => v.parse::().ok(), + None => None, + }, + function_version: env::var(AWS_LAMBDA_FUNCTION_VERSION).ok(), + initialization_type: match env::var(AWS_LAMBDA_INITIALIZATION_TYPE).ok() { + Some(v) => InitializationType::from_string(&v), + None => InitializationType::Unknown, + }, + log_group_name: env::var(AWS_LAMBDA_LOG_GROUP_NAME).ok(), + log_stream_name: env::var(AWS_LAMBDA_LOG_STREAM_NAME).ok(), + access_key: env::var(AWS_ACCESS_KEY).ok(), + access_key_id: env::var(AWS_ACCESS_KEY_ID).ok(), + secret_access_key: env::var(AWS_SECRET_ACCESS_KEY).ok(), + session_token: env::var(AWS_SESSION_TOKEN).ok(), + runtime_api: env::var(AWS_LAMBDA_RUNTIME_API).ok(), + task_root: env::var(LAMBDA_TASK_ROOT).ok(), + runtime_dir: env::var(LAMBDA_RUNTIME_DIR).ok(), + tz: env::var(TZ).ok(), + deadline: None, + function_arn: None, + request_id: None, + cognito_id: None, + client_context: None, } } - // Per-invocation data (event-related) - fn get_deadline(&self) -> Option; - fn invoked_function_arn(&self) -> Option<&str>; - fn aws_request_id(&self) -> Option<&str>; - // Per-runtime data (constant accross the lifetime of the runtime, taken from env-vars) - fn function_name(&self) -> Option<&str>; - fn function_version(&self) -> Option<&str>; - fn memory_limit_in_mb(&self) -> Option; - fn log_group_name(&self) -> Option<&str>; - fn log_stream_name(&self) -> Option<&str>; - // Identity and Client context - see [https://docs.aws.amazon.com/lambda/latest/dg/python-context.html] - // TODO - parse these structures and return a relevant type - fn cognito_identity(&self) -> Option<&str>; - fn client_context(&self) -> Option<&str>; } -/// A generic implementation of [`LambdaContext`] that relies on **borrowing** existing owned -/// instances of types that implement [`crate::data::env::RuntimeEnvVars`] - for reading environment variables - -/// and [`crate::data::response::LambdaAPIResponse`] - for reading event-related data. -/// -/// It can be used so long that its lifetime is less than or equal to its referents. -/// -/// This implementation is used to avoid needlessly copying data that is immutable by definition, -/// however it is assumed that types implementing [`crate::data::response::LambdaAPIResponse`] can be read from -/// immutably - which is not the always case with HTTP Response types, -/// for example [ureq::Response](https://docs.rs/ureq/2.4.0/ureq/struct.Response.html#method.into_string) consumes itself upon reading the response body. -/// See [`crate::data::response::LambdaAPIResponse`]. -pub struct RefLambdaContext<'a, E, R> -where - E: RuntimeEnvVars, - R: LambdaAPIResponse, -{ - /// A shared reference to a type implementing [`crate::data::env::RuntimeEnvVars`]. - pub env_vars: &'a E, - /// A shared reference to a type implementing [`crate::data::response::LambdaAPIResponse`]. - pub invo_resp: &'a R, -} +impl LambdaEnvVars for EventContext { + #[inline(always)] + fn get_handler_location(&self) -> Option<&str> { + self.handler.as_deref() + } -impl<'a, E, R> LambdaContext for RefLambdaContext<'a, E, R> -where - E: RuntimeEnvVars, - R: LambdaAPIResponse, -{ - #[inline] - fn get_deadline(&self) -> Option { - self.invo_resp.deadline() + #[inline(always)] + fn get_aws_default_region(&self) -> Option<&str> { + self.default_region.as_deref() + } + + #[inline(always)] + fn get_aws_region(&self) -> Option<&str> { + self.region.as_deref() + } + + #[inline(always)] + fn get_x_ray_tracing_id(&self) -> Option<&str> { + self.trace_id.as_deref() + } + + #[inline(always)] + fn get_execution_env(&self) -> Option<&str> { + self.execution_env.as_deref() + } + + #[inline(always)] + fn get_lambda_function_name(&self) -> Option<&str> { + self.function_name.as_deref() + } + + #[inline(always)] + fn get_lambda_function_memory_size(&self) -> Option { + self.function_memory_size + } + + #[inline(always)] + fn get_lambda_function_version(&self) -> Option<&str> { + self.function_version.as_deref() + } + + #[inline(always)] + fn get_lambda_initialization_type(&self) -> InitializationType { + self.initialization_type + } + #[inline(always)] + fn get_lambda_log_group_name(&self) -> Option<&str> { + self.log_group_name.as_deref() } #[inline(always)] - fn invoked_function_arn(&self) -> Option<&str> { - self.invo_resp.invoked_function_arn() + fn get_lambda_log_stream_name(&self) -> Option<&str> { + self.log_stream_name.as_deref() } #[inline(always)] - fn aws_request_id(&self) -> Option<&str> { - self.invo_resp.aws_request_id() + fn get_access_key(&self) -> Option<&str> { + self.access_key.as_deref() } #[inline(always)] - fn function_name(&self) -> Option<&str> { - self.env_vars.get_function_name() + fn get_access_key_id(&self) -> Option<&str> { + self.access_key_id.as_deref() } #[inline(always)] - fn function_version(&self) -> Option<&str> { - self.env_vars.get_function_version() + fn get_secret_access_key(&self) -> Option<&str> { + self.secret_access_key.as_deref() } #[inline(always)] - fn memory_limit_in_mb(&self) -> Option { - self.env_vars.get_function_memory_size() + fn get_session_token(&self) -> Option<&str> { + self.session_token.as_deref() } #[inline(always)] - fn log_group_name(&self) -> Option<&str> { - self.env_vars.get_log_group_name() + fn get_lambda_runtime_api(&self) -> Option<&str> { + self.runtime_api.as_deref() } #[inline(always)] - fn log_stream_name(&self) -> Option<&str> { - self.env_vars.get_log_stream_name() + fn get_lambda_task_root(&self) -> Option<&str> { + self.task_root.as_deref() } #[inline(always)] - fn cognito_identity(&self) -> Option<&str> { - self.invo_resp.cognito_identity() + fn get_lambda_runtime_dir(&self) -> Option<&str> { + self.runtime_dir.as_deref() } #[inline(always)] - fn client_context(&self) -> Option<&str> { - self.invo_resp.client_context() + fn get_tz(&self) -> Option<&str> { + self.tz.as_deref() + } +} + +impl LambdaEnvSetter for EventContext { + #[inline] + fn set_x_ray_tracing_id(&mut self, new_id: Option<&str>) { + // If AWS returns the "Lambda-Runtime-Trace-Id" header, assign its value to the - + // "_X_AMZN_TRACE_ID" env var + if let Some(req_id) = new_id { + set_var(_X_AMZN_TRACE_ID, req_id); + self.trace_id = new_id.map(|v| v.to_string()); + } else { + remove_var(_X_AMZN_TRACE_ID); + self.trace_id = None; + }; + } +} + +impl LambdaContext for EventContext { + #[inline] + fn get_deadline(&self) -> Option { + self.deadline + } + + #[inline(always)] + fn get_invoked_function_arn(&self) -> Option<&str> { + self.function_arn.as_deref() + } + + #[inline(always)] + fn get_aws_request_id(&self) -> Option<&str> { + self.request_id.as_deref() + } + + #[inline(always)] + fn get_cognito_identity(&self) -> Option<&str> { + self.cognito_id.as_deref() + } + + #[inline(always)] + fn get_client_context(&self) -> Option<&str> { + self.client_context.as_deref() + } +} + +impl LambdaContextSetter for EventContext { + fn set_deadline(&mut self, dl: Option) { + self.deadline = dl; + } + + fn set_invoked_function_arn(&mut self, arn: Option<&str>) { + self.function_arn = arn.map(|s| s.to_string()); + } + + fn set_aws_request_id(&mut self, request_id: Option<&str>) { + self.request_id = request_id.map(|s| s.to_string()); + } + + fn set_cognito_identity(&mut self, identity: Option<&str>) { + self.cognito_id = identity.map(|s| s.to_string()); + } + + fn set_client_context(&mut self, ctx: Option<&str>) { + self.client_context = ctx.map(|s| s.to_string()); } } diff --git a/src/data/env.rs b/src/data/env.rs deleted file mode 100644 index 7cdff1e..0000000 --- a/src/data/env.rs +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2022 Guy Or and the "rtlambda" authors. All rights reserved. - -// `SPDX-License-Identifier: MIT OR Apache-2.0` - -/// An enum representing the `InitializationType` choices set as an env-var on the instance by AWS Lambda. -/// See [Defined runtime environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime). -#[derive(Clone, Copy, Debug)] -pub enum InitializationType { - OnDemand, - ProvisionedConcurrency, - Unknown, -} - -impl InitializationType { - /// Returns the [`InitializationType`] value corresponding to the input string. - /// itype must be lowercase. - fn from_string(itype: &str) -> InitializationType { - match itype { - "on-demand" => Self::OnDemand, - "provisioned-concurrency" => Self::ProvisionedConcurrency, - // Shouldn't reach here but if for some reason AWS doesn't get it right... - _ => Self::Unknown, - } - } -} - -/// An interface trait for reading the environment variables set by the AWS Lambda service. -/// -/// Based on - [Defined runtime environment variables](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime). -pub trait RuntimeEnvVars: Default { - fn get_handler(&self) -> Option<&str>; - fn get_region(&self) -> Option<&str>; - fn get_trace_id(&self) -> Option<&str>; - fn get_execution_env(&self) -> Option<&str>; - fn get_function_name(&self) -> Option<&str>; - fn get_function_memory_size(&self) -> Option; - fn get_function_version(&self) -> Option<&str>; - fn get_initialization_type(&self) -> InitializationType; - fn get_log_group_name(&self) -> Option<&str>; - fn get_log_stream_name(&self) -> Option<&str>; - fn get_access_key(&self) -> Option<&str>; - fn get_access_key_id(&self) -> Option<&str>; - fn get_secret_access_key(&self) -> Option<&str>; - fn get_session_token(&self) -> Option<&str>; - fn get_runtime_api(&self) -> Option<&str>; - fn get_task_root(&self) -> Option<&str>; - fn get_runtime_dir(&self) -> Option<&str>; - fn get_tz(&self) -> Option<&str>; - /// Returns the string value of an env-var `var_name` wrapped in an [`Option`], - /// or `None` if the env-var is not set or the [`std::env::var`] function returns an error. - fn get_var(var_name: &str) -> Option { - use std::env; - env::var(var_name).ok() - } - /// Signals that the previous tracing id has changed as a result of a new incoming event. - fn set_trace_id(&mut self, new_id: Option<&str>); -} - -/// A struct implementing [`RuntimeEnvVars`] by caching the default runtime env-vars, -/// and supports a default initialization using std::env::var calls. -#[derive(Debug, Clone)] -pub struct LambdaRuntimeEnv { - pub handler: Option, - // This value should be set by the runtime after each next invocation request where a new id is given - pub trace_id: Option, - pub region: Option, - // Custom runtimes currently don't have this value set as per AWS docs - pub execution_env: Option, - pub function_name: Option, - pub function_memory_size: Option, - pub function_version: Option, - pub initialization_type: InitializationType, - pub log_group_name: Option, - pub log_stream_name: Option, - pub access_key: Option, - pub access_key_id: Option, - pub secret_access_key: Option, - pub session_token: Option, - pub runtime_api: Option, - pub task_root: Option, - pub runtime_dir: Option, - pub tz: Option, -} - -impl LambdaRuntimeEnv { - /// Constructs a new [`LambdaRuntimeEnv`] by reading the process' environment variables, - /// and caching the [default env-vars](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime). - pub fn from_env() -> LambdaRuntimeEnv { - use std::env; - LambdaRuntimeEnv { - handler: env::var("_HANDLER").ok(), - region: env::var("AWS_REGION").ok(), - trace_id: None, - execution_env: env::var("AWS_EXECUTION_ENV").ok(), - function_name: env::var("AWS_LAMBDA_FUNCTION_NAME").ok(), - function_memory_size: match env::var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE").ok() { - Some(v) => v.parse::().ok(), - None => None, - }, - function_version: env::var("AWS_LAMBDA_FUNCTION_VERSION").ok(), - initialization_type: match env::var("AWS_LAMBDA_INITIALIZATION_TYPE").ok() { - Some(v) => InitializationType::from_string(&v), - None => InitializationType::Unknown, - }, - log_group_name: env::var("AWS_LAMBDA_LOG_GROUP_NAME").ok(), - log_stream_name: env::var("AWS_LAMBDA_LOG_STREAM_NAME").ok(), - access_key: env::var("AWS_ACCESS_KEY").ok(), - access_key_id: env::var("AWS_ACCESS_KEY_ID").ok(), - secret_access_key: env::var("AWS_SECRET_ACCESS_KEY").ok(), - session_token: env::var("AWS_SESSION_TOKEN").ok(), - runtime_api: env::var("AWS_LAMBDA_RUNTIME_API").ok(), - task_root: env::var("LAMBDA_TASK_ROOT").ok(), - runtime_dir: env::var("LAMBDA_RUNTIME_DIR").ok(), - tz: env::var("TZ").ok(), - } - } -} - -impl Default for LambdaRuntimeEnv { - fn default() -> Self { - Self::from_env() - } -} - -impl RuntimeEnvVars for LambdaRuntimeEnv { - #[inline(always)] - fn get_handler(&self) -> Option<&str> { - self.handler.as_deref() - } - - #[inline(always)] - fn get_region(&self) -> Option<&str> { - self.region.as_deref() - } - - #[inline(always)] - fn get_trace_id(&self) -> Option<&str> { - self.trace_id.as_deref() - } - - #[inline(always)] - fn get_execution_env(&self) -> Option<&str> { - self.execution_env.as_deref() - } - - #[inline(always)] - fn get_function_name(&self) -> Option<&str> { - self.function_name.as_deref() - } - - #[inline(always)] - fn get_function_memory_size(&self) -> Option { - self.function_memory_size - } - - #[inline(always)] - fn get_function_version(&self) -> Option<&str> { - self.function_version.as_deref() - } - - #[inline(always)] - fn get_initialization_type(&self) -> InitializationType { - self.initialization_type - } - #[inline(always)] - fn get_log_group_name(&self) -> Option<&str> { - self.log_group_name.as_deref() - } - - #[inline(always)] - fn get_log_stream_name(&self) -> Option<&str> { - self.log_stream_name.as_deref() - } - - #[inline(always)] - fn get_access_key(&self) -> Option<&str> { - self.access_key.as_deref() - } - - #[inline(always)] - fn get_access_key_id(&self) -> Option<&str> { - self.access_key_id.as_deref() - } - - #[inline(always)] - fn get_secret_access_key(&self) -> Option<&str> { - self.secret_access_key.as_deref() - } - - #[inline(always)] - fn get_session_token(&self) -> Option<&str> { - self.session_token.as_deref() - } - - #[inline(always)] - fn get_runtime_api(&self) -> Option<&str> { - self.runtime_api.as_deref() - } - - #[inline(always)] - fn get_task_root(&self) -> Option<&str> { - self.task_root.as_deref() - } - - #[inline(always)] - fn get_runtime_dir(&self) -> Option<&str> { - self.runtime_dir.as_deref() - } - - #[inline(always)] - fn get_tz(&self) -> Option<&str> { - self.tz.as_deref() - } - - #[inline] - fn set_trace_id(&mut self, new_id: Option<&str>) { - self.trace_id = new_id.map(|v| v.to_string()); - } -} diff --git a/src/data/mod.rs b/src/data/mod.rs index 409ee24..c3959f1 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,10 +1,6 @@ -// Copyright 2022 Guy Or and the "rtlambda" authors. All rights reserved. +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. // `SPDX-License-Identifier: MIT OR Apache-2.0` /// Defines the interface of the context object and provides an implementation for it. pub mod context; -/// Defines an interface for reading env-vars and provides an implementation for it. -pub mod env; -/// Defines the interface used to read a response from the Lambda API. -pub mod response; diff --git a/src/data/response.rs b/src/data/response.rs deleted file mode 100644 index fdfd35c..0000000 --- a/src/data/response.rs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2022 Guy Or and the "rtlambda" authors. All rights reserved. - -// `SPDX-License-Identifier: MIT OR Apache-2.0` - -use std::time::Duration; - -pub static AWS_REQ_ID: &str = "Lambda-Runtime-Aws-Request-Id"; -pub static AWS_DEADLINE_MS: &str = "Lambda-Runtime-Deadline-Ms"; -pub static AWS_FUNC_ARN: &str = "Lambda-Runtime-Invoked-Function-Arn"; -pub static AWS_TRACE_ID: &str = "Lambda-Runtime-Trace-Id"; -pub static AWS_CLIENT_CTX: &str = "Lambda-Runtime-Client-Context"; -pub static AWS_COG_ID: &str = "Lambda-Runtime-Cognito-Identity"; -pub static AWS_FUNC_ERR_TYPE: &str = "Lambda-Runtime-Function-Error-Type"; - -//Based on [https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-next] -/// An interface trait representing a response from the [AWS Lambda runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). -/// -/// It encapsulates all of the 4 possible response types defined by the runtime API. -/// Differentiating between them and correctly reading the response are implementation details. -/// -/// Implementations of this trait *should* enable reading data without requiring ownership of or exclusive reference to the type, -/// therefore it is **not** always possible to implement it **directly** on HTTP Response types exposed by different vendors - -/// for example reading the body from a [ureq::Response](https://docs.rs/ureq/2.4.0/ureq/struct.Response.html#method.into_string) instance -/// moves the instance, making reading any header (for example request id) or the response's status code impossible - -/// or failing entirely if a reference to the instance already exist such as when using a [`crate::data::context::RefLambdaContext`] context implementation. -/// -/// A good approach is to implement this trait on a wrapper type that caches the relevant headers and reads the body, -/// for an example see [`crate::backends::ureq::UreqResponse`]. -pub trait LambdaAPIResponse { - fn get_body(&self) -> Option<&str>; - fn get_status_code(&self) -> u16; - fn aws_request_id(&self) -> Option<&str>; - fn deadline(&self) -> Option; - fn invoked_function_arn(&self) -> Option<&str>; - fn trace_id(&self) -> Option<&str>; - fn client_context(&self) -> Option<&str>; - fn cognito_identity(&self) -> Option<&str>; - - // TODO: find out whether lambda might send a non-UTF-8 encoded json event and change signature if needed - fn event_response(&self) -> Option<&str> { - match self.is_success() { - true => self.get_body(), - false => None, - } - } - - fn error_response(&self) -> Option<&str> { - match self.is_client_err() { - true => self.get_body(), - false => None, - } - } - - #[inline] - fn status_response(&self) -> Option<&str> { - // TODO - only return if the response type is defined to return a StatusResponse object - self.event_response() - } - - fn is_success(&self) -> bool { - matches!(self.get_status_code(), 200..=299) - } - - fn is_client_err(&self) -> bool { - matches!(self.get_status_code(), 400..=499) - } - - fn is_server_err(&self) -> bool { - matches!(self.get_status_code(), 500..=599) - } - - fn is_err(&self) -> bool { - matches!(self.get_status_code(), 400..=499 | 500..=599) - } -} diff --git a/src/error.rs b/src/error.rs index 3cce405..172454e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Guy Or and the "rtlambda" authors. All rights reserved. +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. // `SPDX-License-Identifier: MIT OR Apache-2.0` @@ -13,6 +13,10 @@ impl Error { pub fn new(msg: String) -> Self { Error { msg } } + + pub fn empty() -> Self { + Error::new(String::with_capacity(0)) + } } impl Display for Error { diff --git a/src/lib.rs b/src/lib.rs index 039e4b1..ce54863 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,17 @@ -// Copyright 2022 Guy Or and the "rtlambda" authors. All rights reserved. +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. // `SPDX-License-Identifier: MIT OR Apache-2.0` +/// Defines the public API of `rtlambda`. +pub mod api; /// Implementations of the `rtlambda` API for different HTTP backends. pub mod backends; -/// A collection of traits and default implementations for them, representing the library's core data structures. +/// Defines the library's core data structures. pub mod data; /// Defines error types and constants. pub mod error; -/// Defines the [`crate::runtime::LambdaRuntime`] API and provides a default generic implementation. +/// Defines the [`crate::runtime::DefaultRuntime`] which implements the Rust lambda runtime. pub mod runtime; -/// Defines the [`crate::transport::Transport`] abstraction used to support multiple HTTP backends. -pub mod transport; /// The current Lambda API version used on AWS. pub static LAMBDA_VER: &str = "2018-06-01"; @@ -19,33 +19,25 @@ pub static LAMBDA_VER: &str = "2018-06-01"; /// A prelude that contains all the relevant imports when using the library's default runtime implementation, /// which currently ships with a [ureq](https://crates.io/crates/ureq) based HTTP Backend and [serde_json](https://crates.io/crates/serde_json) for serialization. pub mod prelude { + pub use crate::api::*; pub use crate::backends::ureq::*; - pub use crate::data::context::{LambdaContext, RefLambdaContext}; - pub use crate::data::env::LambdaRuntimeEnv; - pub use crate::runtime::{DefaultRuntime, LambdaRuntime}; + pub use crate::data::context::EventContext; + pub use crate::runtime::DefaultRuntime; pub use crate::LAMBDA_VER; } -/// Creates a [`crate::runtime::DefaultRuntime`] with the given response, transport, env, out, err types as well as version and initializer. +/// Creates a [`crate::runtime::DefaultRuntime`] with the given transport, handler, env, out, err types as well as version and initializer. #[macro_export] macro_rules! create_runtime { - ($response:ty, $transport:ty, $env:ty, $out:ty, $err:ty, $ver:expr, $init:ident) => { - DefaultRuntime::<$response, $transport, $env, $out, $err>::new($ver, $init); + ($transport:ty, $ver:expr, $ev_handler:ty) => { + DefaultRuntime::<$transport, $ev_handler>::new($ver); }; } /// Creates a [`crate::runtime::DefaultRuntime`] with ureq based HTTP backend and the default implementation of env-vars handling. #[macro_export] macro_rules! default_runtime { - ($out:ty, $err:ty, $ver:expr, $init:ident) => { - create_runtime!( - UreqResponse, - UreqTransport, - LambdaRuntimeEnv, - $out, - $err, - $ver, - $init - ) + ($ev_handler:ty) => { + create_runtime!(UreqTransport, LAMBDA_VER, $ev_handler) }; } diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 5c88340..37cc587 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -1,18 +1,15 @@ -// Copyright 2022 Guy Or and the "rtlambda" authors. All rights reserved. +// Copyright 2022-2023 Guy Or and the "rtlambda" authors. All rights reserved. // `SPDX-License-Identifier: MIT OR Apache-2.0` -use crate::data::context::RefLambdaContext; -use crate::data::env::RuntimeEnvVars; -use crate::data::response::{LambdaAPIResponse, AWS_FUNC_ERR_TYPE}; -use crate::error::{Error, CONTAINER_ERR}; -use crate::transport::Transport; - -use std::env::set_var; -use std::ffi::OsStr; -use std::fmt::Display; +use std::time::Duration; -use serde::Serialize; +use crate::api::{ + EventHandler, LambdaAPIResponse, LambdaContext, LambdaContextSetter, LambdaEnvSetter, + LambdaEnvVars, LambdaRuntime, Transport, AWS_FUNC_ERR_TYPE, +}; +use crate::data::context::EventContext; +use crate::error::{Error, CONTAINER_ERR}; // Already handles any panic inducing errors macro_rules! handle_response { @@ -20,7 +17,9 @@ macro_rules! handle_response { let status_code = $resp.get_status_code(); match status_code { 400..=499 => { - let err = $resp.error_response().or(Some("")).unwrap(); + let err = $resp + .get_body() + .unwrap_or_else(|_| String::with_capacity(0)); return Err(Error::new(format!( "Client error ({}). ErrorResponse: {}", status_code, err @@ -42,94 +41,28 @@ macro_rules! format_version_string { }; } -/// A generic trait defining an interface for a Lambda runtime. -/// The HTTP Backend in use is defined by the input types `T` that implements [`Transport`] and `R` implementing [`LambdaAPIResponse`]. -/// The `OUT` type parameter is the user-defined response type which represents the success result of the event handler. -/// -/// The combination of type parameters enables the compiled program to avoid dynamic dispatch when calling the runtime methods. -pub trait LambdaRuntime -where - R: LambdaAPIResponse, - T: Transport, - OUT: Serialize, -{ - /// Used to fetch the next event from the Lambda service. - fn next_invocation(&mut self) -> Result; - /// Sends back a JSON formatted response to the Lambda service, after processing an event. - fn invocation_response(&self, request_id: &str, response: &OUT) -> Result; - /// Used to report an error during initialization to the Lambda service. - fn initialization_error( - &self, - error_type: Option<&str>, - error_req: Option<&str>, - ) -> Result; - /// Used to report an error during function invocation to the Lambda service. - fn invocation_error( - &self, - request_id: &str, - error_type: Option<&str>, - error_req: Option<&str>, - ) -> Result; - /// Implements the runtime loop logic. - fn run(&mut self); -} - /// The default generic implementation of the [`LambdaRuntime`] interface. -/// Works by accepting a pointer to an initialization function or a closure `initializer` - -/// that is run once and initializes "global" variables that are created once -/// and persist across the runtime's life (DB connections, heap allocated static data etc...). -/// -/// The initialization function returns a user-defined closure object that acts as the event handler and can -/// take ownership over those variables by move. -/// The Ok output type of the closure - `OUT` - should implement [`serde::Serialize`]. -/// -/// The `R`, `T` and `OUT` type parameters correspond to the ones defined in [`LambdaRuntime`]. -/// -/// The `ENV` type parameter defines the implementation of [`crate::data::env::RuntimeEnvVars`] for reading the env-vars set for the runtime. -/// -/// The `ERR` type parameter is a user-defined type representing any error that may occur during initialization or invocation of the event handler. -pub struct DefaultRuntime -where - R: LambdaAPIResponse, - T: Transport, - ENV: RuntimeEnvVars, - // I: LambdaContext, - ERR: Display, - OUT: Serialize, -{ - /// An owned instance of a type implementing [`crate::data::env::RuntimeEnvVars`]. - env_vars: ENV, +/// Works by accepting an owned [`EventHandler`] object which is first initialized by the runtime by calling [`EventHandler::initialize`]. +pub struct DefaultRuntime { + /// An owned container that holds a copy of the env vars and the current invocation data. + context: EventContext, /// The Lambda API version string. version: String, /// URI of the Lambda API. api_base: String, /// An owned instance of the HTTP Backend implementing [`crate::transport::Transport`]. transport: T, - /// An initialization function that sets up persistent variables and returns the event handler. - initializer: - fn() - -> Result, RefLambdaContext) -> Result>, ERR>, + /// The event handler instance. It is a lazy field which is initialized when the runtime starts. + /// The reason is that any errors that may occur during initialization are captured and handled by the runtime. + handler: Option, } -impl DefaultRuntime -where - R: LambdaAPIResponse, - T: Transport, - ENV: RuntimeEnvVars, - // I: LambdaContext, - ERR: Display, - OUT: Serialize, -{ - pub fn new( - version: &str, - initializer: fn() -> Result< - Box, RefLambdaContext) -> Result>, - ERR, - >, - ) -> Self { - // Initialize default env vars and check for the host and port of the runtime API. - let env_vars = ENV::default(); - let api_base = match env_vars.get_runtime_api() { +impl DefaultRuntime { + pub fn new(version: &str) -> Self { + // Initialize the context object + let context = EventContext::default(); + // Check for the host and port of the runtime API. + let api_base = match context.get_lambda_runtime_api() { Some(v) => v.to_string(), None => panic!("Failed getting API base URL from env vars"), }; @@ -141,87 +74,70 @@ where let transport = T::default(); Self { - env_vars, + context, version: formatted_version, api_base, transport, - initializer, + handler: None, } } - - #[inline(always)] - pub fn get_env(&self) -> &ENV { - &self.env_vars - } } -impl LambdaRuntime for DefaultRuntime +impl LambdaRuntime for DefaultRuntime where - R: LambdaAPIResponse, - T: Transport, - ENV: RuntimeEnvVars, - // I: LambdaContext, - ERR: Display, - OUT: Serialize, + T: Transport, + H: EventHandler, { + type Handler = H; + type Transport = T; + fn run(&mut self) { // Run the app's initializer and check for errors - let init_result = (self.initializer)(); - let lambda = match init_result { - Err(init_err) => { - // Try reporting to the Lambda service if there is an error during initialization - // TODO: Take error type and request from ERR - match self.initialization_error(Some("Runtime.InitError"), None) { - Ok(r) => r, - // If an error occurs during reporting the previous error, panic. - Err(err) => panic!( - "Failed to report initialization error. Error: {}, AWS Error: {}", - &init_err, err - ), - }; - // After reporting an init error just panic. - panic!("Initialization Error: {}", &init_err); - } - // On successfull init, unwrap the underlying closure (event handler) - Ok(event_handler) => event_handler, - }; + let init_result = Self::Handler::initialize(); + if let Err(init_err) = init_result { + // Report any initialization error to the Lambda service + // TODO: Serialize the init_err and the error type into JSON as specified in + // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-initerror + // If an error occurs during reporting the init error, panic. + if let Err(err) = self.initialization_error(Some("Runtime.InitError"), None) { + panic!( + "Failed to report initialization error. Error: {}, AWS Error: {}", + &init_err, err + ); + }; + + // After reporting an init error just panic. + panic!("Initialization Error: {}", &init_err); + } + self.handler = init_result.ok(); // Start event processing loop as specified in [https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html] loop { - // Get the next event in the queue. - // Failing to get the next event will either panic (on server error) or continue (on client-error codes). - let next: Result = self.next_invocation(); - if next.is_err() { + // Get the next event in the queue and update the context if successful. + // Failing to get the next event will either panic (on server error) or continue with an error (on client-error codes). + let next_invo = match self.next_invocation() { // TODO - perhaps log the error - continue; - } - let next_resp = next.as_ref().unwrap(); - let request_id = match next_resp.aws_request_id() { - Some(rid) => rid, - None => { - // TODO - figure out what we'd like to do with the result returned from success/client-err api responses - let _ = self.initialization_error(Some("Runtime.MissingRequestId"), None); - continue; - } + Err(_e) => continue, + Ok(resp) => resp, }; - // Create the context object for the lambda execution - // TODO - Design a way to pass a generic type implementing LambdaContext and use it to construct the context - let context = RefLambdaContext { - env_vars: &self.env_vars, - invo_resp: next_resp, - }; // Retrieve the event JSON - // TODO - deserialize? Currently user code should deserialize inside their handler - let event = next_resp.event_response(); + // The response body is safe to unwrap at this point. + let event = next_invo.get_body().unwrap(); // Execute the event handler - let lambda_output = lambda(event, context); + // TODO - pass the event an an owned String + let lambda_output = self + .handler + .as_mut() + .unwrap() + .on_event(&event, &self.context); + let request_id = self.context.get_aws_request_id().unwrap(); // TODO - figure out what we'd like to do with the result returned from success/client-err api responses (e.g: log, run a user defined callback...) let _ = match lambda_output { Ok(out) => self.invocation_response(request_id, &out), - // TODO - pass an ErrorRequest json + // TODO - pass an ErrorRequest json - https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-invokeerror Err(err) => { let _err = format!("{}", &err); self.invocation_error(request_id, Some(&_err), Some(&_err)) @@ -230,26 +146,42 @@ where } } - fn next_invocation(&mut self) -> Result { + fn next_invocation(&mut self) -> Result<::Response, Error> { + // TODO - cache this string let url = format!( "http://{}/{}/runtime/invocation/next", self.api_base, self.version ); let resp = self.transport.get(&url, None, None)?; - handle_response!(resp); - // If AWS returns the "Lambda-Runtime-Trace-Id" header, set its value to the - - // "_X_AMZN_TRACE_ID" env var - if let Some(req_id) = resp.trace_id() { - set_var(OsStr::new("_X_AMZN_TRACE_ID"), OsStr::new(req_id)); - self.env_vars.set_trace_id(Some(req_id)); - }; - + // Update the request context + self.context.set_aws_request_id(resp.get_aws_request_id()); + self.context.set_client_context(resp.get_client_context()); + self.context + .set_cognito_identity(resp.get_cognito_identity()); + self.context + .set_deadline(resp.get_deadline().map(Duration::from_millis)); + self.context + .set_invoked_function_arn(resp.get_invoked_function_arn()); + self.context + .set_x_ray_tracing_id(resp.get_x_ray_tracing_id()); + + // Vaidate that request id is present in the response. If not report to Lambda. + if self.context.get_aws_request_id().is_none() { + // TODO - figure out what we'd like to do with the result returned from success/client-err api responses + let _ = self.initialization_error(Some("Runtime.MissingRequestId"), None); + // TODO - return None - requires modifying the function signature + return Err(Error::empty()); + } Ok(resp) } - fn invocation_response(&self, request_id: &str, response: &OUT) -> Result { + fn invocation_response( + &self, + request_id: &str, + response: &::EventOutput, + ) -> Result<::Response, Error> { let url = format!( "http://{}/{}/runtime/invocation/{}/response", self.api_base, self.version, request_id @@ -265,7 +197,6 @@ where } }; let resp = self.transport.post(&url, Some(&serialized), None)?; - handle_response!(resp); Ok(resp) @@ -275,15 +206,13 @@ where &self, error_type: Option<&str>, error_req: Option<&str>, - ) -> Result { + ) -> Result<::Response, Error> { let url = format!( "http://{}/{}/runtime/init/error", self.api_base, self.version ); let headers = error_type.map(|et| (vec![AWS_FUNC_ERR_TYPE], vec![et])); - let resp = self.transport.post(&url, error_req, headers)?; - handle_response!(resp); Ok(resp) @@ -294,15 +223,13 @@ where request_id: &str, error_type: Option<&str>, error_req: Option<&str>, - ) -> Result { + ) -> Result<::Response, Error> { let url = format!( "http://{}/{}/runtime/invocation/{}/error", self.api_base, self.version, request_id ); let headers = error_type.map(|et| (vec![AWS_FUNC_ERR_TYPE], vec![et])); - let resp = self.transport.post(&url, error_req, headers)?; - handle_response!(resp); Ok(resp)