Skip to content

Conversation

@kwsantiago
Copy link
Contributor

@kwsantiago kwsantiago commented Dec 17, 2025

Summary by CodeRabbit

  • New Features

    • Configurable logging level (config/env).
    • Added NIP-2 support.
    • New TCP/WebSocket server with improved connection handling and per-connection send/close controls.
  • Improvements

    • Enhanced graceful shutdown across components.
    • More robust spider/relay sync and reconnect behavior.
  • Chores

    • License updated to AGPL-3.0.
    • Version bumped to 0.1.3 and dependency updates.

✏️ Tip: You can customize this high-level summary in your review settings.

@kwsantiago kwsantiago self-assigned this Dec 17, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Walkthrough

License changed from LGPL to AGPL; HTTP/httpz-based server replaced by a new TCP WebSocket server; graceful shutdown propagated via atomic flags; write path refactored to callback-based writer; nostr dependency bumped; log-level config and NIP 2 were added.

Changes

Cohort / File(s) Summary
License & Docs
LICENSE, README.md
Replaced LGPL-2.1-or-later with AGPL-3.0; README updated to include NIP 2 and reflect license change.
Build & Manifest
build.zig, build.zig.zon
Removed httpz dependency; bumped version to 0.1.3; upgraded nostr dependency (v0.1.6 → v0.1.8); build references nostr/websocket directly.
New TCP WebSocket Server
src/tcp_server.zig
Added TcpServer implementation: WebSocket upgrade, connections, per-IP rate limiting, idle-pruning, graceful shutdown, send/connectionCount APIs.
Removed HTTP Server
src/server.zig
Deleted prior httpz-based Server, WsClient, and index handler — server implementation replaced by tcp_server.zig.
Write Path Abstraction
src/write_queue.zig, src/connection.zig
Replaced websocket-based writes with WriteFn callback and write_ctx; Connection now registers/uses direct writer callbacks.
Shutdown Propagation
src/handler.zig, src/main.zig, src/spider.zig
Introduced atomic shutdown flag usage across Handler, Spider, and main; added interruptible sleep and shouldRun checks; init signatures updated to accept shutdown.
Spider & Relay Logic
src/spider.zig
Made spider loops shutdown-aware; added global_shutdown, negentropy_supported, active_client; several functions now return bool for failure propagation; replaced fixed sleeps with interruptible variants.
Config / Logging
src/config.zig
Added log_level: []const u8 defaulting to "info"; load/env parsing extended to support logging.level and WISP_LOG_LEVEL.
Subscriptions / Connection Helpers
src/subscriptions.zig
Added withConnection, sendToConnection, and closeIdleConnection helper APIs for per-connection operations.
Nostr Exports & NIPs
src/nostr.zig, src/nip11.zig
Exported ws from nostr_lib; added NIP 2 to supported_nips array.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant TcpServer
    participant Subscriptions
    participant Handler
    participant WriteQueue

    Note over Client,TcpServer: New connection flow (HTTP upgrade → WebSocket)
    Client->>TcpServer: HTTP Upgrade / WebSocket handshake
    TcpServer->>Subscriptions: register connection (conn_id)
    TcpServer->>Handler: spawn per-conn read loop (messages)
    Client->>TcpServer: WebSocket Text Message (Nostr)
    TcpServer->>Handler: deliver message for processing
    Handler->>WriteQueue: enqueue reply (uses WriteFn callback)
    WriteQueue->>TcpServer: invoke WriteFn(ctx, data)
    TcpServer->>Client: send WebSocket frame
    Note over TcpServer,Handler: Shutdown path
    alt shutdown signaled
        Handler->>TcpServer: stop accepting / cleanup
        TcpServer->>Subscriptions: unregister connection
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped from HTTP to raw TCP air,
Callbacks now whisk frames with rabbitly care,
Shutdown bells rustle soft through each thread,
NIPs and AGPL snugly tuck us in bed. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Bug Fixes' is overly vague and generic. It does not convey the specific nature or scope of the changes, which include license migration (LGPL to AGPL), architectural refactoring (HTTP-based to TCP-based server), dependency updates, logging configuration, graceful shutdown mechanisms, and more. Use a more descriptive title that highlights the primary change. For example: 'Refactor server to TCP-based implementation with AGPL license and graceful shutdown' or focus on the most impactful change if one primary objective exists.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bug-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kwsantiago kwsantiago changed the title More Tests & Bug fixes Bug Fixes Dec 18, 2025
@kwsantiago kwsantiago marked this pull request as ready for review December 18, 2025 03:50
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/spider.zig (1)

