use std::net::SocketAddr;
use std::time::Duration;
use autumn_web::AppState;
use autumn_web::app::AppBuilder;
use autumn_web::config::ServerConfig;
use autumn_web::plugin::Plugin;
use crate::activation::{RUNTIME_API_ENV, lambda_concurrency, missing_lambda_env};
use crate::proxy::LambdaProxy;
use crate::runtime::{LoopSettings, Startup, spawn_loop};
use crate::upstream::Upstream;
use crate::{Activation, PluginError, ResponseMode};
const TASK_ENV: &str = "AUTUMN_RUN_TASK";
#[derive(Debug, Clone)]
pub struct AwsLambdaPlugin {
activation: Activation,
response_mode: ResponseMode,
upstream: Option<SocketAddr>,
readiness_timeout: Option<Duration>,
timeout_margin: Duration,
}
impl Default for AwsLambdaPlugin {
fn default() -> Self {
Self::new()
}
}
impl AwsLambdaPlugin {
pub const DEFAULT_TIMEOUT_MARGIN: Duration = Duration::from_millis(100);
#[must_use]
pub const fn new() -> Self {
Self {
activation: Activation::Auto,
response_mode: ResponseMode::Buffered,
upstream: None,
readiness_timeout: None,
timeout_margin: Self::DEFAULT_TIMEOUT_MARGIN,
}
}
#[must_use]
pub const fn activation(mut self, activation: Activation) -> Self {
self.activation = activation;
self
}
#[must_use]
pub const fn response_mode(mut self, mode: ResponseMode) -> Self {
self.response_mode = mode;
self
}
#[must_use]
pub const fn upstream(mut self, addr: SocketAddr) -> Self {
self.upstream = Some(addr);
self
}
#[must_use]
pub const fn readiness_timeout(mut self, timeout: Duration) -> Self {
self.readiness_timeout = Some(timeout);
self
}
#[must_use]
pub const fn timeout_margin(mut self, margin: Duration) -> Self {
self.timeout_margin = margin;
self
}
pub(crate) fn plan(
&self,
server: &ServerConfig,
env: impl Fn(&str) -> Option<String>,
) -> Result<Option<Upstream>, PluginError> {
if !self.activation.is_active(env(RUNTIME_API_ENV).is_some()) {
return Ok(None);
}
if env(TASK_ENV).is_some_and(|task| !task.trim().is_empty()) {
return Ok(None);
}
let names = missing_lambda_env(env);
if !names.is_empty() {
return Err(PluginError::MissingEnv { names });
}
let upstream = match self.upstream {
Some(addr) => Upstream::new(addr),
None => Upstream::from_server_config(server)?,
};
Ok(Some(upstream))
}
fn start(&self, state: &AppState) -> Result<(), PluginError> {
let env = |name: &str| std::env::var(name).ok();
let server = &state.config_arc().server;
let Some(upstream) = self.plan(server, env)? else {
tracing::debug!("AWS Lambda is not active; the plugin is idle");
return Ok(());
};
let slow = slow_shutdown_settings(server);
if !slow.is_empty() {
tracing::warn!(
settings = ?slow,
"Lambda gives little time at shutdown; set these to 0 or 1"
);
}
tracing::info!(%upstream, "AWS Lambda detected; the plugin sends events to Autumn");
let settings = LoopSettings {
proxy: LambdaProxy::new(upstream).timeout_margin(self.timeout_margin),
mode: self.response_mode,
readiness_timeout: self.readiness_timeout,
concurrency: lambda_concurrency(env),
runtime_api: env(RUNTIME_API_ENV).unwrap_or_default(),
};
let state = state.clone();
spawn_loop(settings, move || {
let probes = state.probes();
if probes.is_shutting_down() {
Startup::Stopping
} else if probes.is_startup_complete() {
Startup::Complete
} else {
Startup::Pending
}
})
}
}
#[must_use]
pub fn slow_shutdown_settings(server: &ServerConfig) -> Vec<&'static str> {
let mut slow = Vec::new();
if server.prestop_grace_secs > 0 {
slow.push("server.prestop_grace_secs");
}
if server.shutdown_timeout_secs > 1 {
slow.push("server.shutdown_timeout_secs");
}
slow
}
impl Plugin for AwsLambdaPlugin {
fn build(self, app: AppBuilder) -> AppBuilder {
app.on_startup(move |state| {
let result = self.start(&state).map_err(Into::into);
async move { result }
})
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::PluginError;
fn env(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
let map: HashMap<String, String> = pairs
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect();
move |name: &str| map.get(name).cloned()
}
const LAMBDA_ENV: [(&str, &str); 4] = [
("AWS_LAMBDA_RUNTIME_API", "127.0.0.1:9001"),
("AWS_LAMBDA_FUNCTION_NAME", "f"),
("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128"),
("AWS_LAMBDA_FUNCTION_VERSION", "$LATEST"),
];
#[test]
fn plugin_registers_once() {
let app = autumn_web::app()
.plugin(AwsLambdaPlugin::new())
.plugin(AwsLambdaPlugin::new());
assert!(app.has_plugin(std::any::type_name::<AwsLambdaPlugin>()));
}
#[test]
fn auto_is_idle_outside_lambda() {
let plan = AwsLambdaPlugin::new().plan(&ServerConfig::default(), env(&[]));
assert!(matches!(plan, Ok(None)));
}
#[test]
fn never_is_idle_on_lambda() {
let plan = AwsLambdaPlugin::new()
.activation(Activation::Never)
.plan(&ServerConfig::default(), env(&LAMBDA_ENV));
assert!(matches!(plan, Ok(None)));
}
#[test]
fn always_fails_outside_lambda() {
let plan = AwsLambdaPlugin::new()
.activation(Activation::Always)
.plan(&ServerConfig::default(), env(&[]));
assert!(matches!(plan, Err(PluginError::MissingEnv { names, .. }) if names.len() == 4));
}
#[test]
fn auto_fails_on_partial_lambda_env() {
let plan = AwsLambdaPlugin::new().plan(&ServerConfig::default(), env(&LAMBDA_ENV[..1]));
assert!(matches!(plan, Err(PluginError::MissingEnv { names, .. }) if names.len() == 3));
}
#[test]
fn auto_on_lambda_uses_server_config() {
let plan = AwsLambdaPlugin::new()
.plan(&ServerConfig::default(), env(&LAMBDA_ENV))
.expect("valid plan")
.expect("active");
assert_eq!(plan.to_string(), "http://127.0.0.1:3000");
}
#[test]
fn upstream_override_wins() {
let addr: SocketAddr = "0.0.0.0:8080".parse().expect("addr");
let plan = AwsLambdaPlugin::new()
.upstream(addr)
.plan(&ServerConfig::default(), env(&LAMBDA_ENV))
.expect("valid plan")
.expect("active");
assert_eq!(plan.to_string(), "http://127.0.0.1:8080");
}
#[test]
fn task_mode_is_idle_on_lambda() {
let mut pairs = LAMBDA_ENV.to_vec();
pairs.push(("AUTUMN_RUN_TASK", "backfill"));
let plan = AwsLambdaPlugin::new().plan(&ServerConfig::default(), env(&pairs));
assert!(matches!(plan, Ok(None)));
}
#[test]
fn blank_task_name_is_not_task_mode() {
let mut pairs = LAMBDA_ENV.to_vec();
pairs.push(("AUTUMN_RUN_TASK", " "));
let plan = AwsLambdaPlugin::new().plan(&ServerConfig::default(), env(&pairs));
assert!(matches!(plan, Ok(Some(_))));
}
#[test]
fn always_starts_on_lambda() {
let plan = AwsLambdaPlugin::new()
.activation(Activation::Always)
.plan(&ServerConfig::default(), env(&LAMBDA_ENV));
assert!(matches!(plan, Ok(Some(_))));
}
#[test]
fn upstream_override_skips_server_checks() {
let server = ServerConfig {
port: 0,
unix_socket: Some("/tmp/app.sock".to_owned()),
..ServerConfig::default()
};
let addr: SocketAddr = "127.0.0.1:9000".parse().expect("addr");
let plan = AwsLambdaPlugin::new()
.upstream(addr)
.plan(&server, env(&LAMBDA_ENV));
assert!(matches!(plan, Ok(Some(up)) if up.addr() == addr));
}
#[test]
fn server_errors_stop_the_plan() {
let server = ServerConfig {
port: 0,
..ServerConfig::default()
};
let plan = AwsLambdaPlugin::new().plan(&server, env(&LAMBDA_ENV));
assert!(matches!(plan, Err(PluginError::ZeroPort)));
}
#[test]
fn default_equals_new() {
let a = AwsLambdaPlugin::default();
assert_eq!(a.readiness_timeout, None);
assert_eq!(a.timeout_margin, AwsLambdaPlugin::DEFAULT_TIMEOUT_MARGIN);
assert_eq!(a.activation, Activation::Auto);
assert_eq!(a.response_mode, ResponseMode::Buffered);
assert!(a.upstream.is_none());
}
}