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}