lambda_simulator/simulator.rs
1//! Main simulator orchestration and builder.
2
3use crate::error::{SimulatorError, SimulatorResult};
4use crate::extension::{
5 ExtensionState, LifecycleEvent, RegisteredExtension, ShutdownReason, TracingInfo,
6};
7use crate::extension_readiness::ExtensionReadinessTracker;
8use crate::extensions_api::{ExtensionsApiState, create_extensions_api_router};
9use crate::freeze::{FreezeMode, FreezeState};
10use crate::invocation::Invocation;
11use crate::invocation::InvocationStatus;
12use crate::runtime_api::{RuntimeApiState, create_runtime_api_router};
13use crate::state::{InvocationState, RuntimeState};
14use crate::telemetry::{
15 InitializationType, Phase, PlatformInitStart, TelemetryEvent, TelemetryEventType,
16};
17use crate::telemetry_api::{TelemetryApiState, create_telemetry_api_router};
18use crate::telemetry_state::TelemetryState;
19use chrono::Utc;
20use serde_json::{Value, json};
21use std::collections::HashMap;
22use std::net::SocketAddr;
23use std::sync::Arc;
24use std::time::Duration;
25use tokio::net::TcpListener;
26use tokio::task::JoinHandle;
27
28/// The lifecycle phase of the simulator.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum SimulatorPhase {
31 /// Extensions can register, waiting for runtime to initialize.
32 Initializing,
33 /// Runtime is initialized, ready for invocations.
34 Ready,
35 /// Shutting down.
36 ShuttingDown,
37}
38
39/// Configuration for the Lambda runtime simulator.
40///
41/// # Timeout Limitations
42///
43/// Timeout values are used for calculating invocation deadlines
44/// (the `Lambda-Runtime-Deadline-Ms` header). Actual timeout enforcement
45/// is not yet implemented - invocations will not be automatically
46/// terminated when they exceed their configured timeout.
47#[derive(Debug, Clone)]
48pub struct SimulatorConfig {
49 /// Default timeout for invocations in milliseconds.
50 ///
51 /// Used for deadline calculation in the `Lambda-Runtime-Deadline-Ms` header.
52 /// Timeout enforcement is not currently implemented.
53 pub invocation_timeout_ms: u64,
54
55 /// Timeout for initialisation phase in milliseconds.
56 ///
57 /// Not currently enforced.
58 pub init_timeout_ms: u64,
59
60 /// Lambda function name, used in telemetry events and environment variables.
61 pub function_name: String,
62
63 /// Lambda function version (e.g., "$LATEST" or a published version number).
64 pub function_version: String,
65
66 /// Function memory allocation in MB, used in telemetry metrics.
67 pub memory_size_mb: u32,
68
69 /// CloudWatch Logs group name for the function.
70 pub log_group_name: String,
71
72 /// CloudWatch Logs stream name for this execution environment.
73 pub log_stream_name: String,
74
75 /// Timeout for waiting for extensions to be ready in milliseconds.
76 ///
77 /// After the runtime completes an invocation, this is the maximum time
78 /// to wait for all extensions to signal readiness before proceeding
79 /// with platform.report emission and process freezing.
80 pub extension_ready_timeout_ms: u64,
81
82 /// Timeout for graceful shutdown in milliseconds.
83 ///
84 /// During graceful shutdown, this is the maximum time to wait for
85 /// extensions subscribed to SHUTDOWN events to complete their cleanup
86 /// work by polling `/next` again.
87 pub shutdown_timeout_ms: u64,
88
89 /// Unique instance identifier for this simulator run.
90 ///
91 /// Used in telemetry events to identify the Lambda execution environment.
92 pub instance_id: String,
93
94 /// Function handler name (e.g., "index.handler").
95 ///
96 /// Used in extension registration responses.
97 pub handler: Option<String>,
98
99 /// AWS account ID.
100 ///
101 /// Used in extension registration responses and ARN construction.
102 pub account_id: Option<String>,
103
104 /// AWS region.
105 ///
106 /// Used for environment variables and ARN construction.
107 /// Defaults to "us-east-1".
108 pub region: String,
109}
110
111impl Default for SimulatorConfig {
112 fn default() -> Self {
113 Self {
114 invocation_timeout_ms: 3000,
115 init_timeout_ms: 10000,
116 function_name: "test-function".to_string(),
117 function_version: "$LATEST".to_string(),
118 memory_size_mb: 128,
119 log_group_name: "/aws/lambda/test-function".to_string(),
120 log_stream_name: "2024/01/01/[$LATEST]test".to_string(),
121 extension_ready_timeout_ms: 2000,
122 shutdown_timeout_ms: 2000,
123 instance_id: uuid::Uuid::new_v4().to_string(),
124 handler: None,
125 account_id: None,
126 region: "us-east-1".to_string(),
127 }
128 }
129}
130
131/// Builder for creating a Lambda runtime simulator.
132///
133/// # Examples
134///
135/// ```no_run
136/// use lambda_simulator::Simulator;
137/// use std::time::Duration;
138///
139/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
140/// let simulator = Simulator::builder()
141/// .invocation_timeout(Duration::from_secs(30))
142/// .function_name("my-function")
143/// .build()
144/// .await?;
145/// # Ok(())
146/// # }
147/// ```
148#[derive(Debug, Default)]
149#[must_use = "builders do nothing unless .build() is called"]
150pub struct SimulatorBuilder {
151 config: SimulatorConfig,
152 port: Option<u16>,
153 freeze_mode: FreezeMode,
154 runtime_pid: Option<u32>,
155 extension_pids: Vec<u32>,
156}
157
158impl SimulatorBuilder {
159 /// Creates a new simulator builder with default configuration.
160 pub fn new() -> Self {
161 Self::default()
162 }
163
164 /// Sets the invocation timeout.
165 ///
166 /// This timeout is used to calculate the `Lambda-Runtime-Deadline-Ms` header
167 /// sent to the runtime. However, timeout enforcement is not yet implemented
168 /// in Phase 1 - invocations will not be automatically terminated.
169 pub fn invocation_timeout(mut self, timeout: Duration) -> Self {
170 self.config.invocation_timeout_ms = timeout.as_millis() as u64;
171 self
172 }
173
174 /// Sets the initialization timeout.
175 ///
176 /// **Note:** Timeout enforcement is not yet implemented in Phase 1.
177 pub fn init_timeout(mut self, timeout: Duration) -> Self {
178 self.config.init_timeout_ms = timeout.as_millis() as u64;
179 self
180 }
181
182 /// Sets the function name.
183 pub fn function_name(mut self, name: impl Into<String>) -> Self {
184 self.config.function_name = name.into();
185 self
186 }
187
188 /// Sets the function version.
189 pub fn function_version(mut self, version: impl Into<String>) -> Self {
190 self.config.function_version = version.into();
191 self
192 }
193
194 /// Sets the function memory size in MB.
195 pub fn memory_size_mb(mut self, memory: u32) -> Self {
196 self.config.memory_size_mb = memory;
197 self
198 }
199
200 /// Sets the function handler name.
201 ///
202 /// This is used in extension registration responses and the `_HANDLER`
203 /// environment variable.
204 pub fn handler(mut self, handler: impl Into<String>) -> Self {
205 self.config.handler = Some(handler.into());
206 self
207 }
208
209 /// Sets the AWS region.
210 ///
211 /// This is used for `AWS_REGION` and `AWS_DEFAULT_REGION` environment
212 /// variables, as well as ARN construction.
213 ///
214 /// Default: "us-east-1"
215 pub fn region(mut self, region: impl Into<String>) -> Self {
216 self.config.region = region.into();
217 self
218 }
219
220 /// Sets the AWS account ID.
221 ///
222 /// This is used in extension registration responses and ARN construction.
223 pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
224 self.config.account_id = Some(account_id.into());
225 self
226 }
227
228 /// Sets the port to bind to. If not specified, a random available port will be used.
229 pub fn port(mut self, port: u16) -> Self {
230 self.port = Some(port);
231 self
232 }
233
234 /// Sets the timeout for waiting for extensions to be ready.
235 ///
236 /// After the runtime completes an invocation, the simulator will wait up to
237 /// this duration for all extensions to signal readiness by polling `/next`.
238 /// If the timeout expires, the simulator proceeds anyway.
239 ///
240 /// Default: 2 seconds
241 pub fn extension_ready_timeout(mut self, timeout: Duration) -> Self {
242 self.config.extension_ready_timeout_ms = timeout.as_millis() as u64;
243 self
244 }
245
246 /// Sets the timeout for graceful shutdown.
247 ///
248 /// During graceful shutdown, extensions subscribed to SHUTDOWN events have
249 /// this amount of time to complete their cleanup work and signal readiness.
250 /// If the timeout expires, shutdown proceeds anyway.
251 ///
252 /// Default: 2 seconds
253 pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
254 self.config.shutdown_timeout_ms = timeout.as_millis() as u64;
255 self
256 }
257
258 /// Sets the freeze mode for process freezing simulation.
259 ///
260 /// When set to `FreezeMode::Process`, the simulator will send SIGSTOP/SIGCONT
261 /// signals to simulate Lambda's freeze/thaw behaviour. This requires a runtime
262 /// PID to be configured via `runtime_pid()`.
263 ///
264 /// # Platform Support
265 ///
266 /// Process freezing is only supported on Unix platforms. On other platforms,
267 /// `FreezeMode::Process` will fail at build time.
268 ///
269 /// # Examples
270 ///
271 /// ```no_run
272 /// use lambda_simulator::{Simulator, FreezeMode};
273 ///
274 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
275 /// let simulator = Simulator::builder()
276 /// .freeze_mode(FreezeMode::Process)
277 /// .runtime_pid(12345)
278 /// .build()
279 /// .await?;
280 /// # Ok(())
281 /// # }
282 /// ```
283 pub fn freeze_mode(mut self, mode: FreezeMode) -> Self {
284 self.freeze_mode = mode;
285 self
286 }
287
288 /// Sets the PID of the runtime process for freeze/thaw simulation.
289 ///
290 /// This is required when using `FreezeMode::Process`. The simulator will
291 /// send SIGSTOP to this process after each invocation response is fully
292 /// sent, and SIGCONT when a new invocation is enqueued.
293 ///
294 /// # Examples
295 ///
296 /// ```no_run
297 /// use lambda_simulator::{Simulator, FreezeMode};
298 /// use std::process;
299 ///
300 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
301 /// // Use current process PID (for testing)
302 /// let simulator = Simulator::builder()
303 /// .freeze_mode(FreezeMode::Process)
304 /// .runtime_pid(process::id())
305 /// .build()
306 /// .await?;
307 /// # Ok(())
308 /// # }
309 /// ```
310 pub fn runtime_pid(mut self, pid: u32) -> Self {
311 self.runtime_pid = Some(pid);
312 self
313 }
314
315 /// Sets the PIDs of extension processes for freeze/thaw simulation.
316 ///
317 /// In real AWS Lambda, the entire execution environment is frozen between
318 /// invocations, including all extension processes. Use this method to
319 /// register extension PIDs that should be frozen along with the runtime.
320 ///
321 /// # Examples
322 ///
323 /// ```no_run
324 /// use lambda_simulator::{Simulator, FreezeMode};
325 ///
326 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
327 /// let simulator = Simulator::builder()
328 /// .freeze_mode(FreezeMode::Process)
329 /// .runtime_pid(12345)
330 /// .extension_pids(vec![12346, 12347])
331 /// .build()
332 /// .await?;
333 /// # Ok(())
334 /// # }
335 /// ```
336 pub fn extension_pids(mut self, pids: Vec<u32>) -> Self {
337 self.extension_pids = pids;
338 self
339 }
340
341 /// Builds and starts the simulator.
342 ///
343 /// # Returns
344 ///
345 /// A running simulator instance.
346 ///
347 /// # Errors
348 ///
349 /// Returns an error if:
350 /// - The server fails to start or bind to the specified address.
351 /// - `FreezeMode::Process` is used on a non-Unix platform.
352 pub async fn build(self) -> SimulatorResult<Simulator> {
353 // Note: FreezeMode::Process no longer requires runtime_pid at build time.
354 // PIDs can be registered dynamically using register_freeze_pid() after
355 // spawning processes. This supports the realistic workflow where the
356 // simulator must be running before processes can connect to it.
357
358 #[cfg(not(unix))]
359 if self.freeze_mode == FreezeMode::Process {
360 return Err(SimulatorError::InvalidConfiguration(
361 "FreezeMode::Process is only supported on Unix platforms".to_string(),
362 ));
363 }
364
365 let all_pids: Vec<u32> = self
366 .runtime_pid
367 .into_iter()
368 .chain(self.extension_pids)
369 .collect();
370 let freeze_state = Arc::new(FreezeState::with_pids(self.freeze_mode, all_pids));
371 let runtime_state = Arc::new(RuntimeState::new());
372 let extension_state = Arc::new(ExtensionState::new());
373 let telemetry_state = Arc::new(TelemetryState::new());
374 let readiness_tracker = Arc::new(ExtensionReadinessTracker::new());
375 let config = Arc::new(self.config);
376
377 let runtime_api_state = RuntimeApiState {
378 runtime: runtime_state.clone(),
379 telemetry: telemetry_state.clone(),
380 freeze: freeze_state.clone(),
381 readiness: readiness_tracker.clone(),
382 config: config.clone(),
383 };
384 let runtime_router = create_runtime_api_router(runtime_api_state);
385
386 let extensions_api_state = ExtensionsApiState {
387 extensions: extension_state.clone(),
388 readiness: readiness_tracker.clone(),
389 config: config.clone(),
390 runtime: runtime_state.clone(),
391 };
392 let extensions_router = create_extensions_api_router(extensions_api_state);
393
394 let telemetry_api_state = TelemetryApiState {
395 telemetry: telemetry_state.clone(),
396 };
397 let telemetry_router = create_telemetry_api_router(telemetry_api_state);
398
399 let combined_router = runtime_router
400 .merge(extensions_router)
401 .merge(telemetry_router)
402 .fallback(|req: axum::extract::Request| async move {
403 tracing::warn!(
404 method = %req.method(),
405 uri = %req.uri(),
406 "Unhandled request"
407 );
408 axum::http::StatusCode::NOT_FOUND
409 });
410
411 let addr: SocketAddr = if let Some(port) = self.port {
412 ([127, 0, 0, 1], port).into()
413 } else {
414 ([127, 0, 0, 1], 0).into()
415 };
416
417 let listener = TcpListener::bind(addr)
418 .await
419 .map_err(|e| SimulatorError::BindError(e.to_string()))?;
420
421 let local_addr = listener
422 .local_addr()
423 .map_err(|e| SimulatorError::ServerStart(e.to_string()))?;
424
425 let server_handle = tokio::spawn(async move {
426 axum::serve(listener, combined_router)
427 .await
428 .map_err(|e| SimulatorError::ServerStart(e.to_string()))
429 });
430
431 // Emit platform.initStart telemetry event
432 let init_start = PlatformInitStart {
433 initialization_type: InitializationType::OnDemand,
434 phase: Phase::Init,
435 function_name: Some(config.function_name.clone()),
436 function_version: Some(config.function_version.clone()),
437 instance_id: Some(config.instance_id.clone()),
438 instance_max_memory: Some(config.memory_size_mb),
439 runtime_version: None,
440 runtime_version_arn: None,
441 tracing: None,
442 };
443
444 let init_start_event = TelemetryEvent {
445 time: runtime_state.init_started_at(),
446 event_type: "platform.initStart".to_string(),
447 record: json!(init_start),
448 };
449
450 telemetry_state
451 .broadcast_event(init_start_event, TelemetryEventType::Platform)
452 .await;
453
454 tracing::info!(target: "lambda_lifecycle", "");
455 tracing::info!(target: "lambda_lifecycle", "═══════════════════════════════════════════════════════════");
456 tracing::info!(target: "lambda_lifecycle", " INIT PHASE");
457 tracing::info!(target: "lambda_lifecycle", "═══════════════════════════════════════════════════════════");
458 tracing::info!(target: "lambda_lifecycle", "📋 platform.initStart (function: {})", config.function_name);
459
460 Ok(Simulator {
461 runtime_state,
462 extension_state,
463 telemetry_state,
464 freeze_state,
465 readiness_tracker,
466 config,
467 addr: local_addr,
468 server_handle,
469 })
470 }
471}
472
473/// A running Lambda runtime simulator.
474///
475/// The simulator provides an HTTP server that implements both the Lambda Runtime API
476/// and Extensions API, allowing Lambda runtimes and extensions to be tested locally.
477pub struct Simulator {
478 runtime_state: Arc<RuntimeState>,
479 extension_state: Arc<ExtensionState>,
480 telemetry_state: Arc<TelemetryState>,
481 freeze_state: Arc<FreezeState>,
482 readiness_tracker: Arc<ExtensionReadinessTracker>,
483 config: Arc<SimulatorConfig>,
484 addr: SocketAddr,
485 server_handle: JoinHandle<SimulatorResult<()>>,
486}
487
488impl Simulator {
489 /// Creates a new simulator builder.
490 ///
491 /// # Examples
492 ///
493 /// ```no_run
494 /// use lambda_simulator::Simulator;
495 ///
496 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
497 /// let simulator = Simulator::builder()
498 /// .function_name("my-function")
499 /// .build()
500 /// .await?;
501 /// # Ok(())
502 /// # }
503 /// ```
504 pub fn builder() -> SimulatorBuilder {
505 SimulatorBuilder::new()
506 }
507
508 /// Returns the base URL for the Runtime API.
509 ///
510 /// This should be set as the `AWS_LAMBDA_RUNTIME_API` environment variable
511 /// for Lambda runtimes.
512 ///
513 /// # Examples
514 ///
515 /// ```no_run
516 /// use lambda_simulator::Simulator;
517 ///
518 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
519 /// let simulator = Simulator::builder().build().await?;
520 /// let runtime_api_url = simulator.runtime_api_url();
521 /// println!("Set AWS_LAMBDA_RUNTIME_API={}", runtime_api_url);
522 /// # Ok(())
523 /// # }
524 /// ```
525 pub fn runtime_api_url(&self) -> String {
526 format!("http://{}", self.addr)
527 }
528
529 /// Returns the socket address the simulator is listening on.
530 pub fn addr(&self) -> SocketAddr {
531 self.addr
532 }
533
534 /// Enqueues an invocation for processing.
535 ///
536 /// If process freezing is enabled and the process is currently frozen,
537 /// this method will unfreeze (SIGCONT) the process before enqueuing
538 /// the invocation.
539 ///
540 /// # Arguments
541 ///
542 /// * `invocation` - The invocation to enqueue
543 ///
544 /// # Returns
545 ///
546 /// The request ID of the enqueued invocation.
547 ///
548 /// # Examples
549 ///
550 /// ```no_run
551 /// use lambda_simulator::{Simulator, InvocationBuilder};
552 /// use serde_json::json;
553 ///
554 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
555 /// let simulator = Simulator::builder().build().await?;
556 ///
557 /// let invocation = InvocationBuilder::new()
558 /// .payload(json!({"key": "value"}))
559 /// .build()?;
560 ///
561 /// let request_id = simulator.enqueue(invocation).await;
562 /// # Ok(())
563 /// # }
564 /// ```
565 pub async fn enqueue(&self, invocation: Invocation) -> String {
566 let was_frozen = self.freeze_state.is_frozen();
567 if let Err(e) = self.freeze_state.unfreeze() {
568 panic!(
569 "Failed to unfreeze processes before invocation: {}. \
570 The target processes are stuck in SIGSTOP state and cannot continue.",
571 e
572 );
573 }
574
575 let request_id = invocation.request_id.clone();
576
577 tracing::info!(target: "lambda_lifecycle", "");
578 tracing::info!(target: "lambda_lifecycle", "═══════════════════════════════════════════════════════════");
579 tracing::info!(target: "lambda_lifecycle", " INVOKE PHASE");
580 tracing::info!(target: "lambda_lifecycle", "═══════════════════════════════════════════════════════════");
581 if was_frozen {
582 tracing::info!(target: "lambda_lifecycle", "🔥 Thawing environment (SIGCONT)");
583 }
584 tracing::info!(target: "lambda_lifecycle", "▶️ platform.start (request_id: {})", &invocation.request_id[..8]);
585
586 let invoke_subscribers = self.extension_state.get_invoke_subscribers().await;
587 self.readiness_tracker
588 .start_invocation(&request_id, invoke_subscribers)
589 .await;
590
591 let event = LifecycleEvent::Invoke {
592 deadline_ms: invocation.deadline_ms(),
593 request_id: invocation.request_id.clone(),
594 invoked_function_arn: invocation.invoked_function_arn.clone(),
595 tracing: TracingInfo {
596 trace_type: "X-Amzn-Trace-Id".to_string(),
597 value: invocation.trace_id.clone(),
598 },
599 };
600
601 self.runtime_state.enqueue_invocation(invocation).await;
602
603 tracing::info!(target: "lambda_lifecycle", "📤 Broadcasting INVOKE event to extensions");
604 self.extension_state.broadcast_event(event).await;
605
606 request_id
607 }
608
609 /// Enqueues a simple invocation with just a payload.
610 ///
611 /// # Arguments
612 ///
613 /// * `payload` - The JSON payload for the invocation
614 ///
615 /// # Returns
616 ///
617 /// The request ID of the enqueued invocation.
618 ///
619 /// # Examples
620 ///
621 /// ```no_run
622 /// use lambda_simulator::Simulator;
623 /// use serde_json::json;
624 ///
625 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
626 /// let simulator = Simulator::builder().build().await?;
627 /// let request_id = simulator.enqueue_payload(json!({"key": "value"})).await;
628 /// # Ok(())
629 /// # }
630 /// ```
631 pub async fn enqueue_payload(&self, payload: Value) -> String {
632 let invocation = Invocation::new(payload, self.config.invocation_timeout_ms);
633 self.enqueue(invocation).await
634 }
635
636 /// Gets the state of a specific invocation.
637 ///
638 /// # Arguments
639 ///
640 /// * `request_id` - The request ID to look up
641 ///
642 /// # Returns
643 ///
644 /// The invocation state if found.
645 pub async fn get_invocation_state(&self, request_id: &str) -> Option<InvocationState> {
646 self.runtime_state.get_invocation_state(request_id).await
647 }
648
649 /// Gets all invocation states.
650 pub async fn get_all_invocation_states(&self) -> Vec<InvocationState> {
651 self.runtime_state.get_all_states().await
652 }
653
654 /// Checks if the runtime has been initialized.
655 pub async fn is_initialized(&self) -> bool {
656 self.runtime_state.is_initialized().await
657 }
658
659 /// Gets the initialization error if one occurred.
660 pub async fn get_init_error(&self) -> Option<String> {
661 self.runtime_state.get_init_error().await
662 }
663
664 /// Gets all registered extensions.
665 ///
666 /// # Returns
667 ///
668 /// A list of all extensions that have registered with the simulator.
669 pub async fn get_registered_extensions(&self) -> Vec<RegisteredExtension> {
670 self.extension_state.get_all_extensions().await
671 }
672
673 /// Gets the number of registered extensions.
674 pub async fn extension_count(&self) -> usize {
675 self.extension_state.extension_count().await
676 }
677
678 /// Shuts down the simulator immediately without waiting for extensions.
679 ///
680 /// This will unfreeze any frozen process, abort the HTTP server,
681 /// clean up background telemetry tasks, and wait for everything to finish.
682 ///
683 /// For graceful shutdown that allows extensions to complete cleanup work,
684 /// use [`graceful_shutdown`](Self::graceful_shutdown) instead.
685 pub async fn shutdown(self) {
686 self.freeze_state.force_unfreeze();
687
688 self.telemetry_state.shutdown().await;
689
690 self.server_handle.abort();
691 let _ = self.server_handle.await;
692 }
693
694 /// Gracefully shuts down the simulator, allowing extensions to complete cleanup.
695 ///
696 /// This method performs a graceful shutdown sequence that matches Lambda's
697 /// actual behavior:
698 ///
699 /// 1. Unfreezes the runtime process (if frozen)
700 /// 2. Transitions to `ShuttingDown` phase
701 /// 3. Broadcasts a `SHUTDOWN` event to all subscribed extensions
702 /// 4. Waits for extensions to acknowledge by polling `/next`
703 /// 5. Cleans up resources and stops the HTTP server
704 ///
705 /// # Arguments
706 ///
707 /// * `reason` - The reason for shutdown (Spindown, Timeout, or Failure)
708 ///
709 /// # Extension Behavior
710 ///
711 /// Extensions subscribed to SHUTDOWN events will receive the event when they
712 /// poll `/next`. After receiving the SHUTDOWN event, extensions should perform
713 /// their cleanup work (e.g., flushing buffers, sending final telemetry batches)
714 /// and then poll `/next` again to signal completion.
715 ///
716 /// If extensions don't complete within the configured `shutdown_timeout`,
717 /// the simulator proceeds with shutdown anyway.
718 ///
719 /// # Examples
720 ///
721 /// ```no_run
722 /// use lambda_simulator::{Simulator, ShutdownReason};
723 ///
724 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
725 /// let simulator = Simulator::builder().build().await?;
726 ///
727 /// // ... run invocations ...
728 ///
729 /// // Graceful shutdown with SPINDOWN reason
730 /// simulator.graceful_shutdown(ShutdownReason::Spindown).await;
731 /// # Ok(())
732 /// # }
733 /// ```
734 pub async fn graceful_shutdown(self, reason: ShutdownReason) {
735 tracing::info!(target: "lambda_lifecycle", "");
736 tracing::info!(target: "lambda_lifecycle", "═══════════════════════════════════════════════════════════");
737 tracing::info!(target: "lambda_lifecycle", " SHUTDOWN PHASE");
738 tracing::info!(target: "lambda_lifecycle", "═══════════════════════════════════════════════════════════");
739 tracing::info!(target: "lambda_lifecycle", "🛑 Shutdown initiated (reason: {:?})", reason);
740
741 self.freeze_state.force_unfreeze();
742
743 self.runtime_state.mark_shutting_down().await;
744
745 // Flush any buffered telemetry events before sending SHUTDOWN.
746 // The telemetry delivery task uses an interval timer, so events like
747 // platform.report may still be buffered. Flushing ensures extensions
748 // receive all telemetry before they begin shutdown processing.
749 self.telemetry_state.flush_all().await;
750
751 // Wait for the flush operation to fully complete.
752 // This ensures extensions have received and processed all telemetry
753 // before we send the SHUTDOWN event.
754 self.telemetry_state
755 .wait_for_flush_complete(Duration::from_millis(100))
756 .await;
757
758 let shutdown_subscribers = self.extension_state.get_shutdown_subscribers().await;
759 let has_shutdown_subscribers = !shutdown_subscribers.is_empty();
760
761 if has_shutdown_subscribers {
762 self.extension_state.clear_shutdown_acknowledged().await;
763
764 let deadline_ms = Utc::now()
765 .timestamp_millis()
766 .saturating_add(self.config.shutdown_timeout_ms as i64);
767
768 let shutdown_event = LifecycleEvent::Shutdown {
769 shutdown_reason: reason,
770 deadline_ms,
771 };
772
773 tracing::info!(target: "lambda_lifecycle", "📤 Broadcasting SHUTDOWN event to extensions");
774 self.extension_state.broadcast_event(shutdown_event).await;
775
776 let timeout = Duration::from_millis(self.config.shutdown_timeout_ms);
777 let _ = tokio::time::timeout(
778 timeout,
779 self.extension_state
780 .wait_for_shutdown_acknowledged(&shutdown_subscribers),
781 )
782 .await;
783 }
784
785 self.telemetry_state.shutdown().await;
786
787 self.server_handle.abort();
788 let _ = self.server_handle.await;
789 }
790
791 /// Waits for an invocation to reach a terminal state (Success, Error, or Timeout).
792 ///
793 /// This method uses efficient event-driven waiting instead of polling,
794 /// making tests more reliable and faster.
795 ///
796 /// # Arguments
797 ///
798 /// * `request_id` - The request ID to wait for
799 /// * `timeout` - Maximum duration to wait
800 ///
801 /// # Returns
802 ///
803 /// The final invocation state
804 ///
805 /// # Errors
806 ///
807 /// Returns `SimulatorError::Timeout` if the invocation doesn't complete within the timeout,
808 /// or `SimulatorError::InvocationNotFound` if the request ID doesn't exist.
809 ///
810 /// # Examples
811 ///
812 /// ```no_run
813 /// use lambda_simulator::Simulator;
814 /// use serde_json::json;
815 /// use std::time::Duration;
816 ///
817 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
818 /// let simulator = Simulator::builder().build().await?;
819 /// let request_id = simulator.enqueue_payload(json!({"test": "data"})).await;
820 ///
821 /// let state = simulator.wait_for_invocation_complete(&request_id, Duration::from_secs(5)).await?;
822 /// assert_eq!(state.status, lambda_simulator::InvocationStatus::Success);
823 /// # Ok(())
824 /// # }
825 /// ```
826 pub async fn wait_for_invocation_complete(
827 &self,
828 request_id: &str,
829 timeout: Duration,
830 ) -> SimulatorResult<InvocationState> {
831 let deadline = tokio::time::Instant::now() + timeout;
832
833 loop {
834 if let Some(state) = self.runtime_state.get_invocation_state(request_id).await {
835 match state.status {
836 InvocationStatus::Success
837 | InvocationStatus::Error
838 | InvocationStatus::Timeout => {
839 return Ok(state);
840 }
841 _ => {}
842 }
843 } else {
844 return Err(SimulatorError::InvocationNotFound(request_id.to_string()));
845 }
846
847 if tokio::time::Instant::now() >= deadline {
848 return Err(SimulatorError::Timeout(format!(
849 "Invocation {} did not complete within {:?}",
850 request_id, timeout
851 )));
852 }
853
854 tokio::select! {
855 _ = self.runtime_state.wait_for_state_change() => {},
856 _ = tokio::time::sleep_until(deadline) => {
857 return Err(SimulatorError::Timeout(format!(
858 "Invocation {} did not complete within {:?}",
859 request_id, timeout
860 )));
861 }
862 }
863 }
864 }
865
866 /// Waits for a condition to become true.
867 ///
868 /// This is a general-purpose helper that polls a condition function.
869 /// For common conditions, use specific helpers like `wait_for_invocation_complete`.
870 ///
871 /// # Arguments
872 ///
873 /// * `condition` - Async function that returns true when the condition is met
874 /// * `timeout` - Maximum duration to wait
875 ///
876 /// # Errors
877 ///
878 /// Returns `SimulatorError::Timeout` if the condition doesn't become true within the timeout.
879 ///
880 /// # Examples
881 ///
882 /// ```no_run
883 /// use lambda_simulator::Simulator;
884 /// use std::time::Duration;
885 ///
886 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
887 /// let simulator = Simulator::builder().build().await?;
888 ///
889 /// // Wait for 3 extensions to register
890 /// simulator.wait_for(
891 /// || async { simulator.extension_count().await >= 3 },
892 /// Duration::from_secs(5)
893 /// ).await?;
894 /// # Ok(())
895 /// # }
896 /// ```
897 pub async fn wait_for<F, Fut>(&self, condition: F, timeout: Duration) -> SimulatorResult<()>
898 where
899 F: Fn() -> Fut,
900 Fut: std::future::Future<Output = bool>,
901 {
902 let deadline = tokio::time::Instant::now() + timeout;
903 let poll_interval = Duration::from_millis(10);
904
905 loop {
906 if condition().await {
907 return Ok(());
908 }
909
910 if tokio::time::Instant::now() >= deadline {
911 return Err(SimulatorError::Timeout(format!(
912 "Condition did not become true within {:?}",
913 timeout
914 )));
915 }
916
917 tokio::time::sleep(poll_interval).await;
918 }
919 }
920
921 /// Enables test mode telemetry capture.
922 ///
923 /// When enabled, all telemetry events are captured in memory,
924 /// avoiding the need to set up HTTP servers in tests.
925 ///
926 /// # Examples
927 ///
928 /// ```no_run
929 /// use lambda_simulator::Simulator;
930 /// use serde_json::json;
931 ///
932 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
933 /// let simulator = Simulator::builder().build().await?;
934 /// simulator.enable_telemetry_capture().await;
935 ///
936 /// simulator.enqueue_payload(json!({"test": "data"})).await;
937 ///
938 /// let events = simulator.get_telemetry_events().await;
939 /// assert!(!events.is_empty());
940 /// # Ok(())
941 /// # }
942 /// ```
943 pub async fn enable_telemetry_capture(&self) {
944 self.telemetry_state.enable_capture().await;
945 }
946
947 /// Gets all captured telemetry events.
948 ///
949 /// Returns an empty vector if telemetry capture is not enabled.
950 pub async fn get_telemetry_events(&self) -> Vec<TelemetryEvent> {
951 self.telemetry_state.get_captured_events().await
952 }
953
954 /// Gets captured telemetry events filtered by event type.
955 ///
956 /// # Arguments
957 ///
958 /// * `event_type` - The event type to filter by (e.g., "platform.start")
959 ///
960 /// # Examples
961 ///
962 /// ```no_run
963 /// use lambda_simulator::Simulator;
964 /// use serde_json::json;
965 ///
966 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
967 /// let simulator = Simulator::builder().build().await?;
968 /// simulator.enable_telemetry_capture().await;
969 ///
970 /// simulator.enqueue_payload(json!({"test": "data"})).await;
971 ///
972 /// let start_events = simulator.get_telemetry_events_by_type("platform.start").await;
973 /// assert_eq!(start_events.len(), 1);
974 /// # Ok(())
975 /// # }
976 /// ```
977 pub async fn get_telemetry_events_by_type(&self, event_type: &str) -> Vec<TelemetryEvent> {
978 self.telemetry_state
979 .get_captured_events_by_type(event_type)
980 .await
981 }
982
983 /// Clears all captured telemetry events.
984 pub async fn clear_telemetry_events(&self) {
985 self.telemetry_state.clear_captured_events().await;
986 }
987
988 /// Gets the current lifecycle phase of the simulator.
989 ///
990 /// # Examples
991 ///
992 /// ```no_run
993 /// use lambda_simulator::{Simulator, SimulatorPhase};
994 ///
995 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
996 /// let simulator = Simulator::builder().build().await?;
997 /// assert_eq!(simulator.phase().await, SimulatorPhase::Initializing);
998 /// # Ok(())
999 /// # }
1000 /// ```
1001 pub async fn phase(&self) -> SimulatorPhase {
1002 self.runtime_state.get_phase().await
1003 }
1004
1005 /// Waits for the simulator to reach a specific phase.
1006 ///
1007 /// # Arguments
1008 ///
1009 /// * `target_phase` - The phase to wait for
1010 /// * `timeout` - Maximum duration to wait
1011 ///
1012 /// # Errors
1013 ///
1014 /// Returns `SimulatorError::Timeout` if the phase is not reached within the timeout.
1015 ///
1016 /// # Examples
1017 ///
1018 /// ```no_run
1019 /// use lambda_simulator::{Simulator, SimulatorPhase};
1020 /// use std::time::Duration;
1021 ///
1022 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1023 /// let simulator = Simulator::builder().build().await?;
1024 ///
1025 /// // Wait for runtime to initialize
1026 /// simulator.wait_for_phase(SimulatorPhase::Ready, Duration::from_secs(5)).await?;
1027 /// # Ok(())
1028 /// # }
1029 /// ```
1030 pub async fn wait_for_phase(
1031 &self,
1032 target_phase: SimulatorPhase,
1033 timeout: Duration,
1034 ) -> SimulatorResult<()> {
1035 let deadline = tokio::time::Instant::now() + timeout;
1036
1037 tokio::select! {
1038 _ = self.runtime_state.wait_for_phase(target_phase) => Ok(()),
1039 _ = tokio::time::sleep_until(deadline) => {
1040 Err(SimulatorError::Timeout(format!(
1041 "Simulator did not reach phase {:?} within {:?}",
1042 target_phase, timeout
1043 )))
1044 }
1045 }
1046 }
1047
1048 /// Marks the runtime as initialized and transitions to Ready phase.
1049 ///
1050 /// This is typically called internally when the first runtime polls for an invocation,
1051 /// but can be called explicitly in tests.
1052 pub async fn mark_initialized(&self) {
1053 self.runtime_state.mark_initialized().await;
1054 }
1055
1056 /// Returns whether the runtime process is currently frozen.
1057 ///
1058 /// This is useful in tests to verify freeze behaviour.
1059 ///
1060 /// # Examples
1061 ///
1062 /// ```no_run
1063 /// use lambda_simulator::{Simulator, FreezeMode};
1064 ///
1065 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1066 /// let simulator = Simulator::builder()
1067 /// .freeze_mode(FreezeMode::Process)
1068 /// .runtime_pid(12345)
1069 /// .build()
1070 /// .await?;
1071 ///
1072 /// assert!(!simulator.is_frozen());
1073 /// # Ok(())
1074 /// # }
1075 /// ```
1076 pub fn is_frozen(&self) -> bool {
1077 self.freeze_state.is_frozen()
1078 }
1079
1080 /// Waits for the process to become frozen.
1081 ///
1082 /// This is useful in tests to verify freeze behaviour after sending
1083 /// an invocation response.
1084 ///
1085 /// # Arguments
1086 ///
1087 /// * `timeout` - Maximum duration to wait
1088 ///
1089 /// # Errors
1090 ///
1091 /// Returns `SimulatorError::Timeout` if the process doesn't freeze within the timeout.
1092 ///
1093 /// # Examples
1094 ///
1095 /// ```no_run
1096 /// use lambda_simulator::{Simulator, FreezeMode};
1097 /// use std::time::Duration;
1098 ///
1099 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1100 /// let simulator = Simulator::builder()
1101 /// .freeze_mode(FreezeMode::Process)
1102 /// .runtime_pid(12345)
1103 /// .build()
1104 /// .await?;
1105 ///
1106 /// // After an invocation completes...
1107 /// simulator.wait_for_frozen(Duration::from_secs(5)).await?;
1108 /// assert!(simulator.is_frozen());
1109 /// # Ok(())
1110 /// # }
1111 /// ```
1112 pub async fn wait_for_frozen(&self, timeout: Duration) -> SimulatorResult<()> {
1113 let deadline = tokio::time::Instant::now() + timeout;
1114
1115 loop {
1116 if self.freeze_state.is_frozen() {
1117 return Ok(());
1118 }
1119
1120 if tokio::time::Instant::now() >= deadline {
1121 return Err(SimulatorError::Timeout(
1122 "Process did not freeze within timeout".to_string(),
1123 ));
1124 }
1125
1126 tokio::select! {
1127 _ = self.freeze_state.wait_for_state_change() => {},
1128 _ = tokio::time::sleep_until(deadline) => {
1129 return Err(SimulatorError::Timeout(
1130 "Process did not freeze within timeout".to_string(),
1131 ));
1132 }
1133 }
1134 }
1135 }
1136
1137 /// Returns the current freeze mode.
1138 pub fn freeze_mode(&self) -> FreezeMode {
1139 self.freeze_state.mode()
1140 }
1141
1142 /// Returns the current freeze epoch.
1143 ///
1144 /// The epoch increments on each unfreeze operation. This can be used
1145 /// in tests to verify freeze/thaw cycles.
1146 pub fn freeze_epoch(&self) -> u64 {
1147 self.freeze_state.current_epoch()
1148 }
1149
1150 /// Registers a process ID for freeze/thaw operations.
1151 ///
1152 /// In real AWS Lambda, the entire execution environment is frozen between
1153 /// invocations - this includes the runtime and all extension processes.
1154 /// Use this method to register PIDs after spawning processes, so they
1155 /// will be included in freeze/thaw cycles.
1156 ///
1157 /// # Examples
1158 ///
1159 /// ```no_run
1160 /// use lambda_simulator::{Simulator, FreezeMode};
1161 /// use std::process::Command;
1162 ///
1163 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1164 /// let simulator = Simulator::builder()
1165 /// .freeze_mode(FreezeMode::Process)
1166 /// .build()
1167 /// .await?;
1168 ///
1169 /// // Spawn runtime and extension processes
1170 /// let runtime = Command::new("my-runtime")
1171 /// .env("AWS_LAMBDA_RUNTIME_API", simulator.runtime_api_url().replace("http://", ""))
1172 /// .spawn()?;
1173 ///
1174 /// let extension = Command::new("my-extension")
1175 /// .env("AWS_LAMBDA_RUNTIME_API", simulator.runtime_api_url().replace("http://", ""))
1176 /// .spawn()?;
1177 ///
1178 /// // Register PIDs for freezing
1179 /// simulator.register_freeze_pid(runtime.id());
1180 /// simulator.register_freeze_pid(extension.id());
1181 /// # Ok(())
1182 /// # }
1183 /// ```
1184 pub fn register_freeze_pid(&self, pid: u32) {
1185 self.freeze_state.register_pid(pid);
1186 }
1187
1188 /// Spawns a process with Lambda environment variables and registers it for freeze/thaw.
1189 ///
1190 /// This method:
1191 /// 1. Injects all Lambda environment variables (from `lambda_env_vars()`)
1192 /// 2. Spawns the process with the given configuration
1193 /// 3. Automatically registers the PID for freeze/thaw if `FreezeMode::Process` is enabled
1194 ///
1195 /// The spawned process will be automatically terminated when the returned
1196 /// `ManagedProcess` is dropped.
1197 ///
1198 /// # Arguments
1199 ///
1200 /// * `binary_path` - Path to the executable binary
1201 /// * `role` - Whether this is a runtime or extension process
1202 ///
1203 /// # Errors
1204 ///
1205 /// Returns `ProcessError::BinaryNotFound` if the binary doesn't exist.
1206 /// Returns `ProcessError::SpawnFailed` if the process fails to start.
1207 ///
1208 /// # Examples
1209 ///
1210 /// ```no_run
1211 /// use lambda_simulator::{Simulator, FreezeMode};
1212 /// use lambda_simulator::process::ProcessRole;
1213 ///
1214 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1215 /// let simulator = Simulator::builder()
1216 /// .freeze_mode(FreezeMode::Process)
1217 /// .build()
1218 /// .await?;
1219 ///
1220 /// // Spawn a runtime process
1221 /// // In tests, use env!("CARGO_BIN_EXE_<name>") for binary path resolution
1222 /// let runtime = simulator.spawn_process(
1223 /// "/path/to/runtime",
1224 /// ProcessRole::Runtime,
1225 /// )?;
1226 ///
1227 /// println!("Runtime PID: {}", runtime.pid());
1228 /// # Ok(())
1229 /// # }
1230 /// ```
1231 pub fn spawn_process(
1232 &self,
1233 binary_path: impl Into<std::path::PathBuf>,
1234 role: crate::process::ProcessRole,
1235 ) -> Result<crate::process::ManagedProcess, crate::process::ProcessError> {
1236 let config = crate::process::ProcessConfig::new(binary_path, role);
1237 self.spawn_process_with_config(config)
1238 }
1239
1240 /// Spawns a process with custom configuration.
1241 ///
1242 /// This is like `spawn_process` but allows full control over the process
1243 /// configuration, including additional environment variables and arguments.
1244 ///
1245 /// # Examples
1246 ///
1247 /// ```no_run
1248 /// use lambda_simulator::Simulator;
1249 /// use lambda_simulator::process::{ProcessConfig, ProcessRole};
1250 ///
1251 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1252 /// let simulator = Simulator::builder().build().await?;
1253 ///
1254 /// let config = ProcessConfig::new("/path/to/runtime", ProcessRole::Runtime)
1255 /// .env("CUSTOM_VAR", "value")
1256 /// .arg("--verbose")
1257 /// .inherit_stdio(false);
1258 ///
1259 /// let runtime = simulator.spawn_process_with_config(config)?;
1260 /// # Ok(())
1261 /// # }
1262 /// ```
1263 pub fn spawn_process_with_config(
1264 &self,
1265 config: crate::process::ProcessConfig,
1266 ) -> Result<crate::process::ManagedProcess, crate::process::ProcessError> {
1267 let lambda_env = self.lambda_env_vars();
1268 let process = crate::process::spawn_process(config, lambda_env)?;
1269
1270 if self.freeze_state.mode() == FreezeMode::Process {
1271 self.register_freeze_pid(process.pid());
1272 tracing::info!(
1273 target: "lambda_lifecycle",
1274 "📦 Spawned {} process: {} (PID: {}, registered for freeze/thaw)",
1275 process.role(),
1276 process.binary_name(),
1277 process.pid()
1278 );
1279 } else {
1280 tracing::info!(
1281 target: "lambda_lifecycle",
1282 "📦 Spawned {} process: {} (PID: {})",
1283 process.role(),
1284 process.binary_name(),
1285 process.pid()
1286 );
1287 }
1288
1289 Ok(process)
1290 }
1291
1292 /// Waits for all extensions to signal readiness for a specific invocation.
1293 ///
1294 /// Extensions signal readiness by polling the `/next` endpoint after
1295 /// the runtime has submitted its response. This method blocks until
1296 /// all extensions subscribed to INVOKE events have signalled readiness.
1297 ///
1298 /// # Arguments
1299 ///
1300 /// * `request_id` - The request ID to wait for
1301 /// * `timeout` - Maximum duration to wait
1302 ///
1303 /// # Errors
1304 ///
1305 /// Returns `SimulatorError::Timeout` if not all extensions become ready within the timeout.
1306 ///
1307 /// # Examples
1308 ///
1309 /// ```no_run
1310 /// use lambda_simulator::Simulator;
1311 /// use serde_json::json;
1312 /// use std::time::Duration;
1313 ///
1314 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1315 /// let simulator = Simulator::builder().build().await?;
1316 /// let request_id = simulator.enqueue_payload(json!({"test": "data"})).await;
1317 ///
1318 /// // Wait for extensions to be ready
1319 /// simulator.wait_for_extensions_ready(&request_id, Duration::from_secs(5)).await?;
1320 /// # Ok(())
1321 /// # }
1322 /// ```
1323 pub async fn wait_for_extensions_ready(
1324 &self,
1325 request_id: &str,
1326 timeout: Duration,
1327 ) -> SimulatorResult<()> {
1328 let deadline = tokio::time::Instant::now() + timeout;
1329
1330 tokio::select! {
1331 _ = self.readiness_tracker.wait_for_all_ready(request_id) => Ok(()),
1332 _ = tokio::time::sleep_until(deadline) => {
1333 Err(SimulatorError::Timeout(format!(
1334 "Extensions did not become ready for {} within {:?}",
1335 request_id, timeout
1336 )))
1337 }
1338 }
1339 }
1340
1341 /// Checks if all extensions are ready for a specific invocation.
1342 ///
1343 /// This is a non-blocking check that returns immediately.
1344 ///
1345 /// # Arguments
1346 ///
1347 /// * `request_id` - The request ID to check
1348 ///
1349 /// # Returns
1350 ///
1351 /// `true` if all expected extensions have signalled readiness.
1352 pub async fn are_extensions_ready(&self, request_id: &str) -> bool {
1353 self.readiness_tracker.is_all_ready(request_id).await
1354 }
1355
1356 /// Gets the extension overhead time for an invocation.
1357 ///
1358 /// The overhead is the time between when the runtime completed its response
1359 /// and when all extensions signalled readiness.
1360 ///
1361 /// # Arguments
1362 ///
1363 /// * `request_id` - The request ID to get overhead for
1364 ///
1365 /// # Returns
1366 ///
1367 /// The overhead in milliseconds, or `None` if the invocation is not complete
1368 /// or extensions are not yet ready.
1369 ///
1370 /// # Examples
1371 ///
1372 /// ```no_run
1373 /// use lambda_simulator::Simulator;
1374 /// use serde_json::json;
1375 /// use std::time::Duration;
1376 ///
1377 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1378 /// let simulator = Simulator::builder().build().await?;
1379 /// let request_id = simulator.enqueue_payload(json!({"test": "data"})).await;
1380 ///
1381 /// // After invocation completes...
1382 /// if let Some(overhead_ms) = simulator.get_extension_overhead_ms(&request_id).await {
1383 /// println!("Extension overhead: {:.2}ms", overhead_ms);
1384 /// }
1385 /// # Ok(())
1386 /// # }
1387 /// ```
1388 pub async fn get_extension_overhead_ms(&self, request_id: &str) -> Option<f64> {
1389 self.readiness_tracker
1390 .get_extension_overhead_ms(request_id)
1391 .await
1392 }
1393
1394 /// Generates a map of standard AWS Lambda environment variables.
1395 ///
1396 /// This method creates a `HashMap` containing all the environment variables
1397 /// that AWS Lambda sets for function execution. These can be used when
1398 /// spawning a Lambda runtime process to simulate the real Lambda environment.
1399 ///
1400 /// # Environment Variables Included
1401 ///
1402 /// - `AWS_LAMBDA_FUNCTION_NAME` - Function name
1403 /// - `AWS_LAMBDA_FUNCTION_VERSION` - Function version
1404 /// - `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` - Memory allocation in MB
1405 /// - `AWS_LAMBDA_FUNCTION_ARN` - Function ARN (if account_id configured)
1406 /// - `AWS_LAMBDA_LOG_GROUP_NAME` - CloudWatch log group name
1407 /// - `AWS_LAMBDA_LOG_STREAM_NAME` - CloudWatch log stream name
1408 /// - `AWS_LAMBDA_RUNTIME_API` - Runtime API endpoint (host:port)
1409 /// - `AWS_LAMBDA_INITIALIZATION_TYPE` - Always "on-demand" for simulator
1410 /// - `AWS_REGION` / `AWS_DEFAULT_REGION` - AWS region
1411 /// - `AWS_ACCOUNT_ID` - AWS account ID (if configured)
1412 /// - `AWS_EXECUTION_ENV` - Runtime identifier
1413 /// - `LAMBDA_TASK_ROOT` - Path to function code (/var/task)
1414 /// - `LAMBDA_RUNTIME_DIR` - Path to runtime libraries (/var/runtime)
1415 /// - `TZ` - Timezone (UTC)
1416 /// - `LANG` - Locale (en_US.UTF-8)
1417 /// - `PATH` - System path
1418 /// - `LD_LIBRARY_PATH` - Library search path
1419 /// - `_HANDLER` - Handler identifier (if configured)
1420 ///
1421 /// # Examples
1422 ///
1423 /// ```no_run
1424 /// use lambda_simulator::Simulator;
1425 /// use std::process::Command;
1426 ///
1427 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1428 /// let simulator = Simulator::builder()
1429 /// .function_name("my-function")
1430 /// .handler("bootstrap")
1431 /// .build()
1432 /// .await?;
1433 ///
1434 /// let env_vars = simulator.lambda_env_vars();
1435 ///
1436 /// let mut cmd = Command::new("./bootstrap");
1437 /// for (key, value) in &env_vars {
1438 /// cmd.env(key, value);
1439 /// }
1440 /// # Ok(())
1441 /// # }
1442 /// ```
1443 #[must_use]
1444 pub fn lambda_env_vars(&self) -> HashMap<String, String> {
1445 let mut env = HashMap::new();
1446 let config = &self.config;
1447
1448 env.insert(
1449 "AWS_LAMBDA_FUNCTION_NAME".to_string(),
1450 config.function_name.clone(),
1451 );
1452 env.insert(
1453 "AWS_LAMBDA_FUNCTION_VERSION".to_string(),
1454 config.function_version.clone(),
1455 );
1456 env.insert(
1457 "AWS_LAMBDA_FUNCTION_MEMORY_SIZE".to_string(),
1458 config.memory_size_mb.to_string(),
1459 );
1460 env.insert(
1461 "AWS_LAMBDA_LOG_GROUP_NAME".to_string(),
1462 config.log_group_name.clone(),
1463 );
1464 env.insert(
1465 "AWS_LAMBDA_LOG_STREAM_NAME".to_string(),
1466 config.log_stream_name.clone(),
1467 );
1468
1469 let host_port = format!("127.0.0.1:{}", self.addr.port());
1470 env.insert("AWS_LAMBDA_RUNTIME_API".to_string(), host_port);
1471
1472 env.insert(
1473 "AWS_LAMBDA_INITIALIZATION_TYPE".to_string(),
1474 "on-demand".to_string(),
1475 );
1476
1477 env.insert("AWS_REGION".to_string(), config.region.clone());
1478 env.insert("AWS_DEFAULT_REGION".to_string(), config.region.clone());
1479
1480 env.insert(
1481 "AWS_EXECUTION_ENV".to_string(),
1482 "AWS_Lambda_provided.al2".to_string(),
1483 );
1484
1485 env.insert("LAMBDA_TASK_ROOT".to_string(), "/var/task".to_string());
1486 env.insert("LAMBDA_RUNTIME_DIR".to_string(), "/var/runtime".to_string());
1487
1488 if let Some(handler) = &config.handler {
1489 env.insert("_HANDLER".to_string(), handler.clone());
1490 }
1491
1492 if let Some(account_id) = &config.account_id {
1493 env.insert("AWS_ACCOUNT_ID".to_string(), account_id.clone());
1494 let arn = format!(
1495 "arn:aws:lambda:{}:{}:function:{}",
1496 config.region, account_id, config.function_name
1497 );
1498 env.insert("AWS_LAMBDA_FUNCTION_ARN".to_string(), arn);
1499 }
1500
1501 env.insert("TZ".to_string(), ":UTC".to_string());
1502 env.insert("LANG".to_string(), "en_US.UTF-8".to_string());
1503 env.insert(
1504 "PATH".to_string(),
1505 "/usr/local/bin:/usr/bin:/bin:/opt/bin".to_string(),
1506 );
1507 env.insert(
1508 "LD_LIBRARY_PATH".to_string(),
1509 "/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib".to_string(),
1510 );
1511
1512 env
1513 }
1514}