refactor: extract NATS.Server.JetStream.Tests project
Move 225 JetStream-related test files from NATS.Server.Tests into a dedicated NATS.Server.JetStream.Tests project. This includes root-level JetStream*.cs files, storage test files (FileStore, MemStore, StreamStoreContract), and the full JetStream/ subfolder tree (Api, Cluster, Consumers, MirrorSource, Snapshots, Storage, Streams). Updated all namespaces, added InternalsVisibleTo, registered in the solution file, and added the JETSTREAM_INTEGRATION_MATRIX define.
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
using NATS.Server.JetStream.MirrorSource;
|
||||
using NATS.Server.JetStream.Storage;
|
||||
|
||||
namespace NATS.Server.JetStream.Tests.JetStream.MirrorSource;
|
||||
|
||||
// Go reference: server/stream.go:2788-2854 (processMirrorMsgs)
|
||||
// Go reference: server/stream.go:2863-3014 (processInboundMirrorMsg)
|
||||
// Go reference: server/stream.go:3125-3400 (setupMirrorConsumer)
|
||||
|
||||
public class MirrorSyncTests
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Direct in-process synchronization tests
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
// Go reference: server/stream.go:2915 — sseq == mset.mirror.sseq+1 (normal in-order)
|
||||
public async Task Mirror_applies_single_message_and_tracks_sequence()
|
||||
{
|
||||
var target = new MemStore();
|
||||
var mirror = new MirrorCoordinator(target);
|
||||
|
||||
var msg = MakeMessage(seq: 1, subject: "orders.created", payload: "order-1");
|
||||
await mirror.OnOriginAppendAsync(msg, default);
|
||||
|
||||
mirror.LastOriginSequence.ShouldBe(1UL);
|
||||
mirror.LastSyncUtc.ShouldNotBe(default(DateTime));
|
||||
mirror.Lag.ShouldBe(0UL);
|
||||
|
||||
var stored = await target.LoadAsync(1, default);
|
||||
stored.ShouldNotBeNull();
|
||||
stored.Subject.ShouldBe("orders.created");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// Go reference: server/stream.go:2915-2917 — sequential messages increment sseq/dseq
|
||||
public async Task Mirror_applies_sequential_messages_in_order()
|
||||
{
|
||||
var target = new MemStore();
|
||||
var mirror = new MirrorCoordinator(target);
|
||||
|
||||
for (ulong i = 1; i <= 5; i++)
|
||||
{
|
||||
await mirror.OnOriginAppendAsync(
|
||||
MakeMessage(seq: i, subject: $"orders.{i}", payload: $"payload-{i}"), default);
|
||||
}
|
||||
|
||||
mirror.LastOriginSequence.ShouldBe(5UL);
|
||||
var state = await target.GetStateAsync(default);
|
||||
state.Messages.ShouldBe(5UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// Go reference: server/stream.go:2918-2921 — sseq <= mset.mirror.sseq (ignore older)
|
||||
public async Task Mirror_ignores_older_duplicate_messages()
|
||||
{
|
||||
var target = new MemStore();
|
||||
var mirror = new MirrorCoordinator(target);
|
||||
|
||||
await mirror.OnOriginAppendAsync(MakeMessage(seq: 5, subject: "a", payload: "1"), default);
|
||||
await mirror.OnOriginAppendAsync(MakeMessage(seq: 3, subject: "b", payload: "2"), default); // older
|
||||
await mirror.OnOriginAppendAsync(MakeMessage(seq: 5, subject: "c", payload: "3"), default); // same
|
||||
|
||||
mirror.LastOriginSequence.ShouldBe(5UL);
|
||||
var state = await target.GetStateAsync(default);
|
||||
state.Messages.ShouldBe(1UL); // only seq 5 stored
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// Go reference: server/stream.go:2927-2936 — gap handling (sseq > mirror.sseq+1)
|
||||
public async Task Mirror_handles_sequence_gaps_from_origin()
|
||||
{
|
||||
var target = new MemStore();
|
||||
var mirror = new MirrorCoordinator(target);
|
||||
|
||||
await mirror.OnOriginAppendAsync(MakeMessage(seq: 1, subject: "a", payload: "1"), default);
|
||||
// Gap: origin deleted seq 2-4
|
||||
await mirror.OnOriginAppendAsync(MakeMessage(seq: 5, subject: "b", payload: "2"), default);
|
||||
|
||||
mirror.LastOriginSequence.ShouldBe(5UL);
|
||||
var state = await target.GetStateAsync(default);
|
||||
state.Messages.ShouldBe(2UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mirror_first_message_at_arbitrary_sequence()
|
||||
{
|
||||
var target = new MemStore();
|
||||
var mirror = new MirrorCoordinator(target);
|
||||
|
||||
// First message arrives at seq 100 (origin has prior history)
|
||||
await mirror.OnOriginAppendAsync(MakeMessage(seq: 100, subject: "a", payload: "1"), default);
|
||||
|
||||
mirror.LastOriginSequence.ShouldBe(100UL);
|
||||
var stored = await target.LoadAsync(1, default);
|
||||
stored.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Health reporting tests
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
// Go reference: server/stream.go:2739-2743 (mirrorInfo)
|
||||
public async Task Health_report_reflects_current_state()
|
||||
{
|
||||
var target = new MemStore();
|
||||
var mirror = new MirrorCoordinator(target);
|
||||
|
||||
var report = mirror.GetHealthReport(originLastSeq: 10);
|
||||
report.LastOriginSequence.ShouldBe(0UL);
|
||||
report.Lag.ShouldBe(10UL);
|
||||
report.IsRunning.ShouldBeFalse();
|
||||
|
||||
await mirror.OnOriginAppendAsync(MakeMessage(seq: 7, subject: "a", payload: "1"), default);
|
||||
|
||||
report = mirror.GetHealthReport(originLastSeq: 10);
|
||||
report.LastOriginSequence.ShouldBe(7UL);
|
||||
report.Lag.ShouldBe(3UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Health_report_shows_zero_lag_when_caught_up()
|
||||
{
|
||||
var target = new MemStore();
|
||||
var mirror = new MirrorCoordinator(target);
|
||||
|
||||
await mirror.OnOriginAppendAsync(MakeMessage(seq: 10, subject: "a", payload: "1"), default);
|
||||
|
||||
var report = mirror.GetHealthReport(originLastSeq: 10);
|
||||
report.Lag.ShouldBe(0UL);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Background sync loop: channel-based
|
||||
// Go reference: server/stream.go:2788-2854 (processMirrorMsgs goroutine)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Channel_sync_loop_processes_enqueued_messages()
|
||||
{
|
||||
var target = new MemStore();
|
||||
await using var mirror = new MirrorCoordinator(target);
|
||||
|
||||
mirror.StartSyncLoop();
|
||||
mirror.IsRunning.ShouldBeTrue();
|
||||
|
||||
mirror.TryEnqueue(MakeMessage(seq: 1, subject: "a", payload: "1"));
|
||||
mirror.TryEnqueue(MakeMessage(seq: 2, subject: "b", payload: "2"));
|
||||
|
||||
await WaitForConditionAsync(() => mirror.LastOriginSequence >= 2, TimeSpan.FromSeconds(5));
|
||||
|
||||
mirror.LastOriginSequence.ShouldBe(2UL);
|
||||
var state = await target.GetStateAsync(default);
|
||||
state.Messages.ShouldBe(2UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Channel_sync_loop_can_be_stopped()
|
||||
{
|
||||
var target = new MemStore();
|
||||
await using var mirror = new MirrorCoordinator(target);
|
||||
|
||||
mirror.StartSyncLoop();
|
||||
mirror.IsRunning.ShouldBeTrue();
|
||||
|
||||
await mirror.StopAsync();
|
||||
mirror.IsRunning.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Channel_sync_loop_ignores_duplicates()
|
||||
{
|
||||
var target = new MemStore();
|
||||
await using var mirror = new MirrorCoordinator(target);
|
||||
|
||||
mirror.StartSyncLoop();
|
||||
|
||||
mirror.TryEnqueue(MakeMessage(seq: 1, subject: "a", payload: "1"));
|
||||
mirror.TryEnqueue(MakeMessage(seq: 1, subject: "a", payload: "1")); // duplicate
|
||||
mirror.TryEnqueue(MakeMessage(seq: 2, subject: "b", payload: "2"));
|
||||
|
||||
await WaitForConditionAsync(() => mirror.LastOriginSequence >= 2, TimeSpan.FromSeconds(5));
|
||||
|
||||
var state = await target.GetStateAsync(default);
|
||||
state.Messages.ShouldBe(2UL);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Background sync loop: pull-based
|
||||
// Go reference: server/stream.go:3125-3400 (setupMirrorConsumer)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Pull_sync_loop_fetches_from_origin_store()
|
||||
{
|
||||
var origin = new MemStore();
|
||||
var target = new MemStore();
|
||||
await using var mirror = new MirrorCoordinator(target);
|
||||
|
||||
// Pre-populate origin
|
||||
await origin.AppendAsync("a", "1"u8.ToArray(), default);
|
||||
await origin.AppendAsync("b", "2"u8.ToArray(), default);
|
||||
await origin.AppendAsync("c", "3"u8.ToArray(), default);
|
||||
|
||||
mirror.StartPullSyncLoop(origin);
|
||||
|
||||
await WaitForConditionAsync(() => mirror.LastOriginSequence >= 3, TimeSpan.FromSeconds(5));
|
||||
|
||||
mirror.LastOriginSequence.ShouldBe(3UL);
|
||||
var state = await target.GetStateAsync(default);
|
||||
state.Messages.ShouldBe(3UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Pull_sync_loop_catches_up_after_restart()
|
||||
{
|
||||
var origin = new MemStore();
|
||||
var target = new MemStore();
|
||||
|
||||
// Phase 1: sync first 2 messages
|
||||
{
|
||||
await using var mirror = new MirrorCoordinator(target);
|
||||
await origin.AppendAsync("a", "1"u8.ToArray(), default);
|
||||
await origin.AppendAsync("b", "2"u8.ToArray(), default);
|
||||
|
||||
mirror.StartPullSyncLoop(origin);
|
||||
await WaitForConditionAsync(() => mirror.LastOriginSequence >= 2, TimeSpan.FromSeconds(5));
|
||||
await mirror.StopAsync();
|
||||
}
|
||||
|
||||
// Phase 2: add more messages and restart with new coordinator
|
||||
await origin.AppendAsync("c", "3"u8.ToArray(), default);
|
||||
await origin.AppendAsync("d", "4"u8.ToArray(), default);
|
||||
|
||||
{
|
||||
// Simulate restart: new coordinator, same target store
|
||||
await using var mirror2 = new MirrorCoordinator(target);
|
||||
|
||||
// Manually sync to simulate catchup from seq 2
|
||||
await mirror2.OnOriginAppendAsync(
|
||||
new StoredMessage { Sequence = 3, Subject = "c", Payload = "3"u8.ToArray() }, default);
|
||||
await mirror2.OnOriginAppendAsync(
|
||||
new StoredMessage { Sequence = 4, Subject = "d", Payload = "4"u8.ToArray() }, default);
|
||||
|
||||
mirror2.LastOriginSequence.ShouldBe(4UL);
|
||||
}
|
||||
|
||||
var state = await target.GetStateAsync(default);
|
||||
state.Messages.ShouldBe(4UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Pull_sync_loop_updates_lag()
|
||||
{
|
||||
var origin = new MemStore();
|
||||
var target = new MemStore();
|
||||
await using var mirror = new MirrorCoordinator(target);
|
||||
|
||||
// Pre-populate origin with 10 messages
|
||||
for (var i = 0; i < 10; i++)
|
||||
await origin.AppendAsync($"subj.{i}", System.Text.Encoding.UTF8.GetBytes($"payload-{i}"), default);
|
||||
|
||||
mirror.StartPullSyncLoop(origin, batchSize: 3);
|
||||
|
||||
// Wait for some progress
|
||||
await WaitForConditionAsync(() => mirror.LastOriginSequence >= 3, TimeSpan.FromSeconds(5));
|
||||
|
||||
// Eventually should catch up to all 10
|
||||
await WaitForConditionAsync(() => mirror.LastOriginSequence >= 10, TimeSpan.FromSeconds(10));
|
||||
|
||||
var report = mirror.GetHealthReport(originLastSeq: 10);
|
||||
report.Lag.ShouldBe(0UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Pull_sync_loop_handles_empty_origin()
|
||||
{
|
||||
var origin = new MemStore();
|
||||
var target = new MemStore();
|
||||
await using var mirror = new MirrorCoordinator(target);
|
||||
|
||||
mirror.StartPullSyncLoop(origin);
|
||||
|
||||
// Wait a bit to ensure it doesn't crash
|
||||
await Task.Delay(200);
|
||||
|
||||
mirror.IsRunning.ShouldBeTrue();
|
||||
mirror.LastOriginSequence.ShouldBe(0UL);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Dispose / lifecycle tests
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Dispose_stops_running_sync_loop()
|
||||
{
|
||||
var target = new MemStore();
|
||||
var mirror = new MirrorCoordinator(target);
|
||||
|
||||
mirror.StartSyncLoop();
|
||||
mirror.IsRunning.ShouldBeTrue();
|
||||
|
||||
await mirror.DisposeAsync();
|
||||
mirror.IsRunning.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Multiple_start_calls_are_idempotent()
|
||||
{
|
||||
var target = new MemStore();
|
||||
await using var mirror = new MirrorCoordinator(target);
|
||||
|
||||
mirror.StartSyncLoop();
|
||||
mirror.StartSyncLoop(); // second call should be no-op
|
||||
|
||||
mirror.IsRunning.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static StoredMessage MakeMessage(ulong seq, string subject, string payload) => new()
|
||||
{
|
||||
Sequence = seq,
|
||||
Subject = subject,
|
||||
Payload = System.Text.Encoding.UTF8.GetBytes(payload),
|
||||
TimestampUtc = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
private static async Task WaitForConditionAsync(Func<bool> condition, TimeSpan timeout)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(timeout);
|
||||
while (!condition())
|
||||
{
|
||||
await Task.Delay(25, cts.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user