Skip to content

Commit 31b2ec0

Browse files
committed
Add lambda runtime support
Summary: This PR adds the support to run the user services on Amazon Lambda
1 parent 01168ff commit 31b2ec0

File tree

4 files changed

+324
-13
lines changed

4 files changed

+324
-13
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ default = ["http_server", "rand", "uuid", "tracing-span-filter"]
2222
hyper = ["dep:hyper", "http-body-util", "restate-sdk-shared-core/http"]
2323
http_server = ["hyper", "hyper/server", "hyper/http2", "hyper-util", "tokio/net", "tokio/signal", "tokio/macros"]
2424
tracing-span-filter = ["dep:tracing-subscriber"]
25+
lambda = [ "dep:http-serde", "dep:lambda_runtime", "dep:aws_lambda_events"]
2526

2627
[dependencies]
2728
bytes = "1.10"
@@ -44,6 +45,9 @@ tokio = { version = "1.44", default-features = false, features = ["sync"] }
4445
tracing = "0.1"
4546
tracing-subscriber = { version = "0.3", features = ["registry"], optional = true }
4647
uuid = { version = "1.16.0", optional = true }
48+
http-serde = { version = "2.1.1", optional = true }
49+
aws_lambda_events = { version = "0.16.1", optional = true }
50+
lambda_runtime = { version = "0.14.2", optional = true }
4751

4852
[dev-dependencies]
4953
tokio = { version = "1", features = ["full"] }

README.md

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
## Community
1212

