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.
826 lines
33 KiB
C#
826 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;
|
|
using NATS.Server.TestUtilities;
|
|
|
|
namespace NATS.Server.JetStream.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);
|
|
}
|
|
}
|