120-134: closeClient is a no-op - connections won't be interrupted

The stop() method iterates over relay connections and calls closeClient(), but closeClient() at lines 956-958 does nothing (_ = self;). This means active websocket connections won't be forcibly closed during shutdown, and threads will only exit when they naturally timeout or encounter an error.

🔎 Consider implementing closeClient:
 fn closeClient(self: *RelayConn) void {
-    _ = self;
+    if (self.active_client) |client| {
+        client.close() catch {};
+    }
 }

Note: This requires the websocket client to support a close() method. If not available, you may need to use a different approach such as setting a flag that the read loop checks.

🧹 Nitpick comments (6)
src/tcp_server.zig (3)

95-108: Consider using explicit flags for idempotent deinit.

The capacity checks are fragile heuristics. A cleaner approach would use explicit boolean flags:

+    deinitialized: bool = false,
+
     pub fn deinit(self: *TcpServer) void {
+        if (self.deinitialized) return;
+        self.deinitialized = true;
+
         if (self.listener) |*l| {
             l.deinit();
             self.listener = null;
         }
-        // Only deinit limiters if they haven't been freed
-        // This makes deinit idempotent
-        if (self.conn_limiter.ip_buckets.capacity() > 0) {
-            self.conn_limiter.deinit();
-        }
-        if (self.ip_filter.whitelist.capacity() > 0) {
-            self.ip_filter.deinit();
-        }
+        self.conn_limiter.deinit();
+        self.ip_filter.deinit();
     }

127-142: Detached threads may cause resource leaks on shutdown.

Detached connection threads cannot be joined, and the fixed 200ms sleep may be insufficient for in-flight connections to complete cleanup. Consider using a tracked thread pool or wait group to ensure graceful shutdown:

// Alternative: track active connections with atomic counter
active_connections: std.atomic.Value(u32) = std.atomic.Value(u32).init(0),

// In handleConnection:
_ = self.active_connections.fetchAdd(1, .monotonic);
defer _ = self.active_connections.fetchSub(1, .monotonic);

// In run() shutdown:
while (self.active_connections.load(.acquire) > 0) {
    std.Thread.sleep(10 * std.time.ns_per_ms);
}

337-343: IPv6 addresses will include brackets in the extracted IP.

For IPv6 connections like [2001:db8::1]:8080, this extracts [2001:db8::1] (with brackets). This may cause issues with IP-based rate limiting if the same IP connects via different address formats. Consider stripping brackets for consistency:

     fn extractIp(address: net.Address, buf: []u8) []const u8 {
         const formatted = std.fmt.bufPrint(buf, "{any}", .{address}) catch return "unknown";
         if (std.mem.lastIndexOf(u8, formatted, ":")) |colon| {
-            return formatted[0..colon];
+            const ip = formatted[0..colon];
+            if (ip.len > 2 and ip[0] == '[' and ip[ip.len - 1] == ']') {
+                return ip[1 .. ip.len - 1];
+            }
+            return ip;
         }
         return formatted;
     }
src/write_queue.zig (2)

6-11: Naming may be misleading after refactor

The struct is named WriteQueue with methods like enqueue, but the implementation no longer uses a queue - it directly invokes the callback synchronously. Consider renaming to DirectWriter or similar to better reflect the current behavior.


56-58: queueDepth always returns 0

This method no longer reflects any meaningful state since there's no actual queue. It's effectively dead code. Consider removing it or documenting it as deprecated.

src/handler.zig (1)

711-760: Consider consolidating shutdown checks in send methods

All send* methods now have identical shutdown checks at the start. While this is safe, it creates repetition. Consider extracting a helper or documenting why each method needs its own check (e.g., if callers might call send methods directly without going through handle()).

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 039bae0 and e64e91b.

