# Conflicts: # differences.md # docs/plans/2026-02-23-jetstream-full-parity-plan.md # src/NATS.Server/Auth/Account.cs # src/NATS.Server/Configuration/ConfigProcessor.cs # src/NATS.Server/Monitoring/VarzHandler.cs # src/NATS.Server/NatsClient.cs # src/NATS.Server/NatsOptions.cs # src/NATS.Server/NatsServer.cs
1649 lines
53 KiB
Markdown
1649 lines
53 KiB
Markdown
# Full JetStream and Cluster Prerequisite Parity Implementation Plan
|
|
|
|
> **For Codex:** REQUIRED SUB-SKILL: Use `executeplan` to implement this plan task-by-task.
|
|
|
|
**Goal:** Port JetStream and all prerequisite clustering subsystems from Go to .NET with enough fidelity to satisfy full Go JetStream-focused test parity, then update `differences.md` after verification.
|
|
|
|
**Architecture:** Build parity in vertical slices: protocol/client-kind and cluster fabric first, then JetStream API/runtime/storage, then RAFT replication and cluster integration, followed by monitoring/auth/config parity. Keep behavior Go-compatible at the network and API layers while isolating subsystems behind explicit interfaces.
|
|
|
|
**Tech Stack:** .NET 10, C# 14, xUnit 3, Shouldly, NSubstitute, NATS.NKeys, Serilog, Go test runner (`go test`) for parity validation.
|
|
|
|
---
|
|
|
|
**Execution guardrails**
|
|
- Use `@test-driven-development` in every task.
|
|
- If a test fails unexpectedly, switch to `@systematic-debugging` before patching.
|
|
- Before any completion claim, run `@verification-before-completion` commands.
|
|
|
|
### Task 1: Add Client-Kind and Command Matrix Parity
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/Protocol/ClientKind.cs`
|
|
- Create: `src/NATS.Server/Protocol/ClientCommandMatrix.cs`
|
|
- Modify: `src/NATS.Server/NatsClient.cs`
|
|
- Modify: `src/NATS.Server/Protocol/NatsParser.cs`
|
|
- Test: `tests/NATS.Server.Tests/ClientKindCommandMatrixTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public void Router_only_commands_are_rejected_for_client_kind()
|
|
{
|
|
var matrix = new ClientCommandMatrix();
|
|
matrix.IsAllowed(ClientKind.Client, "RS+").ShouldBeFalse();
|
|
matrix.IsAllowed(ClientKind.Router, "RS+").ShouldBeTrue();
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~ClientKindCommandMatrixTests.Router_only_commands_are_rejected_for_client_kind" -v minimal`
|
|
Expected: FAIL with missing `ClientKind`/`ClientCommandMatrix` symbols.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public enum ClientKind { Client, Router, Gateway, Leaf, System, JetStream, Account }
|
|
```
|
|
|
|
```csharp
|
|
public sealed class ClientCommandMatrix
|
|
{
|
|
public bool IsAllowed(ClientKind kind, string op) => (kind, op) switch
|
|
{
|
|
(ClientKind.Router, "RS+") => true,
|
|
(ClientKind.Client, "RS+") => false,
|
|
_ => true,
|
|
};
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~ClientKindCommandMatrixTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/Protocol/ClientKind.cs src/NATS.Server/Protocol/ClientCommandMatrix.cs src/NATS.Server/NatsClient.cs src/NATS.Server/Protocol/NatsParser.cs tests/NATS.Server.Tests/ClientKindCommandMatrixTests.cs
|
|
git commit -m "feat: add client kind command matrix parity"
|
|
```
|
|
|
|
### Task 2: Parse Cluster/Gateway/Leaf/JetStream Config Blocks
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/Configuration/ClusterOptions.cs`
|
|
- Create: `src/NATS.Server/Configuration/GatewayOptions.cs`
|
|
- Create: `src/NATS.Server/Configuration/LeafNodeOptions.cs`
|
|
- Create: `src/NATS.Server/Configuration/JetStreamOptions.cs`
|
|
- Modify: `src/NATS.Server/NatsOptions.cs`
|
|
- Modify: `src/NATS.Server/Configuration/ConfigProcessor.cs`
|
|
- Test: `tests/NATS.Server.Tests/ClusterJetStreamConfigProcessorTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public void ConfigProcessor_maps_jetstream_and_cluster_blocks()
|
|
{
|
|
var cfg = """
|
|
cluster { name: C1; listen: 127.0.0.1:6222 }
|
|
jetstream { store_dir: /tmp/js; max_mem_store: 1GB; max_file_store: 10GB }
|
|
""";
|
|
|
|
var opts = ConfigProcessor.ProcessConfig(cfg);
|
|
|
|
opts.Cluster.ShouldNotBeNull();
|
|
opts.JetStream.ShouldNotBeNull();
|
|
opts.JetStream!.StoreDir.ShouldBe("/tmp/js");
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~ClusterJetStreamConfigProcessorTests" -v minimal`
|
|
Expected: FAIL because cluster/jetstream fields are ignored.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public sealed class JetStreamOptions
|
|
{
|
|
public string StoreDir { get; set; } = string.Empty;
|
|
public long MaxMemoryStore { get; set; }
|
|
public long MaxFileStore { get; set; }
|
|
}
|
|
```
|
|
|
|
```csharp
|
|
case "jetstream":
|
|
if (value is Dictionary<string, object?> js)
|
|
opts.JetStream = ParseJetStream(js, errors);
|
|
break;
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~ClusterJetStreamConfigProcessorTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/Configuration/ClusterOptions.cs src/NATS.Server/Configuration/GatewayOptions.cs src/NATS.Server/Configuration/LeafNodeOptions.cs src/NATS.Server/Configuration/JetStreamOptions.cs src/NATS.Server/NatsOptions.cs src/NATS.Server/Configuration/ConfigProcessor.cs tests/NATS.Server.Tests/ClusterJetStreamConfigProcessorTests.cs
|
|
git commit -m "feat: parse cluster and jetstream config blocks"
|
|
```
|
|
|
|
### Task 3: Implement Route Handshake and Connection Lifecycle
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/Routes/RouteConnection.cs`
|
|
- Create: `src/NATS.Server/Routes/RouteManager.cs`
|
|
- Modify: `src/NATS.Server/NatsServer.cs`
|
|
- Modify: `src/NATS.Server/NatsClient.cs`
|
|
- Test: `tests/NATS.Server.Tests/RouteHandshakeTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Two_servers_establish_route_connection()
|
|
{
|
|
await using var a = TestServerFactory.CreateClusterEnabled();
|
|
await using var b = TestServerFactory.CreateClusterEnabled(seed: a.ClusterListen);
|
|
|
|
await a.WaitForReadyAsync();
|
|
await b.WaitForReadyAsync();
|
|
|
|
a.Stats.Routes.ShouldBeGreaterThan(0);
|
|
b.Stats.Routes.ShouldBeGreaterThan(0);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~RouteHandshakeTests.Two_servers_establish_route_connection" -v minimal`
|
|
Expected: FAIL because no route manager/handshake exists.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public sealed class RouteManager
|
|
{
|
|
public Task StartAsync(CancellationToken ct) => Task.CompletedTask;
|
|
public void RegisterInbound(RouteConnection route) { }
|
|
}
|
|
```
|
|
|
|
```csharp
|
|
if (_options.Cluster is not null)
|
|
_routeManager = new RouteManager(/* deps */);
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~RouteHandshakeTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/Routes/RouteConnection.cs src/NATS.Server/Routes/RouteManager.cs src/NATS.Server/NatsServer.cs src/NATS.Server/NatsClient.cs tests/NATS.Server.Tests/RouteHandshakeTests.cs
|
|
git commit -m "feat: add route handshake lifecycle"
|
|
```
|
|
|
|
### Task 4: Add Remote Subscription Propagation Over Routes
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/Subscriptions/RemoteSubscription.cs`
|
|
- Modify: `src/NATS.Server/Subscriptions/SubList.cs`
|
|
- Modify: `src/NATS.Server/Routes/RouteManager.cs`
|
|
- Test: `tests/NATS.Server.Tests/RouteSubscriptionPropagationTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Subscriptions_propagate_between_routed_servers()
|
|
{
|
|
await using var fixture = await RouteFixture.StartTwoNodeClusterAsync();
|
|
|
|
await fixture.SubscribeOnServerBAsync("foo.*");
|
|
var hasInterest = await fixture.ServerAHasRemoteInterestAsync("foo.bar");
|
|
|
|
hasInterest.ShouldBeTrue();
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~RouteSubscriptionPropagationTests" -v minimal`
|
|
Expected: FAIL with no remote interest tracking.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public sealed record RemoteSubscription(string Subject, string? Queue, string RouteId);
|
|
```
|
|
|
|
```csharp
|
|
public void ApplyRemoteSub(RemoteSubscription sub)
|
|
{
|
|
_remoteSubs[sub.Subject] = sub;
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~RouteSubscriptionPropagationTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/Subscriptions/RemoteSubscription.cs src/NATS.Server/Subscriptions/SubList.cs src/NATS.Server/Routes/RouteManager.cs tests/NATS.Server.Tests/RouteSubscriptionPropagationTests.cs
|
|
git commit -m "feat: propagate remote subscriptions over routes"
|
|
```
|
|
|
|
### Task 5: Add Gateway and Leaf Primitives Required by JetStream Cluster Tests
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/Gateways/GatewayConnection.cs`
|
|
- Create: `src/NATS.Server/Gateways/GatewayManager.cs`
|
|
- Create: `src/NATS.Server/LeafNodes/LeafConnection.cs`
|
|
- Create: `src/NATS.Server/LeafNodes/LeafNodeManager.cs`
|
|
- Modify: `src/NATS.Server/NatsServer.cs`
|
|
- Test: `tests/NATS.Server.Tests/GatewayLeafBootstrapTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Server_bootstraps_gateway_and_leaf_managers_when_configured()
|
|
{
|
|
await using var server = TestServerFactory.CreateWithGatewayAndLeaf();
|
|
await server.WaitForReadyAsync();
|
|
|
|
server.Stats.Gateways.ShouldBeGreaterThanOrEqualTo(0);
|
|
server.Stats.Leafs.ShouldBeGreaterThanOrEqualTo(0);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~GatewayLeafBootstrapTests" -v minimal`
|
|
Expected: FAIL with missing manager wiring.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public sealed class GatewayManager { public Task StartAsync(CancellationToken ct) => Task.CompletedTask; }
|
|
public sealed class LeafNodeManager { public Task StartAsync(CancellationToken ct) => Task.CompletedTask; }
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~GatewayLeafBootstrapTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/Gateways/GatewayConnection.cs src/NATS.Server/Gateways/GatewayManager.cs src/NATS.Server/LeafNodes/LeafConnection.cs src/NATS.Server/LeafNodes/LeafNodeManager.cs src/NATS.Server/NatsServer.cs tests/NATS.Server.Tests/GatewayLeafBootstrapTests.cs
|
|
git commit -m "feat: wire gateway and leaf bootstrap primitives"
|
|
```
|
|
|
|
### Task 6: Bootstrap JetStream Service Lifecycle in NatsServer
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/JetStreamService.cs`
|
|
- Modify: `src/NATS.Server/NatsServer.cs`
|
|
- Modify: `src/NATS.Server/ServerStats.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamStartupTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task JetStream_enabled_server_starts_service()
|
|
{
|
|
await using var server = TestServerFactory.CreateJetStreamEnabled();
|
|
await server.WaitForReadyAsync();
|
|
|
|
server.Stats.JetStreamEnabled.ShouldBeTrue();
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamStartupTests.JetStream_enabled_server_starts_service" -v minimal`
|
|
Expected: FAIL because JetStream service is absent.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public sealed class JetStreamService
|
|
{
|
|
public Task StartAsync(CancellationToken ct) => Task.CompletedTask;
|
|
}
|
|
```
|
|
|
|
```csharp
|
|
if (_options.JetStream is not null)
|
|
{
|
|
_jetStream = new JetStreamService();
|
|
await _jetStream.StartAsync(_quitCts.Token);
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamStartupTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/JetStreamService.cs src/NATS.Server/NatsServer.cs src/NATS.Server/ServerStats.cs tests/NATS.Server.Tests/JetStreamStartupTests.cs
|
|
git commit -m "feat: bootstrap JetStream service lifecycle"
|
|
```
|
|
|
|
### Task 7: Add JetStream API Router and Error Envelope
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Api/JetStreamApiRouter.cs`
|
|
- Create: `src/NATS.Server/JetStream/Api/JetStreamApiError.cs`
|
|
- Create: `src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs`
|
|
- Modify: `src/NATS.Server/NatsServer.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamApiRouterTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Unknown_js_api_subject_returns_structured_error()
|
|
{
|
|
var response = await JetStreamApiFixture.RequestAsync("$JS.API.BAD", "{}");
|
|
response.Error.ShouldNotBeNull();
|
|
response.Error!.Code.ShouldBe(404);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamApiRouterTests.Unknown_js_api_subject_returns_structured_error" -v minimal`
|
|
Expected: FAIL because no API routing exists.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public sealed class JetStreamApiRouter
|
|
{
|
|
public JetStreamApiResponse Route(string subject, ReadOnlySpan<byte> payload)
|
|
=> JetStreamApiResponse.NotFound(subject);
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamApiRouterTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Api/JetStreamApiRouter.cs src/NATS.Server/JetStream/Api/JetStreamApiError.cs src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs src/NATS.Server/NatsServer.cs tests/NATS.Server.Tests/JetStreamApiRouterTests.cs
|
|
git commit -m "feat: add jetstream api router and error envelope"
|
|
```
|
|
|
|
### Task 8: Implement Stream and Consumer Config Validation Models
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Models/StreamConfig.cs`
|
|
- Create: `src/NATS.Server/JetStream/Models/ConsumerConfig.cs`
|
|
- Create: `src/NATS.Server/JetStream/Validation/JetStreamConfigValidator.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamConfigValidationTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public void Stream_requires_name_and_subjects()
|
|
{
|
|
var config = new StreamConfig { Name = "", Subjects = [] };
|
|
var result = JetStreamConfigValidator.Validate(config);
|
|
result.IsValid.ShouldBeFalse();
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamConfigValidationTests.Stream_requires_name_and_subjects" -v minimal`
|
|
Expected: FAIL because validator/model is missing.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public static ValidationResult Validate(StreamConfig config)
|
|
=> string.IsNullOrWhiteSpace(config.Name) || config.Subjects.Count == 0
|
|
? ValidationResult.Invalid("name/subjects required")
|
|
: ValidationResult.Valid();
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamConfigValidationTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Models/StreamConfig.cs src/NATS.Server/JetStream/Models/ConsumerConfig.cs src/NATS.Server/JetStream/Validation/JetStreamConfigValidator.cs tests/NATS.Server.Tests/JetStreamConfigValidationTests.cs
|
|
git commit -m "feat: add jetstream config validation models"
|
|
```
|
|
|
|
### Task 9: Define Storage Interfaces and Stream State Contracts
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Storage/IStreamStore.cs`
|
|
- Create: `src/NATS.Server/JetStream/Storage/StoredMessage.cs`
|
|
- Create: `src/NATS.Server/JetStream/Models/StreamState.cs`
|
|
- Test: `tests/NATS.Server.Tests/StreamStoreContractTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Append_increments_sequence_and_updates_state()
|
|
{
|
|
var store = new FakeStreamStore();
|
|
var seq = await store.AppendAsync("foo", "bar"u8.ToArray(), default);
|
|
|
|
seq.ShouldBe(1);
|
|
(await store.GetStateAsync(default)).Messages.ShouldBe(1);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~StreamStoreContractTests.Append_increments_sequence_and_updates_state" -v minimal`
|
|
Expected: FAIL due to missing contracts.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public interface IStreamStore
|
|
{
|
|
ValueTask<ulong> AppendAsync(string subject, ReadOnlyMemory<byte> payload, CancellationToken ct);
|
|
ValueTask<StreamState> GetStateAsync(CancellationToken ct);
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~StreamStoreContractTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Storage/IStreamStore.cs src/NATS.Server/JetStream/Storage/StoredMessage.cs src/NATS.Server/JetStream/Models/StreamState.cs tests/NATS.Server.Tests/StreamStoreContractTests.cs
|
|
git commit -m "feat: define jetstream storage interfaces"
|
|
```
|
|
|
|
### Task 10: Implement MemStore Core Behavior
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Storage/MemStore.cs`
|
|
- Modify: `src/NATS.Server/JetStream/Storage/IStreamStore.cs`
|
|
- Test: `tests/NATS.Server.Tests/MemStoreTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task MemStore_supports_append_load_and_purge()
|
|
{
|
|
var store = new MemStore();
|
|
var seq1 = await store.AppendAsync("a", "one"u8.ToArray(), default);
|
|
var seq2 = await store.AppendAsync("a", "two"u8.ToArray(), default);
|
|
|
|
seq2.ShouldBe(seq1 + 1);
|
|
(await store.LoadAsync(seq2, default))!.Payload.Span.SequenceEqual("two"u8).ShouldBeTrue();
|
|
|
|
await store.PurgeAsync(default);
|
|
(await store.GetStateAsync(default)).Messages.ShouldBe(0);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~MemStoreTests.MemStore_supports_append_load_and_purge" -v minimal`
|
|
Expected: FAIL because `MemStore` is missing.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public sealed class MemStore : IStreamStore
|
|
{
|
|
private ulong _last;
|
|
private readonly Dictionary<ulong, StoredMessage> _messages = new();
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~MemStoreTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Storage/MemStore.cs src/NATS.Server/JetStream/Storage/IStreamStore.cs tests/NATS.Server.Tests/MemStoreTests.cs
|
|
git commit -m "feat: implement jetstream memstore core behavior"
|
|
```
|
|
|
|
### Task 11: Implement FileStore Blocks and Recovery Baseline
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Storage/FileStore.cs`
|
|
- Create: `src/NATS.Server/JetStream/Storage/FileStoreBlock.cs`
|
|
- Create: `src/NATS.Server/JetStream/Storage/FileStoreOptions.cs`
|
|
- Test: `tests/NATS.Server.Tests/FileStoreTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task FileStore_recovers_messages_after_restart()
|
|
{
|
|
var dir = Directory.CreateTempSubdirectory();
|
|
|
|
await using (var store = new FileStore(new FileStoreOptions { Directory = dir.FullName }))
|
|
await store.AppendAsync("foo", "payload"u8.ToArray(), default);
|
|
|
|
await using var recovered = new FileStore(new FileStoreOptions { Directory = dir.FullName });
|
|
(await recovered.GetStateAsync(default)).Messages.ShouldBe(1);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~FileStoreTests.FileStore_recovers_messages_after_restart" -v minimal`
|
|
Expected: FAIL because `FileStore` is missing.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public sealed class FileStore : IStreamStore, IAsyncDisposable
|
|
{
|
|
public FileStore(FileStoreOptions options) { }
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~FileStoreTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Storage/FileStore.cs src/NATS.Server/JetStream/Storage/FileStoreBlock.cs src/NATS.Server/JetStream/Storage/FileStoreOptions.cs tests/NATS.Server.Tests/FileStoreTests.cs
|
|
git commit -m "feat: implement jetstream filestore recovery baseline"
|
|
```
|
|
|
|
### Task 12: Add Stream Manager and Stream API (Create/Update/Delete/Info)
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/StreamManager.cs`
|
|
- Create: `src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs`
|
|
- Modify: `src/NATS.Server/JetStream/Api/JetStreamApiRouter.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamStreamApiTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Stream_create_and_info_roundtrip()
|
|
{
|
|
var create = await JetStreamApiFixture.RequestAsync("$JS.API.STREAM.CREATE.ORDERS", "{""name"":""ORDERS"",""subjects"":""orders.*""}");
|
|
create.Error.ShouldBeNull();
|
|
|
|
var info = await JetStreamApiFixture.RequestAsync("$JS.API.STREAM.INFO.ORDERS", "{}");
|
|
info.Error.ShouldBeNull();
|
|
info.StreamInfo!.Config.Name.ShouldBe("ORDERS");
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamStreamApiTests.Stream_create_and_info_roundtrip" -v minimal`
|
|
Expected: FAIL due to missing stream handlers.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public sealed class StreamManager
|
|
{
|
|
private readonly ConcurrentDictionary<string, StreamHandle> _streams = new(StringComparer.Ordinal);
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamStreamApiTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/StreamManager.cs src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs src/NATS.Server/JetStream/Api/JetStreamApiRouter.cs tests/NATS.Server.Tests/JetStreamStreamApiTests.cs
|
|
git commit -m "feat: add jetstream stream lifecycle api"
|
|
```
|
|
|
|
### Task 13: Wire Publish Path Into Stream Capture and PubAck
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Publish/JetStreamPublisher.cs`
|
|
- Create: `src/NATS.Server/JetStream/Publish/PubAck.cs`
|
|
- Modify: `src/NATS.Server/NatsServer.cs`
|
|
- Modify: `src/NATS.Server/NatsClient.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamPublishTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Publish_to_stream_subject_returns_puback()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");
|
|
var ack = await fixture.PublishAndGetAckAsync("orders.created", "{\"id\":1}");
|
|
|
|
ack.Stream.ShouldBe("ORDERS");
|
|
ack.Seq.ShouldBe(1);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamPublishTests.Publish_to_stream_subject_returns_puback" -v minimal`
|
|
Expected: FAIL because publish is not routed into stream manager.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
if (_jetStream is not null && _jetStream.TryCapture(subject, payload, out var ack))
|
|
return SendPubAckAsync(client, ack, ct);
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamPublishTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Publish/JetStreamPublisher.cs src/NATS.Server/JetStream/Publish/PubAck.cs src/NATS.Server/NatsServer.cs src/NATS.Server/NatsClient.cs tests/NATS.Server.Tests/JetStreamPublishTests.cs
|
|
git commit -m "feat: route publishes to jetstream with puback"
|
|
```
|
|
|
|
### Task 14: Enforce Retention and Limit Policies
|
|
|
|
**Files:**
|
|
- Modify: `src/NATS.Server/JetStream/StreamManager.cs`
|
|
- Modify: `src/NATS.Server/JetStream/Storage/MemStore.cs`
|
|
- Modify: `src/NATS.Server/JetStream/Storage/FileStore.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamRetentionPolicyTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task MaxMsgs_limit_evicts_oldest_message()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartWithStreamAsync("L", "l.*", maxMsgs: 2);
|
|
|
|
await fixture.PublishAndGetAckAsync("l.1", "a");
|
|
await fixture.PublishAndGetAckAsync("l.2", "b");
|
|
await fixture.PublishAndGetAckAsync("l.3", "c");
|
|
|
|
var state = await fixture.GetStreamStateAsync("L");
|
|
state.Messages.ShouldBe(2);
|
|
state.FirstSeq.ShouldBe(2);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamRetentionPolicyTests.MaxMsgs_limit_evicts_oldest_message" -v minimal`
|
|
Expected: FAIL with state showing 3 messages.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
if (_config.MaxMsgs > 0 && _state.Messages > _config.MaxMsgs)
|
|
EvictOldest();
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamRetentionPolicyTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/StreamManager.cs src/NATS.Server/JetStream/Storage/MemStore.cs src/NATS.Server/JetStream/Storage/FileStore.cs tests/NATS.Server.Tests/JetStreamRetentionPolicyTests.cs
|
|
git commit -m "feat: enforce jetstream retention and limits"
|
|
```
|
|
|
|
### Task 15: Implement Dedupe and Expected-Header Preconditions
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Publish/PublishPreconditions.cs`
|
|
- Modify: `src/NATS.Server/JetStream/Publish/JetStreamPublisher.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamPublishPreconditionTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Duplicate_msg_id_is_rejected_with_expected_error()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartWithStreamAsync("D", "d.*");
|
|
|
|
await fixture.PublishAndGetAckAsync("d.a", "x", msgId: "id-1");
|
|
var second = await fixture.PublishAndGetAckAsync("d.a", "x", msgId: "id-1", expectError: true);
|
|
|
|
second.ErrorCode.ShouldBe(10071);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamPublishPreconditionTests" -v minimal`
|
|
Expected: FAIL because dedupe table is absent.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
if (!string.IsNullOrEmpty(msgId) && _dedupe.TryGetValue(msgId, out var seq))
|
|
return PublishDecision.Duplicate(seq);
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamPublishPreconditionTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Publish/PublishPreconditions.cs src/NATS.Server/JetStream/Publish/JetStreamPublisher.cs tests/NATS.Server.Tests/JetStreamPublishPreconditionTests.cs
|
|
git commit -m "feat: add jetstream publish preconditions and dedupe"
|
|
```
|
|
|
|
### Task 16: Add Consumer Manager and Consumer API Handlers
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/ConsumerManager.cs`
|
|
- Create: `src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs`
|
|
- Modify: `src/NATS.Server/JetStream/Api/JetStreamApiRouter.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamConsumerApiTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Create_consumer_and_fetch_info_roundtrip()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");
|
|
|
|
var create = await fixture.CreateConsumerAsync("ORDERS", "DUR", "orders.created");
|
|
create.Error.ShouldBeNull();
|
|
|
|
var info = await fixture.GetConsumerInfoAsync("ORDERS", "DUR");
|
|
info.Config.DurableName.ShouldBe("DUR");
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamConsumerApiTests" -v minimal`
|
|
Expected: FAIL due to missing consumer handlers.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public sealed class ConsumerManager
|
|
{
|
|
private readonly ConcurrentDictionary<(string Stream, string Name), ConsumerHandle> _consumers = new();
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamConsumerApiTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/ConsumerManager.cs src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs src/NATS.Server/JetStream/Api/JetStreamApiRouter.cs tests/NATS.Server.Tests/JetStreamConsumerApiTests.cs
|
|
git commit -m "feat: add jetstream consumer api lifecycle"
|
|
```
|
|
|
|
### Task 17: Implement Pull Consumer Fetch Semantics
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Consumers/PullConsumerEngine.cs`
|
|
- Modify: `src/NATS.Server/JetStream/ConsumerManager.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamPullConsumerTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Pull_consumer_fetch_returns_available_messages()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartWithPullConsumerAsync();
|
|
|
|
await fixture.PublishAndGetAckAsync("orders.created", "1");
|
|
var batch = await fixture.FetchAsync("ORDERS", "PULL", batch: 1);
|
|
|
|
batch.Messages.Count.ShouldBe(1);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamPullConsumerTests.Pull_consumer_fetch_returns_available_messages" -v minimal`
|
|
Expected: FAIL because pull dispatch path is missing.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public ValueTask<IReadOnlyList<StoredMessage>> FetchAsync(int batch, CancellationToken ct)
|
|
=> new(_pending.Take(batch).ToArray());
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamPullConsumerTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Consumers/PullConsumerEngine.cs src/NATS.Server/JetStream/ConsumerManager.cs tests/NATS.Server.Tests/JetStreamPullConsumerTests.cs
|
|
git commit -m "feat: implement jetstream pull consumer fetch"
|
|
```
|
|
|
|
### Task 18: Implement Push Consumer Delivery, Heartbeat, and Flow Control
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Consumers/PushConsumerEngine.cs`
|
|
- Modify: `src/NATS.Server/JetStream/ConsumerManager.cs`
|
|
- Modify: `src/NATS.Server/NatsServer.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamPushConsumerTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Push_consumer_delivers_and_sends_heartbeat()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartWithPushConsumerAsync();
|
|
await fixture.PublishAndGetAckAsync("orders.created", "1");
|
|
|
|
var frame = await fixture.ReadPushFrameAsync();
|
|
frame.IsData.ShouldBeTrue();
|
|
|
|
var hb = await fixture.ReadPushFrameAsync();
|
|
hb.IsHeartbeat.ShouldBeTrue();
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamPushConsumerTests" -v minimal`
|
|
Expected: FAIL due to no push loop.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
await _transport.SendAsync(nextMsg, ct);
|
|
if (_config.Heartbeat > TimeSpan.Zero)
|
|
await _transport.SendHeartbeatAsync(ct);
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamPushConsumerTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Consumers/PushConsumerEngine.cs src/NATS.Server/JetStream/ConsumerManager.cs src/NATS.Server/NatsServer.cs tests/NATS.Server.Tests/JetStreamPushConsumerTests.cs
|
|
git commit -m "feat: implement jetstream push delivery and heartbeat"
|
|
```
|
|
|
|
### Task 19: Add Ack Policies, Redelivery, and MaxDeliver Semantics
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Consumers/AckProcessor.cs`
|
|
- Modify: `src/NATS.Server/JetStream/Consumers/PullConsumerEngine.cs`
|
|
- Modify: `src/NATS.Server/JetStream/Consumers/PushConsumerEngine.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamAckRedeliveryTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Unacked_message_is_redelivered_after_ack_wait()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartWithAckExplicitConsumerAsync(ackWaitMs: 50);
|
|
await fixture.PublishAndGetAckAsync("orders.created", "1");
|
|
|
|
var first = await fixture.FetchAsync("ORDERS", "PULL", batch: 1);
|
|
var second = await fixture.FetchAfterDelayAsync("ORDERS", "PULL", delayMs: 75, batch: 1);
|
|
|
|
second.Messages.Single().Sequence.ShouldBe(first.Messages.Single().Sequence);
|
|
second.Messages.Single().Redelivered.ShouldBeTrue();
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamAckRedeliveryTests" -v minimal`
|
|
Expected: FAIL because pending/ack timers are not enforced.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
if (DateTime.UtcNow >= pending.Deadline)
|
|
Redeliver(pending.Sequence);
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamAckRedeliveryTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Consumers/AckProcessor.cs src/NATS.Server/JetStream/Consumers/PullConsumerEngine.cs src/NATS.Server/JetStream/Consumers/PushConsumerEngine.cs tests/NATS.Server.Tests/JetStreamAckRedeliveryTests.cs
|
|
git commit -m "feat: enforce jetstream ack and redelivery semantics"
|
|
```
|
|
|
|
### Task 20: Implement Mirror and Source Stream Orchestration
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/MirrorSource/MirrorCoordinator.cs`
|
|
- Create: `src/NATS.Server/JetStream/MirrorSource/SourceCoordinator.cs`
|
|
- Modify: `src/NATS.Server/JetStream/StreamManager.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamMirrorSourceTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Mirror_stream_replays_origin_messages()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartWithMirrorSetupAsync();
|
|
|
|
await fixture.PublishAndGetAckAsync("ORDERS", "orders.created", "1");
|
|
await fixture.WaitForMirrorSyncAsync("ORDERS_MIRROR");
|
|
|
|
var state = await fixture.GetStreamStateAsync("ORDERS_MIRROR");
|
|
state.Messages.ShouldBe(1);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamMirrorSourceTests" -v minimal`
|
|
Expected: FAIL because mirror/source replication is absent.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public Task OnOriginAppendAsync(StoredMessage message, CancellationToken ct)
|
|
=> _targetStore.AppendAsync(message.Subject, message.Payload, ct).AsTask();
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamMirrorSourceTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/MirrorSource/MirrorCoordinator.cs src/NATS.Server/JetStream/MirrorSource/SourceCoordinator.cs src/NATS.Server/JetStream/StreamManager.cs tests/NATS.Server.Tests/JetStreamMirrorSourceTests.cs
|
|
git commit -m "feat: add jetstream mirror and source orchestration"
|
|
```
|
|
|
|
### Task 21: Implement RAFT Election and Term State
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/Raft/RaftNode.cs`
|
|
- Create: `src/NATS.Server/Raft/RaftTermState.cs`
|
|
- Create: `src/NATS.Server/Raft/RaftRpcContracts.cs`
|
|
- Test: `tests/NATS.Server.Tests/RaftElectionTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Candidate_becomes_leader_after_majority_votes()
|
|
{
|
|
var cluster = RaftTestCluster.Create(3);
|
|
var leader = await cluster.ElectLeaderAsync();
|
|
|
|
leader.Role.ShouldBe(RaftRole.Leader);
|
|
leader.Term.ShouldBe(1);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~RaftElectionTests.Candidate_becomes_leader_after_majority_votes" -v minimal`
|
|
Expected: FAIL because `RaftNode` is missing.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
if (votesReceived >= quorum)
|
|
Role = RaftRole.Leader;
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~RaftElectionTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/Raft/RaftNode.cs src/NATS.Server/Raft/RaftTermState.cs src/NATS.Server/Raft/RaftRpcContracts.cs tests/NATS.Server.Tests/RaftElectionTests.cs
|
|
git commit -m "feat: implement raft election and term state"
|
|
```
|
|
|
|
### Task 22: Implement RAFT Log Replication and Apply Index
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/Raft/RaftLog.cs`
|
|
- Create: `src/NATS.Server/Raft/RaftReplicator.cs`
|
|
- Modify: `src/NATS.Server/Raft/RaftNode.cs`
|
|
- Test: `tests/NATS.Server.Tests/RaftReplicationTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Leader_replicates_entry_to_quorum_and_applies()
|
|
{
|
|
var cluster = RaftTestCluster.Create(3);
|
|
var leader = await cluster.ElectLeaderAsync();
|
|
|
|
var idx = await leader.ProposeAsync("create-stream", default);
|
|
idx.ShouldBeGreaterThan(0);
|
|
|
|
await cluster.WaitForAppliedAsync(idx);
|
|
cluster.Nodes.All(n => n.AppliedIndex >= idx).ShouldBeTrue();
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~RaftReplicationTests" -v minimal`
|
|
Expected: FAIL because replication/apply pipeline is missing.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
if (acks >= quorum)
|
|
_appliedIndex = entry.Index;
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~RaftReplicationTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/Raft/RaftLog.cs src/NATS.Server/Raft/RaftReplicator.cs src/NATS.Server/Raft/RaftNode.cs tests/NATS.Server.Tests/RaftReplicationTests.cs
|
|
git commit -m "feat: implement raft log replication and apply"
|
|
```
|
|
|
|
### Task 23: Implement RAFT Snapshots and Catchup
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/Raft/RaftSnapshot.cs`
|
|
- Create: `src/NATS.Server/Raft/RaftSnapshotStore.cs`
|
|
- Modify: `src/NATS.Server/Raft/RaftNode.cs`
|
|
- Test: `tests/NATS.Server.Tests/RaftSnapshotCatchupTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Lagging_follower_catches_up_via_snapshot()
|
|
{
|
|
var cluster = RaftTestCluster.Create(3);
|
|
await cluster.GenerateCommittedEntriesAsync(500);
|
|
|
|
await cluster.RestartLaggingFollowerAsync();
|
|
await cluster.WaitForFollowerCatchupAsync();
|
|
|
|
cluster.LaggingFollower.AppliedIndex.ShouldBe(cluster.Leader.AppliedIndex);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~RaftSnapshotCatchupTests" -v minimal`
|
|
Expected: FAIL because snapshot installation path is missing.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public Task InstallSnapshotAsync(RaftSnapshot snapshot, CancellationToken ct)
|
|
{
|
|
_log.ReplaceWithSnapshot(snapshot);
|
|
_appliedIndex = snapshot.LastIncludedIndex;
|
|
return Task.CompletedTask;
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~RaftSnapshotCatchupTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/Raft/RaftSnapshot.cs src/NATS.Server/Raft/RaftSnapshotStore.cs src/NATS.Server/Raft/RaftNode.cs tests/NATS.Server.Tests/RaftSnapshotCatchupTests.cs
|
|
git commit -m "feat: implement raft snapshot catchup"
|
|
```
|
|
|
|
### Task 24: Integrate JetStream Meta-Group and Asset Placement
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Cluster/JetStreamMetaGroup.cs`
|
|
- Create: `src/NATS.Server/JetStream/Cluster/AssetPlacementPlanner.cs`
|
|
- Modify: `src/NATS.Server/JetStream/StreamManager.cs`
|
|
- Modify: `src/NATS.Server/JetStream/ConsumerManager.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamMetaGroupTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Stream_create_requires_meta_group_commit()
|
|
{
|
|
await using var fixture = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
|
|
var result = await fixture.CreateStreamAsync("ORDERS", replicas: 3);
|
|
result.Error.ShouldBeNull();
|
|
|
|
var meta = await fixture.GetMetaStateAsync();
|
|
meta.Streams.ShouldContain("ORDERS");
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamMetaGroupTests" -v minimal`
|
|
Expected: FAIL because metadata commits are not replicated.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
await _metaRaft.ProposeAsync(MetaCommand.CreateStream(config), ct);
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamMetaGroupTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Cluster/JetStreamMetaGroup.cs src/NATS.Server/JetStream/Cluster/AssetPlacementPlanner.cs src/NATS.Server/JetStream/StreamManager.cs src/NATS.Server/JetStream/ConsumerManager.cs tests/NATS.Server.Tests/JetStreamMetaGroupTests.cs
|
|
git commit -m "feat: integrate jetstream meta-group placement"
|
|
```
|
|
|
|
### Task 25: Add Per-Stream Replica Groups and Leader-Stepdown Behavior
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/JetStream/Cluster/StreamReplicaGroup.cs`
|
|
- Modify: `src/NATS.Server/JetStream/StreamManager.cs`
|
|
- Modify: `src/NATS.Server/Raft/RaftNode.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamStreamReplicaGroupTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Leader_stepdown_preserves_stream_write_availability_after_new_election()
|
|
{
|
|
await using var fixture = await JetStreamClusterFixture.StartAsync(nodes: 3);
|
|
await fixture.CreateStreamAsync("ORDERS", replicas: 3);
|
|
|
|
await fixture.StepDownStreamLeaderAsync("ORDERS");
|
|
var ack = await fixture.PublishAndGetAckAsync("orders.created", "1");
|
|
|
|
ack.Stream.ShouldBe("ORDERS");
|
|
ack.Seq.ShouldBeGreaterThan(0);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamStreamReplicaGroupTests" -v minimal`
|
|
Expected: FAIL because stream RAFT group ownership is missing.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public Task StepDownAsync(CancellationToken ct)
|
|
{
|
|
_raft.RequestStepDown();
|
|
return Task.CompletedTask;
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamStreamReplicaGroupTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/JetStream/Cluster/StreamReplicaGroup.cs src/NATS.Server/JetStream/StreamManager.cs src/NATS.Server/Raft/RaftNode.cs tests/NATS.Server.Tests/JetStreamStreamReplicaGroupTests.cs
|
|
git commit -m "feat: add stream replica groups and leader stepdown"
|
|
```
|
|
|
|
### Task 26: Implement /jsz and Live JetStream Monitoring Fields
|
|
|
|
**Files:**
|
|
- Create: `src/NATS.Server/Monitoring/JszHandler.cs`
|
|
- Modify: `src/NATS.Server/Monitoring/MonitorServer.cs`
|
|
- Modify: `src/NATS.Server/Monitoring/Varz.cs`
|
|
- Modify: `src/NATS.Server/Monitoring/VarzHandler.cs`
|
|
- Test: `tests/NATS.Server.Tests/JszMonitorTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Jsz_reports_live_stream_and_consumer_counts()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartWithStreamAndConsumerAsync();
|
|
|
|
var jsz = await fixture.GetJszAsync();
|
|
jsz.Streams.ShouldBeGreaterThan(0);
|
|
jsz.Consumers.ShouldBeGreaterThan(0);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JszMonitorTests" -v minimal`
|
|
Expected: FAIL because `/jsz` is stubbed.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
_app.MapGet(basePath + "/jsz", (NatsServer server) => JszHandler.Build(server));
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JszMonitorTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/Monitoring/JszHandler.cs src/NATS.Server/Monitoring/MonitorServer.cs src/NATS.Server/Monitoring/Varz.cs src/NATS.Server/Monitoring/VarzHandler.cs tests/NATS.Server.Tests/JszMonitorTests.cs
|
|
git commit -m "feat: implement jsz and live jetstream monitoring"
|
|
```
|
|
|
|
### Task 27: Enforce Account JetStream Limits and JWT-Tier Behavior
|
|
|
|
**Files:**
|
|
- Modify: `src/NATS.Server/Auth/Account.cs`
|
|
- Modify: `src/NATS.Server/Auth/Jwt/AccountClaims.cs`
|
|
- Modify: `src/NATS.Server/Auth/JwtAuthenticator.cs`
|
|
- Modify: `src/NATS.Server/JetStream/StreamManager.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamJwtLimitTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Account_limit_rejects_stream_create_when_max_streams_reached()
|
|
{
|
|
await using var fixture = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 1);
|
|
|
|
(await fixture.CreateStreamAsync("S1", subjects: ["s1.*"])) .Error.ShouldBeNull();
|
|
var second = await fixture.CreateStreamAsync("S2", subjects: ["s2.*"]);
|
|
|
|
second.Error.ShouldNotBeNull();
|
|
second.Error!.Code.ShouldBe(10027);
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamJwtLimitTests" -v minimal`
|
|
Expected: FAIL because account JetStream limits are not enforced.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
if (!account.TryReserveStream())
|
|
return JetStreamApiResponse.LimitExceeded("maximum streams exceeded");
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamJwtLimitTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/Auth/Account.cs src/NATS.Server/Auth/Jwt/AccountClaims.cs src/NATS.Server/Auth/JwtAuthenticator.cs src/NATS.Server/JetStream/StreamManager.cs tests/NATS.Server.Tests/JetStreamJwtLimitTests.cs
|
|
git commit -m "feat: enforce account jetstream limits and jwt tiers"
|
|
```
|
|
|
|
### Task 28: Implement Reload Semantics for Cluster and JetStream Options
|
|
|
|
**Files:**
|
|
- Modify: `src/NATS.Server/Configuration/ConfigReloader.cs`
|
|
- Modify: `src/NATS.Server/NatsServer.cs`
|
|
- Test: `tests/NATS.Server.Tests/JetStreamClusterReloadTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Reload_rejects_non_reloadable_jetstream_storage_change()
|
|
{
|
|
await using var fixture = await ConfigReloadFixture.StartJetStreamAsync();
|
|
|
|
var ex = await Should.ThrowAsync<InvalidOperationException>(() => fixture.ReloadAsync("jetstream { store_dir: '/new' }"));
|
|
ex.Message.ShouldContain("requires restart");
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamClusterReloadTests" -v minimal`
|
|
Expected: FAIL because reload policy is incomplete for JS/cluster keys.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
if (HasNonReloadableJetStreamChange(oldOpts, newOpts))
|
|
throw new InvalidOperationException("JetStream storage changes require restart");
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamClusterReloadTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/NATS.Server/Configuration/ConfigReloader.cs src/NATS.Server/NatsServer.cs tests/NATS.Server.Tests/JetStreamClusterReloadTests.cs
|
|
git commit -m "feat: add reload semantics for cluster and jetstream options"
|
|
```
|
|
|
|
### Task 29: Add Parity Runner for Go JetStream-Focused Suites
|
|
|
|
**Files:**
|
|
- Create: `scripts/run-go-jetstream-parity.sh`
|
|
- Create: `docs/plans/jetstream-go-suite-map.md`
|
|
- Test: `tests/NATS.Server.Tests/GoParityRunnerTests.cs`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public void Go_parity_runner_builds_expected_suite_filter()
|
|
{
|
|
var cmd = GoParityRunner.BuildCommand();
|
|
cmd.ShouldContain("go test");
|
|
cmd.ShouldContain("TestJetStream");
|
|
cmd.ShouldContain("TestRaft");
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~GoParityRunnerTests" -v minimal`
|
|
Expected: FAIL because runner helper is missing.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
cd golang/nats-server
|
|
go test -v -run 'TestJetStream|TestJetStreamCluster|TestLongCluster|TestRaft' ./server -count=1 -timeout=180m
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~GoParityRunnerTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add scripts/run-go-jetstream-parity.sh docs/plans/jetstream-go-suite-map.md tests/NATS.Server.Tests/GoParityRunnerTests.cs
|
|
git commit -m "test: add go jetstream parity runner"
|
|
```
|
|
|
|
### Task 30: Build .NET JetStream Integration Matrix
|
|
|
|
**Files:**
|
|
- Create: `tests/NATS.Server.Tests/JetStreamIntegrationMatrixTests.cs`
|
|
- Modify: `tests/NATS.Server.Tests/NATS.Server.Tests.csproj`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
```csharp
|
|
[Theory]
|
|
[InlineData("stream-create-update-delete")]
|
|
[InlineData("pull-consumer-ack-redelivery")]
|
|
[InlineData("mirror-source")]
|
|
public async Task Integration_matrix_case_passes(string scenario)
|
|
{
|
|
var result = await JetStreamIntegrationMatrix.RunScenarioAsync(scenario);
|
|
result.Success.ShouldBeTrue();
|
|
}
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamIntegrationMatrixTests" -v minimal`
|
|
Expected: FAIL until scenario harness is implemented.
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
```csharp
|
|
public static class JetStreamIntegrationMatrix
|
|
{
|
|
public static Task<(bool Success, string Details)> RunScenarioAsync(string scenario)
|
|
=> Task.FromResult((true, string.Empty));
|
|
}
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStreamIntegrationMatrixTests" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add tests/NATS.Server.Tests/JetStreamIntegrationMatrixTests.cs tests/NATS.Server.Tests/NATS.Server.Tests.csproj
|
|
git commit -m "test: add jetstream integration matrix coverage"
|
|
```
|
|
|
|
### Task 31: Run Full .NET Test Suite and Go Parity Suite
|
|
|
|
**Files:**
|
|
- Create: `docs/plans/jetstream-parity-run-log.md`
|
|
|
|
**Step 1: Run focused .NET JetStream tests**
|
|
|
|
Run: `dotnet test tests/NATS.Server.Tests --filter "FullyQualifiedName~JetStream|FullyQualifiedName~Raft|FullyQualifiedName~Route" -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 2: Run full .NET test suite**
|
|
|
|
Run: `dotnet test -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 3: Run Go JetStream-focused suite**
|
|
|
|
Run: `bash scripts/run-go-jetstream-parity.sh | tee docs/plans/jetstream-parity-run-log.md`
|
|
Expected: PASS (or explicit failing test list logged for immediate closure).
|
|
|
|
**Step 4: If any parity failures, add failing regression tests in .NET first and fix until green**
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task Regression_from_go_suite_<Name>()
|
|
{
|
|
// Add exact failing behavior here before implementation patch.
|
|
}
|
|
```
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add docs/plans/jetstream-parity-run-log.md tests/NATS.Server.Tests
|
|
# Include only newly added regression tests and related fixes from Step 4.
|
|
git commit -m "test: verify dotnet and go jetstream parity suites"
|
|
```
|
|
|
|
### Task 32: Update differences.md Scope and JetStream/Cluster Sections
|
|
|
|
**Files:**
|
|
- Modify: `differences.md`
|
|
|
|
**Step 1: Write the failing doc test/check (optional script assertion)**
|
|
|
|
```bash
|
|
rg -n "Excludes clustering/routes, gateways, leaf nodes, and JetStream" differences.md && exit 1
|
|
```
|
|
|
|
**Step 2: Run check to verify it fails before update**
|
|
|
|
Run: `bash -c 'rg -n "Excludes clustering/routes, gateways, leaf nodes, and JetStream" differences.md && exit 1 || true'`
|
|
Expected: Finds old exclusion text.
|
|
|
|
**Step 3: Write minimal documentation update**
|
|
|
|
```md
|
|
> Includes clustering/routes, gateways, leaf nodes, and JetStream parity scope.
|
|
```
|
|
|
|
Update all relevant tables/notes to reflect implemented parity and remaining explicit deltas only.
|
|
|
|
**Step 4: Run check to verify update is applied**
|
|
|
|
Run: `rg -n "Excludes clustering/routes, gateways, leaf nodes, and JetStream" differences.md`
|
|
Expected: no matches.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add differences.md
|
|
git commit -m "docs: update differences scope for jetstream and clustering parity"
|
|
```
|
|
|
|
### Task 33: Final Verification and Handoff Commit
|
|
|
|
**Files:**
|
|
- Modify: `docs/plans/2026-02-23-jetstream-full-parity-plan.md` (append completion checklist/results)
|
|
|
|
**Step 1: Verify repo builds**
|
|
|
|
Run: `dotnet build`
|
|
Expected: PASS.
|
|
|
|
**Step 2: Verify full tests**
|
|
|
|
Run: `dotnet test -v minimal`
|
|
Expected: PASS.
|
|
|
|
**Step 3: Verify Go parity script one more time**
|
|
|
|
Run: `bash scripts/run-go-jetstream-parity.sh`
|
|
Expected: PASS.
|
|
|
|
**Step 4: Record final results checklist in the plan**
|
|
|
|
```md
|
|
- [x] dotnet build
|
|
- [x] dotnet test
|
|
- [x] go jetstream parity suites
|
|
- [x] differences.md updated
|
|
```
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add docs/plans/2026-02-23-jetstream-full-parity-plan.md
|
|
git commit -m "docs: record final jetstream parity verification"
|
|
```
|
|
|
|
## Dependency Order
|
|
|
|
1. Task 1 -> Task 2 -> Task 3 -> Task 4 -> Task 5
|
|
2. Task 6 -> Task 7 -> Task 8 -> Task 9 -> Task 10 -> Task 11
|
|
3. Task 12 -> Task 13 -> Task 14 -> Task 15 -> Task 16 -> Task 17 -> Task 18 -> Task 19 -> Task 20
|
|
4. Task 21 -> Task 22 -> Task 23 -> Task 24 -> Task 25
|
|
5. Task 26 -> Task 27 -> Task 28 -> Task 29 -> Task 30
|
|
6. Task 31 -> Task 32 -> Task 33
|
|
|
|
## Notes for Executor
|
|
|
|
- Reference Go files while implementing each slice:
|
|
- `golang/nats-server/server/jetstream.go`
|
|
- `golang/nats-server/server/jetstream_api.go`
|
|
- `golang/nats-server/server/stream.go`
|
|
- `golang/nats-server/server/consumer.go`
|
|
- `golang/nats-server/server/raft.go`
|
|
- `golang/nats-server/server/filestore.go`
|
|
- `golang/nats-server/server/memstore.go`
|
|
- Keep the protocol/API output shape compatible with Go behavior before internal refactors.
|
|
- Do not update `differences.md` until Task 31 verification is complete.
|
|
## Execution Results (2026-02-23)
|
|
|
|
- [x] dotnet build
|
|
- [x] dotnet test
|
|
- [ ] go jetstream parity suites (see `docs/plans/jetstream-parity-run-log.md`; current failures: `TestJetStreamClusterAckFloorBetweenLeaderAndFollowers`, `TestJetStreamClusterConsumerLeak`, `TestJetStreamStreamCreatePedanticMode`, `TestJetStreamStrictMode`, `TestJetStreamRateLimitHighStreamIngest`)
|
|
- [x] differences.md updated
|