Skip to content

Conversation

@lalinsky
Copy link
Owner

Summary

Adds support for delayed negative acknowledgments in JetStream messages, allowing consumers to specify a redelivery delay to prevent rapid redelivery of failed messages.

Key Features

  • nakWithDelay(delay_ms) method with millisecond precision
  • Automatic conversion to nanoseconds for NATS protocol compliance
  • Zero delay falls back to regular NAK behavior
  • Thread-safe implementation using atomic operations
  • Clean, unified implementation without code duplication

Implementation Details

  • Refactored sendAck to accept optional delay parameter
  • Protocol message format: -NAK {"delay": <nanoseconds>}
  • Consistent with existing codebase timeout patterns using milliseconds
  • Eliminated duplicate sendAckWithDelay method

Testing

  • ✅ 500ms delay timing verification (400ms minimum tolerance)
  • ✅ Zero delay behaves identically to regular NAK
  • ✅ Proper message redelivery after specified delays
  • ✅ No regressions in existing NAK functionality
  • ✅ All tests pass (5/5 NAK tests, full suite 122/122 tests)

Protocol Compliance

Follows NATS JetStream protocol specification for delayed NAK acknowledgments:

  • Uses nanosecond precision in JSON payload as required
  • Compatible with nats-server and other NATS clients
  • Based on feature originally requested in nats-server issue #2729

Test Plan

  • Run existing NAK tests to ensure no regressions
  • Verify delay timing accuracy with realistic tolerances
  • Test zero delay edge case
  • Validate protocol message format
  • Full integration test suite passes

Add support for delayed negative acknowledgments in JetStream messages,
allowing consumers to specify a redelivery delay in milliseconds.

Key features:
- nakWithDelay(delay_ms) method for delayed NAK with millisecond precision
- Automatic conversion to nanoseconds for NATS protocol compliance
- Zero delay falls back to regular NAK behavior
- Thread-safe implementation using atomic operations
- Comprehensive test coverage including timing verification

Implementation details:
- Refactored sendAck to accept optional delay parameter
- Eliminated code duplication between regular and delayed ACK methods
- Protocol message format: -NAK {"delay": <nanoseconds>}
- Consistent with existing timeout patterns using milliseconds

Tests verify:
- 500ms delay timing accuracy (400ms minimum tolerance)
- Zero delay behaves identically to regular NAK
- Proper message redelivery after specified delays
- No regressions in existing NAK functionality
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 13, 2025

Walkthrough

Implements delay-aware JetStream acknowledgments by extending the ack send path to accept an optional delay, adds nakWithDelay(delay_ms), and updates ack/nak/term/inProgress to route through it. The payload switches between plain strings and structured messages with delay (ns). Root re-exports nuid. New tests cover delayed and zero-delay NAK behavior.

Changes

Cohort / File(s) Change summary
JetStream ack path
src/jetstream_message.zig
Added nakWithDelay(delay_ms). Updated sendAck to accept optional delay and construct payload accordingly (string vs structured with delay in ns). Routed ack/nak/term/inProgress through new path. Preserved atomic final-ack guards with errdefer rollback on publish failure.
Root exports
src/root.zig
Re-exported nuid module via pub const nuid = @import("nuid.zig");.
JetStream NAK tests
tests/jetstream_nak_test.zig
Added tests verifying NAK with delay redelivery timing and NAK with zero delay behaving like regular NAK, using push consumers and timing/count assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title accurately and concisely describes the main change — adding nakWithDelay support for JetStream message acknowledgments — and it maps directly to the implemented API and sendAck refactor without extraneous details.
Description Check ✅ Passed The description is directly related to the changeset and provides clear details about the implementation, protocol format, refactor to sendAck, testing performed, and expected behavior for zero and nonzero delays.
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 implement-nak-with-delay

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: 1

🧹 Nitpick comments (3)
src/jetstream_message.zig (1)

71-74: Check for accidental non-NAK usage of delay.

sendAck(.nak, delay) is the only current path, but a defensive assertion keeps future call sites honest.

Also applies to: 93-121

tests/jetstream_nak_test.zig (2)

339-457: Prefer monotonic time for delay assertions.

Wall-clock jumps can flake the 400ms check. Consider using a monotonic source (e.g., nano timestamp divided by ns_per_ms) for current_time.


420-438: Optional: add a small grace wait after hitting the target count.

A brief extra sleep (e.g., 100–200ms) after count >= 2 helps ensure no unexpected extra deliveries when timing is tight.

Also applies to: 520-531

📜 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 0cba64e and 0961d0f.