📒 Files selected for processing (15)
  • LICENSE (1 hunks)
  • README.md (1 hunks)
  • build.zig (2 hunks)
  • build.zig.zon (1 hunks)
  • src/config.zig (4 hunks)
  • src/connection.zig (4 hunks)
  • src/handler.zig (9 hunks)
  • src/main.zig (5 hunks)
  • src/nostr.zig (1 hunks)
  • src/server.zig (0 hunks)
  • src/spider.zig (26 hunks)
  • src/store.zig (3 hunks)
  • src/subscriptions.zig (1 hunks)
  • src/tcp_server.zig (1 hunks)
  • src/write_queue.zig (1 hunks)
💤 Files with no reviewable changes (1)
  • src/server.zig
🔇 Additional comments (27)
README.md (1)

65-65: The commit message explicitly states "memory fixes, WS upgrade, update LICENSE," so the license change from LGPL-2.1-or-later to AGPL-3.0 was already communicated in the title. The scope is transparent and intentional—not misaligned.

LICENSE (1)

1-664: AGPL-3.0 is appropriate for network relay software, but this is an initial license addition, not a replacement.

This is the first license file explicitly added to the project (commit e64e91b). Git history shows no previous LICENSE file existed. The visible dependencies (websocket.zig under MIT License) are compatible with AGPL-3.0. AGPL-3.0 is specifically designed for network server software like Nostr relays, so this licensing choice aligns well with the project's purpose.

Likely an incorrect or invalid review comment.

src/nostr.zig (1)

34-34: LGTM!

The ws export aligns with the new TCP/WebSocket implementation in tcp_server.zig, providing access to WebSocket handshake and frame handling utilities.

build.zig (1)

7-28: LGTM!

The build configuration correctly sets up the nostr and websocket dependencies and imports them into the executable module.

src/config.zig (1)

48-49: LGTM!

The log_level configuration field is properly integrated:

  • Default value of "info" is sensible
  • Config file parsing uses allocString for proper memory management
  • Environment variable handling follows the existing pattern

Also applies to: 91-91, 233-236, 298-298

src/store.zig (2)

506-531: LGTM!

The filter cloning logic correctly addresses potential lifetime issues where input filters could be freed while the iterator is in use. The error handling properly:

  • Returns an empty-filter iterator on allocation failure
  • Cleans up partially cloned filters on clone failure before returning
  • Sets both filters and owned_filters to the owned slice for consistent usage

684-691: LGTM!

The deinit properly cleans up owned filters by deinitializing each filter before freeing the array, preventing memory leaks.

src/subscriptions.zig (1)

59-86: LGTM!

The new helper methods are well-designed:

  • withConnection safely executes a callback while holding the shared lock
  • sendToConnection provides a convenient wrapper for sending data
  • closeIdleConnection correctly uses the shared lock to protect map access while relying on the connection's internal synchronization for write operations
src/tcp_server.zig (3)

15-56: LGTM!

The WsWriter correctly implements WebSocket frame encoding per RFC 6455, with proper handling for all payload length cases (7-bit, 16-bit extended, and 64-bit extended). The mutex ensures thread-safe writes.


306-331: LGTM!

The HTTP handler correctly serves NIP-11 relay information for clients requesting application/nostr+json and provides a simple HTML landing page for browsers.


229-247: Retract: no use-after-free risk exists with ws_writer.

The WriteQueue is not a background thread—it synchronously calls the write function via enqueue(). When stopWriteQueue() executes, it nullifies write_fn and write_ctx under mutex protection. Similarly, clearDirectWriter() nullifies the direct write pointers. By the time the defer block completes and the stack frame is destroyed, no code path can access ws_writer. Any concurrent calls to send() from other threads will either fail to find the connection (after removeConnection()) or encounter null pointers and return without dereferencing the stack variable.

The suggested heap allocation is unnecessary.

Likely an incorrect or invalid review comment.

src/connection.zig (3)

27-29: LGTM - Mutex-protected direct write mechanism

The introduction of direct_write_fn, direct_write_ctx, and write_mutex is a clean approach for decoupling from the websocket dependency. The mutex ensures thread-safe access to the callback.


104-112: Consider null check order consistency

The nested if checks for direct_write_fn then direct_write_ctx work correctly but could be simplified. If setDirectWriter always sets both atomically (which it does), the context should never be null when the function is set.

However, the current defensive approach is acceptable for robustness.


114-126: LGTM - Proper mutex protection for writer lifecycle

Both setDirectWriter and clearDirectWriter correctly acquire the mutex before modifying the callback state, ensuring thread-safe transitions.

