Skip to content

Conversation

@lalinsky
Copy link
Owner

@lalinsky lalinsky commented Sep 9, 2025

Summary

  • Auto-generate inbox for deliver_subject when not provided in push subscriptions
  • Match the behavior of the official Go NATS client for better user experience
  • Maintain backward compatibility with explicit deliver_subject configuration

Changes

  • Updated subscribe() method to auto-generate deliver_subject if not provided
  • Updated subscribeSync() method to auto-generate deliver_subject if not provided
  • Added proper memory management with errdefer cleanup to prevent leaks
  • Updated test expectations to reflect new behavior

Test plan

  • All existing tests pass
  • Memory leak test verifies proper cleanup on error conditions
  • Push subscription creation works with and without explicit deliver_subject

This matches the Go client behavior where push subscriptions automatically generate an inbox when no deliver_subject is specified, providing a more convenient API.

Push subscriptions now automatically generate an inbox for deliver_subject
when none is provided, matching the behavior of the official Go client.
This provides a more convenient API while maintaining compatibility with
explicit deliver_subject configuration.

Changes:
- Updated subscribe() to auto-generate deliver_subject if not provided
- Updated subscribeSync() to auto-generate deliver_subject if not provided
- Fixed memory management with proper errdefer cleanup
- Updated test to expect StreamNotFound instead of MissingDeliverSubject
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 9, 2025

Walkthrough

Auto-generate deliver_subject for push subscriptions when absent, create push consumers before subscribing, wrap push handlers to convert messages and intercept STATUS_CONTROL (idle heartbeat / flow-control) messages, mirror behavior in subscribeSync, free generated inboxes, and add a deliver_subject_owned field to JetStreamSubscription. Tests updated and handler signature adjusted.

Changes

Cohort / File(s) Summary of changes
Push subscription core & status handling
src/jetstream.zig
Auto-generate deliver_subject (inbox.newInbox) when null and free it after use; build and validate push_config, remove pull-only fields, call addConsumer before subscribing; subscribe to the computed deliver subject and wrap the handler with JSHandler.wrappedHandler that converts messages to JetStreamMessage, filters STATUS_CONTROL messages, and delegates to user handler; add handleStatusMessage to ignore idle heartbeats and reply to flow-control requests; add deliver_subject_owned field to JetStreamSubscription.
Synchronous subscribe path
src/jetstream.zig (same file)
Mirror auto-generation and consumer creation flow in subscribeSync, removing prior requirement for an explicit deliver subject.
Tests: push subscription behavior & handler signature
tests/jetstream_push_test.zig
Update test to use config_without_deliver_subject (new durable) and expect StreamNotFound instead of MissingDeliverSubject; change dummy handler signature to accept only JetStreamMessage; adjust subscribe invocation and assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly highlights the principal change—auto-generation of the deliver_subject inbox for push subscriptions—without extraneous details or noise, making it clear and specific.
Description Check ✅ Passed The description closely aligns with the changeset by summarizing the auto-generation of deliver_subject, detailing code updates, and outlining the test plan, ensuring it remains focused and relevant.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/auto-generate-deliver-subject

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.

Copy link
Contributor

@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/jetstream.zig (1)

1126-1138: Prevent resource leak if allocation fails after subscribe

If create(JetStreamSubscription) fails, the underlying NATS subscription stays active. Add errdefer to unsubscribe, and store the owned subject.

-        const subscription = try self.nc.subscribe(deliver_subject, JSHandler.wrappedHandler, .{ self.nc, args });
+        const subscription = try self.nc.subscribe(deliver_subject, JSHandler.wrappedHandler, .{ self.nc, args });
+        errdefer self.nc.unsubscribe(subscription);
@@
         const js_sub = try self.nc.allocator.create(JetStreamSubscription);
         js_sub.* = JetStreamSubscription{
             .subscription = subscription,
             .js = self,
             .consumer_info = consumer_info,
+            .deliver_subject_owned = generated_deliver_subject,
         };
🧹 Nitpick comments (1)
src/jetstream.zig (1)

1096-1123: Don’t swallow conversion errors silently in wrapped handler

If createJetStreamMessage fails, we drop the message with no log. Emit a debug/error to help diagnose malformed messages.

-                const js_msg = jetstream_message.createJetStreamMessage(nc, msg) catch {
-                    msg.deinit(); // Clean up on error
-                    return;
-                };
+                const js_msg = jetstream_message.createJetStreamMessage(nc, msg) catch |err| {
+                    log.err("Failed to wrap JetStream message: {}", .{err});
+                    msg.deinit(); // Clean up on error
+                    return;
+                };
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 380cd9c and 8ef1711.

