Skip to main content

fakecloud_lambda/runtime/
backend.rs

1//! Pluggable Lambda execution backend abstraction.
2//!
3//! The facade in [`super::facade::LambdaRuntime`] owns the warm-pool
4//! bookkeeping, per-function startup serialization, and the HTTP
5//! invocation logic that's identical across backends. Backends only
6//! implement how to bring up a fresh runtime instance for a function
7//! (Docker container, Kubernetes pod, ...), how to tear it down, and
8//! optionally how to sweep instances left behind by a previous
9//! fakecloud process.
10
11use async_trait::async_trait;
12
13use crate::state::LambdaFunction;
14
15#[derive(Debug, thiserror::Error)]
16pub enum RuntimeError {
17    #[error("no code ZIP provided for function {0}")]
18    NoCodeZip(String),
19    #[error("unsupported runtime: {0}")]
20    UnsupportedRuntime(String),
21    #[error("container failed to start: {0}")]
22    ContainerStartFailed(String),
23    #[error("invocation failed: {0}")]
24    InvocationFailed(String),
25    #[error("ZIP extraction failed: {0}")]
26    ZipExtractionFailed(String),
27}
28
29/// Opaque per-backend identifier for a launched runtime instance. The
30/// facade hands this back to the backend on teardown so the backend can
31/// find the right resource to delete.
32#[derive(Debug, Clone)]
33pub enum BackendHandle {
34    Container { id: String },
35    Pod { namespace: String, name: String },
36}
37
38/// What [`LambdaBackend::launch`] returns. `endpoint` is the `host:port`
39/// the facade POSTs invocation payloads to; `handle` is the
40/// backend-specific identifier handed back on `terminate`.
41#[derive(Debug, Clone)]
42pub struct WarmInstance {
43    pub endpoint: String,
44    pub handle: BackendHandle,
45}
46
47/// Wrapper around an in-flight streaming invocation. Yields raw body
48/// chunks via [`Self::next_chunk`] until the RIE closes the response,
49/// at which point the final `Ok(None)` signals the caller to emit the
50/// terminal `InvokeComplete` frame.
51pub struct StreamingInvocation {
52    pub(crate) resp: reqwest::Response,
53    /// Holds the warm instance's per-instance busy lock for the lifetime
54    /// of the stream. The RIE handles exactly one event at a time, so the
55    /// slot must stay reserved until the caller finishes draining the
56    /// response; dropping this guard frees the instance for the next
57    /// invocation. `None` only in unit tests that build the wrapper
58    /// directly.
59    pub(crate) _slot_guard: Option<tokio::sync::OwnedMutexGuard<()>>,
60}
61
62impl StreamingInvocation {
63    /// Read the next chunk of the function's response body. Returns
64    /// `Ok(None)` once the RIE has finished streaming. Buffered
65    /// handlers tend to deliver a single chunk; streaming handlers
66    /// deliver one chunk per `responseStream.write(...)` call.
67    pub async fn next_chunk(&mut self) -> Result<Option<bytes::Bytes>, RuntimeError> {
68        match self.resp.chunk().await {
69            Ok(Some(b)) => Ok(Some(b)),
70            Ok(None) => Ok(None),
71            Err(e) => Err(RuntimeError::InvocationFailed(e.to_string())),
72        }
73    }
74}
75
76#[async_trait]
77pub trait LambdaBackend: Send + Sync + 'static {
78    /// Short identifier surfaced via logs and introspection (e.g.
79    /// `"docker"`, `"podman"`, `"kubernetes"`).
80    fn name(&self) -> &str;
81
82    /// Launch a fresh runtime instance for `func` using the supplied
83    /// code (for zip-package functions) or `func.image_uri` (for image
84    /// package functions). `layers` are the attached layer ZIPs in
85    /// attach order. `deploy_id` is the facade-computed fingerprint
86    /// used to label resources so reaper logic can correlate.
87    async fn launch(
88        &self,
89        func: &LambdaFunction,
90        code_zip: Option<&[u8]>,
91        layers: &[Vec<u8>],
92        deploy_id: &str,
93    ) -> Result<WarmInstance, RuntimeError>;
94
95    /// Tear down one instance. Must be idempotent — the facade may call
96    /// this against an already-gone instance during cleanup races.
97    async fn terminate(&self, handle: &BackendHandle);
98
99    /// Sweep instances that belong to a previous fakecloud process so
100    /// their function names don't leak across restarts. Default no-op;
101    /// backends with out-of-process state should override.
102    async fn reap_stale(&self) {}
103
104    /// Optional hook: pre-warm the runtime image so the first `launch()`
105    /// for a function doesn't pay the cold-pull cost. Called in the
106    /// background after `CreateFunction` persists; backends that don't
107    /// benefit from pre-pulling (e.g. Kubernetes, which pulls images on
108    /// the scheduling node anyway) leave this as a no-op.
109    ///
110    /// `image` is the registry URI fetched from `runtime_to_image` for
111    /// Zip-package functions, or the user-supplied `ImageUri` (already
112    /// translated to the local registry if applicable) for Image-package
113    /// functions.
114    async fn prepull_image(&self, _image: &str) -> Result<(), RuntimeError> {
115        Ok(())
116    }
117
118    /// Fetch the recent stdout/stderr of a running instance, used to build the
119    /// `X-Amz-Log-Result` tail an `Invoke` with `LogType=Tail` returns. Default
120    /// `None` (no logs available); the container backends override it.
121    async fn instance_logs(&self, _handle: &BackendHandle) -> Option<String> {
122        None
123    }
124}