src/write_queue.zig (1)

37-50: Synchronous write under mutex may block other operations

The enqueue method now holds the mutex while invoking write_fn, which could block if the write operation is slow. This differs from a true queue pattern where enqueueing is fast and writing happens asynchronously.

This is acceptable if write operations are expected to be fast, but be aware this could cause contention under high load.

src/main.zig (3)

4-9: LGTM - Log level configuration

The std_options configuration sets a reasonable default log level and suppresses verbose websocket logging to error level only.


104-104: Good fix for config file parsing

The check !std.mem.endsWith(u8, arg, ".toml") correctly prevents TOML config files from being interpreted as commands.


217-236: LGTM - Improved cleanup thread with responsive shutdown

The new implementation with 1-second check intervals allows the cleanup thread to respond to shutdown signals quickly while still performing hourly cleanup. This is a good pattern for graceful shutdown.

src/handler.zig (3)

151-172: LGTM - Shutdown flag integration

The addition of the shutdown flag to Handler and its initialization in init() is clean. This enables graceful shutdown throughout the handler.


376-386: Good fix - Subscription ID buffer copy

Copying sub_id_raw into sub_id_buf ensures the subscription ID remains valid throughout the function, even if the original message is deallocated. This prevents potential use-after-free issues.


437-444: LGTM - Consistent shutdown-aware iteration pattern

The pattern of checking shutdown.load(.acquire) before each iteration and using iter.next() catch null orelse break is applied consistently across all query loops. This ensures responsive shutdown without leaving operations half-complete.

Also applies to: 453-460, 470-477

src/spider.zig (6)

106-118: LGTM - Clean shutdown helpers

shouldRun() elegantly combines both local and global shutdown flags. interruptibleSleep() breaks long sleeps into 100ms chunks, enabling responsive shutdown while maintaining the intended delay.


380-381: Good pattern - Track active client for shutdown

Storing the client in conn.active_client during connection allows the shutdown path to potentially close it. This is the right approach, but it requires closeClient() to be implemented (see other comment).


425-493: LGTM - Catch-up with proper error propagation

The performCatchup function now returns bool to indicate success/failure, allowing the caller to handle connection loss appropriately. The read_error tracking and early return on connection loss is correct.


507-511: Good defensive limit for negentropy sync

Limiting negentropy sync to 100 pubkeys prevents potential performance issues or protocol limitations. Setting negentropy_supported = false ensures the relay falls back to catch-up sync.


584-604: LGTM - Graceful negentropy fallback

The logic correctly handles both timeout (no response) and explicit rejection (NEG-ERR) by disabling negentropy for the relay and allowing the connection to continue with regular sync.


789-814: Consider WouldBlock handling in readLoop

The read loop handles WouldBlock by continuing, which is correct for non-blocking reads. However, the readTimeout(1000) at line 792 should make the client block for up to 1 second, so WouldBlock should be less common. The handling is still correct.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/tcp_server.zig (3)

98-111: Consider a more explicit idempotency mechanism.

The capacity checks to prevent double-free are fragile and rely on internal implementation details of conn_limiter and ip_filter. If the underlying HashMap implementation changes, this could break.

Consider using a boolean flag or making the underlying types' deinit methods idempotent instead.

🔎 Alternative approach using a flag
     pub fn deinit(self: *TcpServer) void {
+        if (self.deinitialized) return;
+        self.deinitialized = true;
+        
         if (self.listener) |*l| {
             l.deinit();
             self.listener = null;
         }
-        // Only deinit limiters if they haven't been freed
-        // This makes deinit idempotent
-        if (self.conn_limiter.ip_buckets.capacity() > 0) {
-            self.conn_limiter.deinit();
-        }
-        if (self.ip_filter.whitelist.capacity() > 0) {
-            self.ip_filter.deinit();
-        }
+        self.conn_limiter.deinit();
+        self.ip_filter.deinit();
     }

Note: This would require adding a deinitialized: bool = false field to the struct.


130-135: Thread-per-connection model has scalability limits.

The current design spawns a detached thread for each connection. Under high connection load, this can exhaust system thread limits and degrade performance. Consider documenting the expected connection limits or evaluating alternative approaches (thread pool, async I/O) for future improvements.


232-238: Both direct writer and write queue use the same writer callback.

