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,744 @@
|
||||
// Go ref: TestJetStreamClusterXxx — jetstream_cluster_4_test.go
|
||||
// Covers: large clusters, many-subject streams, wildcard streams, high-message-count
|
||||
// publishes, multi-stream mixed replica counts, create/delete/recreate cycles,
|
||||
// consumer on high-message streams, purge/republish, stream delete cascades,
|
||||
// node removal and restart lifecycle markers.
|
||||
using System.Text;
|
||||
using NATS.Server.JetStream.Api;
|
||||
using NATS.Server.JetStream.Cluster;
|
||||
using NATS.Server.JetStream.Models;
|
||||
using NATS.Server.TestUtilities;
|
||||
|
||||
namespace NATS.Server.JetStream.Tests.JetStream.Cluster;
|
||||
|
||||
/// <summary>
|
||||
/// Advanced JetStream cluster tests covering high-load scenarios, large clusters,
|
||||
/// many-subject streams, wildcard subjects, multi-stream environments, consumer
|
||||
/// lifecycle edge cases, purge/republish cycles, and node lifecycle markers.
|
||||
/// Ported from Go jetstream_cluster_4_test.go.
|
||||
/// </summary>
|
||||
public class JsClusterAdvancedTests
|
||||
{
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterLargeClusterR5 — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Large_seven_node_cluster_with_R5_stream_accepts_publishes()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterLargeClusterR5 — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(7);
|
||||
|
||||
cluster.NodeCount.ShouldBe(7);
|
||||
|
||||
var resp = await cluster.CreateStreamAsync("R5LARGE", ["r5.>"], replicas: 5);
|
||||
resp.Error.ShouldBeNull();
|
||||
resp.StreamInfo.ShouldNotBeNull();
|
||||
resp.StreamInfo!.Config.Replicas.ShouldBe(5);
|
||||
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
var ack = await cluster.PublishAsync("r5.event", $"msg-{i}");
|
||||
ack.Stream.ShouldBe("R5LARGE");
|
||||
ack.Seq.ShouldBe((ulong)(i + 1));
|
||||
}
|
||||
|
||||
var state = await cluster.GetStreamStateAsync("R5LARGE");
|
||||
state.Messages.ShouldBe(20UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterStreamWithManySubjects — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_with_twenty_subjects_routes_all_correctly()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterStreamWithManySubjects — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
var subjects = Enumerable.Range(1, 20).Select(i => $"topic.{i}").ToArray();
|
||||
var resp = await cluster.CreateStreamAsync("MANYSUBJ", subjects, replicas: 3);
|
||||
resp.Error.ShouldBeNull();
|
||||
resp.StreamInfo!.Config.Subjects.Count.ShouldBe(20);
|
||||
|
||||
// Publish to each subject
|
||||
for (var i = 1; i <= 20; i++)
|
||||
{
|
||||
var ack = await cluster.PublishAsync($"topic.{i}", $"payload-{i}");
|
||||
ack.Stream.ShouldBe("MANYSUBJ");
|
||||
}
|
||||
|
||||
var state = await cluster.GetStreamStateAsync("MANYSUBJ");
|
||||
state.Messages.ShouldBe(20UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterWildcardSubjectStream — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_with_wildcard_gt_subject_captures_all_sub_subjects()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterWildcardSubjectStream — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
var resp = await cluster.CreateStreamAsync("WILDCARD", [">"], replicas: 3);
|
||||
resp.Error.ShouldBeNull();
|
||||
|
||||
await cluster.PublishAsync("any.subject.here", "msg1");
|
||||
await cluster.PublishAsync("totally.different", "msg2");
|
||||
await cluster.PublishAsync("nested.deep.path.to.leaf", "msg3");
|
||||
|
||||
var state = await cluster.GetStreamStateAsync("WILDCARD");
|
||||
state.Messages.ShouldBe(3UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterPublish1000MessagesToReplicatedStream — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Publish_1000_messages_to_R3_stream_all_acknowledged()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterPublish1000MessagesToReplicatedStream — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("BIG3", ["big.>"], replicas: 3);
|
||||
|
||||
var lastSeq = 0UL;
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
var ack = await cluster.PublishAsync("big.event", $"msg-{i}");
|
||||
ack.Stream.ShouldBe("BIG3");
|
||||
ack.ErrorCode.ShouldBeNull();
|
||||
lastSeq = ack.Seq;
|
||||
}
|
||||
|
||||
lastSeq.ShouldBe(1000UL);
|
||||
|
||||
var state = await cluster.GetStreamStateAsync("BIG3");
|
||||
state.Messages.ShouldBe(1000UL);
|
||||
state.FirstSeq.ShouldBe(1UL);
|
||||
state.LastSeq.ShouldBe(1000UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterPublish1000MessagesToR1Stream — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Publish_1000_messages_to_R1_stream_all_acknowledged()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterPublish1000MessagesToR1Stream — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("BIG1", ["b1.>"], replicas: 1);
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
var ack = await cluster.PublishAsync("b1.event", $"msg-{i}");
|
||||
ack.Stream.ShouldBe("BIG1");
|
||||
ack.ErrorCode.ShouldBeNull();
|
||||
}
|
||||
|
||||
var state = await cluster.GetStreamStateAsync("BIG1");
|
||||
state.Messages.ShouldBe(1000UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterStreamStateAfter1000Messages — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_state_accurate_after_1000_messages()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterStreamStateAfter1000Messages — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("STATE1K", ["s1k.>"], replicas: 3);
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
await cluster.PublishAsync("s1k.data", $"payload-{i}");
|
||||
|
||||
var state = await cluster.GetStreamStateAsync("STATE1K");
|
||||
state.Messages.ShouldBe(1000UL);
|
||||
state.FirstSeq.ShouldBe(1UL);
|
||||
state.LastSeq.ShouldBe(1000UL);
|
||||
state.Bytes.ShouldBeGreaterThan(0UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterMultipleStreamsMixedReplicas — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Ten_streams_with_mixed_replica_counts_all_independent()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterMultipleStreamsMixedReplicas — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var replicas = (i % 3) + 1;
|
||||
var resp = await cluster.CreateStreamAsync($"MIX{i}", [$"mix{i}.>"], replicas: replicas);
|
||||
resp.Error.ShouldBeNull();
|
||||
}
|
||||
|
||||
// Publish to each stream independently
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var ack = await cluster.PublishAsync($"mix{i}.event", $"stream-{i}-msg");
|
||||
ack.Stream.ShouldBe($"MIX{i}");
|
||||
}
|
||||
|
||||
// Verify each stream has exactly 1 message
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var state = await cluster.GetStreamStateAsync($"MIX{i}");
|
||||
state.Messages.ShouldBe(1UL);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterCreatePublishDeleteRecreate — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Create_publish_delete_recreate_cycle_three_times()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterCreatePublishDeleteRecreate — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
for (var cycle = 0; cycle < 3; cycle++)
|
||||
{
|
||||
// Create stream
|
||||
var create = await cluster.CreateStreamAsync("CYCLE", ["cyc.>"], replicas: 3);
|
||||
create.Error.ShouldBeNull();
|
||||
|
||||
// Publish messages
|
||||
for (var i = 0; i < 5; i++)
|
||||
await cluster.PublishAsync("cyc.event", $"cycle-{cycle}-msg-{i}");
|
||||
|
||||
var state = await cluster.GetStreamStateAsync("CYCLE");
|
||||
state.Messages.ShouldBe(5UL);
|
||||
|
||||
// Delete stream
|
||||
var del = await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}CYCLE", "{}");
|
||||
del.Success.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterConsumerOn1000MessageStream — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_on_stream_with_1000_messages_fetches_correctly()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterConsumerOn1000MessageStream — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("FETCH1K", ["f1k.>"], replicas: 3);
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
await cluster.PublishAsync("f1k.event", $"msg-{i}");
|
||||
|
||||
await cluster.CreateConsumerAsync("FETCH1K", "fetcher", filterSubject: "f1k.>");
|
||||
|
||||
var batch = await cluster.FetchAsync("FETCH1K", "fetcher", 100);
|
||||
batch.Messages.Count.ShouldBe(100);
|
||||
batch.Messages[0].Sequence.ShouldBe(1UL);
|
||||
batch.Messages[99].Sequence.ShouldBe(100UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterAckAllFor1000Messages — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task AckAll_for_1000_messages_reduces_pending_to_zero()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterAckAllFor1000Messages — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("ACKBIG", ["ab.>"], replicas: 3);
|
||||
await cluster.CreateConsumerAsync("ACKBIG", "acker", filterSubject: "ab.>",
|
||||
ackPolicy: AckPolicy.All);
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
await cluster.PublishAsync("ab.event", $"msg-{i}");
|
||||
|
||||
var batch = await cluster.FetchAsync("ACKBIG", "acker", 1000);
|
||||
batch.Messages.Count.ShouldBe(1000);
|
||||
|
||||
// AckAll up to last sequence
|
||||
cluster.AckAll("ACKBIG", "acker", 1000);
|
||||
|
||||
// After acking all 1000, state remains but pending is cleared
|
||||
var state = await cluster.GetStreamStateAsync("ACKBIG");
|
||||
state.Messages.ShouldBe(1000UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterStreamInfoConsistentAfterManyOps — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_info_consistent_after_many_operations()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterStreamInfoConsistentAfterManyOps — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("INFOCONSIST", ["ic.>"], replicas: 3);
|
||||
|
||||
// Interleave publishes and info requests
|
||||
for (var i = 0; i < 50; i++)
|
||||
{
|
||||
await cluster.PublishAsync("ic.event", $"msg-{i}");
|
||||
var info = await cluster.GetStreamInfoAsync("INFOCONSIST");
|
||||
info.StreamInfo.ShouldNotBeNull();
|
||||
info.StreamInfo!.State.Messages.ShouldBe((ulong)(i + 1));
|
||||
}
|
||||
|
||||
var finalInfo = await cluster.GetStreamInfoAsync("INFOCONSIST");
|
||||
finalInfo.StreamInfo!.Config.Name.ShouldBe("INFOCONSIST");
|
||||
finalInfo.StreamInfo.Config.Replicas.ShouldBe(3);
|
||||
finalInfo.StreamInfo.State.Messages.ShouldBe(50UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterMetaStateAfter10StreamOps — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Meta_state_after_creating_and_deleting_ten_streams()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterMetaStateAfter10StreamOps — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
await cluster.CreateStreamAsync($"META{i}", [$"meta{i}.>"], replicas: 3);
|
||||
|
||||
// Delete half
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
var del = await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}META{i}", "{}");
|
||||
del.Success.ShouldBeTrue();
|
||||
}
|
||||
|
||||
var metaState = cluster.GetMetaState();
|
||||
metaState.ShouldNotBeNull();
|
||||
|
||||
var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}");
|
||||
names.StreamNames.ShouldNotBeNull();
|
||||
names.StreamNames!.Count.ShouldBe(5);
|
||||
for (var i = 5; i < 10; i++)
|
||||
names.StreamNames.ShouldContain($"META{i}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterMultipleConsumersIndependentPending — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Five_consumers_on_same_stream_have_independent_pending()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterMultipleConsumersIndependentPending — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("MULTIDUP", ["md.>"], replicas: 3);
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
await cluster.PublishAsync("md.event", $"msg-{i}");
|
||||
|
||||
for (var c = 0; c < 5; c++)
|
||||
await cluster.CreateConsumerAsync("MULTIDUP", $"consumer{c}", filterSubject: "md.>");
|
||||
|
||||
// Each consumer should independently see all 10 messages
|
||||
for (var c = 0; c < 5; c++)
|
||||
{
|
||||
var batch = await cluster.FetchAsync("MULTIDUP", $"consumer{c}", 10);
|
||||
batch.Messages.Count.ShouldBe(10);
|
||||
batch.Messages[0].Sequence.ShouldBe(1UL);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterConsumerWildcardFilter — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_with_wildcard_filter_delivers_only_matching_messages()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterConsumerWildcardFilter — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("WFILT", ["wf.>"], replicas: 3);
|
||||
await cluster.CreateConsumerAsync("WFILT", "wildcons", filterSubject: "wf.alpha.>");
|
||||
|
||||
await cluster.PublishAsync("wf.alpha.one", "match1");
|
||||
await cluster.PublishAsync("wf.beta.two", "no-match");
|
||||
await cluster.PublishAsync("wf.alpha.three", "match2");
|
||||
await cluster.PublishAsync("wf.gamma.four", "no-match2");
|
||||
await cluster.PublishAsync("wf.alpha.five", "match3");
|
||||
|
||||
var batch = await cluster.FetchAsync("WFILT", "wildcons", 10);
|
||||
batch.Messages.Count.ShouldBe(3);
|
||||
foreach (var msg in batch.Messages)
|
||||
msg.Subject.ShouldStartWith("wf.alpha.");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterStreamUpdateAddSubjectsAfterPublish — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_update_adding_subjects_after_publishes_works()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterStreamUpdateAddSubjectsAfterPublish — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("ADDSUB", ["as.alpha"], replicas: 3);
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
await cluster.PublishAsync("as.alpha", $"msg-{i}");
|
||||
|
||||
var state = await cluster.GetStreamStateAsync("ADDSUB");
|
||||
state.Messages.ShouldBe(5UL);
|
||||
|
||||
// Add more subjects via update
|
||||
var update = cluster.UpdateStream("ADDSUB", ["as.alpha", "as.beta", "as.gamma"], replicas: 3);
|
||||
update.Error.ShouldBeNull();
|
||||
update.StreamInfo!.Config.Subjects.Count.ShouldBe(3);
|
||||
update.StreamInfo.Config.Subjects.ShouldContain("as.beta");
|
||||
|
||||
// Now publish to new subjects
|
||||
await cluster.PublishAsync("as.beta", "beta-msg");
|
||||
await cluster.PublishAsync("as.gamma", "gamma-msg");
|
||||
|
||||
var finalState = await cluster.GetStreamStateAsync("ADDSUB");
|
||||
finalState.Messages.ShouldBe(7UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterStreamPurgeAndRepublish — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_purge_in_cluster_then_republish_works_correctly()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterStreamPurgeAndRepublish — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("PURGEREP", ["pr.>"], replicas: 3);
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
await cluster.PublishAsync("pr.data", $"msg-{i}");
|
||||
|
||||
var before = await cluster.GetStreamStateAsync("PURGEREP");
|
||||
before.Messages.ShouldBe(100UL);
|
||||
|
||||
// Purge
|
||||
var purge = await cluster.RequestAsync($"{JetStreamApiSubjects.StreamPurge}PURGEREP", "{}");
|
||||
purge.Success.ShouldBeTrue();
|
||||
|
||||
var afterPurge = await cluster.GetStreamStateAsync("PURGEREP");
|
||||
afterPurge.Messages.ShouldBe(0UL);
|
||||
|
||||
// Re-publish
|
||||
for (var i = 0; i < 50; i++)
|
||||
{
|
||||
var ack = await cluster.PublishAsync("pr.data", $"new-msg-{i}");
|
||||
ack.ErrorCode.ShouldBeNull();
|
||||
}
|
||||
|
||||
var final = await cluster.GetStreamStateAsync("PURGEREP");
|
||||
final.Messages.ShouldBe(50UL);
|
||||
// Sequences restart after purge
|
||||
final.FirstSeq.ShouldBeGreaterThan(0UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterFetchEmptyAfterPurge — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Fetch_empty_after_stream_purge()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterFetchEmptyAfterPurge — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("PURGEDRAIN", ["pd.>"], replicas: 3);
|
||||
await cluster.CreateConsumerAsync("PURGEDRAIN", "reader", filterSubject: "pd.>");
|
||||
|
||||
for (var i = 0; i < 20; i++)
|
||||
await cluster.PublishAsync("pd.event", $"msg-{i}");
|
||||
|
||||
// Fetch to advance the consumer
|
||||
var pre = await cluster.FetchAsync("PURGEDRAIN", "reader", 20);
|
||||
pre.Messages.Count.ShouldBe(20);
|
||||
|
||||
// Purge the stream
|
||||
(await cluster.RequestAsync($"{JetStreamApiSubjects.StreamPurge}PURGEDRAIN", "{}")).Success.ShouldBeTrue();
|
||||
|
||||
// Fetch should now return empty
|
||||
var post = await cluster.FetchAsync("PURGEDRAIN", "reader", 20);
|
||||
post.Messages.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterStreamDeleteCascadesConsumers — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_delete_cascades_consumer_removal()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterStreamDeleteCascadesConsumers — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("CASCADE", ["cas.>"], replicas: 3);
|
||||
await cluster.CreateConsumerAsync("CASCADE", "c1");
|
||||
await cluster.CreateConsumerAsync("CASCADE", "c2");
|
||||
await cluster.CreateConsumerAsync("CASCADE", "c3");
|
||||
|
||||
// Verify consumers exist
|
||||
var names = await cluster.RequestAsync($"{JetStreamApiSubjects.ConsumerNames}CASCADE", "{}");
|
||||
names.ConsumerNames!.Count.ShouldBe(3);
|
||||
|
||||
// Delete the stream
|
||||
(await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}CASCADE", "{}")).Success.ShouldBeTrue();
|
||||
|
||||
// Stream no longer exists
|
||||
var info = await cluster.GetStreamInfoAsync("CASCADE");
|
||||
info.Error.ShouldNotBeNull();
|
||||
info.Error!.Code.ShouldBe(404);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterNodeRemovalPreservesDataReads — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Node_removal_does_not_affect_stream_data_reads()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterNodeRemovalPreservesDataReads — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(5);
|
||||
|
||||
await cluster.CreateStreamAsync("NODEREM", ["nr.>"], replicas: 3);
|
||||
|
||||
for (var i = 0; i < 30; i++)
|
||||
await cluster.PublishAsync("nr.event", $"msg-{i}");
|
||||
|
||||
var before = await cluster.GetStreamStateAsync("NODEREM");
|
||||
before.Messages.ShouldBe(30UL);
|
||||
|
||||
// Simulate removing a node
|
||||
cluster.RemoveNode(4);
|
||||
|
||||
// Data reads should still work on remaining nodes
|
||||
var after = await cluster.GetStreamStateAsync("NODEREM");
|
||||
after.Messages.ShouldBe(30UL);
|
||||
after.LastSeq.ShouldBe(30UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterNodeRestartPreservesLifecycleMarkers — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Node_restart_records_lifecycle_markers_correctly()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterNodeRestartPreservesLifecycleMarkers — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("RESTART", ["rs.>"], replicas: 3);
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
await cluster.PublishAsync("rs.event", $"msg-{i}");
|
||||
|
||||
// Simulate node removal
|
||||
cluster.RemoveNode(2);
|
||||
|
||||
// State still accessible with remaining nodes
|
||||
var mid = await cluster.GetStreamStateAsync("RESTART");
|
||||
mid.Messages.ShouldBe(10UL);
|
||||
|
||||
// Publish more while node is "down"
|
||||
for (var i = 10; i < 20; i++)
|
||||
await cluster.PublishAsync("rs.event", $"msg-{i}");
|
||||
|
||||
// Simulate node restart
|
||||
cluster.SimulateNodeRestart(2);
|
||||
|
||||
// All messages still accessible
|
||||
var final = await cluster.GetStreamStateAsync("RESTART");
|
||||
final.Messages.ShouldBe(20UL);
|
||||
final.LastSeq.ShouldBe(20UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterLeaderStepdownDuringPublish — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Leader_stepdown_during_publish_sequence_is_monotonic()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterLeaderStepdownDuringPublish — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("SEQSTEP", ["seq.>"], replicas: 3);
|
||||
|
||||
var seqs = new List<ulong>();
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var ack = await cluster.PublishAsync("seq.event", $"msg-{i}");
|
||||
seqs.Add(ack.Seq);
|
||||
}
|
||||
|
||||
// Step down leader
|
||||
(await cluster.StepDownStreamLeaderAsync("SEQSTEP")).Success.ShouldBeTrue();
|
||||
|
||||
for (var i = 10; i < 20; i++)
|
||||
{
|
||||
var ack = await cluster.PublishAsync("seq.event", $"msg-{i}");
|
||||
seqs.Add(ack.Seq);
|
||||
}
|
||||
|
||||
// All sequences must be strictly increasing
|
||||
for (var i = 1; i < seqs.Count; i++)
|
||||
seqs[i].ShouldBeGreaterThan(seqs[i - 1]);
|
||||
|
||||
seqs[^1].ShouldBe(20UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterStreamInfoAfterStepdown — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_info_accurate_after_leader_stepdown_with_many_messages()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterStreamInfoAfterStepdown — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("INFOSD1K", ["isd.>"], replicas: 3);
|
||||
|
||||
for (var i = 0; i < 500; i++)
|
||||
await cluster.PublishAsync("isd.event", $"msg-{i}");
|
||||
|
||||
(await cluster.StepDownStreamLeaderAsync("INFOSD1K")).Success.ShouldBeTrue();
|
||||
|
||||
for (var i = 500; i < 1000; i++)
|
||||
await cluster.PublishAsync("isd.event", $"msg-{i}");
|
||||
|
||||
var info = await cluster.GetStreamInfoAsync("INFOSD1K");
|
||||
info.StreamInfo.ShouldNotBeNull();
|
||||
info.StreamInfo!.State.Messages.ShouldBe(1000UL);
|
||||
info.StreamInfo.State.FirstSeq.ShouldBe(1UL);
|
||||
info.StreamInfo.State.LastSeq.ShouldBe(1000UL);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterStreamReplicaGroupHasCorrectNodes — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Replica_group_for_stream_has_correct_node_count()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterStreamReplicaGroupHasCorrectNodes — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(5);
|
||||
|
||||
await cluster.CreateStreamAsync("GRPCHECK", ["gc.>"], replicas: 3);
|
||||
|
||||
var group = cluster.GetReplicaGroup("GRPCHECK");
|
||||
group.ShouldNotBeNull();
|
||||
group!.Nodes.Count.ShouldBe(3);
|
||||
group.Leader.ShouldNotBeNull();
|
||||
group.Leader.IsLeader.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterConsumerLeaderAfterStreamStepdown — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Consumer_leader_remains_valid_after_stream_stepdown()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterConsumerLeaderAfterStreamStepdown — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("CONSLEADER", ["cl.>"], replicas: 3);
|
||||
await cluster.CreateConsumerAsync("CONSLEADER", "durable1");
|
||||
|
||||
var leaderBefore = cluster.GetConsumerLeaderId("CONSLEADER", "durable1");
|
||||
leaderBefore.ShouldNotBeNullOrWhiteSpace();
|
||||
|
||||
(await cluster.StepDownStreamLeaderAsync("CONSLEADER")).Success.ShouldBeTrue();
|
||||
|
||||
var leaderAfter = cluster.GetConsumerLeaderId("CONSLEADER", "durable1");
|
||||
leaderAfter.ShouldNotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterWaitOnStreamLeaderAfterCreation — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task WaitOnStreamLeader_resolves_immediately_for_existing_stream()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterWaitOnStreamLeaderAfterCreation — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("WLEADER", ["wl.>"], replicas: 3);
|
||||
|
||||
// Should complete immediately, no timeout
|
||||
await cluster.WaitOnStreamLeaderAsync("WLEADER", timeoutMs: 1000);
|
||||
|
||||
var leaderId = cluster.GetStreamLeaderId("WLEADER");
|
||||
leaderId.ShouldNotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterConsumerWaitOnLeaderAfterCreation — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task WaitOnConsumerLeader_resolves_for_existing_consumer()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterConsumerWaitOnLeaderAfterCreation — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
await cluster.CreateStreamAsync("WCLEADER2", ["wcl2.>"], replicas: 3);
|
||||
await cluster.CreateConsumerAsync("WCLEADER2", "dur-wc");
|
||||
|
||||
await cluster.WaitOnConsumerLeaderAsync("WCLEADER2", "dur-wc", timeoutMs: 1000);
|
||||
|
||||
var leaderId = cluster.GetConsumerLeaderId("WCLEADER2", "dur-wc");
|
||||
leaderId.ShouldNotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Go ref: TestJetStreamClusterAccountInfoAfterBatchDelete — jetstream_cluster_4_test.go
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task Account_info_reflects_accurate_stream_count_after_batch_delete()
|
||||
{
|
||||
// Go ref: TestJetStreamClusterAccountInfoAfterBatchDelete — jetstream_cluster_4_test.go
|
||||
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
|
||||
|
||||
for (var i = 0; i < 8; i++)
|
||||
await cluster.CreateStreamAsync($"BATCH{i}", [$"batch{i}.>"], replicas: 3);
|
||||
|
||||
var pre = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}");
|
||||
pre.AccountInfo!.Streams.ShouldBe(8);
|
||||
|
||||
// Delete 3 streams
|
||||
for (var i = 0; i < 3; i++)
|
||||
(await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}BATCH{i}", "{}")).Success.ShouldBeTrue();
|
||||
|
||||
var post = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}");
|
||||
post.AccountInfo!.Streams.ShouldBe(5);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user