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.
618 lines
21 KiB
C#
618 lines
21 KiB
C#
// Go parity: golang/nats-server/server/jetstream_cluster_1_test.go
|
|
// Covers: cluster metadata operations, asset placement planner,
|
|
// replica group management, stream scaling, config validation,
|
|
// cluster expand, account info in cluster, max streams.
|
|
using System.Text;
|
|
using NATS.Server.Configuration;
|
|
using NATS.Server.JetStream;
|
|
using NATS.Server.JetStream.Api;
|
|
using NATS.Server.JetStream.Cluster;
|
|
using NATS.Server.JetStream.Models;
|
|
using NATS.Server.JetStream.Publish;
|
|
using NATS.Server.JetStream.Validation;
|
|
|
|
namespace NATS.Server.JetStream.Tests.JetStream.Cluster;
|
|
|
|
/// <summary>
|
|
/// Tests covering JetStream cluster metadata operations: asset placement,
|
|
/// replica group management, config validation, scaling, and account operations.
|
|
/// Ported from Go jetstream_cluster_1_test.go.
|
|
/// </summary>
|
|
public class JetStreamClusterMetaTests
|
|
{
|
|
// ---------------------------------------------------------------
|
|
// Go: TestJetStreamClusterConfig server/jetstream_cluster_1_test.go:43
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Config_requires_server_name_for_jetstream_cluster()
|
|
{
|
|
var options = new NatsOptions
|
|
{
|
|
ServerName = null,
|
|
JetStream = new JetStreamOptions { StoreDir = "/tmp/js" },
|
|
Cluster = new ClusterOptions { Port = 6222 },
|
|
};
|
|
var result = JetStreamConfigValidator.ValidateClusterConfig(options);
|
|
result.IsValid.ShouldBeFalse();
|
|
result.Message.ShouldContain("server_name");
|
|
}
|
|
|
|
[Fact]
|
|
public void Config_requires_cluster_name_for_jetstream_cluster()
|
|
{
|
|
var options = new NatsOptions
|
|
{
|
|
ServerName = "S1",
|
|
JetStream = new JetStreamOptions { StoreDir = "/tmp/js" },
|
|
Cluster = new ClusterOptions { Name = null, Port = 6222 },
|
|
};
|
|
var result = JetStreamConfigValidator.ValidateClusterConfig(options);
|
|
result.IsValid.ShouldBeFalse();
|
|
result.Message.ShouldContain("cluster.name");
|
|
}
|
|
|
|
[Fact]
|
|
public void Config_valid_when_server_and_cluster_names_set()
|
|
{
|
|
var options = new NatsOptions
|
|
{
|
|
ServerName = "S1",
|
|
JetStream = new JetStreamOptions { StoreDir = "/tmp/js" },
|
|
Cluster = new ClusterOptions { Name = "JSC", Port = 6222 },
|
|
};
|
|
var result = JetStreamConfigValidator.ValidateClusterConfig(options);
|
|
result.IsValid.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Config_skips_cluster_checks_when_no_cluster_configured()
|
|
{
|
|
var options = new NatsOptions
|
|
{
|
|
JetStream = new JetStreamOptions { StoreDir = "/tmp/js" },
|
|
};
|
|
var result = JetStreamConfigValidator.ValidateClusterConfig(options);
|
|
result.IsValid.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Config_skips_cluster_checks_when_no_jetstream_configured()
|
|
{
|
|
var options = new NatsOptions
|
|
{
|
|
Cluster = new ClusterOptions { Port = 6222 },
|
|
};
|
|
var result = JetStreamConfigValidator.ValidateClusterConfig(options);
|
|
result.IsValid.ShouldBeTrue();
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Placement planner tests
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Placement_planner_returns_requested_replica_count()
|
|
{
|
|
var planner = new AssetPlacementPlanner(nodes: 5);
|
|
var placement = planner.PlanReplicas(replicas: 3);
|
|
placement.Count.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void Placement_planner_caps_at_cluster_size()
|
|
{
|
|
var planner = new AssetPlacementPlanner(nodes: 3);
|
|
var placement = planner.PlanReplicas(replicas: 5);
|
|
placement.Count.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void Placement_planner_minimum_is_one_replica()
|
|
{
|
|
var planner = new AssetPlacementPlanner(nodes: 3);
|
|
var placement = planner.PlanReplicas(replicas: 0);
|
|
placement.Count.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public void Placement_planner_handles_single_node_cluster()
|
|
{
|
|
var planner = new AssetPlacementPlanner(nodes: 1);
|
|
var placement = planner.PlanReplicas(replicas: 3);
|
|
placement.Count.ShouldBe(1);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Meta group lifecycle tests
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Meta_group_initial_state_is_correct()
|
|
{
|
|
var meta = new JetStreamMetaGroup(3);
|
|
var state = meta.GetState();
|
|
|
|
state.ClusterSize.ShouldBe(3);
|
|
state.LeaderId.ShouldNotBeNullOrWhiteSpace();
|
|
state.LeadershipVersion.ShouldBe(1);
|
|
state.Streams.Count.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Meta_group_tracks_stream_proposals()
|
|
{
|
|
var meta = new JetStreamMetaGroup(3);
|
|
|
|
await meta.ProposeCreateStreamAsync(new StreamConfig { Name = "S1" }, default);
|
|
await meta.ProposeCreateStreamAsync(new StreamConfig { Name = "S2" }, default);
|
|
|
|
var state = meta.GetState();
|
|
state.Streams.Count.ShouldBe(2);
|
|
state.Streams.ShouldContain("S1");
|
|
state.Streams.ShouldContain("S2");
|
|
}
|
|
|
|
[Fact]
|
|
public void Meta_group_stepdown_cycles_leader()
|
|
{
|
|
var meta = new JetStreamMetaGroup(3);
|
|
var leader1 = meta.GetState().LeaderId;
|
|
|
|
meta.StepDown();
|
|
var leader2 = meta.GetState().LeaderId;
|
|
leader2.ShouldNotBe(leader1);
|
|
|
|
meta.StepDown();
|
|
var leader3 = meta.GetState().LeaderId;
|
|
leader3.ShouldNotBe(leader2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Meta_group_stepdown_wraps_around()
|
|
{
|
|
var meta = new JetStreamMetaGroup(2);
|
|
var leaders = new HashSet<string>();
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
{
|
|
leaders.Add(meta.GetState().LeaderId);
|
|
meta.StepDown();
|
|
}
|
|
|
|
// Should cycle between 2 leaders
|
|
leaders.Count.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Meta_group_leadership_version_increments()
|
|
{
|
|
var meta = new JetStreamMetaGroup(3);
|
|
meta.GetState().LeadershipVersion.ShouldBe(1);
|
|
|
|
meta.StepDown();
|
|
meta.GetState().LeadershipVersion.ShouldBe(2);
|
|
|
|
meta.StepDown();
|
|
meta.GetState().LeadershipVersion.ShouldBe(3);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Replica group tests
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Replica_group_creates_correct_node_count()
|
|
{
|
|
var group = new StreamReplicaGroup("TEST", replicas: 3);
|
|
group.Nodes.Count.ShouldBe(3);
|
|
group.StreamName.ShouldBe("TEST");
|
|
}
|
|
|
|
[Fact]
|
|
public void Replica_group_elects_initial_leader()
|
|
{
|
|
var group = new StreamReplicaGroup("TEST", replicas: 3);
|
|
group.Leader.ShouldNotBeNull();
|
|
group.Leader.IsLeader.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Replica_group_stepdown_changes_leader()
|
|
{
|
|
var group = new StreamReplicaGroup("TEST", replicas: 3);
|
|
var leaderBefore = group.Leader.Id;
|
|
|
|
await group.StepDownAsync(default);
|
|
var leaderAfter = group.Leader.Id;
|
|
|
|
leaderAfter.ShouldNotBe(leaderBefore);
|
|
group.Leader.IsLeader.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Replica_group_leader_accepts_proposals()
|
|
{
|
|
var group = new StreamReplicaGroup("TEST", replicas: 3);
|
|
|
|
var index = await group.ProposeAsync("PUB test.1", default);
|
|
index.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Replica_group_apply_placement_scales_up()
|
|
{
|
|
var group = new StreamReplicaGroup("TEST", replicas: 1);
|
|
group.Nodes.Count.ShouldBe(1);
|
|
|
|
await group.ApplyPlacementAsync([1, 2, 3], default);
|
|
group.Nodes.Count.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Replica_group_apply_placement_scales_down()
|
|
{
|
|
var group = new StreamReplicaGroup("TEST", replicas: 5);
|
|
group.Nodes.Count.ShouldBe(5);
|
|
|
|
await group.ApplyPlacementAsync([1, 2], default);
|
|
group.Nodes.Count.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Replica_group_apply_same_size_is_noop()
|
|
{
|
|
var group = new StreamReplicaGroup("TEST", replicas: 3);
|
|
var leaderBefore = group.Leader.Id;
|
|
|
|
await group.ApplyPlacementAsync([1, 2, 3], default);
|
|
group.Nodes.Count.ShouldBe(3);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Go: TestJetStreamClusterAccountInfo server/jetstream_cluster_1_test.go:94
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Account_info_tracks_streams_and_consumers_in_cluster()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
await fx.CreateStreamAsync("ACCT1", ["a1.>"], replicas: 3);
|
|
await fx.CreateStreamAsync("ACCT2", ["a2.>"], replicas: 3);
|
|
await fx.CreateConsumerAsync("ACCT1", "c1");
|
|
await fx.CreateConsumerAsync("ACCT1", "c2");
|
|
|
|
var resp = await fx.RequestAsync(JetStreamApiSubjects.Info, "{}");
|
|
resp.AccountInfo.ShouldNotBeNull();
|
|
resp.AccountInfo!.Streams.ShouldBe(2);
|
|
resp.AccountInfo.Consumers.ShouldBe(2);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Go: TestJetStreamClusterExtendedAccountInfo server/jetstream_cluster_1_test.go:3389
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Account_info_after_stream_delete_reflects_removal()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
await fx.CreateStreamAsync("DEL1", ["d1.>"], replicas: 3);
|
|
await fx.CreateStreamAsync("DEL2", ["d2.>"], replicas: 3);
|
|
|
|
(await fx.RequestAsync($"{JetStreamApiSubjects.StreamDelete}DEL1", "{}")).Success.ShouldBeTrue();
|
|
|
|
var resp = await fx.RequestAsync(JetStreamApiSubjects.Info, "{}");
|
|
resp.AccountInfo!.Streams.ShouldBe(1);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Go: TestJetStreamClusterAccountPurge server/jetstream_cluster_1_test.go:3891
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Account_purge_returns_success()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("PURGE1", ["pur.>"], replicas: 3);
|
|
|
|
var resp = await fx.RequestAsync($"{JetStreamApiSubjects.AccountPurge}GLOBAL", "{}");
|
|
resp.Success.ShouldBeTrue();
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Go: TestJetStreamClusterStreamLimitWithAccountDefaults server/jetstream_cluster_1_test.go:124
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Stream_with_max_bytes_and_replicas_created_successfully()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
var cfg = new StreamConfig
|
|
{
|
|
Name = "MBLIMIT",
|
|
Subjects = ["mbl.>"],
|
|
Replicas = 2,
|
|
MaxBytes = 4 * 1024 * 1024,
|
|
};
|
|
var resp = fx.CreateStreamDirect(cfg);
|
|
resp.Error.ShouldBeNull();
|
|
resp.StreamInfo!.Config.MaxBytes.ShouldBe(4 * 1024 * 1024);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Go: TestJetStreamClusterMaxStreamsReached server/jetstream_cluster_1_test.go:3177
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Multiple_streams_tracked_correctly_in_meta()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
for (var i = 0; i < 10; i++)
|
|
await fx.CreateStreamAsync($"MS{i}", [$"ms{i}.>"], replicas: 3);
|
|
|
|
var names = await fx.RequestAsync(JetStreamApiSubjects.StreamNames, "{}");
|
|
names.StreamNames!.Count.ShouldBe(10);
|
|
|
|
var meta = fx.GetMetaState();
|
|
meta.Streams.Count.ShouldBe(10);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Direct API tests (DirectGet)
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Direct_get_returns_message_by_sequence()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
var cfg = new StreamConfig
|
|
{
|
|
Name = "DIRECT",
|
|
Subjects = ["dir.>"],
|
|
Replicas = 3,
|
|
AllowDirect = true,
|
|
};
|
|
fx.CreateStreamDirect(cfg);
|
|
|
|
for (var i = 0; i < 5; i++)
|
|
await fx.PublishAsync("dir.event", $"msg-{i}");
|
|
|
|
var resp = await fx.RequestAsync($"{JetStreamApiSubjects.DirectGet}DIRECT", """{"seq":3}""");
|
|
resp.DirectMessage.ShouldNotBeNull();
|
|
resp.DirectMessage!.Sequence.ShouldBe(3UL);
|
|
resp.DirectMessage.Subject.ShouldBe("dir.event");
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Stream message get
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Stream_message_get_returns_correct_payload()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
await fx.CreateStreamAsync("MSGGET", ["mg.>"], replicas: 3);
|
|
|
|
await fx.PublishAsync("mg.event", "payload-1");
|
|
await fx.PublishAsync("mg.event", "payload-2");
|
|
|
|
var resp = await fx.RequestAsync($"{JetStreamApiSubjects.StreamMessageGet}MSGGET", """{"seq":2}""");
|
|
resp.StreamMessage.ShouldNotBeNull();
|
|
resp.StreamMessage!.Sequence.ShouldBe(2UL);
|
|
resp.StreamMessage.Payload.ShouldBe("payload-2");
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Consumer list and names
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Consumer_list_via_api_router()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
await fx.CreateStreamAsync("CLISTM", ["clm.>"], replicas: 3);
|
|
await fx.CreateConsumerAsync("CLISTM", "d1");
|
|
await fx.CreateConsumerAsync("CLISTM", "d2");
|
|
|
|
var names = await fx.RequestAsync($"{JetStreamApiSubjects.ConsumerNames}CLISTM", "{}");
|
|
names.ConsumerNames.ShouldNotBeNull();
|
|
names.ConsumerNames!.Count.ShouldBe(2);
|
|
|
|
var list = await fx.RequestAsync($"{JetStreamApiSubjects.ConsumerList}CLISTM", "{}");
|
|
list.ConsumerNames.ShouldNotBeNull();
|
|
list.ConsumerNames!.Count.ShouldBe(2);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Account stream move returns success shape
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Account_stream_move_api_returns_success()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
var resp = await fx.RequestAsync($"{JetStreamApiSubjects.AccountStreamMove}TEST", "{}");
|
|
resp.Success.ShouldBeTrue();
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Account stream move cancel returns success shape
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Account_stream_move_cancel_api_returns_success()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
var resp = await fx.RequestAsync($"{JetStreamApiSubjects.AccountStreamMoveCancel}TEST", "{}");
|
|
resp.Success.ShouldBeTrue();
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Stream create requires name
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Stream_create_without_name_returns_error()
|
|
{
|
|
var streamManager = new StreamManager();
|
|
var resp = streamManager.CreateOrUpdate(new StreamConfig { Name = "" });
|
|
resp.Error.ShouldNotBeNull();
|
|
resp.Error!.Description.ShouldContain("name");
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// NotFound for unknown API subject
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Unknown_api_subject_returns_not_found()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
var resp = await fx.RequestAsync("$JS.API.UNKNOWN.SUBJECT", "{}");
|
|
resp.Error.ShouldNotBeNull();
|
|
resp.Error!.Code.ShouldBe(404);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Stream info for non-existent stream returns 404
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Stream_info_nonexistent_returns_not_found()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
var resp = await fx.RequestAsync($"{JetStreamApiSubjects.StreamInfo}NOSTREAM", "{}");
|
|
resp.Error.ShouldNotBeNull();
|
|
resp.Error!.Code.ShouldBe(404);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Consumer info for non-existent consumer returns 404
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Consumer_info_nonexistent_returns_not_found()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("NOCONS", ["nc.>"], replicas: 3);
|
|
|
|
var resp = await fx.RequestAsync($"{JetStreamApiSubjects.ConsumerInfo}NOCONS.MISSING", "{}");
|
|
resp.Error.ShouldNotBeNull();
|
|
resp.Error!.Code.ShouldBe(404);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Delete non-existent stream returns 404
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Delete_nonexistent_stream_returns_not_found()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
|
|
var resp = await fx.RequestAsync($"{JetStreamApiSubjects.StreamDelete}GONE", "{}");
|
|
resp.Error.ShouldNotBeNull();
|
|
resp.Error!.Code.ShouldBe(404);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Delete non-existent consumer returns 404
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task Delete_nonexistent_consumer_returns_not_found()
|
|
{
|
|
await using var fx = await ClusterMetaFixture.StartAsync(nodes: 3);
|
|
await fx.CreateStreamAsync("NODEL", ["nd.>"], replicas: 3);
|
|
|
|
var resp = await fx.RequestAsync($"{JetStreamApiSubjects.ConsumerDelete}NODEL.MISSING", "{}");
|
|
resp.Error.ShouldNotBeNull();
|
|
resp.Error!.Code.ShouldBe(404);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Self-contained fixture for JetStream cluster meta tests.
|
|
/// </summary>
|
|
internal sealed class ClusterMetaFixture : IAsyncDisposable
|
|
{
|
|
private readonly JetStreamMetaGroup _metaGroup;
|
|
private readonly StreamManager _streamManager;
|
|
private readonly ConsumerManager _consumerManager;
|
|
private readonly JetStreamApiRouter _router;
|
|
private readonly JetStreamPublisher _publisher;
|
|
|
|
private ClusterMetaFixture(
|
|
JetStreamMetaGroup metaGroup,
|
|
StreamManager streamManager,
|
|
ConsumerManager consumerManager,
|
|
JetStreamApiRouter router,
|
|
JetStreamPublisher publisher)
|
|
{
|
|
_metaGroup = metaGroup;
|
|
_streamManager = streamManager;
|
|
_consumerManager = consumerManager;
|
|
_router = router;
|
|
_publisher = publisher;
|
|
}
|
|
|
|
public static Task<ClusterMetaFixture> StartAsync(int nodes)
|
|
{
|
|
var meta = new JetStreamMetaGroup(nodes);
|
|
var consumerManager = new ConsumerManager(meta);
|
|
var streamManager = new StreamManager(meta, consumerManager: consumerManager);
|
|
var router = new JetStreamApiRouter(streamManager, consumerManager, meta);
|
|
var publisher = new JetStreamPublisher(streamManager);
|
|
return Task.FromResult(new ClusterMetaFixture(meta, streamManager, consumerManager, router, publisher));
|
|
}
|
|
|
|
public Task CreateStreamAsync(string name, string[] subjects, int replicas)
|
|
{
|
|
var response = _streamManager.CreateOrUpdate(new StreamConfig
|
|
{
|
|
Name = name,
|
|
Subjects = [.. subjects],
|
|
Replicas = replicas,
|
|
});
|
|
if (response.Error is not null)
|
|
throw new InvalidOperationException(response.Error.Description);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public JetStreamApiResponse CreateStreamDirect(StreamConfig config)
|
|
=> _streamManager.CreateOrUpdate(config);
|
|
|
|
public Task<JetStreamApiResponse> CreateConsumerAsync(string stream, string durableName)
|
|
{
|
|
return Task.FromResult(_consumerManager.CreateOrUpdate(stream, new ConsumerConfig
|
|
{
|
|
DurableName = durableName,
|
|
}));
|
|
}
|
|
|
|
public Task<PubAck> PublishAsync(string subject, string payload)
|
|
{
|
|
if (_publisher.TryCapture(subject, Encoding.UTF8.GetBytes(payload), null, out var ack))
|
|
return Task.FromResult(ack);
|
|
throw new InvalidOperationException($"Publish to '{subject}' did not match a stream.");
|
|
}
|
|
|
|
public MetaGroupState GetMetaState() => _metaGroup.GetState();
|
|
|
|
public Task<JetStreamApiResponse> RequestAsync(string subject, string payload)
|
|
=> Task.FromResult(_router.Route(subject, Encoding.UTF8.GetBytes(payload)));
|
|
|
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
}
|