Lines 237-238 set up both a direct writer and a write queue pointing to the same WsWriter.write callback. This creates two code paths to the same underlying writer, which could be confusing for maintenance. Consider documenting why both paths are needed or consolidating to a single write path.

src/connection.zig (1)

102-108: Consider enforcing write_fn and write_ctx as a pair.

The nested checks allow write_fn and write_ctx to be set independently, but they should always be set or cleared together. If they get out of sync (e.g., write_fn set but write_ctx is null), the write is silently skipped, which could hide bugs.

Consider asserting they're both set or both null, or using a single optional tuple/struct.

🔎 Example with assertion
     pub fn sendDirect(self: *Connection, data: []const u8) void {
         if (self.direct_write_fn) |write_fn| {
             if (self.direct_write_ctx) |ctx| {
                 write_fn(ctx, data);
+            } else {
+                std.debug.assert(false); // write_fn set but ctx null - invariant violation
             }
         }
     }
src/write_queue.zig (1)

48-50: Misleading name: WriteQueue doesn't actually queue.

The WriteQueue struct name and enqueue method suggest queuing behavior, but queueDepth() returns 0 and enqueue() calls the write function immediately. This is essentially a write dispatcher, not a queue.

Consider renaming to WriteDispatcher or Writer to better reflect the actual behavior, or document why "queue" terminology is used despite direct execution.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6122d6 and 41abeb5.

📒 Files selected for processing (5)
  • build.zig.zon
  • src/connection.zig
  • src/handler.zig
  • src/tcp_server.zig
  • src/write_queue.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: benchmark
🔇 Additional comments (18)
build.zig.zon (2)

3-3: LGTM: Version bump aligns with the bug fixes.

The patch version increment is appropriate for the changes in this PR.


12-13: The nostr v0.1.8 update is safe to deploy. Release notes confirm no breaking changes, and all APIs used in the codebase (WebSocket handling, negentropy, filters) remain compatible. The update includes a bug fix for the negentropy buffer check in encodeVarInt, which improves stability.

src/tcp_server.zig (5)

26-58: WebSocket frame encoding looks correct.

The implementation properly follows RFC 6455 for WebSocket text frame encoding with appropriate length handling for all size ranges. The use of writev is efficient.


76-96: LGTM: Clean initialization with proper error handling.

The initialization properly sets up all components and propagates errors from IP filter loading.


147-171: LGTM: Idle timeout handling with proper shutdown awareness.

The implementation properly checks for shutdown and cleans up idle connections. The frequent shutdown checks ensure responsive teardown.


180-212: LGTM: Connection handling with proper filtering.

The connection handling properly checks IP filtering and rate limits before processing. The shutdown checks ensure resources aren't accessed after teardown.


259-307: LGTM: WebSocket frame handling follows protocol correctly.

The frame parsing, ping/pong handling, and close handshake properly implement the WebSocket protocol with appropriate error handling.

src/connection.zig (3)

27-28: LGTM: Clean separation of writer callback from WebSocket implementation.

The new fields properly decouple the connection from specific transport implementations using a generic callback mechanism.

Also applies to: 59-60


68-70: LGTM: Updated signature properly delegates to WriteQueue.

The method correctly passes the writer callback and context to the underlying write queue.


110-118: LGTM: Proper writer lifecycle management.

These methods correctly set and clear both the function pointer and context together, maintaining the invariant.

src/write_queue.zig (2)

4-4: LGTM: Clean callback type definition.

The WriteFn type and struct fields properly implement a generic write callback mechanism.

Also applies to: 7-8


31-42: LGTM: Proper error tracking for missing writer.

The method correctly tracks dropped messages when the writer isn't available and returns success status to the caller.

src/handler.zig (6)

151-151: LGTM: Shutdown flag properly integrated.

The atomic shutdown flag enables coordinated graceful shutdown across all handler methods.

Also applies to: 161-161, 171-171


175-176: LGTM: Early shutdown check prevents processing during teardown.

The shutdown check at the entry point ensures the handler doesn't process messages after shutdown begins.


430-430: LGTM: Shutdown checks prevent long-running queries during teardown.

The shutdown checks before each query path ensure that expensive database operations don't start after shutdown is initiated.

Also applies to: 444-444, 459-459


700-749: LGTM: Consistent shutdown guards across all send methods.

