diff --git a/demo/src/app/middleware/DemoMiddleware.zig b/demo/src/app/middleware/DemoMiddleware.zig index a6758d2..d390535 100644 --- a/demo/src/app/middleware/DemoMiddleware.zig +++ b/demo/src/app/middleware/DemoMiddleware.zig @@ -20,11 +20,11 @@ const jetzig = @import("jetzig"); /// can also be modified. my_custom_value: []const u8, -const Self = @This(); +const DemoMiddleware = @This(); /// Initialize middleware. -pub fn init(request: *jetzig.http.Request) !*Self { - var middleware = try request.allocator.create(Self); +pub fn init(request: *jetzig.http.Request) !*DemoMiddleware { + var middleware = try request.allocator.create(DemoMiddleware); middleware.my_custom_value = "initial value"; return middleware; } @@ -32,7 +32,7 @@ pub fn init(request: *jetzig.http.Request) !*Self { /// Invoked immediately after the request is received but before it has started processing. /// Any calls to `request.render` or `request.redirect` will prevent further processing of the /// request, including any other middleware in the chain. -pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void { +pub fn afterRequest(self: *DemoMiddleware, request: *jetzig.http.Request) !void { try request.server.logger.DEBUG( "[DemoMiddleware:afterRequest] my_custom_value: {s}", .{self.my_custom_value}, @@ -42,7 +42,11 @@ pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void { /// Invoked immediately before the response renders to the client. /// The response can be modified here if needed. -pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { +pub fn beforeResponse( + self: *DemoMiddleware, + request: *jetzig.http.Request, + response: *jetzig.http.Response, +) !void { try request.server.logger.DEBUG( "[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}", .{ self.my_custom_value, @tagName(response.status_code) }, @@ -51,7 +55,11 @@ pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jet /// Invoked immediately after the response has been finalized and sent to the client. /// Response data can be accessed for logging, but any modifications will have no impact. -pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { +pub fn afterResponse( + self: *DemoMiddleware, + request: *jetzig.http.Request, + response: *jetzig.http.Response, +) !void { _ = self; _ = response; try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{}); @@ -60,6 +68,6 @@ pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetz /// Invoked after `afterResponse` is called. Use this function to do any clean-up. /// Note that `request.allocator` is an arena allocator, so any allocations are automatically /// freed before the next request starts processing. -pub fn deinit(self: *Self, request: *jetzig.http.Request) void { +pub fn deinit(self: *DemoMiddleware, request: *jetzig.http.Request) void { request.allocator.destroy(self); } diff --git a/demo/src/main.zig b/demo/src/main.zig index 941ec5a..e89b27a 100644 --- a/demo/src/main.zig +++ b/demo/src/main.zig @@ -37,6 +37,12 @@ pub const jetzig_options = struct { // performance. Log messages should aim to fit in the message buffer. // pub const log_message_buffer_len: usize = 4096; + // Maximum log pool size. When a log buffer is no longer required it is returned to a pool + // for recycling. When logging i/o is slow, a high volume of requests will result in this + // pool growing. When the pool size reaches the maximum value defined here, log events are + // freed instead of recycled. + // pub const max_log_pool_len: usize = 256; + // Path relative to cwd() to serve public content from. Symlinks are not followed. // pub const public_content_path = "public"; diff --git a/src/jetzig.zig b/src/jetzig.zig index 489d82e..fc5026d 100644 --- a/src/jetzig.zig +++ b/src/jetzig.zig @@ -88,6 +88,12 @@ pub const config = struct { /// performance. Log messages should aim to fit in the message buffer. pub const log_message_buffer_len: usize = 4096; + /// Maximum log pool size. When a log buffer is no longer required it is returned to a pool + /// for recycling. When logging i/o is slow, a high volume of requests will result in this + /// pool growing. When the pool size reaches the maximum value defined here, log events are + /// freed instead of recycled. + pub const max_log_pool_len: usize = 256; + /// Path relative to cwd() to serve public content from. Symlinks are not followed. pub const public_content_path = "public"; diff --git a/src/jetzig/loggers/LogQueue.zig b/src/jetzig/loggers/LogQueue.zig index 27343ce..b49edd6 100644 --- a/src/jetzig/loggers/LogQueue.zig +++ b/src/jetzig/loggers/LogQueue.zig @@ -4,6 +4,7 @@ const builtin = @import("builtin"); const jetzig = @import("../../jetzig.zig"); const buffer_size = jetzig.config.get(usize, "log_message_buffer_len"); +const max_pool_len = jetzig.config.get(usize, "max_log_pool_len"); const List = std.DoublyLinkedList(Event); const Buffer = [buffer_size]u8; @@ -41,8 +42,8 @@ const Event = struct { pub fn init(allocator: std.mem.Allocator) LogQueue { return .{ .allocator = allocator, - .node_allocator = std.heap.MemoryPool(List.Node).init(allocator), - .buffer_allocator = std.heap.MemoryPool(Buffer).init(allocator), + .node_allocator = initPool(allocator, List.Node), + .buffer_allocator = initPool(allocator, Buffer), .list = List{}, .condition = std.Thread.Condition{}, .condition_mutex = std.Thread.Mutex{}, @@ -58,11 +59,6 @@ pub fn deinit(self: *LogQueue) void { self.node_pool.deinit(); self.buffer_pool.deinit(); - while (self.list.popFirst()) |node| { - if (node.data.ptr) |ptr| self.allocator.free(ptr); - self.allocator.destroy(node.data.message); - } - self.buffer_allocator.deinit(); self.node_allocator.deinit(); @@ -211,7 +207,12 @@ pub const Reader = struct { if (self.queue.writer.position < self.queue.buffer_pool.items.len) { self.queue.buffer_pool.items[self.queue.writer.position] = event.message; } else { - try self.queue.buffer_pool.append(event.message); // TODO: Prevent unlimited inflation + if (self.queue.buffer_pool.items.len >= max_pool_len) { + self.queue.buffer_allocator.destroy(@alignCast(event.message)); + self.queue.writer.position += 1; + } else { + try self.queue.buffer_pool.append(event.message); + } } } @@ -253,7 +254,12 @@ fn popFirst(self: *LogQueue) !?Event { if (self.position < self.node_pool.items.len) { self.node_pool.items[self.position] = node; } else { - try self.node_pool.append(node); // TODO: Set a maximum here to avoid never-ending inflation. + if (self.node_pool.items.len >= max_pool_len) { + self.node_allocator.destroy(node); + self.position += 1; + } else { + try self.node_pool.append(node); + } } return value; } else { @@ -261,6 +267,10 @@ fn popFirst(self: *LogQueue) !?Event { } } +fn initPool(allocator: std.mem.Allocator, T: type) std.heap.MemoryPool(T) { + return std.heap.MemoryPool(T).initPreheated(allocator, max_pool_len) catch @panic("OOM"); +} + fn writeWindows(file: std.fs.File, writer: anytype, event: Event) !void { var info: std.os.windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; const config: std.io.tty.Config = if (std.os.windows.kernel32.GetConsoleScreenBufferInfo( diff --git a/src/jetzig/middleware/HtmxMiddleware.zig b/src/jetzig/middleware/HtmxMiddleware.zig index 7334077..8c56431 100644 --- a/src/jetzig/middleware/HtmxMiddleware.zig +++ b/src/jetzig/middleware/HtmxMiddleware.zig @@ -15,7 +15,7 @@ pub fn init(request: *jetzig.http.Request) !*Self { /// content rendered directly by the view function. pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void { _ = self; - if (request.getHeader("HX-Target")) |target| { + if (request.headers.get("HX-Target")) |target| { try request.server.logger.DEBUG( "[middleware-htmx] htmx request detected, disabling layout. (#{s})", .{target},