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

support async init for long initialization lambda functions #53

Merged
merged 5 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ lambda-extension = "0.6.0"
lambda_http = "0.6.0"
log = "0.4.14"
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json"] }
tokio = { version = "1.20.0", features = ["macros", "io-util", "sync", "rt-multi-thread"] }
tokio = { version = "1.20.0", features = ["macros", "io-util", "sync", "rt-multi-thread", "time"] }
tokio-retry = "0.3"

[[bin]]
Expand Down
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,20 @@ After passing readiness check, Lambda Web Adapter will start Lambda Runtime and

The readiness check port/path and traffic port can be configured using environment variables. These environment variables can be defined either within docker file or as Lambda function configuration.

|Environment Variable| Description | Default |
|--------------------|------------------------------------------------------------|---------|
|PORT | traffic port | "8080" |
|READINESS_CHECK_PORT| readiness check port, default to the fraffic port | PORT |
|READINESS_CHECK_PATH| readiness check path | "/" |
|REMOVE_BASE_PATH | (optional) the base path to be removed from request path | None |
| Environment Variable | Description | Default |
|----------------------|----------------------------------------------------------------------|---------|
| PORT | traffic port | "8080" |
| READINESS_CHECK_PORT | readiness check port, default to the traffic port | PORT |
| READINESS_CHECK_PATH | readiness check path | "/" |
| ASYNC_INIT | enable asynchronous initialization for long initialization functions | "false" |
| REMOVE_BASE_PATH | (optional) the base path to be removed from request path | None |

**ASYNC_INIT** Lambda managed runtimes offer up to 10 seconds for function initialization. During this period of time, Lambda functions have burst of CPU to accelerate initialization, and it is free.
If a lambda function couldn't complete the initialization within 10 seconds, Lambda will restart the function, and bill for the initialization.
To help functions to use this 10 seconds free initialization time and avoid the restart, Lambda Web Adapter supports asynchronous initialization.
When this feature is enabled, Lambda Web Adapter performs readiness check up to 9.8 seconds. If the web app is not ready by then,
Lambda Web Adapter signals to Lambda service that the init is completed, and continues readiness check in the handler.
This feature is disabled by default. Enable it by setting environment variable `ASYNC_INIT` to `true`.

**REMOVE_BASE_PATH** - The value of this environment variable tells the adapter whether the application is running under a base path.
For example, you could have configured your API Gateway to have a /orders/{proxy+} and a /catalog/{proxy+} resource.
Expand Down
50 changes: 36 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use log::*;
use reqwest::{redirect, Client};
use std::time::Duration;
use std::{env, future, mem};
use tokio::time::timeout;
use tokio_retry::{strategy::FixedInterval, Retry};

type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
Expand All @@ -17,6 +18,7 @@ struct AdapterOptions {
readiness_check_port: String,
readiness_check_path: String,
base_path: Option<String>,
async_init: bool,
}

#[tokio::main]
Expand All @@ -31,6 +33,10 @@ async fn main() -> Result<(), Error> {
.unwrap_or_else(|_| env::var("PORT").unwrap_or_else(|_| "8080".to_string())),
readiness_check_path: env::var("READINESS_CHECK_PATH").unwrap_or_else(|_| "/".to_string()),
base_path: env::var("REMOVE_BASE_PATH").ok(),
async_init: env::var("ASYNC_INIT")
.unwrap_or_else(|_| "false".to_string())
.parse()
.unwrap_or(false),
};

// register as an external extension
Expand All @@ -43,19 +49,14 @@ async fn main() -> Result<(), Error> {
.expect("extension thread error");
});

// check if the application is ready every 10 milliseconds
Retry::spawn(FixedInterval::from_millis(10), || {
let readiness_check_url = format!(
"http://{}:{}{}",
options.host, options.readiness_check_port, options.readiness_check_path
);
match reqwest::blocking::get(readiness_check_url) {
Ok(response) if { response.status().is_success() } => future::ready(Ok(())),
_ => future::ready(Err::<(), i32>(-1)),
}
})
.await
.expect("application server is not ready");
// check if the application is ready
let is_ready = if options.async_init {
timeout(Duration::from_secs_f32(9.8), check_readiness(options))
.await
.is_ok()
} else {
check_readiness(options).await
};

// start lambda runtime
let http_client = &Client::builder()
Expand All @@ -64,17 +65,38 @@ async fn main() -> Result<(), Error> {
.build()
.unwrap();
lambda_http::run(http_handler(|event: Request| async move {
http_proxy_handler(event, http_client, options).await
http_proxy_handler(event, http_client, options, is_ready).await
}))
.await?;
Ok(())
}

async fn check_readiness(options: &AdapterOptions) -> bool {
Retry::spawn(FixedInterval::from_millis(10), || {
let readiness_check_url = format!(
"http://{}:{}{}",
options.host, options.readiness_check_port, options.readiness_check_path
);
match reqwest::blocking::get(readiness_check_url) {
Ok(response) if { response.status().is_success() } => future::ready(Ok(())),
_ => future::ready(Err::<(), i32>(-1)),
}
})
.await
.is_ok()
}

async fn http_proxy_handler(
event: Request,
http_client: &Client,
options: &AdapterOptions,
is_app_ready: bool,
) -> Result<Response<Body>, Error> {
// continue checking readiness if async_init is configured and the app is not ready
if options.async_init && !is_app_ready {
check_readiness(options).await;
Copy link
Contributor

Choose a reason for hiding this comment

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

this is going to add an extra request to the server in every call in this case, even after the server is ready, isn't it? For example, if the same micro-vm serves two requests, one after the other, both of them check if the server is ready, even when we already know that it's ready after the first one.

It might be better to implement a dedicated Tower service as a handler that can keep that state, so any requests after the first one don't need to check if the server is ready.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. I have a simple fix for it.

}

let raw_path = event.raw_http_path();
let (parts, body) = event.into_parts();

Expand Down