autumn_plugin_aws_lambda/
runtime.rs1use std::time::Duration;
4
5use crate::PluginError;
6use crate::proxy::LambdaProxy;
7use crate::timing::{as_ms, next_delay_ms};
8
9const READY_INITIAL: Duration = Duration::from_millis(5);
11const READY_MAX: Duration = Duration::from_millis(100);
13const THREAD_NAME: &str = "aws-lambda-runtime";
15const WATCH_INTERVAL: Duration = Duration::from_secs(1);
17const WATCH_LIMIT: u32 = 5;
19
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum ResponseMode {
24 #[default]
27 Buffered,
28 Streaming,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub(crate) enum Startup {
36 Pending,
38 Complete,
40 Stopping,
42}
43
44#[derive(Debug)]
46pub(crate) struct LoopSettings {
47 pub proxy: LambdaProxy,
48 pub mode: ResponseMode,
49 pub readiness_timeout: Option<Duration>,
51 pub concurrency: u32,
53 pub runtime_api: String,
55}
56
57pub async fn wait_until(
63 ready: impl Fn() -> bool,
64 timeout: Duration,
65 initial: Duration,
66 max: Duration,
67) -> bool {
68 let timeout_ms = as_ms(timeout);
69 let mut delay_ms = as_ms(initial).max(1);
70 let max_ms = as_ms(max).max(delay_ms);
71 let start = tokio::time::Instant::now();
72 loop {
73 if ready() {
74 return true;
75 }
76 let slept_ms = as_ms(start.elapsed());
77 if slept_ms >= timeout_ms {
78 return false;
79 }
80 let step = delay_ms.min(timeout_ms - slept_ms);
81 tokio::time::sleep(Duration::from_millis(step)).await;
82 delay_ms = next_delay_ms(delay_ms, max_ms);
83 }
84}
85
86pub(crate) fn spawn_loop(
101 settings: LoopSettings,
102 startup: impl Fn() -> Startup + Send + 'static,
103) -> Result<(), PluginError> {
104 std::thread::Builder::new()
105 .name(THREAD_NAME.to_owned())
106 .spawn(move || {
107 let runtime = match build_runtime(settings.concurrency) {
108 Ok(runtime) => runtime,
109 Err(error) => {
110 tracing::error!(%error, "cannot build the Lambda runtime; exiting");
111 std::process::exit(1);
112 }
113 };
114 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
115 runtime.block_on(run_when_ready(settings, startup))
116 }));
117 if matches!(outcome, Ok(None)) {
118 return;
119 }
120 tracing::error!(?outcome, "AWS Lambda runtime loop stopped; exiting");
121 std::process::exit(1);
122 })
123 .map_err(PluginError::Runtime)?;
124 Ok(())
125}
126
127#[allow(
130 clippy::future_not_send,
131 reason = "the lambda_http loop is not Send; it runs on its own thread"
132)]
133async fn run_when_ready(
134 settings: LoopSettings,
135 startup: impl Fn() -> Startup,
136) -> Option<Result<(), lambda_http::Error>> {
137 let limit = settings.readiness_timeout.unwrap_or(Duration::MAX);
138 wait_until(
139 || startup() != Startup::Pending,
140 limit,
141 READY_INITIAL,
142 READY_MAX,
143 )
144 .await;
145 match startup() {
146 Startup::Stopping => {
147 tracing::info!("Autumn shuts down; the Lambda loop does not start");
148 return None;
149 }
150 Startup::Pending => tracing::warn!(
151 ?limit,
152 "Autumn startup is not complete; the Lambda loop starts now"
153 ),
154 Startup::Complete => {}
155 }
156 if settings.concurrency > 1 {
157 tokio::spawn(watch_runtime_api(settings.runtime_api));
160 }
161 tracing::info!(mode = ?settings.mode, concurrency = settings.concurrency, "AWS Lambda runtime loop runs");
162 Some(run(settings.proxy, settings.mode).await)
163}
164
165fn build_runtime(concurrency: u32) -> std::io::Result<tokio::runtime::Runtime> {
167 if concurrency <= 1 {
168 return tokio::runtime::Builder::new_current_thread()
169 .enable_all()
170 .build();
171 }
172 let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
173 let wanted = usize::try_from(concurrency).unwrap_or(usize::MAX);
174 tokio::runtime::Builder::new_multi_thread()
175 .worker_threads(cores.min(wanted))
176 .enable_all()
177 .build()
178}
179
180async fn watch_runtime_api(address: String) {
183 let mut failures = 0;
184 loop {
185 tokio::time::sleep(WATCH_INTERVAL).await;
186 let connect = tokio::net::TcpStream::connect(address.as_str());
187 match tokio::time::timeout(WATCH_INTERVAL, connect).await {
188 Ok(Ok(_)) => failures = 0,
189 _ => failures += 1,
190 }
191 if failures >= WATCH_LIMIT {
192 tracing::error!(%address, "AWS Lambda Runtime API is not reachable; exiting");
193 std::process::exit(1);
194 }
195 }
196}
197
198#[allow(
203 clippy::future_not_send,
204 reason = "the lambda_http loop is not Send; it runs on its own thread"
205)]
206async fn run(proxy: LambdaProxy, mode: ResponseMode) -> Result<(), lambda_http::Error> {
207 let proxy = proxy.response_mode(mode);
208 match mode {
209 ResponseMode::Streaming => lambda_http::run_with_streaming_response_concurrent(proxy).await,
210 _ => lambda_http::run_concurrent(proxy).await,
211 }
212}