📒 Files selected for processing (3)
  • src/jetstream_message.zig (3 hunks)
  • src/root.zig (1 hunks)
  • tests/jetstream_nak_test.zig (1 hunks)
🧰 Additional context used
🧠 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:

  • src/root.zig
  • tests/jetstream_nak_test.zig
🔇 Additional comments (6)
src/root.zig (1)

32-32: Re-export looks good.

pub const nuid = @import("nuid.zig"); mirrors the existing inbox pattern and cleanly exposes the module.

src/jetstream_message.zig (4)

61-69: Unifying ACK paths via sendAck(..., null) is clean.

This removes duplication and keeps behavior consistent across ack, nak, term, and inProgress.

Also applies to: 76-85


71-74: API for nakWithDelay(ms) is clear; zero-as-regular NAK is intuitive.

Nice, matches server expectations and keeps call sites simple.


93-105: Atomic final-ACK guard with errdefer rollback is correct.

Prevents duplicate final acks and restores state on publish failure.

Also applies to: 119-121


61-69: NAK-as-final semantics are intentional — tests enforce a single terminal signal per delivery.

tests/jetstream_nak_test.zig uses NAK to trigger redelivery; tests/jetstream_duplicate_ack_test.zig asserts NAK after ACK fails. Behavior matches "one terminal signal per delivery" and needs no change.

tests/jetstream_nak_test.zig (1)

459-542: Zero-delay NAK behavior test looks solid.

Clear two-delivery assertion with explicit ACK on redelivery.

Comment on lines +106 to +118
// Format the acknowledgment message with optional delay
var ack_message: [256]u8 = undefined;
const ack_body = if (delay_ms) |delay| blk: {
if (delay > 0) {
// Convert milliseconds to nanoseconds for the protocol message
const delay_ns = delay * std.time.ns_per_ms;
const formatted = try std.fmt.bufPrint(&ack_message, "{s} {{\"delay\": {}}}", .{ ack_type.toString(), delay_ns });
break :blk formatted;
} else {
break :blk ack_type.toString();
}
} else ack_type.toString();

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against delay overflow (ms → ns) and cap to i64 range.

delay * ns_per_ms can overflow u64; the server expects an int64 duration. Clamp to math.maxInt(i64) in ns and format as i64. Optionally assert delay-with-json is only used for NAK.

Apply this diff inside sendAck:

-            // Format the acknowledgment message with optional delay
-            var ack_message: [256]u8 = undefined;
-            const ack_body = if (delay_ms) |delay| blk: {
-                if (delay > 0) {
-                    // Convert milliseconds to nanoseconds for the protocol message
-                    const delay_ns = delay * std.time.ns_per_ms;
-                    const formatted = try std.fmt.bufPrint(&ack_message, "{s} {{\"delay\": {}}}", .{ ack_type.toString(), delay_ns });
-                    break :blk formatted;
-                } else {
-                    break :blk ack_type.toString();
-                }
-            } else ack_type.toString();
+            // Format the acknowledgment message with optional delay
+            var ack_message: [256]u8 = undefined;
+            const ack_body = if (delay_ms) |delay| blk: {
+                if (delay > 0) {
+                    // ms -> ns with clamping to i64 range expected by the server
+                    const max_ms_for_i64: u64 = @as(u64, std.math.maxInt(i64)) / std.time.ns_per_ms;
+                    const clamped_ms: u64 = if (delay > max_ms_for_i64) max_ms_for_i64 else delay;
+                    const delay_ns_i64: i64 = @intCast(clamped_ms * std.time.ns_per_ms);
+                    // Only NAK supports a JSON delay payload
+                    std.debug.assert(ack_type == .nak);
+                    const formatted = try std.fmt.bufPrint(&ack_message, "{s} {{\"delay\": {}}}", .{ ack_type.toString(), delay_ns_i64 });
+                    break :blk formatted;
+                } else {
+                    break :blk ack_type.toString();
+                }
+            } else ack_type.toString();

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/jetstream_message.zig around lines 106-118, the conversion delay_ms *
std.time.ns_per_ms can overflow u64 and the server expects an int64 nanosecond
value; change the logic to compute delay_ns with checked/saturating arithmetic
and clamp to @as(i64, std.math.maxInt(i64)) when overflow would occur, then
format the JSON using the i64 value; additionally, ensure (or assert) that the
delay-with-json branch is only used for the NAK ack_type if that constraint
applies.

@lalinsky lalinsky merged commit 8cbfec6 into main Sep 13, 2025
4 checks passed
@lalinsky lalinsky deleted the implement-nak-with-delay branch September 13, 2025 05:03
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