atproto relay implementation in zig zlay.waow.tech

exp-002: add GPA leak detection build option

build with -Duse_gpa=true to wrap all allocations in
GeneralPurposeAllocator. on clean shutdown (SIGTERM), GPA reports
every allocation that was never freed, with 8-frame stack traces.

zero overhead when disabled (default).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+58 -3
+39
EXPERIMENTS.md
··· 54 54 allocated and never freed. reverted to c_allocator. 55 55 56 56 **status**: reverted (2026-03-07) 57 + 58 + --- 59 + 60 + ## exp-002: GPA leak detection (2026-03-07) 61 + 62 + **goal**: identify exactly which allocations are leaking by using zig's 63 + GeneralPurposeAllocator as a wrapper. GPA tracks every alloc/free and reports 64 + unfreed allocations with stack traces on clean shutdown. 65 + 66 + **what changed**: 67 + - `build.zig`: added `-Duse_gpa=true` build option 68 + - `src/main.zig`: conditional GPA wrapper — when enabled, all allocations go through 69 + GPA backed by c_allocator. on SIGTERM, after all components deinit, GPA reports leaks. 70 + 71 + **how to use**: 72 + ```bash 73 + # build with GPA enabled (on the server): 74 + just zlay publish-remote ReleaseSafe --gpa 75 + # or manually: 76 + zig build -Doptimize=ReleaseSafe -Duse_gpa=true -Dtarget=x86_64-linux-gnu 77 + 78 + # let it run for 10-30 minutes, then: 79 + kubectl exec -n zlay deploy/zlay -- kill -TERM 1 80 + 81 + # read the leak report: 82 + kubectl logs -n zlay deploy/zlay --previous | grep -A5 "GPA" 83 + ``` 84 + 85 + **performance impact**: GPA adds a mutex + metadata tracking per alloc/free. 86 + expect ~2-5x slower throughput. this is a diagnostic build, not for production. 87 + 88 + **what to look for in output**: 89 + - GPA logs to stderr on deinit. each leaked allocation shows the stack trace 90 + of where it was allocated. 91 + - look for the most frequently repeated stack traces — those are the hot leak sites. 92 + 93 + **revert**: just rebuild without `-Duse_gpa=true` (default is false, zero overhead). 94 + 95 + **status**: active
+2
build.zig
··· 40 40 } 41 41 }); 42 42 build_options.addOption([]const u8, "optimize", @tagName(optimize)); 43 + const use_gpa = b.option(bool, "use_gpa", "use GeneralPurposeAllocator for leak detection (slow)") orelse false; 44 + build_options.addOption(bool, "use_gpa", use_gpa); 43 45 44 46 // relay executable 45 47 const relay_mod = b.createModule(.{
+17 -3
src/main.zig
··· 33 33 const collection_index_mod = @import("collection_index.zig"); 34 34 const backfill_mod = @import("backfill.zig"); 35 35 const api = @import("api.zig"); 36 + const build_options = @import("build_options"); 36 37 37 38 const log = std.log.scoped(.relay); 38 39 ··· 119 120 } 120 121 121 122 pub fn main() !void { 122 - // use libc allocator (glibc malloc). SmpAllocator was tested (exp-001) 123 - // and grew RSS even faster — the leak is genuine, not glibc fragmentation. 124 - const allocator = std.heap.c_allocator; 123 + // exp-002: optional GPA wrapper for leak detection. 124 + // build with -Duse_gpa=true to enable. on clean shutdown (SIGTERM), 125 + // GPA logs every allocation that was never freed, with stack traces. 126 + var gpa: std.heap.GeneralPurposeAllocator(.{ 127 + .stack_trace_frames = if (build_options.use_gpa) 8 else 0, 128 + }) = .init; 129 + defer if (build_options.use_gpa) { 130 + log.info("GPA: checking for leaks...", .{}); 131 + const status = gpa.deinit(); 132 + if (status == .leak) { 133 + log.err("GPA: leaks detected! see stderr for details", .{}); 134 + } else { 135 + log.info("GPA: no leaks detected", .{}); 136 + } 137 + }; 138 + const allocator = if (build_options.use_gpa) gpa.allocator() else std.heap.c_allocator; 125 139 126 140 // parse config from env 127 141 const port = parseEnvInt(u16, "RELAY_PORT", 3000);