13-
* 🤗️ [Join our online community](https://discord.gg/skW3AZ6uGd) for help, sharing feedback and talking to the community.
14-
* 📖 [Check out our documentation](https://docs.restate.dev) to get quickly started!
15-
* 📣 [Follow us on Twitter](https://twitter.com/restatedev) for staying up to date.
16-
* 🙋 [Create a GitHub issue](https://github.com/restatedev/sdk-java/issues) for requesting a new feature or reporting a problem.
17-
* 🏠 [Visit our GitHub org](https://github.com/restatedev) for exploring other repositories.
13+
- 🤗️ [Join our online community](https://discord.gg/skW3AZ6uGd) for help, sharing feedback and talking to the community.
14+
- 📖 [Check out our documentation](https://docs.restate.dev) to get quickly started!
15+
- 📣 [Follow us on Twitter](https://twitter.com/restatedev) for staying up to date.
16+
- 🙋 [Create a GitHub issue](https://github.com/restatedev/sdk-java/issues) for requesting a new feature or reporting a problem.
17+
- 🏠 [Visit our GitHub org](https://github.com/restatedev) for exploring other repositories.
1818

1919
## Using the SDK
2020

@@ -58,6 +58,74 @@ async fn main() {
5858
}
5959
```
6060

61+
## Running on Lambda
62+
63+
The Restate Rust SDK supports running services on AWS Lambda using Lambda Function URLs. This allows you to deploy your Restate services as serverless functions.
64+
65+
### Setup
66+
67+
First, enable the `lambda` feature in your `Cargo.toml`:
68+
69+
```toml
70+
[dependencies]
71+
restate-sdk = { version = "0.1", features = ["lambda"] }
72+
tokio = { version = "1", features = ["full"] }
73+
```
74+
75+
### Basic Lambda Service
76+
77+
Here's how to create a simple Lambda service:
78+
79+
```rust
80+
use restate_sdk::prelude::*;
81+
82+
#[restate_sdk::service]
83+
trait Greeter {
84+
async fn greet(name: String) -> HandlerResult<String>;
85+
}
86+
87+
struct GreeterImpl;
88+
89+
impl Greeter for GreeterImpl {
90+
async fn greet(&self, _: Context<'_>, name: String) -> HandlerResult<String> {
91+
Ok(format!("Greetings {name}"))
92+
}
93+
}
94+
95+
#[tokio::main]
96+
async fn main() {
97+
// To enable logging/tracing
98+
// tracing_subscriber::fmt::init();
99+
100+
// Build and run the Lambda endpoint
101+
Endpoint::builder()
102+
.bind(GreeterImpl.serve())
103+
.build_lambda()
104+
.run()
105+
.await
106+
.unwrap();
107+
}
108+
```
109+
110+
### Deployment
111+
112+
1. Install `cargo-lambda`
113+
```
114+
cargo install cargo-lambda
115+
```
116+
2. Build your Lambda function:
117+
118+
```bash
119+
cargo lambda build --release --arm64 --output-format zip
120+
```
121+
122+
3. Create a Lambda function with the following configuration:
123+
124+
- **Runtime**: Amazon Linux 2023
125+
- **Architecture**: arm64
126+
127+
4. Upload your `zip` file to the Lambda function.
128+
61129
### Logging
62130

63131
The SDK uses tokio's [`tracing`](https://docs.rs/tracing/latest/tracing/) crate to generate logs.
@@ -121,15 +189,15 @@ The Rust SDK is currently in active development, and might break across releases
121189

122190
The compatibility with Restate is described in the following table:
123191

124-
| Restate Server\sdk-rust | 0.0 - 0.2 | 0.3 | 0.4 - 0.5 | 0.6 |
125-
|-------------------------|-----------|-----|-----------|------------------|
126-
| 1.0 | | | ||
127-
| 1.1 | | | ||
128-
| 1.2 | | | ||
129-
| 1.3 | | | | ✅ <sup>(1)</sup> |
130-
| 1.4 | | | ||
192+
| Restate Server\sdk-rust | 0.0 - 0.2 | 0.3 | 0.4 - 0.5 | 0.6 |
193+
| ----------------------- | --------- | --- | --------- | ----------------- |
194+
| 1.0 |||||
195+
| 1.1 |||||
196+
| 1.2 |||||
197+
| 1.3 |||| ✅ <sup>(1)</sup> |
198+
| 1.4 |||||
131199

132-
<sup>(1)</sup> **Note** `bind_with_options` works only from Restate 1.4 onward.
200+
<sup>(1)</sup> **Note** `bind_with_options` works only from Restate 1.4 onward.
133201

134202
## Contributing
135203

src/lambda.rs

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
//! Lambda integration.
2+
3+
use std::convert::Infallible;
4+
use std::future::Future;
5+
use std::pin::Pin;
6+
use std::task::{Context, Poll};
7+
8+
use aws_lambda_events::encodings::Base64Data;
9+
use bytes::Bytes;
10+
use futures::Stream;
11+
use http::header::CONTENT_TYPE;
12+
use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Uri};
13+
use http_body_util::{BodyExt, Full};
14+
use lambda_runtime::service_fn;
15+
use lambda_runtime::tower::ServiceExt;
16+
use lambda_runtime::{FunctionResponse, LambdaEvent};
17+
use serde::{Deserialize, Serialize};
18+
use tracing::debug;
19+
20+
use crate::endpoint::{Endpoint, Error, HandleOptions, ProtocolMode};
21+
22+
#[allow(clippy::declare_interior_mutable_const)]
23+
const X_RESTATE_SERVER: HeaderName = HeaderName::from_static("x-restate-server");
24+
const X_RESTATE_SERVER_VALUE: HeaderValue =
25+
HeaderValue::from_static(concat!("restate-sdk-rust/", env!("CARGO_PKG_VERSION")));
26+
27+
/// Represents an incoming request from AWS Lambda when using Lambda Function URLs.
28+
///
29+
/// This struct is used to deserialize the JSON payload from Lambda.
30+
#[doc(hidden)]
31+
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
32+
#[serde(rename_all = "camelCase")]
33+
pub struct LambdaRequest {
34+
/// The HTTP method of the request.
35+
// #[serde(with = "http_method")]
36+
#[serde(with = "http_serde::method")]
37+
pub http_method: Method,
38+
/// The path of the request.
39+
#[serde(default)]
40+
#[serde(with = "http_serde::uri")]
41+
pub path: Uri,
42+
/// The headers of the request.
43+
#[serde(with = "http_serde::header_map", default)]
44+
pub headers: HeaderMap,
45+
/// Whether the request body is Base64 encoded.
46+
pub is_base64_encoded: bool,
47+
/// The request body, if any.
48+
pub body: Option<Base64Data>,
49+
}
50+
51+
/// Represents a response to be sent back to AWS Lambda.
52+
///
53+
/// This struct is serialized to JSON to form the response payload for Lambda.
54+
#[doc(hidden)]
55+
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
56+
#[serde(rename_all = "camelCase")]
57+
pub struct LambdaResponse {
58+
/// The HTTP status code.
59+
pub status_code: u16,
60+
/// An optional status description.
61+
#[serde(default)]
62+
pub status_description: Option<String>,
63+
/// The response headers.
64+
#[serde(with = "http_serde::header_map", default)]
65+
pub headers: HeaderMap,
66+
/// The optional response body, Base64 encoded.
67+
#[serde(skip_serializing_if = "Option::is_none")]
68+
pub body: Option<Base64Data>,
69+
/// Whether the response body is Base64 encoded. This should generally be `true`
70+
/// when a body is present.
71+
#[serde(default)]
72+
pub is_base64_encoded: bool,
73+
}
74+
75+
impl LambdaResponse {
76+
fn builder() -> LambdaResponseBuilder {
77+
LambdaResponseBuilder {
78+
status_code: 200,
79+
status_description: None,
80+
headers: HeaderMap::default(),
81+
body: None,
82+
}
83+
}
84+
85+
fn from_message<M: ToString>(code: u16, message: M) -> Self {
86+
Self::builder()
87+
.status_code(code)
88+
.header(X_RESTATE_SERVER, X_RESTATE_SERVER_VALUE)
89+
.header(CONTENT_TYPE, "text/plain".parse().unwrap())
90+
.body(Bytes::from(message.to_string()))
91+
.build()
92+
}
93+
}
94+
95+
impl From<LambdaResponse> for FunctionResponse<LambdaResponse, ClosedStream> {
96+
fn from(response: LambdaResponse) -> Self {
97+
FunctionResponse::BufferedResponse(response)
98+
}
99+
}
100+
101+
struct LambdaResponseBuilder {
102+
status_code: u16,
103+
status_description: Option<String>,
104+
headers: HeaderMap,
105+
body: Option<Base64Data>,
106+
}
107+
108+
impl LambdaResponseBuilder {
109+
pub fn status_code(mut self, status_code: u16) -> Self {
110+
self.status_code = status_code;
111+
self.status_description = http::StatusCode::from_u16(status_code)
112+
.map(|s| s.to_string())
113+
.ok();
114+
self
115+
}
116+
117+
pub fn header(mut self, key: HeaderName, value: HeaderValue) -> Self {
118+
self.headers.insert(key, value.into());
119+
self
120+
}
121+
122+
pub fn body(mut self, body: Bytes) -> Self {
123+
self.body = Some(Base64Data(body.into()));
124+
self
125+
}
126+
127+
pub fn build(self) -> LambdaResponse {
128+
LambdaResponse {
129+
status_code: self.status_code,
130+
status_description: self.status_description,
131+
headers: self.headers,
132+
body: self.body,
133+
is_base64_encoded: true,
134+
}
135+
}
136+
}
137+
138+
impl From<Error> for LambdaResponse {
139+
fn from(err: Error) -> Self {
140+
LambdaResponse::from_message(err.status_code(), err.to_string())
141+
}
142+
}
143+
144+
/// A [`Stream`] that is immediately closed.
145+
///
146+
/// This is used as a placeholder body for buffered responses in the Lambda integration,
147+
/// where the entire response is sent at once and no streaming body is needed.
148+
#[doc(hidden)]
149+
pub struct ClosedStream;
150+
impl Stream for ClosedStream {
151+
type Item = Result<Bytes, Infallible>;
152+
fn poll_next(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
153+
Poll::Ready(None)
154+
}
155+
}
156+
157+
/// Wraps an [`Endpoint`] to implement the `lambda_runtime::Service` trait for AWS Lambda.
158+
///
159+
/// This adapter allows a Restate endpoint to be deployed as an AWS Lambda function.
160+
/// It handles the conversion between Lambda's request/response format and the
161+
/// internal representation used by the SDK.
162+
#[derive(Clone)]
163+
pub struct LambdaEndpoint(Endpoint);
164+
165+
impl LambdaEndpoint {
166+
pub fn new(endpoint: Endpoint) -> Self {
167+
Self(endpoint)
168+
}
169+
170+
/// Runs the Lambda service.
171+
///
172+
/// This function starts the `lambda_runtime` and begins processing incoming
173+
/// Lambda events, passing them to the wrapped [`Endpoint`].
174+
pub fn run(self) -> impl Future<Output = Result<(), lambda_runtime::Error>> {
175+
let svc = service_fn(handle);
176+
let svc = svc.map_request(move |req| {
177+
let endpoint = self.0.clone();
178+
LambdaEventWithEndpoint {
179+
inner: req,
180+
endpoint,
181+
}
182+
});
183+
184+
lambda_runtime::run(svc)
185+
}
186+
}
187+
188+
struct LambdaEventWithEndpoint {
189+
inner: LambdaEvent<LambdaRequest>,
190+
endpoint: Endpoint,
191+
}
192+
193+
async fn handle(req: LambdaEventWithEndpoint) -> Result<LambdaResponse, Infallible> {
194+
let (request, _) = req.inner.into_parts();
195+
196+
let mut http_request = Request::builder()
197+
.method(request.http_method)
198+
.uri(request.path)
199+
.body(request.body.map(|b| Full::from(b.0)).unwrap_or_default())
200+
.expect("to build");
201+
202+
http_request.headers_mut().extend(request.headers);
203+
204+
let response = match req.endpoint.handle_with_options(
205+
http_request,
206+
HandleOptions {
207+
protocol_mode: ProtocolMode::RequestResponse,
208+
},
209+
) {
210+
Ok(res) => res,
211+
Err(err) => {
212+
debug!("Error when trying to handle incoming request: {err}");
213+
return Ok(err.into());
214+
}
215+
};
216+
217+
let (parts, body) = response.into_parts();
218+
// collect the response
219+
let body = match body.collect().await {
220+
Ok(body) => body.to_bytes(),
221+
Err(err) => {
222+
debug!("Error when trying to collect response body: {err}");
223+
return Ok(LambdaResponse::from_message(500, err));
224+
}
225+
};
226+
227+
let mut builder = LambdaResponse::builder()
228+
.status_code(parts.status.as_u16())
229+
.header(X_RESTATE_SERVER, X_RESTATE_SERVER_VALUE);
230+
231+
builder.headers.extend(parts.headers);
232+
233+
Ok(builder.body(body).build())
234+
}

src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ pub mod filter;
226226
pub mod http_server;
227227
#[cfg(feature = "hyper")]
228228
pub mod hyper;
229+
#[cfg(feature = "lambda")]
230+
pub mod lambda;
229231
pub mod serde;
230232

231233
/// Entry-point macro to define a Restate [Service](https://docs.restate.dev/concepts/services#services-1).
@@ -509,6 +511,9 @@ pub mod prelude {
509511
#[cfg(feature = "http_server")]
510512
pub use crate::http_server::HttpServer;
511513

514+
#[cfg(feature = "lambda")]
515+
pub use crate::lambda::LambdaEndpoint;
516+
512517
pub use crate::context::{
513518
CallFuture, Context, ContextAwakeables, ContextClient, ContextPromises, ContextReadState,
514519
ContextSideEffects, ContextTimers, ContextWriteState, HeaderMap, InvocationHandle,

0 commit comments

Comments
 (0)