All send methods properly check the shutdown flag before attempting to send, preventing writes to connections during teardown.


680-698: LGTM: Multiple shutdown checks in long-running reconciliation.

The strategically placed shutdown checks ensure that expensive negentropy reconciliation can be interrupted promptly.


377-386: The stack-allocated sub_id buffer is safe in this context. The addSubscription() method explicitly copies the subscription ID via alloc.dupe(u8, sub_id) before storing it, and sendClosed() uses the ID immediately without storing a reference. No changes needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/tcp_server.zig (1)

139-148: Fixed 200ms cleanup delay remains insufficient.

This is the same issue flagged in the previous review. Detached threads cannot be joined, and the 200ms sleep is arbitrary—it may be too short if connections are processing long operations or have queued writes.

🧹 Nitpick comments (3)
src/tcp_server.zig (2)

24-27: Partial improvement: write failures now set a flag but remain invisible.

Setting the failed flag is better than the previous silent swallow, but write failures still have no diagnostic visibility. This makes debugging connection issues difficult.

Consider adding debug logging as previously suggested to aid troubleshooting.

🔎 Proposed enhancement
     fn write(ctx: *anyopaque, data: []const u8) void {
         const self: *WsWriter = @ptrCast(@alignCast(ctx));
         self.mutex.lock();
         defer self.mutex.unlock();
         if (self.failed) return;
-        self.writeWsFrame(data) catch {
+        self.writeWsFrame(data) catch |err| {
+            std.log.debug("WebSocket write failed: {}", .{err});
             self.failed = true;
         };
     }

109-114: Fragile idempotency check via capacity inspection.

Checking capacity() > 0 to determine whether a resource has been deinitialized is fragile and relies on internal implementation details. If the hash map implementation changes or if capacity can legitimately be zero, this logic breaks.

Consider an explicit boolean flag or state enum to track initialization status.

src/main.zig (1)

73-74: Allocator switched from GPA to c_allocator—loses leak detection.

Changing from GeneralPurposeAllocator to c_allocator improves runtime characteristics but disables memory leak detection. In production this is often acceptable, but during development or testing you lose visibility into memory issues.

Consider making this configurable via build mode (e.g., GPA for debug, c_allocator for release).

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41abeb5 and dcdd542.

📒 Files selected for processing (2)
  • src/main.zig
  • src/tcp_server.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: benchmark
🔇 Additional comments (5)
src/tcp_server.zig (1)

66-66: Unused allocator field.

The allocator field is stored in the TcpServer struct but never used in any of the methods. Only self.allocator.create(Connection) at line 242 and self.allocator.destroy(connection) at line 257 use it, which are valid uses.

Actually, reviewing more carefully: line 165 uses self.allocator.free(idle_conn_ids), line 242 uses self.allocator.create(Connection), and line 257 uses self.allocator.destroy(connection). So the allocator IS used.

src/main.zig (4)

4-9: Log configuration added for noise reduction.

The new std_options configuration suppresses verbose websocket logs while keeping info-level logs for the application. This is a sensible default for production use.


104-104: Command parsing fix prevents misinterpreting config files.

The !std.mem.endsWith(u8, arg, ".toml") guard prevents TOML configuration filenames from being treated as commands. This is a sensible fix for the argument parsing logic.


209-218: Cleanup thread now responsive to shutdown signals.

Replacing the fixed 1-hour sleep with a 1-second check interval and hour counter makes the cleanup thread respond to shutdown within 1 second instead of waiting up to an hour. This significantly improves shutdown latency.


166-176: Verify Handler, TcpServer, and Spider accept the shutdown parameter.

The code passes &g_shutdown to Handler.init, TcpServer.init, and Spider.init. All three functions have been updated to accept the shutdown parameter:

  • Handler.init accepts shutdown: *std.atomic.Value(bool)
  • TcpServer.init accepts shutdown: *std.atomic.Value(bool)
  • Spider.init accepts global_shutdown: *std.atomic.Value(bool)

Each function stores the parameter as a field for graceful shutdown coordination.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main.zig (1)

73-74: Clarify the allocator comment.

The comment "better memory behavior than GPA" is misleading. c_allocator doesn't necessarily have better memory behavior—it has different trade-offs:

  • GPA: Memory leak detection, double-free detection, safety checks, slight overhead
  • c_allocator: Direct malloc/free, no safety checks, no overhead

For production deployments where performance is prioritized over debugging, c_allocator is reasonable, but the comment should reflect this trade-off rather than implying superiority.

🔎 Suggested clarification
-    // Use c_allocator for production - better memory behavior than GPA
+    // Use c_allocator for production - lower overhead, no safety checks
     const allocator = std.heap.c_allocator;
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcdd542 and afbfdc2.

📒 Files selected for processing (2)
  • src/main.zig
  • src/tcp_server.zig
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tcp_server.zig
🔇 Additional comments (6)
src/main.zig (6)

15-15: LGTM: TcpServer migration.

The import update from server.zig to tcp_server.zig aligns with the architectural change described in the PR summary.


20-20: LGTM: Global server type updated.

The global server variable type correctly updated to match the TcpServer migration.


104-104: Good fix for TOML file handling.

The added check prevents TOML configuration files from being incorrectly parsed as commands, which fixes a command-line parsing bug.


166-166: LGTM: Graceful shutdown propagation.

The addition of &g_shutdown to Handler.init, TcpServer.init, and Spider.init implements consistent graceful shutdown signaling across all major components, as described in the PR objectives.

Also applies to: 168-168, 176-176


203-203: LGTM: Server run call simplified.

The removal of the &g_shutdown parameter from server.run() is appropriate since the shutdown reference is now passed during TcpServer.init and stored internally.


209-218: Good refactor for responsive shutdown.

The cleanup thread now checks every second instead of sleeping for an entire hour, which significantly improves shutdown responsiveness (from potentially 1 hour to 1 second maximum wait) while still performing cleanup hourly. The logic correctly implements this with 3600 checks * 1 second = 1 hour.

Comment on lines +4 to +9
pub const std_options = std.Options{
.log_level = .info,
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .websocket, .level = .err },
},
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the main.zig file and check lines 4-9
find . -name "main.zig" -type f

