Files
natsdotnet/tests/NATS.Server.Tests/JetStream/Cluster/JsClusterStreamPlacementTests.cs
Joseph Doherty 5a22fd3213 feat: add JetStream cluster stream replication and placement tests (Go parity)
Adds 97 tests across two new files covering stream replication semantics
(R1/R3 creation, replica group size, publish preservation, state accuracy,
purge, update, delete, max limits, subjects, wildcards, storage type) and
placement semantics (replica caps at cluster size, various cluster sizes,
concurrent creation, stepdown resilience, long names, re-create after delete).
2026-02-24 07:53:28 -05:00

825 lines
33 KiB
C#

// Go parity: golang/nats-server/server/jetstream_cluster_1_test.go
// Covers: placement caps, cluster size variations, replica defaults, R1/R3/R5/R7
// placement, stepdown and info consistency, concurrent creation, long names,
// subject overlap, re-create after delete, update without message loss.
using System.Text;
using NATS.Server.JetStream.Api;
using NATS.Server.JetStream.Cluster;
using NATS.Server.JetStream.Models;
namespace NATS.Server.Tests.JetStream.Cluster;
/// <summary>
/// Tests covering JetStream cluster stream placement semantics:
/// replica caps at cluster size, various cluster sizes, replica defaults,
/// concurrent creation, leader stepdown, info consistency, and edge cases.
/// Ported from Go jetstream_cluster_1_test.go.
/// </summary>
public class JsClusterStreamPlacementTests
{
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_caps_five_replicas_in_three_node_cluster()
{
var planner = new AssetPlacementPlanner(nodes: 3);
var placement = planner.PlanReplicas(replicas: 5);
placement.Count.ShouldBe(3);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_allows_exact_cluster_size_replicas()
{
var planner = new AssetPlacementPlanner(nodes: 3);
var placement = planner.PlanReplicas(replicas: 3);
placement.Count.ShouldBe(3);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_zero_replicas_defaults_to_one()
{
var planner = new AssetPlacementPlanner(nodes: 3);
var placement = planner.PlanReplicas(replicas: 0);
placement.Count.ShouldBe(1);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_negative_replicas_treated_as_one()
{
var planner = new AssetPlacementPlanner(nodes: 3);
var placement = planner.PlanReplicas(replicas: -1);
placement.Count.ShouldBe(1);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterSingleReplicaStreams server/jetstream_cluster_1_test.go:223
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_R1_in_single_node_cluster()
{
var planner = new AssetPlacementPlanner(nodes: 1);
var placement = planner.PlanReplicas(replicas: 1);
placement.Count.ShouldBe(1);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterExpandCluster server/jetstream_cluster_1_test.go:86
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_caps_to_single_node_in_one_node_cluster()
{
var planner = new AssetPlacementPlanner(nodes: 1);
var placement = planner.PlanReplicas(replicas: 3);
placement.Count.ShouldBe(1);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_R1_in_three_node_cluster()
{
var planner = new AssetPlacementPlanner(nodes: 3);
var placement = planner.PlanReplicas(replicas: 1);
placement.Count.ShouldBe(1);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_R3_in_five_node_cluster()
{
var planner = new AssetPlacementPlanner(nodes: 5);
var placement = planner.PlanReplicas(replicas: 3);
placement.Count.ShouldBe(3);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_R5_in_seven_node_cluster()
{
var planner = new AssetPlacementPlanner(nodes: 7);
var placement = planner.PlanReplicas(replicas: 5);
placement.Count.ShouldBe(5);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_R7_in_seven_node_cluster_exact_match()
{
var planner = new AssetPlacementPlanner(nodes: 7);
var placement = planner.PlanReplicas(replicas: 7);
placement.Count.ShouldBe(7);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_caps_R7_in_five_node_cluster_to_five()
{
var planner = new AssetPlacementPlanner(nodes: 5);
var placement = planner.PlanReplicas(replicas: 7);
placement.Count.ShouldBe(5);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamInfoList server/jetstream_cluster_1_test.go:1284
// ---------------------------------------------------------------
[Fact]
public async Task Multiple_streams_with_different_placements_coexist()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(5);
await cluster.CreateStreamAsync("P1", ["p1.>"], replicas: 1);
await cluster.CreateStreamAsync("P3", ["p3.>"], replicas: 3);
await cluster.CreateStreamAsync("P5", ["p5.>"], replicas: 5);
var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}");
names.StreamNames.ShouldNotBeNull();
names.StreamNames!.Count.ShouldBe(3);
names.StreamNames.ShouldContain("P1");
names.StreamNames.ShouldContain("P3");
names.StreamNames.ShouldContain("P5");
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public async Task Stream_with_replicas_equal_to_cluster_size_succeeds()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
var resp = await cluster.CreateStreamAsync("FULL3", ["full3.>"], replicas: 3);
resp.Error.ShouldBeNull();
var group = cluster.GetReplicaGroup("FULL3");
group.ShouldNotBeNull();
group!.Nodes.Count.ShouldBe(3);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamInfoList server/jetstream_cluster_1_test.go:1284
// ---------------------------------------------------------------
[Fact]
public async Task Stream_creation_after_another_stream_exists_succeeds()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("FIRST", ["first.>"], replicas: 3);
var resp = await cluster.CreateStreamAsync("SECOND", ["second.>"], replicas: 3);
resp.Error.ShouldBeNull();
resp.StreamInfo.ShouldNotBeNull();
resp.StreamInfo!.Config.Name.ShouldBe("SECOND");
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMaxStreamsReached server/jetstream_cluster_1_test.go:3177
// ---------------------------------------------------------------
[Fact]
public async Task Ten_streams_in_same_cluster_all_exist()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
for (var i = 0; i < 10; i++)
await cluster.CreateStreamAsync($"PLACE{i}", [$"place{i}.>"], replicas: 3);
var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}");
names.StreamNames.ShouldNotBeNull();
names.StreamNames!.Count.ShouldBe(10);
for (var i = 0; i < 10; i++)
names.StreamNames.ShouldContain($"PLACE{i}");
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925
// ---------------------------------------------------------------
[Fact]
public async Task Replicated_stream_survives_meta_leader_stepdown()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("SURV", ["surv.>"], replicas: 3);
for (var i = 0; i < 5; i++)
await cluster.PublishAsync("surv.event", $"msg-{i}");
var metaBefore = cluster.GetMetaLeaderId();
cluster.StepDownMetaLeader();
var metaAfter = cluster.GetMetaLeaderId();
metaAfter.ShouldNotBe(metaBefore);
// Stream still accessible after meta stepdown
var state = await cluster.GetStreamStateAsync("SURV");
state.Messages.ShouldBe(5UL);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925
// ---------------------------------------------------------------
[Fact]
public async Task Stream_info_consistent_after_meta_stepdown()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("INFOSTEP", ["infostep.>"], replicas: 3);
for (var i = 0; i < 7; i++)
await cluster.PublishAsync("infostep.event", $"msg-{i}");
cluster.StepDownMetaLeader();
var info = await cluster.GetStreamInfoAsync("INFOSTEP");
info.Error.ShouldBeNull();
info.StreamInfo.ShouldNotBeNull();
info.StreamInfo!.Config.Name.ShouldBe("INFOSTEP");
info.StreamInfo.State.Messages.ShouldBe(7UL);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public void Placement_more_replicas_than_nodes_caps_not_errors()
{
// Verifies AssetPlacementPlanner silently caps rather than throwing
var planner = new AssetPlacementPlanner(nodes: 3);
var act = () => planner.PlanReplicas(replicas: 999);
act.ShouldNotThrow();
var result = planner.PlanReplicas(replicas: 999);
result.Count.ShouldBe(3);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterExpandCluster server/jetstream_cluster_1_test.go:86
// ---------------------------------------------------------------
[Fact]
public void Placement_cluster_size_one_always_returns_one_replica()
{
var planner = new AssetPlacementPlanner(nodes: 1);
for (var r = 1; r <= 10; r++)
planner.PlanReplicas(replicas: r).Count.ShouldBe(1);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamNormalCatchup server/jetstream_cluster_1_test.go:1607
// ---------------------------------------------------------------
[Fact]
public async Task Stream_exists_after_remove_and_restart_node_simulation()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("NODEREMOVE", ["noderemove.>"], replicas: 3);
for (var i = 0; i < 5; i++)
await cluster.PublishAsync("noderemove.event", $"msg-{i}");
cluster.RemoveNode(2);
cluster.SimulateNodeRestart(2);
var state = await cluster.GetStreamStateAsync("NODEREMOVE");
state.Messages.ShouldBe(5UL);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamInfoList server/jetstream_cluster_1_test.go:1284
// ---------------------------------------------------------------
[Fact]
public async Task Concurrent_stream_creation_all_streams_verify_exist()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
var tasks = Enumerable.Range(0, 5)
.Select(i => cluster.CreateStreamAsync($"CONC{i}", [$"conc{i}.>"], replicas: 3))
.ToArray();
await Task.WhenAll(tasks);
var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}");
names.StreamNames.ShouldNotBeNull();
names.StreamNames!.Count.ShouldBe(5);
for (var i = 0; i < 5; i++)
names.StreamNames.ShouldContain($"CONC{i}");
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public async Task Stream_names_can_be_long_strings()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
var longName = new string('A', 60);
var resp = await cluster.CreateStreamAsync(longName, [$"{longName.ToLowerInvariant()}.>"], replicas: 3);
resp.Error.ShouldBeNull();
resp.StreamInfo!.Config.Name.ShouldBe(longName);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamOverlapSubjects server/jetstream_cluster_1_test.go:1248
// ---------------------------------------------------------------
[Fact]
public async Task Stream_subjects_can_be_completely_distinct_from_others()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("DISTINCT1", ["ns1.>"], replicas: 3);
await cluster.CreateStreamAsync("DISTINCT2", ["ns2.>"], replicas: 3);
await cluster.CreateStreamAsync("DISTINCT3", ["ns3.>"], replicas: 3);
var ack1 = await cluster.PublishAsync("ns1.event", "msg1");
ack1.Stream.ShouldBe("DISTINCT1");
var ack2 = await cluster.PublishAsync("ns2.event", "msg2");
ack2.Stream.ShouldBe("DISTINCT2");
var ack3 = await cluster.PublishAsync("ns3.event", "msg3");
ack3.Stream.ShouldBe("DISTINCT3");
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamUpdate server/jetstream_cluster_1_test.go:1433
// ---------------------------------------------------------------
[Fact]
public async Task Re_creating_deleted_stream_with_same_placement_works()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("REDEL", ["redel.>"], replicas: 3);
await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}REDEL", "{}");
var resp = await cluster.CreateStreamAsync("REDEL", ["redel.>"], replicas: 3);
resp.Error.ShouldBeNull();
resp.StreamInfo.ShouldNotBeNull();
resp.StreamInfo!.Config.Name.ShouldBe("REDEL");
resp.StreamInfo.Config.Replicas.ShouldBe(3);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamUpdate server/jetstream_cluster_1_test.go:1433
// ---------------------------------------------------------------
[Fact]
public async Task Stream_update_does_not_lose_published_messages()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("NOLOSS", ["noloss.>"], replicas: 3);
for (var i = 0; i < 15; i++)
await cluster.PublishAsync("noloss.event", $"msg-{i}");
var update = cluster.UpdateStream("NOLOSS", ["noloss.>"], replicas: 3, maxMsgs: 100);
update.Error.ShouldBeNull();
var state = await cluster.GetStreamStateAsync("NOLOSS");
state.Messages.ShouldBe(15UL);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925
// ---------------------------------------------------------------
[Fact]
public async Task R3_stream_leader_stepdown_elects_new_leader()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("PLSTEP", ["plstep.>"], replicas: 3);
var before = cluster.GetStreamLeaderId("PLSTEP");
before.ShouldNotBeNullOrWhiteSpace();
var resp = await cluster.StepDownStreamLeaderAsync("PLSTEP");
resp.Success.ShouldBeTrue();
var after = cluster.GetStreamLeaderId("PLSTEP");
after.ShouldNotBe(before);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925
// ---------------------------------------------------------------
[Fact]
public async Task Stream_info_consistent_after_R3_stream_leader_stepdown()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("PLINFOSTEP", ["plinfostep.>"], replicas: 3);
for (var i = 0; i < 5; i++)
await cluster.PublishAsync("plinfostep.event", $"msg-{i}");
await cluster.StepDownStreamLeaderAsync("PLINFOSTEP");
var info = await cluster.GetStreamInfoAsync("PLINFOSTEP");
info.Error.ShouldBeNull();
info.StreamInfo.ShouldNotBeNull();
info.StreamInfo!.Config.Replicas.ShouldBe(3);
info.StreamInfo.State.Messages.ShouldBe(5UL);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public async Task Placement_validation_replicas_capped_at_cluster_node_count()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
// StreamReplicaGroup internally caps replicas at cluster size
var group = cluster.GetReplicaGroup("NOTEXIST");
group.ShouldBeNull();
// Creating with excess replicas should work (streamed to cluster-size)
var resp = await cluster.CreateStreamAsync("CAPTEST", ["captest.>"], replicas: 3);
resp.Error.ShouldBeNull();
var g = cluster.GetReplicaGroup("CAPTEST");
g.ShouldNotBeNull();
g!.Nodes.Count.ShouldBeLessThanOrEqualTo(cluster.NodeCount);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterExpandCluster server/jetstream_cluster_1_test.go:86
// ---------------------------------------------------------------
[Fact]
public void Placement_planner_cluster_size_reflected_correctly_for_different_sizes()
{
// 1-node cluster
new AssetPlacementPlanner(1).PlanReplicas(3).Count.ShouldBe(1);
// 3-node cluster
new AssetPlacementPlanner(3).PlanReplicas(3).Count.ShouldBe(3);
// 5-node cluster
new AssetPlacementPlanner(5).PlanReplicas(3).Count.ShouldBe(3);
// 7-node cluster
new AssetPlacementPlanner(7).PlanReplicas(3).Count.ShouldBe(3);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMetaSnapshotsAndCatchup server/jetstream_cluster_1_test.go:833
// ---------------------------------------------------------------
[Fact]
public async Task Meta_group_tracks_stream_placement_changes_through_stepdown()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("META_P1", ["meta_p1.>"], replicas: 1);
await cluster.CreateStreamAsync("META_P3", ["meta_p3.>"], replicas: 3);
var stateBefore = cluster.GetMetaState();
stateBefore.ShouldNotBeNull();
stateBefore!.Streams.ShouldContain("META_P1");
stateBefore.Streams.ShouldContain("META_P3");
cluster.StepDownMetaLeader();
var stateAfter = cluster.GetMetaState();
stateAfter.ShouldNotBeNull();
stateAfter!.Streams.ShouldContain("META_P1");
stateAfter.Streams.ShouldContain("META_P3");
stateAfter.LeadershipVersion.ShouldBeGreaterThan(stateBefore.LeadershipVersion);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamInfoList server/jetstream_cluster_1_test.go:1284
// ---------------------------------------------------------------
[Fact]
public async Task Stream_list_api_returns_all_streams_in_five_node_cluster()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(5);
await cluster.CreateStreamAsync("FL1", ["fl1.>"], replicas: 1);
await cluster.CreateStreamAsync("FL3", ["fl3.>"], replicas: 3);
await cluster.CreateStreamAsync("FL5", ["fl5.>"], replicas: 5);
var list = await cluster.RequestAsync(JetStreamApiSubjects.StreamList, "{}");
list.StreamNames.ShouldNotBeNull();
list.StreamNames!.Count.ShouldBe(3);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterSingleReplicaStreams server/jetstream_cluster_1_test.go:223
// ---------------------------------------------------------------
[Fact]
public async Task R1_placement_in_five_node_cluster_creates_one_node_group()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(5);
await cluster.CreateStreamAsync("R1IN5", ["r1in5.>"], replicas: 1);
var group = cluster.GetReplicaGroup("R1IN5");
group.ShouldNotBeNull();
group!.Nodes.Count.ShouldBe(1);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public async Task R3_placement_in_five_node_cluster_creates_three_node_group()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(5);
await cluster.CreateStreamAsync("R3IN5", ["r3in5.>"], replicas: 3);
var group = cluster.GetReplicaGroup("R3IN5");
group.ShouldNotBeNull();
group!.Nodes.Count.ShouldBe(3);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925
// ---------------------------------------------------------------
[Fact]
public async Task Consecutive_meta_stepdowns_preserve_stream_placements()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("CONSEC1", ["consec1.>"], replicas: 3);
await cluster.CreateStreamAsync("CONSEC2", ["consec2.>"], replicas: 1);
// Perform multiple stepdowns
cluster.StepDownMetaLeader();
cluster.StepDownMetaLeader();
cluster.StepDownMetaLeader();
var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}");
names.StreamNames.ShouldNotBeNull();
names.StreamNames!.ShouldContain("CONSEC1");
names.StreamNames.ShouldContain("CONSEC2");
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamUpdate server/jetstream_cluster_1_test.go:1433
// ---------------------------------------------------------------
[Fact]
public async Task Publish_after_stream_update_works_correctly()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("POSTUPD", ["postupd.>"], replicas: 3);
for (var i = 0; i < 5; i++)
await cluster.PublishAsync("postupd.event", $"before-{i}");
cluster.UpdateStream("POSTUPD", ["postupd.>"], replicas: 3, maxMsgs: 100);
for (var i = 0; i < 5; i++)
await cluster.PublishAsync("postupd.event", $"after-{i}");
var state = await cluster.GetStreamStateAsync("POSTUPD");
state.Messages.ShouldBe(10UL);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamPurge server/jetstream_cluster_1_test.go:522
// ---------------------------------------------------------------
[Fact]
public async Task R3_stream_purge_after_stepdown_clears_messages()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("PURGESTEP", ["purgestep.>"], replicas: 3);
for (var i = 0; i < 10; i++)
await cluster.PublishAsync("purgestep.event", $"msg-{i}");
await cluster.StepDownStreamLeaderAsync("PURGESTEP");
var purge = await cluster.RequestAsync($"{JetStreamApiSubjects.StreamPurge}PURGESTEP", "{}");
purge.Success.ShouldBeTrue();
var state = await cluster.GetStreamStateAsync("PURGESTEP");
state.Messages.ShouldBe(0UL);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public async Task R3_stream_has_leader_with_naming_convention()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("LEADNM", ["leadnm.>"], replicas: 3);
var group = cluster.GetReplicaGroup("LEADNM");
group.ShouldNotBeNull();
group!.Leader.Id.ShouldNotBeNullOrWhiteSpace();
group.Leader.IsLeader.ShouldBeTrue();
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMaxStreamsReached server/jetstream_cluster_1_test.go:3177
// ---------------------------------------------------------------
[Fact]
public async Task Account_info_reflects_correct_stream_count_after_placements()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("ACCP1", ["accp1.>"], replicas: 1);
await cluster.CreateStreamAsync("ACCP3", ["accp3.>"], replicas: 3);
var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}");
info.AccountInfo.ShouldNotBeNull();
info.AccountInfo!.Streams.ShouldBe(2);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamNormalCatchup server/jetstream_cluster_1_test.go:1607
// ---------------------------------------------------------------
[Fact]
public async Task Wait_on_stream_leader_completes_for_newly_placed_stream()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("WAITPL", ["waitpl.>"], replicas: 3);
await cluster.WaitOnStreamLeaderAsync("WAITPL", timeoutMs: 2000);
var leaderId = cluster.GetStreamLeaderId("WAITPL");
leaderId.ShouldNotBeNullOrWhiteSpace();
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterDelete server/jetstream_cluster_1_test.go:472
// ---------------------------------------------------------------
[Fact]
public async Task Stream_delete_reduces_account_stream_count()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("DEL_A", ["del_a.>"], replicas: 3);
await cluster.CreateStreamAsync("DEL_B", ["del_b.>"], replicas: 3);
await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}DEL_A", "{}");
var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}");
info.AccountInfo!.Streams.ShouldBe(1);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamInfoList server/jetstream_cluster_1_test.go:1284
// ---------------------------------------------------------------
[Fact]
public async Task Stream_placement_info_accessible_via_api_router_subject()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("APIPLC", ["apiplc.>"], replicas: 3);
var resp = await cluster.RequestAsync($"{JetStreamApiSubjects.StreamInfo}APIPLC", "{}");
resp.Error.ShouldBeNull();
resp.StreamInfo.ShouldNotBeNull();
resp.StreamInfo!.Config.Name.ShouldBe("APIPLC");
resp.StreamInfo.Config.Replicas.ShouldBe(3);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMemoryStore server/jetstream_cluster_1_test.go:423
// ---------------------------------------------------------------
[Fact]
public async Task Memory_store_placement_in_three_node_cluster_accepts_publishes()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("MEMPLACE", ["memplace.>"], replicas: 3, storage: StorageType.Memory);
for (var i = 0; i < 20; i++)
await cluster.PublishAsync("memplace.event", $"msg-{i}");
var state = await cluster.GetStreamStateAsync("MEMPLACE");
state.Messages.ShouldBe(20UL);
cluster.GetStoreBackendType("MEMPLACE").ShouldBe("memory");
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterLeader server/jetstream_cluster_1_test.go:73
// ---------------------------------------------------------------
[Fact]
public async Task Meta_leadership_version_increments_on_each_stepdown()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
var initial = cluster.GetMetaState();
initial.ShouldNotBeNull();
initial!.LeadershipVersion.ShouldBe(1L);
cluster.StepDownMetaLeader();
var v2 = cluster.GetMetaState()!.LeadershipVersion;
v2.ShouldBe(2L);
cluster.StepDownMetaLeader();
var v3 = cluster.GetMetaState()!.LeadershipVersion;
v3.ShouldBe(3L);
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925
// ---------------------------------------------------------------
[Fact]
public async Task Placement_group_leader_changes_on_stream_stepdown()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(3);
await cluster.CreateStreamAsync("STEPPL", ["steppl.>"], replicas: 3);
var groupBefore = cluster.GetReplicaGroup("STEPPL");
groupBefore.ShouldNotBeNull();
var leaderBefore = groupBefore!.Leader.Id;
await cluster.StepDownStreamLeaderAsync("STEPPL");
var groupAfter = cluster.GetReplicaGroup("STEPPL");
groupAfter.ShouldNotBeNull();
var leaderAfter = groupAfter!.Leader.Id;
leaderAfter.ShouldNotBe(leaderBefore);
groupAfter.Leader.IsLeader.ShouldBeTrue();
}
// ---------------------------------------------------------------
// Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299
// ---------------------------------------------------------------
[Fact]
public async Task Placement_node_count_consistent_with_requested_replicas()
{
await using var cluster = await JetStreamClusterFixture.StartAsync(5);
await cluster.CreateStreamAsync("NODECNT1", ["nc1.>"], replicas: 1);
await cluster.CreateStreamAsync("NODECNT2", ["nc2.>"], replicas: 2);
await cluster.CreateStreamAsync("NODECNT5", ["nc5.>"], replicas: 5);
cluster.GetReplicaGroup("NODECNT1")!.Nodes.Count.ShouldBe(1);
cluster.GetReplicaGroup("NODECNT2")!.Nodes.Count.ShouldBe(2);
cluster.GetReplicaGroup("NODECNT5")!.Nodes.Count.ShouldBe(5);
}
}