📒 Files selected for processing (2)
  • src/jetstream.zig (2 hunks)
  • tests/jetstream_push_test.zig (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

Format code using zig fmt before commit

Files:

  • tests/jetstream_push_test.zig
  • src/jetstream.zig
🧠 Learnings (1)
📚 Learning: 2025-08-25T14:51:33.570Z
Learnt from: lalinsky
PR: lalinsky/nats.zig#31
File: tests/jetstream_pull_test.zig:21-23
Timestamp: 2025-08-25T14:51:33.570Z
Learning: The nats.zig project uses a "tests:beforeEach" test in tests/all_tests.zig that runs before each test to clean up all JetStream streams and consumers, ensuring test isolation. This eliminates the need for unique test resource names as each test starts with a clean JetStream state.

Applied to files:

  • tests/jetstream_push_test.zig
🪛 GitHub Actions: CI
src/jetstream.zig

[error] 1-1: Zig fmt --check failed (exit code 1). Command 'zig fmt --check .' reported formatting issues. Run 'zig fmt' to auto-format.

🔇 Additional comments (4)
tests/jetstream_push_test.zig (2)

133-139: LGTM: exercising auto-generated deliver_subject on missing stream

Config without deliver_subject and expecting a stream lookup error is correct with the new behavior.


147-149: LGTM: explicit assertion for StreamNotFound

The error expectation matches the server-side response when the stream is absent.

src/jetstream.zig (2)

1-1: CI failed: run zig fmt

Formatter check failed in CI. Please run:

  • zig fmt .
  • git add -A && git commit

This repo mandates zig fmt for all Zig sources.


1162-1165: Consider adding queue subscription support for deliver_group
Searches in connection.zig and jetstream.zig did not reveal any queue‐based subscribe or subscribeSync variants. If push_config.deliver_group is set today we fall back to plain subscriptions. Confirm whether the underlying NATS connection exposes a queue‐subscribe API or plan to implement it so push consumers with a deliver_group can share workload.

@lalinsky
Copy link
Owner Author

lalinsky commented Sep 9, 2025

@claude address coderabbit's comments

@claude
Copy link
Contributor

claude bot commented Sep 9, 2025

Claude finished @lalinsky's task —— View job


I've successfully addressed all of CodeRabbit's review comments and fixed the memory management issues.

Completed Tasks

  • Read current code to understand structure
  • Fix memory leak: generated deliver_subject not freed on success
  • Add deliver_subject_owned field to JetStreamSubscription
  • Add errdefer cleanup for subscription allocation failure
  • Add error logging in wrapped handler instead of silent swallowing
  • Add deliver_subject validation
  • Run zig fmt to fix formatting issues
  • Test changes with zig build test
  • Commit and push fixes

Changes Made

Memory Management Fixes:

  • Added deliver_subject_owned: ?[]u8 = null field to JetStreamSubscription struct
  • Updated deinit() method to free the owned deliver_subject when present
  • Set deliver_subject_owned = generated_deliver_subject in both subscription creation functions

Error Handling Improvements:

  • Added errdefer self.nc.unsubscribe(subscription) to prevent subscription leaks on allocation failure
  • Changed silent error swallowing to log.err("Failed to wrap JetStream message: {}", .{err}) in wrapped handler

Validation:

  • Added try validation.validateSubject(deliver_subject) in both subscribe() and subscribeSync() methods

Code Quality:

  • Ran zig fmt to fix formatting issues
  • All 191 tests pass successfully

The implementation now properly manages memory for auto-generated deliver subjects, provides better error handling, validates inputs, and maintains the same API behavior while preventing resource leaks.

Addresses CodeRabbit's review comments:
- Add deliver_subject_owned field to JetStreamSubscription to prevent memory leak of generated deliver_subject
- Update deinit() to free owned deliver_subject
- Add errdefer cleanup for subscription on allocation failure
- Add error logging instead of silently swallowing conversion errors in wrapped handler
- Add deliver_subject validation for both subscribe() and subscribeSync()
- Apply fixes to both subscribe() and subscribeSync() methods

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Lukáš Lalinský <[email protected]>
Copy link
Contributor

@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

♻️ Duplicate comments (3)
src/jetstream.zig (3)

1081-1092: Auto-generate + validate deliver_subject; strip pull-only fields — solid

This matches Go client behavior, validates inputs, and avoids mixing pull-only fields into push configs. This also addresses the earlier leak/validation review.

Also applies to: 1093-1097


1143-1144: Good: retain generated subject on the subscription

Ensures lifetime matches the subscription and frees on teardown.


1177-1183: Ownership mirrored in sync path — resolved

The generated deliver subject is now owned by the subscription and freed in deinit().

🧹 Nitpick comments (2)
src/jetstream.zig (2)

1116-1118: Include more context in the wrap-error log

Adding subject/status makes field issues diagnosable without packet capture.

-                    log.err("Failed to wrap JetStream message: {}", .{err});
+                    log.err("Failed to wrap JetStream message (subject={s}, status={d}): {}", .{ msg.subject, msg.status_code, err });

1081-1092: DRY the push-config setup

The deliver_subject auto-gen/validation and pull-field stripping are duplicated in subscribe and subscribeSync. Consider a small helper to reduce drift.

fn preparePushConfig(nc: *Connection, consumer_config: ConsumerConfig)
    !struct { push_config: ConsumerConfig, deliver_subject: []const u8, owned: ?[]u8 } {
    var cfg = consumer_config;
    const owned = if (consumer_config.deliver_subject == null) try inbox.newInbox(nc.allocator) else null;
    errdefer if (owned) |ds| nc.allocator.free(ds);
    const deliver = consumer_config.deliver_subject orelse owned.?;
    try validation.validateSubject(deliver);
    cfg.deliver_subject = deliver;
    cfg.max_waiting = 0;
    cfg.max_batch = null;
    cfg.max_expires = null;
    return .{ .push_config = cfg, .deliver_subject = deliver, .owned = owned };
}

Also applies to: 1151-1162

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ef1711 and 1103a45.

📒 Files selected for processing (1)
  • src/jetstream.zig (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

Format code using zig fmt before commit

Files:

  • src/jetstream.zig
🧠 Learnings (1)
📚 Learning: 2025-09-06T20:28:53.149Z
Learnt from: lalinsky
PR: lalinsky/nats.zig#98
File: src/jetstream_kv.zig:235-279
Timestamp: 2025-09-06T20:28:53.149Z
Learning: JetStreamMessage wrappers in nats.zig are allocated on the Message's owned arena, so they are automatically freed when Message.deinit() is called and don't require separate cleanup.

Applied to files:

  • src/jetstream.zig
🔇 Additional comments (3)
src/jetstream.zig (3)

528-536: Leak fixed and ownership clarified — good call

Tracking the auto-generated deliver subject on the subscription and freeing it in deinit() is the right fix.


1134-1136: Good: unsubscribe on failure path

errdefer unsubscribe avoids dangling subs when consumer creation later fails.


1174-1174: Good: unsubscribe on failure path (sync)

Matches the async path; no leaks.

Comment on lines +1151 to +1162
// Generate deliver_subject if not provided and create push consumer config
var push_config = consumer_config;
const generated_deliver_subject = if (consumer_config.deliver_subject == null)
try inbox.newInbox(self.nc.allocator)
else
null;
errdefer if (generated_deliver_subject) |ds| self.nc.allocator.free(ds);

const deliver_subject = consumer_config.deliver_subject orelse generated_deliver_subject.?;
try validation.validateSubject(deliver_subject);
push_config.deliver_subject = deliver_subject;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Sync push path misses STATUS_CONTROL handling (heartbeats/flow control)

Without intercepting STATUS_CONTROL in subscribeSync, users will receive heartbeats as messages and flow-control requests won’t be answered, which can stall delivery. Handle these in nextMsg() similarly to the async wrapper.

Apply this diff to JetStreamSubscription.nextMsg:

@@
-    pub fn nextMsg(self: *JetStreamSubscription, timeout_ms: u64) !*JetStreamMessage {
-        // Get the next message from the underlying subscription
-        const msg = try self.subscription.nextMsg(timeout_ms);
-        errdefer msg.deinit();
-
-        // Convert to JetStream message
-        return try jetstream_message.createJetStreamMessage(self.js.nc, msg);
-    }
+    pub fn nextMsg(self: *JetStreamSubscription, timeout_ms: u64) !*JetStreamMessage {
+        var remaining_ms: u64 = timeout_ms;
+        const deadline_ms: i64 = std.time.milliTimestamp() + @as(i64, @intCast(timeout_ms));
+        while (true) {
+            const msg = self.subscription.nextMsg(remaining_ms) catch |err| switch (err) {
+                error.Timeout => return error.Timeout,
+                else => return err,
+            };
+            errdefer msg.deinit();
+
+            if (msg.status_code == STATUS_CONTROL) {
+                handleStatusMessage(msg, self.js.nc) catch |e| {
+                    log.err("Failed to handle status message: {}", .{e});
+                };
+                msg.deinit();
+                const now = std.time.milliTimestamp();
+                if (now >= deadline_ms) return error.Timeout;
+                remaining_ms = @intCast(u64, deadline_ms - now);
+                continue;
+            }
+
+            return try jetstream_message.createJetStreamMessage(self.js.nc, msg);
+        }
+    }

Also applies to: 1163-1167

🤖 Prompt for AI Agents
In src/jetstream.zig around lines 1151 to 1167, the sync push path
(subscribeSync/nextMsg) is not intercepting STATUS_CONTROL messages — heartbeats
are being returned to callers and flow-control requests are not being answered.
Update nextMsg() to mirror the async wrapper: after receiving a message check if
it is a STATUS_CONTROL/control message, and if so handle it internally (do not
return it to the caller): for heartbeat-type control messages simply ignore and
continue the receive loop; for flow-control/control messages with a reply
subject send the appropriate empty/ack response to msg.reply (using the same
reply behavior as the async path) and continue the loop; ensure any error
handling, timers, and counters used by the async path are applied the same way
so control messages don’t surface to callers and flow-control is replied to
immediately.

@lalinsky lalinsky merged commit 9c2788c into main Sep 9, 2025
4 checks passed
@lalinsky lalinsky deleted the feature/auto-generate-deliver-subject branch September 9, 2025 09:46
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.

1 participant