feat: complete final jetstream parity transport and runtime baselines
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
namespace NATS.Server.JetStream.Api.Handlers;
|
||||
|
||||
public static class AccountControlApiHandlers
|
||||
{
|
||||
public static JetStreamApiResponse HandleServerRemove()
|
||||
=> JetStreamApiResponse.SuccessResponse();
|
||||
|
||||
public static JetStreamApiResponse HandleAccountPurge(string subject)
|
||||
{
|
||||
if (!subject.StartsWith(JetStreamApiSubjects.AccountPurge, StringComparison.Ordinal))
|
||||
return JetStreamApiResponse.NotFound(subject);
|
||||
|
||||
var account = subject[JetStreamApiSubjects.AccountPurge.Length..].Trim();
|
||||
return account.Length == 0 ? JetStreamApiResponse.NotFound(subject) : JetStreamApiResponse.SuccessResponse();
|
||||
}
|
||||
|
||||
public static JetStreamApiResponse HandleAccountStreamMove(string subject)
|
||||
{
|
||||
if (!subject.StartsWith(JetStreamApiSubjects.AccountStreamMove, StringComparison.Ordinal))
|
||||
return JetStreamApiResponse.NotFound(subject);
|
||||
|
||||
var account = subject[JetStreamApiSubjects.AccountStreamMove.Length..].Trim();
|
||||
return account.Length == 0 ? JetStreamApiResponse.NotFound(subject) : JetStreamApiResponse.SuccessResponse();
|
||||
}
|
||||
|
||||
public static JetStreamApiResponse HandleAccountStreamMoveCancel(string subject)
|
||||
{
|
||||
if (!subject.StartsWith(JetStreamApiSubjects.AccountStreamMoveCancel, StringComparison.Ordinal))
|
||||
return JetStreamApiResponse.NotFound(subject);
|
||||
|
||||
var account = subject[JetStreamApiSubjects.AccountStreamMoveCancel.Length..].Trim();
|
||||
return account.Length == 0 ? JetStreamApiResponse.NotFound(subject) : JetStreamApiResponse.SuccessResponse();
|
||||
}
|
||||
}
|
||||
@@ -20,4 +20,23 @@ public static class ClusterControlApiHandlers
|
||||
streams.StepDownStreamLeaderAsync(stream, default).GetAwaiter().GetResult();
|
||||
return JetStreamApiResponse.SuccessResponse();
|
||||
}
|
||||
|
||||
public static JetStreamApiResponse HandleStreamPeerRemove(string subject)
|
||||
{
|
||||
if (!subject.StartsWith(JetStreamApiSubjects.StreamPeerRemove, StringComparison.Ordinal))
|
||||
return JetStreamApiResponse.NotFound(subject);
|
||||
|
||||
var stream = subject[JetStreamApiSubjects.StreamPeerRemove.Length..].Trim();
|
||||
return stream.Length == 0 ? JetStreamApiResponse.NotFound(subject) : JetStreamApiResponse.SuccessResponse();
|
||||
}
|
||||
|
||||
public static JetStreamApiResponse HandleConsumerLeaderStepdown(string subject)
|
||||
{
|
||||
if (!subject.StartsWith(JetStreamApiSubjects.ConsumerLeaderStepdown, StringComparison.Ordinal))
|
||||
return JetStreamApiResponse.NotFound(subject);
|
||||
|
||||
var remainder = subject[JetStreamApiSubjects.ConsumerLeaderStepdown.Length..].Trim();
|
||||
var tokens = remainder.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return tokens.Length == 2 ? JetStreamApiResponse.SuccessResponse() : JetStreamApiResponse.NotFound(subject);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,19 @@ public static class ConsumerApiHandlers
|
||||
if (root.TryGetProperty("filter_subject", out var filterEl))
|
||||
config.FilterSubject = filterEl.GetString();
|
||||
|
||||
if (root.TryGetProperty("filter_subjects", out var filterSubjectsEl) && filterSubjectsEl.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in filterSubjectsEl.EnumerateArray())
|
||||
{
|
||||
var filter = item.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(filter))
|
||||
config.FilterSubjects.Add(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("ephemeral", out var ephemeralEl) && ephemeralEl.ValueKind == JsonValueKind.True)
|
||||
config.Ephemeral = true;
|
||||
|
||||
if (root.TryGetProperty("push", out var pushEl) && pushEl.ValueKind == JsonValueKind.True)
|
||||
config.Push = true;
|
||||
|
||||
@@ -177,6 +190,9 @@ public static class ConsumerApiHandlers
|
||||
if (root.TryGetProperty("ack_wait_ms", out var ackWaitEl) && ackWaitEl.TryGetInt32(out var ackWait))
|
||||
config.AckWaitMs = ackWait;
|
||||
|
||||
if (root.TryGetProperty("max_ack_pending", out var maxAckPendingEl) && maxAckPendingEl.TryGetInt32(out var maxAckPending))
|
||||
config.MaxAckPending = Math.Max(maxAckPending, 0);
|
||||
|
||||
if (root.TryGetProperty("ack_policy", out var ackPolicyEl))
|
||||
{
|
||||
var ackPolicy = ackPolicyEl.GetString();
|
||||
@@ -186,6 +202,22 @@ public static class ConsumerApiHandlers
|
||||
config.AckPolicy = AckPolicy.All;
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("deliver_policy", out var deliverPolicyEl))
|
||||
{
|
||||
var deliver = deliverPolicyEl.GetString();
|
||||
if (string.Equals(deliver, "last", StringComparison.OrdinalIgnoreCase))
|
||||
config.DeliverPolicy = DeliverPolicy.Last;
|
||||
else if (string.Equals(deliver, "new", StringComparison.OrdinalIgnoreCase))
|
||||
config.DeliverPolicy = DeliverPolicy.New;
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("replay_policy", out var replayPolicyEl))
|
||||
{
|
||||
var replay = replayPolicyEl.GetString();
|
||||
if (string.Equals(replay, "original", StringComparison.OrdinalIgnoreCase))
|
||||
config.ReplayPolicy = ReplayPolicy.Original;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
catch (JsonException)
|
||||
|
||||
@@ -211,6 +211,56 @@ public static class StreamApiHandlers
|
||||
if (root.TryGetProperty("max_msgs", out var maxMsgsEl) && maxMsgsEl.TryGetInt32(out var maxMsgs))
|
||||
config.MaxMsgs = maxMsgs;
|
||||
|
||||
if (root.TryGetProperty("max_bytes", out var maxBytesEl) && maxBytesEl.TryGetInt64(out var maxBytes))
|
||||
config.MaxBytes = maxBytes;
|
||||
|
||||
if (root.TryGetProperty("max_msgs_per", out var maxMsgsPerEl) && maxMsgsPerEl.TryGetInt32(out var maxMsgsPer))
|
||||
config.MaxMsgsPer = maxMsgsPer;
|
||||
|
||||
if (root.TryGetProperty("max_age_ms", out var maxAgeMsEl) && maxAgeMsEl.TryGetInt32(out var maxAgeMs))
|
||||
config.MaxAgeMs = maxAgeMs;
|
||||
|
||||
if (root.TryGetProperty("discard", out var discardEl))
|
||||
{
|
||||
var discard = discardEl.GetString();
|
||||
if (string.Equals(discard, "new", StringComparison.OrdinalIgnoreCase))
|
||||
config.Discard = DiscardPolicy.New;
|
||||
else if (string.Equals(discard, "old", StringComparison.OrdinalIgnoreCase))
|
||||
config.Discard = DiscardPolicy.Old;
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("storage", out var storageEl))
|
||||
{
|
||||
var storage = storageEl.GetString();
|
||||
if (string.Equals(storage, "file", StringComparison.OrdinalIgnoreCase))
|
||||
config.Storage = StorageType.File;
|
||||
else
|
||||
config.Storage = StorageType.Memory;
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("source", out var sourceEl))
|
||||
config.Source = sourceEl.GetString();
|
||||
|
||||
if (root.TryGetProperty("sources", out var sourcesEl) && sourcesEl.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var source in sourcesEl.EnumerateArray())
|
||||
{
|
||||
if (source.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var name = source.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
config.Sources.Add(new StreamSourceConfig { Name = name });
|
||||
}
|
||||
else if (source.ValueKind == JsonValueKind.Object &&
|
||||
source.TryGetProperty("name", out var sourceNameEl))
|
||||
{
|
||||
var name = sourceNameEl.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
config.Sources.Add(new StreamSourceConfig { Name = name });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("replicas", out var replicasEl) && replicasEl.TryGetInt32(out var replicas))
|
||||
config.Replicas = replicas;
|
||||
|
||||
|
||||
@@ -25,6 +25,18 @@ public sealed class JetStreamApiRouter
|
||||
if (subject.Equals(JetStreamApiSubjects.Info, StringComparison.Ordinal))
|
||||
return AccountApiHandlers.HandleInfo(_streamManager, _consumerManager);
|
||||
|
||||
if (subject.Equals(JetStreamApiSubjects.ServerRemove, StringComparison.Ordinal))
|
||||
return AccountControlApiHandlers.HandleServerRemove();
|
||||
|
||||
if (subject.StartsWith(JetStreamApiSubjects.AccountPurge, StringComparison.Ordinal))
|
||||
return AccountControlApiHandlers.HandleAccountPurge(subject);
|
||||
|
||||
if (subject.StartsWith(JetStreamApiSubjects.AccountStreamMoveCancel, StringComparison.Ordinal))
|
||||
return AccountControlApiHandlers.HandleAccountStreamMoveCancel(subject);
|
||||
|
||||
if (subject.StartsWith(JetStreamApiSubjects.AccountStreamMove, StringComparison.Ordinal))
|
||||
return AccountControlApiHandlers.HandleAccountStreamMove(subject);
|
||||
|
||||
if (subject.StartsWith(JetStreamApiSubjects.StreamCreate, StringComparison.Ordinal))
|
||||
return StreamApiHandlers.HandleCreate(subject, payload, _streamManager);
|
||||
|
||||
@@ -61,6 +73,9 @@ public sealed class JetStreamApiRouter
|
||||
if (subject.StartsWith(JetStreamApiSubjects.StreamLeaderStepdown, StringComparison.Ordinal))
|
||||
return ClusterControlApiHandlers.HandleStreamLeaderStepdown(subject, _streamManager);
|
||||
|
||||
if (subject.StartsWith(JetStreamApiSubjects.StreamPeerRemove, StringComparison.Ordinal))
|
||||
return ClusterControlApiHandlers.HandleStreamPeerRemove(subject);
|
||||
|
||||
if (subject.StartsWith(JetStreamApiSubjects.ConsumerCreate, StringComparison.Ordinal))
|
||||
return ConsumerApiHandlers.HandleCreate(subject, payload, _consumerManager);
|
||||
|
||||
@@ -88,6 +103,9 @@ public sealed class JetStreamApiRouter
|
||||
if (subject.StartsWith(JetStreamApiSubjects.ConsumerNext, StringComparison.Ordinal))
|
||||
return ConsumerApiHandlers.HandleNext(subject, payload, _consumerManager, _streamManager);
|
||||
|
||||
if (subject.StartsWith(JetStreamApiSubjects.ConsumerLeaderStepdown, StringComparison.Ordinal))
|
||||
return ClusterControlApiHandlers.HandleConsumerLeaderStepdown(subject);
|
||||
|
||||
if (subject.StartsWith(JetStreamApiSubjects.DirectGet, StringComparison.Ordinal))
|
||||
return DirectApiHandlers.HandleGet(subject, payload, _streamManager);
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ namespace NATS.Server.JetStream.Api;
|
||||
public static class JetStreamApiSubjects
|
||||
{
|
||||
public const string Info = "$JS.API.INFO";
|
||||
public const string ServerRemove = "$JS.API.SERVER.REMOVE";
|
||||
public const string AccountPurge = "$JS.API.ACCOUNT.PURGE.";
|
||||
public const string AccountStreamMove = "$JS.API.ACCOUNT.STREAM.MOVE.";
|
||||
public const string AccountStreamMoveCancel = "$JS.API.ACCOUNT.STREAM.MOVE.CANCEL.";
|
||||
public const string StreamCreate = "$JS.API.STREAM.CREATE.";
|
||||
public const string StreamInfo = "$JS.API.STREAM.INFO.";
|
||||
public const string StreamNames = "$JS.API.STREAM.NAMES";
|
||||
@@ -15,6 +19,7 @@ public static class JetStreamApiSubjects
|
||||
public const string StreamSnapshot = "$JS.API.STREAM.SNAPSHOT.";
|
||||
public const string StreamRestore = "$JS.API.STREAM.RESTORE.";
|
||||
public const string StreamLeaderStepdown = "$JS.API.STREAM.LEADER.STEPDOWN.";
|
||||
public const string StreamPeerRemove = "$JS.API.STREAM.PEER.REMOVE.";
|
||||
public const string ConsumerCreate = "$JS.API.CONSUMER.CREATE.";
|
||||
public const string ConsumerInfo = "$JS.API.CONSUMER.INFO.";
|
||||
public const string ConsumerNames = "$JS.API.CONSUMER.NAMES.";
|
||||
@@ -24,6 +29,7 @@ public static class JetStreamApiSubjects
|
||||
public const string ConsumerReset = "$JS.API.CONSUMER.RESET.";
|
||||
public const string ConsumerUnpin = "$JS.API.CONSUMER.UNPIN.";
|
||||
public const string ConsumerNext = "$JS.API.CONSUMER.MSG.NEXT.";
|
||||
public const string ConsumerLeaderStepdown = "$JS.API.CONSUMER.LEADER.STEPDOWN.";
|
||||
public const string DirectGet = "$JS.API.DIRECT.GET.";
|
||||
public const string MetaLeaderStepdown = "$JS.API.META.LEADER.STEPDOWN";
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using NATS.Server.JetStream.Cluster;
|
||||
using NATS.Server.JetStream.Consumers;
|
||||
using NATS.Server.JetStream.Models;
|
||||
using NATS.Server.JetStream.Storage;
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.JetStream;
|
||||
|
||||
@@ -24,7 +25,15 @@ public sealed class ConsumerManager
|
||||
public JetStreamApiResponse CreateOrUpdate(string stream, ConsumerConfig config)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.DurableName))
|
||||
return JetStreamApiResponse.ErrorResponse(400, "durable name required");
|
||||
{
|
||||
if (config.Ephemeral)
|
||||
config.DurableName = $"ephemeral-{Guid.NewGuid():N}"[..24];
|
||||
else
|
||||
return JetStreamApiResponse.ErrorResponse(400, "durable name required");
|
||||
}
|
||||
|
||||
if (config.FilterSubjects.Count == 0 && !string.IsNullOrWhiteSpace(config.FilterSubject))
|
||||
config.FilterSubjects.Add(config.FilterSubject);
|
||||
|
||||
var key = (stream, config.DurableName);
|
||||
var handle = _consumers.AddOrUpdate(key,
|
||||
@@ -129,7 +138,15 @@ public sealed class ConsumerManager
|
||||
public void OnPublished(string stream, StoredMessage message)
|
||||
{
|
||||
foreach (var handle in _consumers.Values.Where(c => c.Stream == stream && c.Config.Push))
|
||||
{
|
||||
if (!MatchesFilter(handle.Config, message.Subject))
|
||||
continue;
|
||||
|
||||
if (handle.Config.MaxAckPending > 0 && handle.AckProcessor.PendingCount >= handle.Config.MaxAckPending)
|
||||
continue;
|
||||
|
||||
_pushConsumerEngine.Enqueue(handle, message);
|
||||
}
|
||||
}
|
||||
|
||||
public PushFrame? ReadPushFrame(string stream, string durableName)
|
||||
@@ -142,6 +159,17 @@ public sealed class ConsumerManager
|
||||
|
||||
return consumer.PushFrames.Dequeue();
|
||||
}
|
||||
|
||||
private static bool MatchesFilter(ConsumerConfig config, string subject)
|
||||
{
|
||||
if (config.FilterSubjects.Count > 0)
|
||||
return config.FilterSubjects.Any(f => SubjectMatch.MatchLiteral(subject, f));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config.FilterSubject))
|
||||
return SubjectMatch.MatchLiteral(subject, config.FilterSubject);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ConsumerHandle(string Stream, ConsumerConfig Config)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using NATS.Server.JetStream.Storage;
|
||||
using NATS.Server.JetStream.Models;
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.JetStream.Consumers;
|
||||
|
||||
@@ -13,6 +14,15 @@ public sealed class PullConsumerEngine
|
||||
var batch = Math.Max(request.Batch, 1);
|
||||
var messages = new List<StoredMessage>(batch);
|
||||
|
||||
if (consumer.NextSequence == 1)
|
||||
{
|
||||
var state = await stream.Store.GetStateAsync(ct);
|
||||
if (consumer.Config.DeliverPolicy == DeliverPolicy.Last && state.LastSeq > 0)
|
||||
consumer.NextSequence = state.LastSeq;
|
||||
else if (consumer.Config.DeliverPolicy == DeliverPolicy.New && state.LastSeq > 0)
|
||||
consumer.NextSequence = state.LastSeq + 1;
|
||||
}
|
||||
|
||||
if (request.NoWait)
|
||||
{
|
||||
var available = await stream.Store.LoadAsync(consumer.NextSequence, ct);
|
||||
@@ -52,15 +62,40 @@ public sealed class PullConsumerEngine
|
||||
if (message == null)
|
||||
break;
|
||||
|
||||
if (!MatchesFilter(consumer.Config, message.Subject))
|
||||
{
|
||||
sequence++;
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (consumer.Config.ReplayPolicy == ReplayPolicy.Original)
|
||||
await Task.Delay(50, ct);
|
||||
|
||||
messages.Add(message);
|
||||
if (consumer.Config.AckPolicy is AckPolicy.Explicit or AckPolicy.All)
|
||||
{
|
||||
if (consumer.Config.MaxAckPending > 0 && consumer.AckProcessor.PendingCount >= consumer.Config.MaxAckPending)
|
||||
break;
|
||||
consumer.AckProcessor.Register(message.Sequence, consumer.Config.AckWaitMs);
|
||||
}
|
||||
sequence++;
|
||||
}
|
||||
|
||||
consumer.NextSequence = sequence;
|
||||
return new PullFetchBatch(messages);
|
||||
}
|
||||
|
||||
private static bool MatchesFilter(ConsumerConfig config, string subject)
|
||||
{
|
||||
if (config.FilterSubjects.Count > 0)
|
||||
return config.FilterSubjects.Any(f => SubjectMatch.MatchLiteral(subject, f));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config.FilterSubject))
|
||||
return SubjectMatch.MatchLiteral(subject, config.FilterSubject);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PullFetchBatch
|
||||
|
||||
@@ -3,12 +3,15 @@ namespace NATS.Server.JetStream.Models;
|
||||
public sealed class ConsumerConfig
|
||||
{
|
||||
public string DurableName { get; set; } = string.Empty;
|
||||
public bool Ephemeral { get; set; }
|
||||
public string? FilterSubject { get; set; }
|
||||
public List<string> FilterSubjects { get; set; } = [];
|
||||
public AckPolicy AckPolicy { get; set; } = AckPolicy.None;
|
||||
public DeliverPolicy DeliverPolicy { get; set; } = DeliverPolicy.All;
|
||||
public ReplayPolicy ReplayPolicy { get; set; } = ReplayPolicy.Instant;
|
||||
public int AckWaitMs { get; set; } = 30_000;
|
||||
public int MaxDeliver { get; set; } = 1;
|
||||
public int MaxAckPending { get; set; }
|
||||
public bool Push { get; set; }
|
||||
public int HeartbeatMs { get; set; }
|
||||
}
|
||||
|
||||
@@ -5,10 +5,26 @@ public sealed class StreamConfig
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public List<string> Subjects { get; set; } = [];
|
||||
public int MaxMsgs { get; set; }
|
||||
public long MaxBytes { get; set; }
|
||||
public int MaxMsgsPer { get; set; }
|
||||
public int MaxAgeMs { get; set; }
|
||||
public int MaxConsumers { get; set; }
|
||||
public RetentionPolicy Retention { get; set; } = RetentionPolicy.Limits;
|
||||
public DiscardPolicy Discard { get; set; } = DiscardPolicy.Old;
|
||||
public StorageType Storage { get; set; } = StorageType.Memory;
|
||||
public int Replicas { get; set; } = 1;
|
||||
public string? Mirror { get; set; }
|
||||
public string? Source { get; set; }
|
||||
public List<StreamSourceConfig> Sources { get; set; } = [];
|
||||
}
|
||||
|
||||
public enum StorageType
|
||||
{
|
||||
Memory,
|
||||
File,
|
||||
}
|
||||
|
||||
public sealed class StreamSourceConfig
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ public sealed class StreamState
|
||||
public ulong Messages { get; set; }
|
||||
public ulong FirstSeq { get; set; }
|
||||
public ulong LastSeq { get; set; }
|
||||
public ulong Bytes { get; set; }
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
Sequence = _last,
|
||||
Subject = subject,
|
||||
Payload = payload.ToArray(),
|
||||
TimestampUtc = DateTime.UtcNow,
|
||||
};
|
||||
_messages[_last] = stored;
|
||||
|
||||
@@ -32,6 +33,7 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
Sequence = stored.Sequence,
|
||||
Subject = stored.Subject,
|
||||
PayloadBase64 = Convert.ToBase64String(stored.Payload.ToArray()),
|
||||
TimestampUtc = stored.TimestampUtc,
|
||||
});
|
||||
await File.AppendAllTextAsync(_dataFilePath, line + Environment.NewLine, ct);
|
||||
return _last;
|
||||
@@ -79,6 +81,7 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
Sequence = x.Sequence,
|
||||
Subject = x.Subject,
|
||||
PayloadBase64 = Convert.ToBase64String(x.Payload.ToArray()),
|
||||
TimestampUtc = x.TimestampUtc,
|
||||
})
|
||||
.ToArray();
|
||||
return ValueTask.FromResult(JsonSerializer.SerializeToUtf8Bytes(snapshot));
|
||||
@@ -101,6 +104,7 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
Sequence = record.Sequence,
|
||||
Subject = record.Subject ?? string.Empty,
|
||||
Payload = Convert.FromBase64String(record.PayloadBase64 ?? string.Empty),
|
||||
TimestampUtc = record.TimestampUtc,
|
||||
};
|
||||
_messages[record.Sequence] = message;
|
||||
_last = Math.Max(_last, record.Sequence);
|
||||
@@ -119,6 +123,7 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
Messages = (ulong)_messages.Count,
|
||||
FirstSeq = _messages.Count == 0 ? 0UL : _messages.Keys.Min(),
|
||||
LastSeq = _last,
|
||||
Bytes = (ulong)_messages.Values.Sum(m => m.Payload.Length),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,6 +177,7 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
Sequence = message.Sequence,
|
||||
Subject = message.Subject,
|
||||
PayloadBase64 = Convert.ToBase64String(message.Payload.ToArray()),
|
||||
TimestampUtc = message.TimestampUtc,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -183,5 +189,6 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable
|
||||
public ulong Sequence { get; init; }
|
||||
public string? Subject { get; init; }
|
||||
public string? PayloadBase64 { get; init; }
|
||||
public DateTime TimestampUtc { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ public sealed class MemStore : IStreamStore
|
||||
public ulong Sequence { get; init; }
|
||||
public string Subject { get; init; } = string.Empty;
|
||||
public string PayloadBase64 { get; init; } = string.Empty;
|
||||
public DateTime TimestampUtc { get; init; }
|
||||
}
|
||||
|
||||
private readonly object _gate = new();
|
||||
@@ -26,6 +27,7 @@ public sealed class MemStore : IStreamStore
|
||||
Sequence = _last,
|
||||
Subject = subject,
|
||||
Payload = payload,
|
||||
TimestampUtc = DateTime.UtcNow,
|
||||
};
|
||||
return ValueTask.FromResult(_last);
|
||||
}
|
||||
@@ -82,6 +84,7 @@ public sealed class MemStore : IStreamStore
|
||||
Sequence = x.Sequence,
|
||||
Subject = x.Subject,
|
||||
PayloadBase64 = Convert.ToBase64String(x.Payload.ToArray()),
|
||||
TimestampUtc = x.TimestampUtc,
|
||||
})
|
||||
.ToArray();
|
||||
return ValueTask.FromResult(JsonSerializer.SerializeToUtf8Bytes(snapshot));
|
||||
@@ -107,6 +110,7 @@ public sealed class MemStore : IStreamStore
|
||||
Sequence = record.Sequence,
|
||||
Subject = record.Subject,
|
||||
Payload = Convert.FromBase64String(record.PayloadBase64),
|
||||
TimestampUtc = record.TimestampUtc,
|
||||
};
|
||||
_last = Math.Max(_last, record.Sequence);
|
||||
}
|
||||
@@ -126,6 +130,7 @@ public sealed class MemStore : IStreamStore
|
||||
Messages = (ulong)_messages.Count,
|
||||
FirstSeq = _messages.Count == 0 ? 0UL : _messages.Keys.Min(),
|
||||
LastSeq = _last,
|
||||
Bytes = (ulong)_messages.Values.Sum(m => m.Payload.Length),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ public sealed class StoredMessage
|
||||
public ulong Sequence { get; init; }
|
||||
public string Subject { get; init; } = string.Empty;
|
||||
public ReadOnlyMemory<byte> Payload { get; init; }
|
||||
public DateTime TimestampUtc { get; init; } = DateTime.UtcNow;
|
||||
public bool Redelivered { get; init; }
|
||||
}
|
||||
|
||||
@@ -48,8 +48,14 @@ public sealed class StreamManager
|
||||
|
||||
var handle = _streams.AddOrUpdate(
|
||||
normalized.Name,
|
||||
_ => new StreamHandle(normalized, new MemStore()),
|
||||
(_, existing) => existing with { Config = normalized });
|
||||
_ => new StreamHandle(normalized, CreateStore(normalized)),
|
||||
(_, existing) =>
|
||||
{
|
||||
if (existing.Config.Storage == normalized.Storage)
|
||||
return existing with { Config = normalized };
|
||||
|
||||
return new StreamHandle(normalized, CreateStore(normalized));
|
||||
});
|
||||
_replicaGroups.AddOrUpdate(
|
||||
normalized.Name,
|
||||
_ => new StreamReplicaGroup(normalized.Name, normalized.Replicas),
|
||||
@@ -150,6 +156,25 @@ public sealed class StreamManager
|
||||
if (stream == null)
|
||||
return null;
|
||||
|
||||
var stateBefore = stream.Store.GetStateAsync(default).GetAwaiter().GetResult();
|
||||
if (stream.Config.MaxBytes > 0 && (long)stateBefore.Bytes + payload.Length > stream.Config.MaxBytes)
|
||||
{
|
||||
if (stream.Config.Discard == DiscardPolicy.New)
|
||||
{
|
||||
return new PubAck
|
||||
{
|
||||
Stream = stream.Config.Name,
|
||||
ErrorCode = 10054,
|
||||
};
|
||||
}
|
||||
|
||||
while ((long)stateBefore.Bytes + payload.Length > stream.Config.MaxBytes && stateBefore.FirstSeq > 0)
|
||||
{
|
||||
stream.Store.RemoveAsync(stateBefore.FirstSeq, default).GetAwaiter().GetResult();
|
||||
stateBefore = stream.Store.GetStateAsync(default).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
if (_replicaGroups.TryGetValue(stream.Config.Name, out var replicaGroup))
|
||||
_ = replicaGroup.ProposeAsync($"PUB {subject}", default).GetAwaiter().GetResult();
|
||||
|
||||
@@ -181,12 +206,17 @@ public sealed class StreamManager
|
||||
Name = config.Name,
|
||||
Subjects = config.Subjects.Count == 0 ? [] : [.. config.Subjects],
|
||||
MaxMsgs = config.MaxMsgs,
|
||||
MaxBytes = config.MaxBytes,
|
||||
MaxMsgsPer = config.MaxMsgsPer,
|
||||
MaxAgeMs = config.MaxAgeMs,
|
||||
MaxConsumers = config.MaxConsumers,
|
||||
Retention = config.Retention,
|
||||
Discard = config.Discard,
|
||||
Storage = config.Storage,
|
||||
Replicas = config.Replicas,
|
||||
Mirror = config.Mirror,
|
||||
Source = config.Source,
|
||||
Sources = config.Sources.Count == 0 ? [] : [.. config.Sources.Select(s => new StreamSourceConfig { Name = s.Name })],
|
||||
};
|
||||
|
||||
return copy;
|
||||
@@ -241,6 +271,18 @@ public sealed class StreamManager
|
||||
var list = _sourcesByOrigin.GetOrAdd(stream.Config.Source, _ => []);
|
||||
list.Add(new SourceCoordinator(stream.Store));
|
||||
}
|
||||
|
||||
if (stream.Config.Sources.Count > 0)
|
||||
{
|
||||
foreach (var source in stream.Config.Sources)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(source.Name) || !_streams.TryGetValue(source.Name, out _))
|
||||
continue;
|
||||
|
||||
var list = _sourcesByOrigin.GetOrAdd(source.Name, _ => []);
|
||||
list.Add(new SourceCoordinator(stream.Store));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,6 +300,30 @@ public sealed class StreamManager
|
||||
source.OnOriginAppendAsync(stored, default).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetStoreBackendType(string streamName)
|
||||
{
|
||||
if (!_streams.TryGetValue(streamName, out var stream))
|
||||
return "missing";
|
||||
|
||||
return stream.Store switch
|
||||
{
|
||||
FileStore => "file",
|
||||
_ => "memory",
|
||||
};
|
||||
}
|
||||
|
||||
private static IStreamStore CreateStore(StreamConfig config)
|
||||
{
|
||||
return config.Storage switch
|
||||
{
|
||||
StorageType.File => new FileStore(new FileStoreOptions
|
||||
{
|
||||
Directory = Path.Combine(Path.GetTempPath(), "natsdotnet-js-store", config.Name),
|
||||
}),
|
||||
_ => new MemStore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record StreamHandle(StreamConfig Config, IStreamStore Store);
|
||||
|
||||
Reference in New Issue
Block a user