Skip to content

Commit 6904685

Browse files
committed
Support async handlers
1 parent 9d9c889 commit 6904685

4 files changed

Lines changed: 118 additions & 104 deletions

File tree

README.md

Lines changed: 78 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Write _AWS Lambda_ functions in the Zig programming language to achieve blazing
2323

2424
### Benchmark
2525
Using zig allows creating small and fast functions.<br />
26-
Minimal [Hello World demo](#hello-world) (_arm64_ 256 MiB, Amazon Linux 2023):
26+
Minimal [Hello World demo](#hello-world) (arm64, 256 MiB, Amazon Linux 2023):
2727

2828
- ❄️ `~13ms` cold start invocation duration
2929
-`~1.5ms` warm invocation duration
@@ -36,16 +36,13 @@ Minimal [Hello World demo](#hello-world) (_arm64_ 256 MiB, Amazon Linux 2023):
3636
> comprehensive Zig-based AWS cloud solution.
3737
3838
## Quick Start
39-
1. Add a dependency to your project:
39+
1. Add a dependency to your project (replace `VERSION` with the desired version tag):
4040
```console
41-
zig fetch --save git+https://github.com/by-nir/aws-lambda-zig#0.2.1
41+
zig fetch --save git+https://github.com/by-nir/aws-lambda-zig#VERSION
4242
```
43-
2. Configure the [executable build](#build-script):
44-
- Named the executable _bootstrap_.
45-
- Import the Lambda Runtime module.
46-
3. Implement a handler (exmaples: [minimal handler](#minimal-handler), [streaming handler](#streaming-handler)).
47-
4. Build the handler executable for _Linux_ with either _x86_ or _arm_ architecture.<br />
48-
If you used the library provided [managed architecture target](#build-target) utility, specify the architecture:
43+
2. Configure the build script.
44+
3. Implement a handler function (either an event handler or a streaming handler).
45+
4. Build a handler executable for the preferred target architecture:
4946
```console
5047
zig build --release -Darch=x86
5148
zig build --release -Darch=arm
@@ -58,22 +55,19 @@ Minimal [Hello World demo](#hello-world) (_arm64_ 256 MiB, Amazon Linux 2023):
5855
- Configure it with _Amazon Linux 2023_ or other **OS-only runtime**.
5956
- Use you prefered deployment method: console, CLI, SAM or any CI solution.
6057

61-
### Build Script
58+
### Example Build Script
6259
```zig
6360
const std = @import("std");
6461
const lambda = @import("aws-lambda");
6562

6663
pub fn build(b: *std.Build) void {
67-
// Add an architecture confuration option and resolves a target query.
68-
const target = lambda.resolveTargetQuery(b, lambda.archOption(b));
69-
70-
// Alternatively, hard-code an architecture.
71-
// const target = lambda.resolveTargetQuery(b, .arm);
72-
7364
const optimize = b.standardOptimizeOption(.{
7465
.preferred_optimize_mode = .ReleaseFast,
7566
});
7667

68+
// Add an architecture confuration option and resolves a target query.
69+
const target = lambda.resolveTargetQuery(b, lambda.archOption(b));
70+
7771
// Add the handler executable.
7872
const exe = b.addExecutable(.{
7973
// Note the executable name must be "bootstrap".
@@ -90,7 +84,7 @@ pub fn build(b: *std.Build) void {
9084
}
9185
```
9286

93-
### Minimal Handler
87+
### Example Event Handler
9488
```zig
9589
const lambda = @import("aws-lambda");
9690
@@ -110,73 +104,54 @@ fn handler(
110104
}
111105
```
112106

113-
### Streaming Handler
114-
```zig
115-
const lambda = @import("aws-lambda");
116-
117-
// Entry point for the Lambda function.
118-
pub fn main() void {
119-
// Bind the handler to the runtime:
120-
lambda.handleStream(handler, .{});
121-
}
122-
123-
// Eeach event is processed separetly the handler function.
124-
// The function must have the following signature:
125-
fn handler(
126-
ctx: lambda.Context, // Metadata and utilities
127-
event: []const u8, // Raw event payload (JSON)
128-
stream: lambda.Stream, // Response stream
129-
) !void {
130-
// Start streaming the response for a given content type.
131-
try stream.open("text/event-stream");
132-
133-
// Append to the streaming buffer.
134-
try stream.write("data: Message");
135-
try stream.writeFmt(" number {d}\n\n", .{1});
136-
137-
// Publish the buffer to the client.
138-
try stream.flush();
139-
140-
// Wait for half a second.
141-
std.time.sleep(500_000_000);
142-
143-
// Append to streaming buffer and immediatly publish to the client.
144-
try stream.publish("data: Message number 2\n\n");
145-
std.time.sleep(100_000_000);
107+
## Documentation
146108

147-
// Publish also supports formatting.
148-
try stream.publishFmt("data: Message number {d}\n\n", .{3});
109+
### Build
110+
This library provides a runtime module that handles the Lambda lifecycle and communication with the execution environment.
111+
To use it, follow the following requirements:
112+
- Import the Lambda Runtime module this library provides and wrap a handler function with it.
113+
- Build an executable named _bootstrap_ and archive it in a Zip file.
114+
- Use _Amazon Linux 2023_ or any other supported OS-only runtime.
149115

150-
// Optionally close the stream.
151-
try stream.close();
152-
}
153-
```
154-
155-
## Documentation
116+
#### Managed Target
117+
AWS Lambda supports two architectures: _x86_64_ and _arm64_ based on _Graviton2_. In order to build the event handler correctly and to squeeze the best performance, the build target must be configured accordingly.
156118

157-
### Build Target
158-
AWS Lambda provides two architectures for the runtime environment: _x86_64_ and _arm64_ based on _Graviton2_. In order to build the event handler correctly and to squeeze the best performance, the build target must be configured accordingly.
119+
The mananged target resolver sets the optimal operating system, architecture and specific CPU supported features. Call `lambda.resolveTargetQuery(*std.Build, arch)` to resolve the target for the given architecture (either `.x86` or `.arm`).
159120

160-
The mananged target resolver sets the operating system, architecture and specific CPU supported features. It can be utilizes in the build script in two forms:
121+
To add a CLI configuration option call `lambda.archOption(*std.Build)` the following and pass the result to `lambda.resolveTargetQuery`.
122+
It can then by set through `-Darch=x86` or `-Darch=arm` (defaults to _x86_ when to manualy used).
161123

124+
#### Example Build Script
162125
```zig
163126
const std = @import("std");
164127
const lambda = @import("aws-lambda");
165128
166129
pub fn build(b: *std.Build) void {
167-
// Option A – CLI confuration:
168-
const arch = lambda.archOption(b);
130+
const optimize = b.standardOptimizeOption(.{
131+
.preferred_optimize_mode = .ReleaseFast,
132+
});
133+
134+
// Add an architecture CLI option (or hard code either `.x86` or `.arm`)
135+
const arch: lambda.Arch = lambda.archOption(b);
136+
137+
// Managed architecture target resolver
169138
const target = lambda.resolveTargetQuery(b, arch);
170-
171-
// Option B – Hard-coded architecture (`.x86` or `.arm`):
172-
const target = lambda.resolveTargetQuery(b, .arm);
173139
174-
// ···
140+
// Add the handler’s executable
141+
const exe = b.addExecutable(.{
142+
.name = "bootstrap", // The executable name must be "bootstrap"!
143+
.root_source_file = b.path("src/main.zig"),
144+
.target = target,
145+
.optimize = optimize,
146+
});
147+
b.installArtifact(exe);
148+
149+
// Import the runtime module
150+
const runtime = b.dependency("aws-lambda", .{}).module("lambda");
151+
exe.root_module.addImport("aws-lambda", runtime);
175152
}
176153
```
177154

178-
If `lambda.archOption(b)` is used, the configuration option `-Darch=x86|arm` is added (defaults to _x86_).
179-
180155
### Event Handler
181156
The event handler is the entry point for the Lambda function.
182157

@@ -188,23 +163,32 @@ Since the library manages the lifecycle, it expects the handler to have a specif
188163
const lambda = @import("aws-lambda");
189164
190165
// Entry point for the Lambda function.
166+
// Eeach event is processed separetly the handler function.
191167
pub fn main() void {
192168
// Bind the handler to the runtime:
193-
lambda.handle(handler, .{});
169+
lambda.handle(handlerSync, .{});
170+
171+
// Alternatively, for asynchronous handlers:
172+
// lambda.handleAsync(handlerAsync, .{});
194173
}
195174
196-
// Eeach event is processed separetly the handler function.
197-
// The function must have the following signature:
198-
fn handler(
175+
fn handlerSync(
199176
ctx: lambda.Context, // Metadata and utilities
200177
event: []const u8, // Raw event payload (JSON)
201178
) ![]const u8 {
202-
// The handler’s implement should process the `event` payload and return a response payload.
179+
// Process the `event` payload and return a response payload.
203180
return switch(payload.len) {
204181
0 => "Empty payload.",
205-
else => payload,
182+
else => event,
206183
};
207184
}
185+
186+
fn handlerAsync(
187+
ctx: lambda.Context, // Metadata and utilities
188+
event: []const u8, // Raw event payload (JSON)
189+
) !void {
190+
// Process the `event` payload...
191+
}
208192
```
209193

210194
#### Errors & Logging
@@ -295,8 +279,25 @@ fn handler(
295279
// Start streaming the response for a given content type.
296280
try stream.open("text/event-stream");
297281
298-
// Publish a message to the client.
299-
try stream.publish("data: Hello, world!\n\n");
282+
// Append to the streaming buffer.
283+
try stream.write("data: Message");
284+
try stream.writeFmt(" number {d}\n\n", .{1});
285+
286+
// Publish the buffer to the client.
287+
try stream.flush();
288+
289+
// Wait for half a second.
290+
std.time.sleep(500_000_000);
291+
292+
// Append to streaming buffer and immediatly publish to the client.
293+
try stream.publish("data: Message number 2\n\n");
294+
std.time.sleep(100_000_000);
295+
296+
// Publish also supports formatting.
297+
try stream.publishFmt("data: Message number {d}\n\n", .{3});
298+
299+
// Optionally close the stream.
300+
try stream.close();
300301
}
301302
```
302303

src/root.zig

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
1-
const serve = @import("runtime/serve.zig");
2-
pub const Options = serve.ServerOptions;
3-
41
const hdl = @import("runtime/handle.zig");
52
pub const Stream = hdl.Stream;
6-
pub const handle = hdl.handleBuffered;
3+
pub const Options = hdl.Options;
4+
pub const Context = hdl.Context;
5+
pub const handle = hdl.handleSync;
6+
pub const handleAsync = hdl.handleAsync;
77
pub const handleStream = hdl.handleStreaming;
88

9-
const ctx = @import("runtime/context.zig");
10-
pub const Context = ctx.Context;
11-
129
pub const log = @import("utils/log.zig").handler;
1310

1411
test {
1512
_ = @import("utils/Http.zig");
1613
_ = @import("runtime/api.zig");
17-
_ = ctx;
14+
_ = @import("runtime/serve.zig");
15+
_ = @import("runtime/context.zig");
1816
_ = hdl;
19-
_ = serve;
2017
}

src/runtime/handle.zig

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
const std = @import("std");
2+
const log = @import("../utils/log.zig").runtime;
23
const srv = @import("serve.zig");
34
const Server = srv.Server;
4-
const Context = @import("context.zig").Context;
5-
const log = @import("../utils/log.zig").runtime;
5+
pub const Options = srv.Options;
6+
pub const Context = @import("context.zig").Context;
67

7-
const HandlerFn = *const fn (context: Context, event: []const u8) anyerror![]const u8;
8-
const StreamHandlerFn = *const fn (context: Context, event: []const u8, stream: Stream) anyerror!void;
8+
const AsyncHandlerFn = *const fn (context: Context, event: []const u8) anyerror!void;
9+
const SyncHandlerFn = *const fn (context: Context, event: []const u8) anyerror![]const u8;
10+
const StreamingHandlerFn = *const fn (context: Context, event: []const u8, stream: Stream) anyerror!void;
911

10-
fn serve(options: srv.ServerOptions, processor: srv.ProcessorFn) void {
12+
fn serve(options: srv.Options, processorFn: srv.ProcessorFn) void {
1113
// Initialize the server.
1214
// If it fails we can return since the server already logged the error.
1315
var server: Server = undefined;
1416
Server.init(&server, options) catch return;
1517

1618
// Run the event loop until an error occurs or the process is terminated.
1719
defer server.deinit();
18-
server.listen(processor);
20+
server.listen(processorFn);
1921
}
2022

21-
/// The entry point for an AWS Lambda function.
22-
///
23-
/// Accepts a const reference to a handler function that will process each event separetly.
24-
pub fn handleBuffered(comptime handler: HandlerFn, options: srv.ServerOptions) void {
23+
/// The entry point for a synchronous AWS Lambda function.
24+
/// Accepts a handler function that will process each event separetly.
25+
pub fn handleSync(comptime handlerFn: SyncHandlerFn, options: srv.Options) void {
2526
serve(options, struct {
2627
fn f(server: *Server, context: Context, event: []const u8) srv.InvocationResult {
27-
if (handler(context, event)) |output| {
28+
if (handlerFn(context, event)) |output| {
2829
server.respondSuccess(output) catch return .abort;
2930
} else |e| {
3031
server.respondFailure(e, @errorReturnTrace()) catch return .abort;
@@ -35,16 +36,31 @@ pub fn handleBuffered(comptime handler: HandlerFn, options: srv.ServerOptions) v
3536
}.f);
3637
}
3738

38-
/// The entry point for a streaming AWS Lambda function.
39-
///
40-
/// Accepts a const reference to a streaming handler function that will process each event separetly.
41-
pub fn handleStreaming(comptime handler: StreamHandlerFn, options: srv.ServerOptions) void {
39+
/// The entry point for an asynchronous AWS Lambda function.
40+
/// Accepts a handler function that will process each event separetly.
41+
pub fn handleAsync(comptime handlerFn: AsyncHandlerFn, options: srv.Options) void {
42+
serve(options, struct {
43+
fn f(server: *Server, context: Context, event: []const u8) srv.InvocationResult {
44+
if (handlerFn(context, event)) {
45+
server.respondSuccess("") catch return .abort;
46+
} else |e| {
47+
server.respondFailure(e, @errorReturnTrace()) catch return .abort;
48+
}
49+
50+
return .success;
51+
}
52+
}.f);
53+
}
54+
55+
/// The entry point for a response streaming AWS Lambda function.
56+
/// Accepts a streaming handler function that will process each event separetly.
57+
pub fn handleStreaming(comptime handlerFn: StreamingHandlerFn, options: srv.Options) void {
4258
serve(options, struct {
4359
fn f(server: *Server, context: Context, event: []const u8) srv.InvocationResult {
4460
var stream: Server.Stream = undefined;
4561
var stream_ctx: StreamingContext = .{};
4662

47-
handler(context, event, .{
63+
handlerFn(context, event, .{
4864
.server = server,
4965
.stream = &stream,
5066
.context = &stream_ctx,

src/runtime/serve.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const HttpClient = @import("../utils/Http.zig");
66
const environ = @import("../utils/environ.zig");
77
const log = @import("../utils/log.zig").runtime;
88

9-
pub const ServerOptions = struct {};
9+
pub const Options = struct {};
1010

1111
pub const ProcessorFn = *const fn (
1212
server: *Server,
@@ -28,7 +28,7 @@ pub const Server = struct {
2828
env: std.process.EnvMap,
2929
request_id: []const u8 = "",
3030

31-
pub fn init(self: *Server, _: ServerOptions) !void {
31+
pub fn init(self: *Server, _: Options) !void {
3232
errdefer self.* = undefined;
3333

3434
self.gpa = std.heap.GeneralPurposeAllocator(.{}){};

0 commit comments

Comments
 (0)