Skip to content

Conversation

@thedonmon
Copy link
Contributor

@thedonmon thedonmon commented Dec 31, 2025

Summary

For the love of the game, I've been using NATS for all of my messaging infrastructure and really fallen in love with it. I thought it would be a great fit for Wolverine since it has a lot of powerful features combining the likes of Kafka and Rabbit with one protocol. There are other features that could be cool for persistence like the ObjectStore (S3 like) and KeyValue Store (Redis like) but for now focusing on the core messaging infrastructure to incorporate into Wolverine.

Happy to add more docs, or tests and open to any feedback or questions!

Adds NATS as a first-class transport for Wolverine, supporting both Core NATS (fire-and-forget pub/sub) and JetStream (persistent, durable messaging with acknowledgments).

Closes #1038

Features

Core NATS Support

  • Simple pub/sub messaging with at-most-once delivery
  • Request-reply pattern support
  • Ideal for real-time, low-latency scenarios where persistence isn't required

JetStream Support

  • Persistent message storage with configurable retention
  • Full acknowledgment support (ack, nak, requeue)
  • Durable consumers that survive restarts
  • Dead letter queue support via AckTerminate
  • Work queue semantics with WorkQueuePolicy
  • Native scheduled message delivery (NATS Server 2.12+)

Configuration Examples

// Core NATS - simple pub/sub
opts.UseNats("nats://localhost:4222")
    .AutoProvision();

opts.ListenToNatsSubject("orders.received")
    .ProcessInline();

opts.PublishAllMessages()
    .ToNatsSubject("orders.received");
// JetStream - durable messaging
opts.UseNats("nats://localhost:4222")
    .AutoProvision()
    .UseJetStream()
    .DefineWorkQueueStream("ORDERS", "orders.>");

opts.ListenToNatsSubject("orders.received")
    .UseJetStream("ORDERS", "orders-consumer");
// JetStream with scheduled message delivery (NATS 2.12+)
opts.UseNats("nats://localhost:4222")
    .AutoProvision()
    .UseJetStream()
    .DefineWorkQueueStream("ORDERS", s => s.EnableScheduledDelivery(), "orders.>");

Architecture: Why Core NATS and JetStream Are Separate

NATS has two distinct messaging models with fundamentally different guarantees:

Feature Core NATS JetStream
Persistence None (memory only) Configurable (memory/file)
Delivery Guarantee At-most-once At-least-once
Acknowledgments None (application-level only) Full support (ack/nak/term)
Requeue Not possible Native via NakAsync()
Dead Letter Not possible Via AckTerminateAsync()

The transport implementation reflects this split:

  • CoreNatsSubscriber / CoreNatsPublisher - For simple pub/sub
  • JetStreamSubscriber / JetStreamPublisher - For durable messaging

Can they be used together? Yes! In the same application:

  • Use Core NATS for ephemeral messages (notifications, heartbeats)
  • Use JetStream for messages requiring durability (commands, events)

The response endpoint (wolverine.response.*) always uses Core NATS for request-reply, even when the main endpoints use JetStream.

Test Results

All 85 tests pass:

Category Tests Status
Core NATS Inline Compliance 18/18
Core NATS Buffered Compliance 18/18
JetStream Compliance 18/18
Integration Tests 5/5
Multi-tenancy Tests 14/14
Unit Tests 12/12

Native Scheduled Message Delivery

The transport supports native scheduled message delivery when using JetStream with NATS Server 2.12+.

Requirements

  1. NATS Server version >= 2.12
  2. Stream configured with EnableScheduledDelivery()

How It Works

When conditions are met, SupportsNativeScheduledSend returns true and scheduled messages use NATS headers:

  • Nats-Schedule: @at <RFC3339 timestamp>
  • Nats-Schedule-Target: <subject>

The transport automatically detects server version at startup and logs:

NATS server version 2.12.3 supports scheduled message delivery

Configuration

opts.UseNats("nats://localhost:4222")
    .AutoProvision()
    .UseJetStream()
    .DefineWorkQueueStream("ORDERS", s => s.EnableScheduledDelivery(), "orders.>");

Fallback Behavior

When native scheduled send is not available (server < 2.12 or stream not configured), Wolverine automatically falls back to its database-backed scheduled message persistence. This ensures scheduled messages work reliably across all NATS versions.

Known Limitations

Requeue Behavior

  • JetStream: Native requeue via NakAsync() with optional delay
  • Core NATS: Requeue implemented by republishing the message to the subject (since Core NATS has no native requeue)

Files Added/Modified

New Files

  • src/Transports/NATS/Wolverine.Nats/ - Main transport library
    • NatsTransportExtensions.cs - Extension methods for configuration
    • Internal/NatsTransport.cs - Transport implementation
    • Internal/NatsEndpoint.cs - Endpoint configuration
    • Internal/NatsSender.cs - Message sending
    • Internal/NatsListener.cs - Message receiving
    • Internal/CoreNatsSubscriber.cs / JetStreamSubscriber.cs - Subscription handling
    • Internal/CoreNatsPublisher.cs / JetStreamPublisher.cs - Publishing
    • Internal/NatsEnvelopeMapper.cs - Header mapping
  • src/Transports/NATS/Wolverine.Nats.Tests/ - Test project

Modified Files

  • wolverine.sln - Added NATS projects

Testing

# Run all NATS compliance tests
dotnet test src/Transports/NATS/Wolverine.Nats.Tests/Wolverine.Nats.Tests.csproj

# Requires NATS server running (default: localhost:4222)
# For JetStream tests, server must have JetStream enabled (-js flag)

Dependencies

  • NATS.Net (v2.7.0+) - Official NATS .NET client

Breaking Changes

None - this is a new transport addition.

Checklist

  • All tests pass
  • Core NATS pub/sub works
  • JetStream durable messaging works
  • Request-reply pattern works
  • Requeue/defer works (native for JetStream, republish for Core NATS)
  • Multi-tenancy support
  • Native scheduled send (NATS 2.12+ with fallback)

}

private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(0, 1);
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This slipped in here for the issue i raised #1998 can make this a separate PR if thats agreed to use mutex vs signal pattern. But for now I wanted to ensure all tests pass

@jeremydmiller jeremydmiller merged commit 27b1a43 into JasperFx:main Jan 2, 2026
1 check passed
@jeremydmiller
Copy link
Member

@thedonmon Honestly, I'm pulling this in -- after a rebase -- then I'll do a much better review with it in main. Doesn't hurt anything if it isn't released.

But first, thank you for taking this on and pushing it through!

@thedonmon
Copy link
Contributor Author

@thedonmon Honestly, I'm pulling this in -- after a rebase -- then I'll do a much better review with it in main. Doesn't hurt anything if it isn't released.

But first, thank you for taking this on and pushing it through!

Awesome! Yes no problem happy to help with review of anything and more testing as needed! I tried to follow other transports best as possible but there were some nuances with NATS I had to work around.

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.

NATS.io support

2 participants