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}