From b313be24768a08812bdc27059ce17808cb7c4107 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 16 Sep 2023 10:33:29 -0700 Subject: [PATCH] add standalone web server support --- src/standalone_server_build.zig | 17 ++++++ src/universal_lambda.zig | 102 +++++++++++++++++++++++++++++--- src/universal_lambda_build.zig | 3 +- 3 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 src/standalone_server_build.zig diff --git a/src/standalone_server_build.zig b/src/standalone_server_build.zig new file mode 100644 index 0000000..881e27b --- /dev/null +++ b/src/standalone_server_build.zig @@ -0,0 +1,17 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +/// adds a build step to the build +/// +/// * standalone_server: This will run the handler as a standalone web server +/// +pub fn configureBuild(b: *std.build.Builder, exe: *std.Build.Step.Compile) !void { + _ = exe; + // We don't actually need to do much here. Basically we need a dummy step, + // but one which the user will select, which will allow our options mechanism + // to kick in + + // Package step + const standalone_step = b.step("standalone_server", "Run the function in its own web server"); + standalone_step.dependOn(b.getInstallStep()); +} diff --git a/src/universal_lambda.zig b/src/universal_lambda.zig index 7296dc2..2e964e6 100644 --- a/src/universal_lambda.zig +++ b/src/universal_lambda.zig @@ -8,6 +8,14 @@ const log = std.log.scoped(.universal_lambda); // TODO: Should this be union? pub const Context = struct {}; +const runFn = blk: { + switch (build_options.build_type) { + .standalone_server => break :blk runStandaloneServer, + .exe_run => break :blk runExe, + else => @compileError("Provider interface for " ++ @tagName(build_options.build_type) ++ " has not yet been implemented"), + } +}; + fn deinit() void { // if (client) |*c| c.deinit(); // client = null; @@ -19,19 +27,95 @@ fn deinit() void { /// 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: HandlerFn) !void { // TODO: remove inferred error set? - switch (build_options.build_type) { - .exe_run => try runExe(allocator, event_handler), - else => return error.NotImplemented, - } + try runFn(allocator, event_handler); } fn runExe(allocator: ?std.mem.Allocator, event_handler: HandlerFn) !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const gpa_alloc = allocator orelse gpa.allocator(); + var arena = std.heap.ArenaAllocator.init(allocator orelse std.heap.page_allocator); + defer arena.deinit(); - // TODO: set up an arena for this? Are we doing an arena for every type? + const aa = arena.allocator(); + + // We're setting up an arena allocator. While we could use a gpa and get + // some additional safety, this is now "production" runtime, and those + // things are better handled by unit tests const writer = std.io.getStdOut().writer(); - try writer.writeAll(try event_handler(gpa_alloc, "", .{})); + try writer.writeAll(try event_handler(aa, "", .{})); try writer.writeAll("\n"); } + +/// 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: HandlerFn) !void { + 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.*); + } + }; + } +} + +fn processRequest(aa: std.mem.Allocator, server: *std.http.Server, event_handler: 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, .{}) catch |e| brk: { + 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(); +} diff --git a/src/universal_lambda_build.zig b/src/universal_lambda_build.zig index b1d3482..498cbd2 100644 --- a/src/universal_lambda_build.zig +++ b/src/universal_lambda_build.zig @@ -7,7 +7,7 @@ const std = @import("std"); pub const BuildType = enum { awslambda, exe_run, - standalone_run, + standalone_server, cloudflare, flexilib, }; @@ -15,6 +15,7 @@ pub const BuildType = enum { pub fn configureBuild(b: *std.Build, exe: *std.Build.Step.Compile) !void { // Add steps try @import("lambdabuild.zig").configureBuild(b, exe); + try @import("standalone_server_build.zig").configureBuild(b, exe); // Add options module so we can let our universal_lambda know what // type of interface is necessary