universal-lambda-zig/src/universal_lambda.zig
Emil Lerch dcf00d8460
All checks were successful
AWS-Zig Build / build-zig-0.11.0-amd64-host (push) Successful in 1m50s
rework context
This commit is a significant refactor that fixes a number of things.

1. Replaces the optional helpers import (which was always weird) with a
   mandatory interface import on behalf of the application. This is
   actually a good thing as it enables all things below.
2. Removes the severely awkward union that was the lambda context. Now,
   no matter how your handler runs, a single object with everything you
   need is fully populated and (nearly always) works as you would
   expect. There is a slight exception to this with AWS Lambda that is
   related to the service itself. It is also possible that not
   everything is passed in correctly for Cloudflare, which, if true,
   will be addressed later.
3. Allows writes to the context object. These will be added to the
   output, but is implementation dependent, and I'm not 100% sure I've
   got it right yet, but the infrastructure is there.
4. Allows proper tests throughout this project.
5. Allows proper tests in the application too.
6. Removes the need for the handler to be public under flexlib. Flexilib
   handler registration now works just like everything else. Note,
   however, that flexilib is unique in that your handler registration
   function will return before the program ends. If this is important
   for resource cleanup, @import("build_options").build_type is your
   friend.
7. Request method can now be passed into console applications using -m
   or --method
2023-10-24 23:45:12 -07:00

119 lines
4.7 KiB
Zig

const std = @import("std");
const build_options = @import("build_options");
const flexilib = @import("flexilib-interface");
const interface = @import("universal_lambda_interface");
const log = std.log.scoped(.universal_lambda);
const runFn = blk: {
switch (build_options.build_type) {
.awslambda => break :blk @import("lambda.zig").run,
.standalone_server => break :blk runStandaloneServer,
.flexilib => break :blk @import("flexilib.zig").run,
.exe_run, .cloudflare => break :blk @import("console.zig").run,
}
};
/// Starts the universal lambda framework. Handler will be called when an event is processing.
/// Depending on the serverless system used, from a practical sense, this may not return.
///
/// If an allocator is not provided, an approrpriate allocator will be selected and used
/// This function is intended to loop infinitely. If not used in this manner,
/// make sure to call the deinit() function
pub fn run(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn) !u8 { // TODO: remove inferred error set?
return try runFn(allocator, event_handler);
}
/// Will create a web server and marshall all requests back to our event handler
/// To keep things simple, we'll have this on a single thread, at least for now
fn runStandaloneServer(allocator: ?std.mem.Allocator, event_handler: interface.HandlerFn) !u8 {
const alloc = allocator orelse std.heap.page_allocator;
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
var aa = arena.allocator();
var server = std.http.Server.init(aa, .{ .reuse_address = true });
defer server.deinit();
const address = try std.net.Address.parseIp("127.0.0.1", 8080); // TODO: allow config
try server.listen(address);
const server_port = server.socket.listen_address.in.getPort();
var uri: ["http://127.0.0.1:99999".len]u8 = undefined;
_ = try std.fmt.bufPrint(&uri, "http://127.0.0.1:{d}", .{server_port});
log.info("server listening at {s}", .{uri});
// No threads, maybe later
//log.info("starting server thread, tid {d}", .{std.Thread.getCurrentId()});
while (true) {
defer {
if (!arena.reset(.{ .retain_with_limit = 1024 * 1024 })) {
// reallocation failed, arena is degraded
log.warn("Arena reset failed and is degraded. Resetting arena", .{});
arena.deinit();
arena = std.heap.ArenaAllocator.init(alloc);
aa = arena.allocator();
}
}
processRequest(aa, &server, event_handler) catch |e| {
log.err("Unexpected error processing request: {any}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
};
}
return 0;
}
fn processRequest(aa: std.mem.Allocator, server: *std.http.Server, event_handler: interface.HandlerFn) !void {
var res = try server.accept(.{ .allocator = aa });
defer {
_ = res.reset();
if (res.headers.owned and res.headers.list.items.len > 0) res.headers.deinit();
res.deinit();
}
try res.wait(); // wait for client to send a complete request head
const errstr = "Internal Server Error\n";
var errbuf: [errstr.len]u8 = undefined;
@memcpy(&errbuf, errstr);
var response_bytes: []const u8 = errbuf[0..];
var body =
if (res.request.content_length) |l|
try res.reader().readAllAlloc(aa, @as(usize, l))
else
try aa.dupe(u8, "");
// no need to free - will be handled by arena
response_bytes = event_handler(aa, body, .{ .web_request = &res }) catch |e| brk: {
if (res.status.class() == .success) res.status = .internal_server_error;
// TODO: more about this particular request
log.err("Unexpected error from executor processing request: {any}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
break :brk "Unexpected error generating request to lambda";
};
res.transfer_encoding = .{ .content_length = response_bytes.len };
try res.do();
_ = try res.writer().writeAll(response_bytes);
try res.finish();
}
test {
std.testing.refAllDecls(@This());
// if (builtin.os.tag == .wasi) return error.SkipZigTest;
if (@import("builtin").os.tag != .wasi) {
// these use http
std.testing.refAllDecls(@import("lambda.zig"));
std.testing.refAllDecls(@import("cloudflaredeploy.zig"));
std.testing.refAllDecls(@import("CloudflareDeployStep.zig"));
}
std.testing.refAllDecls(@import("console.zig"));
std.testing.refAllDecls(@import("flexilib.zig"));
// The following do not currently have tests
// TODO: Do we want build files here too?
}