Repository: privkeyio/wisp

Length of output: 72


🏁 Script executed:

# Also search for config.zig
find . -name "config.zig" -type f

Repository: privkeyio/wisp

Length of output: 74


🏁 Script executed:

# Check the repository structure
git ls-files | head -20

Repository: privkeyio/wisp

Length of output: 408


🏁 Script executed:

# Check the size of src/main.zig
wc -l src/main.zig

Repository: privkeyio/wisp

Length of output: 74


🏁 Script executed:

# Check the size of src/config.zig
wc -l src/config.zig

Repository: privkeyio/wisp

Length of output: 76


🏁 Script executed:

# Read src/main.zig
cat -n src/main.zig

Repository: privkeyio/wisp

Length of output: 13940


🏁 Script executed:

# Read src/config.zig
cat -n src/config.zig

Repository: privkeyio/wisp

Length of output: 15801


🏁 Script executed:

# Search for any usage of config.log_level or log_level field access
rg "log_level" --type zig

Repository: privkeyio/wisp

Length of output: 348


config.log_level is parsed but never used; logging is controlled only by hardcoded std_options.log_level.

The config.zig file parses log_level from TOML files and environment variables (including WISP_LOG_LEVEL), but config.log_level is never referenced in the code. Meanwhile, std_options at compile time is hardcoded to .info, which is what actually controls the application's logging behavior. Users who attempt to configure logging through the config file or environment variables will find it has no effect.

Since std_options must be compile-time constant in Zig, the configurable log_level field is dead code. Either remove the field and its parsing logic, or implement runtime logging configuration through a different mechanism if dynamic log level control is needed.

🤖 Prompt for AI Agents
In src/main.zig around lines 4–9, std_options.log_level is hardcoded to .info
while config.log_level (parsed from TOML/env) is never used; this makes the
config value dead code. Fix by either (A) removing the log_level field and any
parsing/env handling in config.zig and related code paths so config no longer
exposes an unused option, or (B) keep the config field but apply it at runtime
instead of as a compile-time const: build std_options without a hardcoded
compile-time log level and, during program initialization, read config.log_level
and set the runtime logger/handler levels accordingly (use the std.log runtime
APIs to translate the parsed string/enum into the logger scope/global level and
apply it). Ensure all places that referenced the removed field are updated or
replaced with the runtime setter approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants