From 84d450b4a01c16e6c284cff14391b3abbc60f487 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 26 Feb 2026 16:14:40 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20port=20session=2019=20=E2=80=94=20JetSt?= =?UTF-8?q?ream=20Core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JetStreamTypes: JetStreamConfig, JetStreamStats, JetStreamAccountLimits, JetStreamTier, JetStreamAccountStats, JetStream engine, JsAccount, JsaUsage - JetStreamApiTypes: 50+ JSApi request/response types, API subject constants - JetStreamErrors: JsApiError + JsApiErrors with all 203 error codes - JetStreamVersioning: version constants and API level helpers - JetStreamBatching: Batching, BatchGroup, BatchStagedDiff, BatchApply - Removed JetStreamConfig/JetStreamState stubs from NatsServerTypes.cs - 374 features complete (IDs 1368-1519, 1751-1972) --- .../JetStream/JetStreamApiTypes.cs | 620 ++++++++++++++++++ .../JetStream/JetStreamBatching.cs | 132 ++++ .../JetStream/JetStreamErrors.cs | 323 +++++++++ .../JetStream/JetStreamTypes.cs | 293 +++++++++ .../JetStream/JetStreamVersioning.cs | 106 +++ .../ZB.MOM.NatsNet.Server/NatsServerTypes.cs | 18 +- porting.db | Bin 2473984 -> 2473984 bytes reports/current.md | 8 +- reports/report_3cffa5b.md | 39 ++ 9 files changed, 1519 insertions(+), 20 deletions(-) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamApiTypes.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamBatching.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamTypes.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamVersioning.cs create mode 100644 reports/report_3cffa5b.md diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamApiTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamApiTypes.cs new file mode 100644 index 0000000..e7bf85a --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamApiTypes.cs @@ -0,0 +1,620 @@ +// Copyright 2020-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/jetstream_api.go in the NATS server Go source. + +using System.Text.Json.Serialization; + +namespace ZB.MOM.NatsNet.Server; + +// --------------------------------------------------------------------------- +// Forward stubs for types defined in later sessions +// --------------------------------------------------------------------------- + +/// Stub: full definition in session 20 (stream.go). +public sealed class StreamInfo { } + +/// Stub: full definition in session 20 (consumer.go). +public sealed class ConsumerInfo { } + +/// Stub: stored message type — full definition in session 20. +public sealed class StoredMsg { } + +/// Priority group for pull consumers — full definition in session 20. +public sealed class PriorityGroup { } + +// --------------------------------------------------------------------------- +// API subject constants +// --------------------------------------------------------------------------- + +/// +/// JetStream API subject constants. +/// Mirrors the const block at the top of server/jetstream_api.go. +/// +public static class JsApiSubjects +{ + public const string JsAllApi = "$JS.API.>"; + public const string JsApiPrefix = "$JS.API"; + public const string JsApiAccountInfo = "$JS.API.INFO"; + + public const string JsApiStreamCreate = "$JS.API.STREAM.CREATE.*"; + public const string JsApiStreamCreateT = "$JS.API.STREAM.CREATE.{0}"; + public const string JsApiStreamUpdate = "$JS.API.STREAM.UPDATE.*"; + public const string JsApiStreamUpdateT = "$JS.API.STREAM.UPDATE.{0}"; + public const string JsApiStreams = "$JS.API.STREAM.NAMES"; + public const string JsApiStreamList = "$JS.API.STREAM.LIST"; + public const string JsApiStreamInfo = "$JS.API.STREAM.INFO.*"; + public const string JsApiStreamInfoT = "$JS.API.STREAM.INFO.{0}"; + public const string JsApiStreamDelete = "$JS.API.STREAM.DELETE.*"; + public const string JsApiStreamDeleteT = "$JS.API.STREAM.DELETE.{0}"; + public const string JsApiStreamPurge = "$JS.API.STREAM.PURGE.*"; + public const string JsApiStreamPurgeT = "$JS.API.STREAM.PURGE.{0}"; + public const string JsApiStreamSnapshot = "$JS.API.STREAM.SNAPSHOT.*"; + public const string JsApiStreamSnapshotT = "$JS.API.STREAM.SNAPSHOT.{0}"; + public const string JsApiStreamRestore = "$JS.API.STREAM.RESTORE.*"; + public const string JsApiStreamRestoreT = "$JS.API.STREAM.RESTORE.{0}"; + public const string JsApiMsgDelete = "$JS.API.STREAM.MSG.DELETE.*"; + public const string JsApiMsgDeleteT = "$JS.API.STREAM.MSG.DELETE.{0}"; + public const string JsApiMsgGet = "$JS.API.STREAM.MSG.GET.*"; + public const string JsApiMsgGetT = "$JS.API.STREAM.MSG.GET.{0}"; + public const string JsDirectMsgGet = "$JS.API.DIRECT.GET.*"; + public const string JsDirectMsgGetT = "$JS.API.DIRECT.GET.{0}"; + public const string JsDirectGetLastBySubject = "$JS.API.DIRECT.GET.*.>"; + public const string JsDirectGetLastBySubjectT = "$JS.API.DIRECT.GET.{0}.{1}"; + + public const string JsApiConsumerCreate = "$JS.API.CONSUMER.CREATE.*"; + public const string JsApiConsumerCreateT = "$JS.API.CONSUMER.CREATE.{0}"; + public const string JsApiConsumerCreateEx = "$JS.API.CONSUMER.CREATE.*.>"; + public const string JsApiConsumerCreateExT = "$JS.API.CONSUMER.CREATE.{0}.{1}.{2}"; + public const string JsApiDurableCreate = "$JS.API.CONSUMER.DURABLE.CREATE.*.*"; + public const string JsApiDurableCreateT = "$JS.API.CONSUMER.DURABLE.CREATE.{0}.{1}"; + public const string JsApiConsumers = "$JS.API.CONSUMER.NAMES.*"; + public const string JsApiConsumersT = "$JS.API.CONSUMER.NAMES.{0}"; + public const string JsApiConsumerList = "$JS.API.CONSUMER.LIST.*"; + public const string JsApiConsumerListT = "$JS.API.CONSUMER.LIST.{0}"; + public const string JsApiConsumerInfo = "$JS.API.CONSUMER.INFO.*.*"; + public const string JsApiConsumerInfoT = "$JS.API.CONSUMER.INFO.{0}.{1}"; + public const string JsApiConsumerDelete = "$JS.API.CONSUMER.DELETE.*.*"; + public const string JsApiConsumerDeleteT = "$JS.API.CONSUMER.DELETE.{0}.{1}"; + public const string JsApiConsumerPause = "$JS.API.CONSUMER.PAUSE.*.*"; + public const string JsApiConsumerPauseT = "$JS.API.CONSUMER.PAUSE.{0}.{1}"; + public const string JsApiRequestNextT = "$JS.API.CONSUMER.MSG.NEXT.{0}.{1}"; + public const string JsApiConsumerResetT = "$JS.API.CONSUMER.RESET.{0}.{1}"; + public const string JsApiConsumerUnpin = "$JS.API.CONSUMER.UNPIN.*.*"; + public const string JsApiConsumerUnpinT = "$JS.API.CONSUMER.UNPIN.{0}.{1}"; + + public const string JsApiStreamRemovePeer = "$JS.API.STREAM.PEER.REMOVE.*"; + public const string JsApiStreamRemovePeerT = "$JS.API.STREAM.PEER.REMOVE.{0}"; + public const string JsApiStreamLeaderStepDown = "$JS.API.STREAM.LEADER.STEPDOWN.*"; + public const string JsApiStreamLeaderStepDownT = "$JS.API.STREAM.LEADER.STEPDOWN.{0}"; + public const string JsApiConsumerLeaderStepDown = "$JS.API.CONSUMER.LEADER.STEPDOWN.*.*"; + public const string JsApiConsumerLeaderStepDownT = "$JS.API.CONSUMER.LEADER.STEPDOWN.{0}.{1}"; + public const string JsApiLeaderStepDown = "$JS.API.META.LEADER.STEPDOWN"; + public const string JsApiRemoveServer = "$JS.API.SERVER.REMOVE"; + public const string JsApiAccountPurge = "$JS.API.ACCOUNT.PURGE.*"; + public const string JsApiAccountPurgeT = "$JS.API.ACCOUNT.PURGE.{0}"; + public const string JsApiServerStreamMove = "$JS.API.ACCOUNT.STREAM.MOVE.*.*"; + public const string JsApiServerStreamMoveT = "$JS.API.ACCOUNT.STREAM.MOVE.{0}.{1}"; + public const string JsApiServerStreamCancelMove = "$JS.API.ACCOUNT.STREAM.CANCEL_MOVE.*.*"; + public const string JsApiServerStreamCancelMoveT = "$JS.API.ACCOUNT.STREAM.CANCEL_MOVE.{0}.{1}"; + + // Advisory/metric subjects + public const string JsAdvisoryPrefix = "$JS.EVENT.ADVISORY"; + public const string JsMetricPrefix = "$JS.EVENT.METRIC"; + public const string JsMetricConsumerAckPre = "$JS.EVENT.METRIC.CONSUMER.ACK"; + public const string JsAdvisoryConsumerMaxDelivery = "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES"; + public const string JsAdvisoryConsumerMsgNak = "$JS.EVENT.ADVISORY.CONSUMER.MSG_NAKED"; + public const string JsAdvisoryConsumerMsgTerminated = "$JS.EVENT.ADVISORY.CONSUMER.MSG_TERMINATED"; + public const string JsAdvisoryStreamCreated = "$JS.EVENT.ADVISORY.STREAM.CREATED"; + public const string JsAdvisoryStreamDeleted = "$JS.EVENT.ADVISORY.STREAM.DELETED"; + public const string JsAdvisoryStreamUpdated = "$JS.EVENT.ADVISORY.STREAM.UPDATED"; + public const string JsAdvisoryConsumerCreated = "$JS.EVENT.ADVISORY.CONSUMER.CREATED"; + public const string JsAdvisoryConsumerDeleted = "$JS.EVENT.ADVISORY.CONSUMER.DELETED"; + public const string JsAdvisoryConsumerPause = "$JS.EVENT.ADVISORY.CONSUMER.PAUSE"; + public const string JsAdvisoryConsumerPinned = "$JS.EVENT.ADVISORY.CONSUMER.PINNED"; + public const string JsAdvisoryConsumerUnpinned = "$JS.EVENT.ADVISORY.CONSUMER.UNPINNED"; + public const string JsAdvisoryStreamSnapshotCreate = "$JS.EVENT.ADVISORY.STREAM.SNAPSHOT_CREATE"; + public const string JsAdvisoryStreamSnapshotComplete = "$JS.EVENT.ADVISORY.STREAM.SNAPSHOT_COMPLETE"; + public const string JsAdvisoryStreamRestoreCreate = "$JS.EVENT.ADVISORY.STREAM.RESTORE_CREATE"; + public const string JsAdvisoryStreamRestoreComplete = "$JS.EVENT.ADVISORY.STREAM.RESTORE_COMPLETE"; + public const string JsAdvisoryDomainLeaderElected = "$JS.EVENT.ADVISORY.DOMAIN.LEADER_ELECTED"; + public const string JsAdvisoryStreamLeaderElected = "$JS.EVENT.ADVISORY.STREAM.LEADER_ELECTED"; + public const string JsAdvisoryStreamQuorumLost = "$JS.EVENT.ADVISORY.STREAM.QUORUM_LOST"; + public const string JsAdvisoryStreamBatchAbandoned = "$JS.EVENT.ADVISORY.STREAM.BATCH_ABANDONED"; + public const string JsAdvisoryConsumerLeaderElected = "$JS.EVENT.ADVISORY.CONSUMER.LEADER_ELECTED"; + public const string JsAdvisoryConsumerQuorumLost = "$JS.EVENT.ADVISORY.CONSUMER.QUORUM_LOST"; + public const string JsAdvisoryServerOutOfStorage = "$JS.EVENT.ADVISORY.SERVER.OUT_OF_STORAGE"; + public const string JsAdvisoryServerRemoved = "$JS.EVENT.ADVISORY.SERVER.REMOVED"; + public const string JsAdvisoryApiLimitReached = "$JS.EVENT.ADVISORY.API.LIMIT_REACHED"; + public const string JsAuditAdvisory = "$JS.EVENT.ADVISORY.API"; + + // Response type strings + public const string JsApiSystemResponseType = "io.nats.jetstream.api.v1.system_response"; + public const string JsApiOverloadedType = "io.nats.jetstream.api.v1.system_overloaded"; + public const string JsApiAccountInfoResponseType = "io.nats.jetstream.api.v1.account_info_response"; + public const string JsApiStreamCreateResponseType = "io.nats.jetstream.api.v1.stream_create_response"; + public const string JsApiStreamDeleteResponseType = "io.nats.jetstream.api.v1.stream_delete_response"; + public const string JsApiStreamInfoResponseType = "io.nats.jetstream.api.v1.stream_info_response"; + public const string JsApiStreamNamesResponseType = "io.nats.jetstream.api.v1.stream_names_response"; + public const string JsApiStreamListResponseType = "io.nats.jetstream.api.v1.stream_list_response"; + public const string JsApiStreamPurgeResponseType = "io.nats.jetstream.api.v1.stream_purge_response"; + public const string JsApiStreamUpdateResponseType = "io.nats.jetstream.api.v1.stream_update_response"; + public const string JsApiMsgDeleteResponseType = "io.nats.jetstream.api.v1.stream_msg_delete_response"; + public const string JsApiStreamSnapshotResponseType = "io.nats.jetstream.api.v1.stream_snapshot_response"; + public const string JsApiStreamRestoreResponseType = "io.nats.jetstream.api.v1.stream_restore_response"; + public const string JsApiStreamRemovePeerResponseType = "io.nats.jetstream.api.v1.stream_remove_peer_response"; + public const string JsApiStreamLeaderStepDownResponseType = "io.nats.jetstream.api.v1.stream_leader_stepdown_response"; + public const string JsApiConsumerLeaderStepDownResponseType = "io.nats.jetstream.api.v1.consumer_leader_stepdown_response"; + public const string JsApiLeaderStepDownResponseType = "io.nats.jetstream.api.v1.meta_leader_stepdown_response"; + public const string JsApiMetaServerRemoveResponseType = "io.nats.jetstream.api.v1.meta_server_remove_response"; + public const string JsApiAccountPurgeResponseType = "io.nats.jetstream.api.v1.account_purge_response"; + public const string JsApiMsgGetResponseType = "io.nats.jetstream.api.v1.stream_msg_get_response"; + public const string JsApiConsumerCreateResponseType = "io.nats.jetstream.api.v1.consumer_create_response"; + public const string JsApiConsumerDeleteResponseType = "io.nats.jetstream.api.v1.consumer_delete_response"; + public const string JsApiConsumerPauseResponseType = "io.nats.jetstream.api.v1.consumer_pause_response"; + public const string JsApiConsumerInfoResponseType = "io.nats.jetstream.api.v1.consumer_info_response"; + public const string JsApiConsumerNamesResponseType = "io.nats.jetstream.api.v1.consumer_names_response"; + public const string JsApiConsumerListResponseType = "io.nats.jetstream.api.v1.consumer_list_response"; + public const string JsApiConsumerUnpinResponseType = "io.nats.jetstream.api.v1.consumer_unpin_response"; + public const string JsApiConsumerResetResponseType = "io.nats.jetstream.api.v1.consumer_reset_response"; + + // Limits + public const int JsApiNamesLimit = 1024; + public const int JsApiListLimit = 256; + public const int JsMaxSubjectDetails = 100_000; + public const int JsWaitQueueDefaultMax = 512; + public const int JsMaxDescriptionLen = 4 * 1024; + public const int JsMaxMetadataLen = 128 * 1024; + public const int JsMaxNameLen = 255; + public const int JsDefaultRequestQueueLimit = 10_000; + + // Request headers + public const string JsRequiredApiLevel = "Nats-Required-Api-Level"; +} + +// --------------------------------------------------------------------------- +// Base API types +// --------------------------------------------------------------------------- + +/// +/// Standard base response from the JetStream JSON API. +/// Mirrors ApiResponse in server/jetstream_api.go. +/// +public sealed class ApiResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + + /// Returns the as an exception, or null if none. + public Exception? ToError() => + Error is null ? null : new InvalidOperationException($"{Error.Description} ({Error.ErrCode})"); +} + +/// +/// Paged response metadata included in list responses. +/// Mirrors ApiPaged in server/jetstream_api.go. +/// +public sealed class ApiPaged +{ + [JsonPropertyName("total")] public int Total { get; set; } + [JsonPropertyName("offset")] public int Offset { get; set; } + [JsonPropertyName("limit")] public int Limit { get; set; } +} + +/// +/// Request parameters for paged API responses. +/// Mirrors ApiPagedRequest in server/jetstream_api.go. +/// +public sealed class ApiPagedRequest +{ + [JsonPropertyName("offset")] public int Offset { get; set; } +} + +// --------------------------------------------------------------------------- +// Account +// --------------------------------------------------------------------------- + +/// Account info response. +public sealed class JsApiAccountInfoResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + + // JetStreamAccountStats fields (embedded in Go) + [JsonPropertyName("memory")] public ulong Memory { get; set; } + [JsonPropertyName("storage")] public ulong Store { get; set; } + [JsonPropertyName("reserved_memory")] public ulong ReservedMemory { get; set; } + [JsonPropertyName("reserved_storage")]public ulong ReservedStore { get; set; } + [JsonPropertyName("streams")] public int Streams { get; set; } + [JsonPropertyName("consumers")] public int Consumers { get; set; } + [JsonPropertyName("limits")] public JetStreamAccountLimits Limits { get; set; } = new(); + [JsonPropertyName("domain")] public string? Domain { get; set; } + [JsonPropertyName("api")] public JetStreamApiStats Api { get; set; } = new(); + [JsonPropertyName("tiers")] public Dictionary? Tiers { get; set; } +} + +// --------------------------------------------------------------------------- +// Stream API types +// --------------------------------------------------------------------------- + +/// Stream creation response. +public sealed class JsApiStreamCreateResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("config")] public StreamConfig? Config { get; set; } + [JsonPropertyName("state")] public StreamState? State { get; set; } + [JsonPropertyName("did_create")] public bool DidCreate { get; set; } +} + +/// Stream deletion response. +public sealed class JsApiStreamDeleteResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("success")] public bool Success { get; set; } +} + +/// Stream info request with optional filtering. +public sealed class JsApiStreamInfoRequest +{ + [JsonPropertyName("offset")] public int Offset { get; set; } + [JsonPropertyName("deleted_details")] public bool DeletedDetails { get; set; } + [JsonPropertyName("subjects_filter")] public string? SubjectsFilter { get; set; } +} + +/// Stream info response. +public sealed class JsApiStreamInfoResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("total")] public int Total { get; set; } + [JsonPropertyName("offset")] public int Offset { get; set; } + [JsonPropertyName("limit")] public int Limit { get; set; } + // StreamInfo fields embedded — delegated to StreamInfo stub for now + public StreamInfo? Info { get; set; } +} + +/// Stream names list request. +public sealed class JsApiStreamNamesRequest +{ + [JsonPropertyName("offset")] public int Offset { get; set; } + [JsonPropertyName("subject")] public string? Subject { get; set; } +} + +/// Stream names list response. +public sealed class JsApiStreamNamesResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("total")] public int Total { get; set; } + [JsonPropertyName("offset")] public int Offset { get; set; } + [JsonPropertyName("limit")] public int Limit { get; set; } + [JsonPropertyName("streams")] public List? Streams { get; set; } +} + +/// Detailed stream list request. +public sealed class JsApiStreamListRequest +{ + [JsonPropertyName("offset")] public int Offset { get; set; } + [JsonPropertyName("subject")] public string? Subject { get; set; } +} + +/// Detailed stream list response. +public sealed class JsApiStreamListResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("total")] public int Total { get; set; } + [JsonPropertyName("offset")] public int Offset { get; set; } + [JsonPropertyName("limit")] public int Limit { get; set; } + [JsonPropertyName("streams")] public List? Streams { get; set; } + [JsonPropertyName("missing")] public List? Missing { get; set; } + [JsonPropertyName("offline")] public Dictionary? Offline { get; set; } +} + +/// Stream purge request. +public sealed class JsApiStreamPurgeRequest +{ + [JsonPropertyName("seq")] public ulong Sequence { get; set; } + [JsonPropertyName("filter")] public string? Subject { get; set; } + [JsonPropertyName("keep")] public ulong Keep { get; set; } +} + +/// Stream purge response. +public sealed class JsApiStreamPurgeResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("success")] public bool Success { get; set; } + [JsonPropertyName("purged")] public ulong Purged { get; set; } +} + +/// Stream update response. +public sealed class JsApiStreamUpdateResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + public StreamInfo? Info { get; set; } +} + +/// Message deletion request. +public sealed class JsApiMsgDeleteRequest +{ + [JsonPropertyName("seq")] public ulong Seq { get; set; } + [JsonPropertyName("no_erase")] public bool NoErase { get; set; } +} + +/// Message deletion response. +public sealed class JsApiMsgDeleteResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("success")] public bool Success { get; set; } +} + +/// Stream snapshot request. +public sealed class JsApiStreamSnapshotRequest +{ + [JsonPropertyName("deliver_subject")] public string DeliverSubject { get; set; } = string.Empty; + [JsonPropertyName("no_consumers")] public bool NoConsumers { get; set; } + [JsonPropertyName("chunk_size")] public int ChunkSize { get; set; } + [JsonPropertyName("window_size")] public int WindowSize { get; set; } + [JsonPropertyName("jsck")] public bool CheckMsgs { get; set; } +} + +/// Direct stream snapshot response. +public sealed class JsApiStreamSnapshotResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("config")] public StreamConfig? Config { get; set; } + [JsonPropertyName("state")] public StreamState? State { get; set; } +} + +/// Stream restore request. +public sealed class JsApiStreamRestoreRequest +{ + [JsonPropertyName("config")] public StreamConfig Config { get; set; } = new(); + [JsonPropertyName("state")] public StreamState State { get; set; } = new(); +} + +/// Stream restore response. +public sealed class JsApiStreamRestoreResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("deliver_subject")] public string DeliverSubject { get; set; } = string.Empty; +} + +/// Remove a peer from a stream. +public sealed class JsApiStreamRemovePeerRequest +{ + [JsonPropertyName("peer")] public string Peer { get; set; } = string.Empty; +} + +/// Response to remove peer request. +public sealed class JsApiStreamRemovePeerResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("success")] public bool Success { get; set; } +} + +/// Response to stream leader step-down. +public sealed class JsApiStreamLeaderStepDownResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("success")] public bool Success { get; set; } +} + +/// Response to consumer leader step-down. +public sealed class JsApiConsumerLeaderStepDownResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("success")] public bool Success { get; set; } +} + +/// Meta-leader step-down request with optional placement. +public sealed class JsApiLeaderStepdownRequest +{ + [JsonPropertyName("placement")] public Placement? Placement { get; set; } +} + +/// Response to meta-leader step-down. +public sealed class JsApiLeaderStepDownResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("success")] public bool Success { get; set; } +} + +/// Remove a peer server from the meta group. +public sealed class JsApiMetaServerRemoveRequest +{ + [JsonPropertyName("peer")] public string Server { get; set; } = string.Empty; + [JsonPropertyName("peer_id")] public string? PeerId { get; set; } +} + +/// Response to peer removal. +public sealed class JsApiMetaServerRemoveResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("success")] public bool Success { get; set; } +} + +/// Request to move a stream off a server. +public sealed class JsApiMetaServerStreamMoveRequest +{ + [JsonPropertyName("server")] public string? Server { get; set; } + [JsonPropertyName("cluster")] public string? Cluster { get; set; } + [JsonPropertyName("domain")] public string? Domain { get; set; } + [JsonPropertyName("tags")] public string[]? Tags { get; set; } +} + +/// Account purge response. +public sealed class JsApiAccountPurgeResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("initiated")] public bool Initiated { get; set; } +} + +// --------------------------------------------------------------------------- +// Message get +// --------------------------------------------------------------------------- + +/// +/// Direct message get request (by sequence, last-for-subject, next-for-subject, or batch). +/// Mirrors JSApiMsgGetRequest in server/jetstream_api.go. +/// +public sealed class JsApiMsgGetRequest +{ + [JsonPropertyName("seq")] public ulong Seq { get; set; } + [JsonPropertyName("last_by_subj")] public string? LastFor { get; set; } + [JsonPropertyName("next_by_subj")] public string? NextFor { get; set; } + [JsonPropertyName("batch")] public int Batch { get; set; } + [JsonPropertyName("max_bytes")] public int MaxBytes { get; set; } + [JsonPropertyName("start_time")] public DateTime? StartTime { get; set; } + [JsonPropertyName("multi_last")] public string[]? MultiLastFor { get; set; } + [JsonPropertyName("up_to_seq")] public ulong UpToSeq { get; set; } + [JsonPropertyName("up_to_time")] public DateTime? UpToTime { get; set; } + [JsonPropertyName("no_hdr")] public bool NoHeaders { get; set; } +} + +/// Message get response. +public sealed class JsApiMsgGetResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("message")] public StoredMsg? Message { get; set; } +} + +// --------------------------------------------------------------------------- +// Consumer API types +// --------------------------------------------------------------------------- + +/// Consumer create/update response. +public sealed class JsApiConsumerCreateResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + public ConsumerInfo? Info { get; set; } +} + +/// Consumer delete response. +public sealed class JsApiConsumerDeleteResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("success")] public bool Success { get; set; } +} + +/// Consumer pause request. +public sealed class JsApiConsumerPauseRequest +{ + [JsonPropertyName("pause_until")] public DateTime PauseUntil { get; set; } +} + +/// Consumer pause response. +public sealed class JsApiConsumerPauseResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("paused")] public bool Paused { get; set; } + [JsonPropertyName("pause_until")] public DateTime PauseUntil { get; set; } + [JsonPropertyName("pause_remaining")] public TimeSpan PauseRemaining { get; set; } +} + +/// Consumer info response. +public sealed class JsApiConsumerInfoResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + public ConsumerInfo? Info { get; set; } +} + +/// Consumer names request (paged). +public sealed class JsApiConsumersRequest +{ + [JsonPropertyName("offset")] public int Offset { get; set; } +} + +/// Consumer names list response. +public sealed class JsApiConsumerNamesResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("total")] public int Total { get; set; } + [JsonPropertyName("offset")] public int Offset { get; set; } + [JsonPropertyName("limit")] public int Limit { get; set; } + [JsonPropertyName("consumers")] public List? Consumers { get; set; } +} + +/// Consumer list response. +public sealed class JsApiConsumerListResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("total")] public int Total { get; set; } + [JsonPropertyName("offset")] public int Offset { get; set; } + [JsonPropertyName("limit")] public int Limit { get; set; } + [JsonPropertyName("consumers")] public List? Consumers { get; set; } + [JsonPropertyName("missing")] public List? Missing { get; set; } + [JsonPropertyName("offline")] public Dictionary? Offline { get; set; } +} + +/// +/// Pull consumer next-message request. +/// Mirrors JSApiConsumerGetNextRequest in server/jetstream_api.go. +/// +public sealed class JsApiConsumerGetNextRequest +{ + [JsonPropertyName("expires")] public TimeSpan Expires { get; set; } + [JsonPropertyName("batch")] public int Batch { get; set; } + [JsonPropertyName("max_bytes")] public int MaxBytes { get; set; } + [JsonPropertyName("no_wait")] public bool NoWait { get; set; } + [JsonPropertyName("idle_heartbeat")] public TimeSpan Heartbeat { get; set; } + public PriorityGroup? Priority { get; set; } +} + +/// Consumer reset (seek to sequence) request. +public sealed class JsApiConsumerResetRequest +{ + [JsonPropertyName("seq")] public ulong Seq { get; set; } +} + +/// Consumer reset response. +public sealed class JsApiConsumerResetResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } + [JsonPropertyName("reset_seq")] public ulong ResetSeq { get; set; } + public ConsumerInfo? Info { get; set; } +} + +/// Consumer unpin (priority group) request. +public sealed class JsApiConsumerUnpinRequest +{ + [JsonPropertyName("group")] public string Group { get; set; } = string.Empty; +} + +/// Consumer unpin response. +public sealed class JsApiConsumerUnpinResponse +{ + [JsonPropertyName("type")] public string? Type { get; set; } + [JsonPropertyName("error")] public JsApiError? Error { get; set; } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamBatching.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamBatching.cs new file mode 100644 index 0000000..53ca9e7 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamBatching.cs @@ -0,0 +1,132 @@ +// Copyright 2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/jetstream_batching.go in the NATS server Go source. + +namespace ZB.MOM.NatsNet.Server; + +// --------------------------------------------------------------------------- +// Batching types +// --------------------------------------------------------------------------- + +/// +/// Tracks in-progress atomic publish batch groups for a stream. +/// Mirrors the batching struct in server/jetstream_batching.go. +/// +internal sealed class Batching +{ + private readonly Lock _mu = new(); + private readonly Dictionary _group = new(StringComparer.Ordinal); + + public Lock Mu => _mu; + public Dictionary Group => _group; +} + +/// +/// A single in-progress atomic batch: its temporary store and cleanup timer. +/// Mirrors batchGroup in server/jetstream_batching.go. +/// +internal sealed class BatchGroup +{ + /// Last proposed stream sequence for this batch. + public ulong LastSeq { get; set; } + + /// Temporary backing store for the batch's messages. + public object? Store { get; set; } // IStreamStore — session 20 + + /// Timer that abandons the batch after the configured timeout. + public Timer? BatchTimer { get; set; } + + /// + /// Stops the cleanup timer and flushes pending writes so the batch is + /// ready to be committed. + /// Mirrors batchGroup.readyForCommit. + /// + public bool ReadyForCommit() + { + // Stub — full implementation requires IStreamStore.FlushAllPending (session 20). + return BatchTimer?.Change(Timeout.Infinite, Timeout.Infinite) != null; + } +} + +/// +/// Stages consistency-check data for a single atomic batch before it is committed. +/// Mirrors batchStagedDiff in server/jetstream_batching.go. +/// +internal sealed class BatchStagedDiff +{ + /// Message IDs seen in this batch, for duplicate detection. + public Dictionary? MsgIds { get; set; } + + /// Running counter totals, keyed by subject. + public Dictionary? Counter { get; set; } // map[string]*msgCounterRunningTotal + + /// Inflight subject byte/op totals for DiscardNew checks. + public Dictionary? Inflight { get; set; } // map[string]*inflightSubjectRunningTotal + + /// Expected-last-seq-per-subject checks staged in this batch. + public Dictionary? ExpectedPerSubject { get; set; } +} + +/// +/// Cached expected-last-sequence-per-subject result for a single subject within a batch. +/// Mirrors batchExpectedPerSubject in server/jetstream_batching.go. +/// +internal sealed class BatchExpectedPerSubject +{ + /// Stream sequence of the last message on this subject at proposal time. + public ulong SSeq { get; set; } + + /// Clustered proposal sequence at which this check was computed. + public ulong ClSeq { get; set; } +} + +/// +/// Tracks the in-progress application of a committed batch on the Raft apply path. +/// Mirrors batchApply in server/jetstream_batching.go. +/// +internal sealed class BatchApply +{ + private readonly Lock _mu = new(); + + /// ID of the current batch. + public string Id { get; set; } = string.Empty; + + /// Number of entries expected in the batch (for consistency checks). + public ulong Count { get; set; } + + /// Raft committed entries that make up this batch. + public List? Entries { get; set; } // []*CommittedEntry — session 20+ + + /// Index within an entry indicating the first message of the batch. + public int EntryStart { get; set; } + + /// Applied value before the entry containing the first batch message. + public ulong MaxApplied { get; set; } + + public Lock Mu => _mu; + + /// + /// Clears in-memory apply-batch state. + /// Mirrors batchApply.clearBatchStateLocked. + /// Lock should be held. + /// + public void ClearBatchStateLocked() + { + Id = string.Empty; + Count = 0; + Entries = null; + EntryStart = 0; + MaxApplied = 0; + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs new file mode 100644 index 0000000..5ea497e --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs @@ -0,0 +1,323 @@ +// Copyright 2020-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/jetstream_errors.go and server/jetstream_errors_generated.go +// in the NATS server Go source. + +using System.Text.Json.Serialization; + +namespace ZB.MOM.NatsNet.Server; + +// --------------------------------------------------------------------------- +// JsApiError +// --------------------------------------------------------------------------- + +/// +/// Included in all JetStream API responses when there was an error. +/// Mirrors ApiError in server/jetstream_errors.go. +/// +public sealed class JsApiError +{ + [JsonPropertyName("code")] public int Code { get; set; } + [JsonPropertyName("err_code")] public ushort ErrCode { get; set; } + [JsonPropertyName("description")] public string? Description { get; set; } + + public override string ToString() => $"{Description} ({ErrCode})"; +} + +// --------------------------------------------------------------------------- +// JsApiErrors — all 203 error code constants +// --------------------------------------------------------------------------- + +/// +/// Pre-built instances for all JetStream error codes. +/// Mirrors the ApiErrors map in server/jetstream_errors_generated.go. +/// +public static class JsApiErrors +{ + // ---- Account ---- + public static readonly JsApiError AccountResourcesExceeded = new() { Code = 400, ErrCode = 10002, Description = "resource limits exceeded for account" }; + + // ---- Atomic Publish ---- + public static readonly JsApiError AtomicPublishContainsDuplicateMessage = new() { Code = 400, ErrCode = 10201, Description = "atomic publish batch contains duplicate message id" }; + public static readonly JsApiError AtomicPublishDisabled = new() { Code = 400, ErrCode = 10174, Description = "atomic publish is disabled" }; + public static readonly JsApiError AtomicPublishIncompleteBatch = new() { Code = 400, ErrCode = 10176, Description = "atomic publish batch is incomplete" }; + public static readonly JsApiError AtomicPublishInvalidBatchCommit = new() { Code = 400, ErrCode = 10200, Description = "atomic publish batch commit is invalid" }; + public static readonly JsApiError AtomicPublishInvalidBatchID = new() { Code = 400, ErrCode = 10179, Description = "atomic publish batch ID is invalid" }; + public static readonly JsApiError AtomicPublishMissingSeq = new() { Code = 400, ErrCode = 10175, Description = "atomic publish sequence is missing" }; + public static readonly JsApiError AtomicPublishTooLargeBatch = new() { Code = 400, ErrCode = 10199, Description = "atomic publish batch is too large: {size}" }; + public static readonly JsApiError AtomicPublishUnsupportedHeaderBatch = new() { Code = 400, ErrCode = 10177, Description = "atomic publish unsupported header used: {header}" }; + + // ---- General ---- + public static readonly JsApiError BadRequest = new() { Code = 400, ErrCode = 10003, Description = "bad request" }; + + // ---- Cluster ---- + public static readonly JsApiError ClusterIncomplete = new() { Code = 503, ErrCode = 10004, Description = "incomplete results" }; + public static readonly JsApiError ClusterNoPeers = new() { Code = 400, ErrCode = 10005, Description = "{err}" }; + public static readonly JsApiError ClusterNotActive = new() { Code = 500, ErrCode = 10006, Description = "JetStream not in clustered mode" }; + public static readonly JsApiError ClusterNotAssigned = new() { Code = 500, ErrCode = 10007, Description = "JetStream cluster not assigned to this server" }; + public static readonly JsApiError ClusterNotAvail = new() { Code = 503, ErrCode = 10008, Description = "JetStream system temporarily unavailable" }; + public static readonly JsApiError ClusterNotLeader = new() { Code = 500, ErrCode = 10009, Description = "JetStream cluster can not handle request" }; + public static readonly JsApiError ClusterPeerNotMember = new() { Code = 400, ErrCode = 10040, Description = "peer not a member" }; + public static readonly JsApiError ClusterRequired = new() { Code = 503, ErrCode = 10010, Description = "JetStream clustering support required" }; + public static readonly JsApiError ClusterServerMemberChangeInflight = new() { Code = 400, ErrCode = 10202, Description = "cluster member change is in progress" }; + public static readonly JsApiError ClusterServerNotMember = new() { Code = 400, ErrCode = 10044, Description = "server is not a member of the cluster" }; + public static readonly JsApiError ClusterTags = new() { Code = 400, ErrCode = 10011, Description = "tags placement not supported for operation" }; + public static readonly JsApiError ClusterUnSupportFeature = new() { Code = 503, ErrCode = 10036, Description = "not currently supported in clustered mode" }; + + // ---- Consumer ---- + public static readonly JsApiError ConsumerAckPolicyInvalid = new() { Code = 400, ErrCode = 10181, Description = "consumer ack policy invalid" }; + public static readonly JsApiError ConsumerAckWaitNegative = new() { Code = 400, ErrCode = 10183, Description = "consumer ack wait needs to be positive" }; + public static readonly JsApiError ConsumerAlreadyExists = new() { Code = 400, ErrCode = 10148, Description = "consumer already exists" }; + public static readonly JsApiError ConsumerBackOffNegative = new() { Code = 400, ErrCode = 10184, Description = "consumer backoff needs to be positive" }; + public static readonly JsApiError ConsumerBadDurableName = new() { Code = 400, ErrCode = 10103, Description = "durable name can not contain '.', '*', '>'" }; + public static readonly JsApiError ConsumerConfigRequired = new() { Code = 400, ErrCode = 10078, Description = "consumer config required" }; + public static readonly JsApiError ConsumerCreateDurableAndNameMismatch = new() { Code = 400, ErrCode = 10132, Description = "Consumer Durable and Name have to be equal if both are provided" }; + public static readonly JsApiError ConsumerCreateErr = new() { Code = 500, ErrCode = 10012, Description = "{err}" }; + public static readonly JsApiError ConsumerCreateFilterSubjectMismatch = new() { Code = 400, ErrCode = 10131, Description = "Consumer create request did not match filtered subject from create subject" }; + public static readonly JsApiError ConsumerDeliverCycle = new() { Code = 400, ErrCode = 10081, Description = "consumer deliver subject forms a cycle" }; + public static readonly JsApiError ConsumerDeliverToWildcards = new() { Code = 400, ErrCode = 10079, Description = "consumer deliver subject has wildcards" }; + public static readonly JsApiError ConsumerDescriptionTooLong = new() { Code = 400, ErrCode = 10107, Description = "consumer description is too long, maximum allowed is {max}" }; + public static readonly JsApiError ConsumerDirectRequiresEphemeral = new() { Code = 400, ErrCode = 10091, Description = "consumer direct requires an ephemeral consumer" }; + public static readonly JsApiError ConsumerDirectRequiresPush = new() { Code = 400, ErrCode = 10090, Description = "consumer direct requires a push based consumer" }; + public static readonly JsApiError ConsumerDoesNotExist = new() { Code = 400, ErrCode = 10149, Description = "consumer does not exist" }; + public static readonly JsApiError ConsumerDuplicateFilterSubjects = new() { Code = 400, ErrCode = 10136, Description = "consumer cannot have both FilterSubject and FilterSubjects specified" }; + public static readonly JsApiError ConsumerDurableNameNotInSubject = new() { Code = 400, ErrCode = 10016, Description = "consumer expected to be durable but no durable name set in subject" }; + public static readonly JsApiError ConsumerDurableNameNotMatchSubject = new() { Code = 400, ErrCode = 10017, Description = "consumer name in subject does not match durable name in request" }; + public static readonly JsApiError ConsumerDurableNameNotSet = new() { Code = 400, ErrCode = 10018, Description = "consumer expected to be durable but a durable name was not set" }; + public static readonly JsApiError ConsumerEmptyFilter = new() { Code = 400, ErrCode = 10139, Description = "consumer filter in FilterSubjects cannot be empty" }; + public static readonly JsApiError ConsumerEmptyGroupName = new() { Code = 400, ErrCode = 10161, Description = "Group name cannot be an empty string" }; + public static readonly JsApiError ConsumerEphemeralWithDurableInSubject = new() { Code = 400, ErrCode = 10019, Description = "consumer expected to be ephemeral but detected a durable name set in subject" }; + public static readonly JsApiError ConsumerEphemeralWithDurableName = new() { Code = 400, ErrCode = 10020, Description = "consumer expected to be ephemeral but a durable name was set in request" }; + public static readonly JsApiError ConsumerExistingActive = new() { Code = 400, ErrCode = 10105, Description = "consumer already exists and is still active" }; + public static readonly JsApiError ConsumerFCRequiresPush = new() { Code = 400, ErrCode = 10089, Description = "consumer flow control requires a push based consumer" }; + public static readonly JsApiError ConsumerFilterNotSubset = new() { Code = 400, ErrCode = 10093, Description = "consumer filter subject is not a valid subset of the interest subjects" }; + public static readonly JsApiError ConsumerHBRequiresPush = new() { Code = 400, ErrCode = 10088, Description = "consumer idle heartbeat requires a push based consumer" }; + public static readonly JsApiError ConsumerInactiveThresholdExcess = new() { Code = 400, ErrCode = 10153, Description = "consumer inactive threshold exceeds system limit of {limit}" }; + public static readonly JsApiError ConsumerInvalidDeliverSubject = new() { Code = 400, ErrCode = 10112, Description = "invalid push consumer deliver subject" }; + public static readonly JsApiError ConsumerInvalidGroupName = new() { Code = 400, ErrCode = 10162, Description = "Valid priority group name must match A-Z, a-z, 0-9, -_/=)+ and may not exceed 16 characters" }; + public static readonly JsApiError ConsumerInvalidPolicy = new() { Code = 400, ErrCode = 10094, Description = "{err}" }; + public static readonly JsApiError ConsumerInvalidPriorityGroup = new() { Code = 400, ErrCode = 10160, Description = "Provided priority group does not exist for this consumer" }; + public static readonly JsApiError ConsumerInvalidReset = new() { Code = 400, ErrCode = 10204, Description = "invalid reset: {err}" }; + public static readonly JsApiError ConsumerInvalidSampling = new() { Code = 400, ErrCode = 10095, Description = "failed to parse consumer sampling configuration: {err}" }; + public static readonly JsApiError ConsumerMaxDeliverBackoff = new() { Code = 400, ErrCode = 10116, Description = "max deliver is required to be > length of backoff values" }; + public static readonly JsApiError ConsumerMaxPendingAckExcess = new() { Code = 400, ErrCode = 10121, Description = "consumer max ack pending exceeds system limit of {limit}" }; + public static readonly JsApiError ConsumerMaxPendingAckPolicyRequired = new() { Code = 400, ErrCode = 10082, Description = "consumer requires ack policy for max ack pending" }; + public static readonly JsApiError ConsumerMaxRequestBatchExceeded = new() { Code = 400, ErrCode = 10125, Description = "consumer max request batch exceeds server limit of {limit}" }; + public static readonly JsApiError ConsumerMaxRequestBatchNegative = new() { Code = 400, ErrCode = 10114, Description = "consumer max request batch needs to be > 0" }; + public static readonly JsApiError ConsumerMaxRequestExpiresTooSmall = new() { Code = 400, ErrCode = 10115, Description = "consumer max request expires needs to be >= 1ms" }; + public static readonly JsApiError ConsumerMaxWaitingNegative = new() { Code = 400, ErrCode = 10087, Description = "consumer max waiting needs to be positive" }; + public static readonly JsApiError ConsumerMetadataLength = new() { Code = 400, ErrCode = 10135, Description = "consumer metadata exceeds maximum size of {limit}" }; + public static readonly JsApiError ConsumerMultipleFiltersNotAllowed = new() { Code = 400, ErrCode = 10137, Description = "consumer with multiple subject filters cannot use subject based API" }; + public static readonly JsApiError ConsumerNameContainsPathSeparators = new() { Code = 400, ErrCode = 10127, Description = "Consumer name can not contain path separators" }; + public static readonly JsApiError ConsumerNameExist = new() { Code = 400, ErrCode = 10013, Description = "consumer name already in use" }; + public static readonly JsApiError ConsumerNameTooLong = new() { Code = 400, ErrCode = 10102, Description = "consumer name is too long, maximum allowed is {max}" }; + public static readonly JsApiError ConsumerNotFound = new() { Code = 404, ErrCode = 10014, Description = "consumer not found" }; + public static readonly JsApiError ConsumerOffline = new() { Code = 500, ErrCode = 10119, Description = "consumer is offline" }; + public static readonly JsApiError ConsumerOfflineReason = new() { Code = 500, ErrCode = 10195, Description = "consumer is offline: {err}" }; + public static readonly JsApiError ConsumerOnMapped = new() { Code = 400, ErrCode = 10092, Description = "consumer direct on a mapped consumer" }; + public static readonly JsApiError ConsumerOverlappingSubjectFilters = new() { Code = 400, ErrCode = 10138, Description = "consumer subject filters cannot overlap" }; + public static readonly JsApiError ConsumerPinnedTTLWithoutPriorityPolicyNone = new() { Code = 400, ErrCode = 10197, Description = "PinnedTTL cannot be set when PriorityPolicy is none" }; + public static readonly JsApiError ConsumerPriorityGroupWithPolicyNone = new() { Code = 400, ErrCode = 10196, Description = "consumer can not have priority groups when policy is none" }; + public static readonly JsApiError ConsumerPriorityPolicyWithoutGroup = new() { Code = 400, ErrCode = 10159, Description = "Setting PriorityPolicy requires at least one PriorityGroup to be set" }; + public static readonly JsApiError ConsumerPullNotDurable = new() { Code = 400, ErrCode = 10085, Description = "consumer in pull mode requires a durable name" }; + public static readonly JsApiError ConsumerPullRequiresAck = new() { Code = 400, ErrCode = 10084, Description = "consumer in pull mode requires explicit ack policy on workqueue stream" }; + public static readonly JsApiError ConsumerPullWithRateLimit = new() { Code = 400, ErrCode = 10086, Description = "consumer in pull mode can not have rate limit set" }; + public static readonly JsApiError ConsumerPushMaxWaiting = new() { Code = 400, ErrCode = 10080, Description = "consumer in push mode can not set max waiting" }; + public static readonly JsApiError ConsumerPushWithPriorityGroup = new() { Code = 400, ErrCode = 10178, Description = "priority groups can not be used with push consumers" }; + public static readonly JsApiError ConsumerReplacementWithDifferentName = new() { Code = 400, ErrCode = 10106, Description = "consumer replacement durable config not the same" }; + public static readonly JsApiError ConsumerReplayPolicyInvalid = new() { Code = 400, ErrCode = 10182, Description = "consumer replay policy invalid" }; + public static readonly JsApiError ConsumerReplicasExceedsStream = new() { Code = 400, ErrCode = 10126, Description = "consumer config replica count exceeds parent stream" }; + public static readonly JsApiError ConsumerReplicasShouldMatchStream = new() { Code = 400, ErrCode = 10134, Description = "consumer config replicas must match interest retention stream's replicas" }; + public static readonly JsApiError ConsumerSmallHeartbeat = new() { Code = 400, ErrCode = 10083, Description = "consumer idle heartbeat needs to be >= 100ms" }; + public static readonly JsApiError ConsumerStoreFailed = new() { Code = 500, ErrCode = 10104, Description = "error creating store for consumer: {err}" }; + public static readonly JsApiError ConsumerWQConsumerNotDeliverAll = new() { Code = 400, ErrCode = 10101, Description = "consumer must be deliver all on workqueue stream" }; + public static readonly JsApiError ConsumerWQConsumerNotUnique = new() { Code = 400, ErrCode = 10100, Description = "filtered consumer not unique on workqueue stream" }; + public static readonly JsApiError ConsumerWQMultipleUnfiltered = new() { Code = 400, ErrCode = 10099, Description = "multiple non-filtered consumers not allowed on workqueue stream" }; + public static readonly JsApiError ConsumerWQRequiresExplicitAck = new() { Code = 400, ErrCode = 10098, Description = "workqueue stream requires explicit ack" }; + public static readonly JsApiError ConsumerWithFlowControlNeedsHeartbeats = new() { Code = 400, ErrCode = 10108, Description = "consumer with flow control also needs heartbeats" }; + + // ---- Resources ---- + public static readonly JsApiError InsufficientResources = new() { Code = 503, ErrCode = 10023, Description = "insufficient resources" }; + public static readonly JsApiError InvalidJSON = new() { Code = 400, ErrCode = 10025, Description = "invalid JSON: {err}" }; + public static readonly JsApiError MaximumConsumersLimit = new() { Code = 400, ErrCode = 10026, Description = "maximum consumers limit reached" }; + public static readonly JsApiError MaximumStreamsLimit = new() { Code = 400, ErrCode = 10027, Description = "maximum number of streams reached" }; + public static readonly JsApiError MemoryResourcesExceeded = new() { Code = 500, ErrCode = 10028, Description = "insufficient memory resources available" }; + + // ---- Message Counter ---- + public static readonly JsApiError MessageCounterBroken = new() { Code = 400, ErrCode = 10172, Description = "message counter is broken" }; + public static readonly JsApiError MessageIncrDisabled = new() { Code = 400, ErrCode = 10168, Description = "message counters is disabled" }; + public static readonly JsApiError MessageIncrInvalid = new() { Code = 400, ErrCode = 10171, Description = "message counter increment is invalid" }; + public static readonly JsApiError MessageIncrMissing = new() { Code = 400, ErrCode = 10169, Description = "message counter increment is missing" }; + public static readonly JsApiError MessageIncrPayload = new() { Code = 400, ErrCode = 10170, Description = "message counter has payload" }; + + // ---- Message Schedules ---- + public static readonly JsApiError MessageSchedulesDisabled = new() { Code = 400, ErrCode = 10188, Description = "message schedules is disabled" }; + public static readonly JsApiError MessageSchedulesPatternInvalid = new() { Code = 400, ErrCode = 10189, Description = "message schedules pattern is invalid" }; + public static readonly JsApiError MessageSchedulesRollupInvalid = new() { Code = 400, ErrCode = 10192, Description = "message schedules invalid rollup" }; + public static readonly JsApiError MessageSchedulesSourceInvalid = new() { Code = 400, ErrCode = 10203, Description = "message schedules source is invalid" }; + public static readonly JsApiError MessageSchedulesTTLInvalid = new() { Code = 400, ErrCode = 10191, Description = "message schedules invalid per-message TTL" }; + public static readonly JsApiError MessageSchedulesTargetInvalid = new() { Code = 400, ErrCode = 10190, Description = "message schedules target is invalid" }; + + // ---- Message TTL ---- + public static readonly JsApiError MessageTTLDisabled = new() { Code = 400, ErrCode = 10166, Description = "per-message TTL is disabled" }; + public static readonly JsApiError MessageTTLInvalid = new() { Code = 400, ErrCode = 10165, Description = "invalid per-message TTL" }; + + // ---- Mirror ---- + public static readonly JsApiError MirrorConsumerSetupFailed = new() { Code = 500, ErrCode = 10029, Description = "{err}" }; + public static readonly JsApiError MirrorInvalidStreamName = new() { Code = 400, ErrCode = 10142, Description = "mirrored stream name is invalid" }; + public static readonly JsApiError MirrorInvalidSubjectFilter = new() { Code = 400, ErrCode = 10151, Description = "mirror transform source: {err}" }; + public static readonly JsApiError MirrorInvalidTransformDestination = new() { Code = 400, ErrCode = 10154, Description = "mirror transform: {err}" }; + public static readonly JsApiError MirrorMaxMessageSizeTooBig = new() { Code = 400, ErrCode = 10030, Description = "stream mirror must have max message size >= source" }; + public static readonly JsApiError MirrorMultipleFiltersNotAllowed = new() { Code = 400, ErrCode = 10150, Description = "mirror with multiple subject transforms cannot also have a single subject filter" }; + public static readonly JsApiError MirrorOverlappingSubjectFilters = new() { Code = 400, ErrCode = 10152, Description = "mirror subject filters can not overlap" }; + public static readonly JsApiError MirrorWithAtomicPublish = new() { Code = 400, ErrCode = 10198, Description = "stream mirrors can not also use atomic publishing" }; + public static readonly JsApiError MirrorWithCounters = new() { Code = 400, ErrCode = 10173, Description = "stream mirrors can not also calculate counters" }; + public static readonly JsApiError MirrorWithFirstSeq = new() { Code = 400, ErrCode = 10143, Description = "stream mirrors can not have first sequence configured" }; + public static readonly JsApiError MirrorWithMsgSchedules = new() { Code = 400, ErrCode = 10186, Description = "stream mirrors can not also schedule messages" }; + public static readonly JsApiError MirrorWithSources = new() { Code = 400, ErrCode = 10031, Description = "stream mirrors can not also contain other sources" }; + public static readonly JsApiError MirrorWithStartSeqAndTime = new() { Code = 400, ErrCode = 10032, Description = "stream mirrors can not have both start seq and start time configured" }; + public static readonly JsApiError MirrorWithSubjectFilters = new() { Code = 400, ErrCode = 10033, Description = "stream mirrors can not contain filtered subjects" }; + public static readonly JsApiError MirrorWithSubjects = new() { Code = 400, ErrCode = 10034, Description = "stream mirrors can not contain subjects" }; + + // ---- Misc ---- + public static readonly JsApiError NoAccount = new() { Code = 503, ErrCode = 10035, Description = "account not found" }; + public static readonly JsApiError NoLimits = new() { Code = 400, ErrCode = 10120, Description = "no JetStream default or applicable tiered limit present" }; + public static readonly JsApiError NoMessageFound = new() { Code = 404, ErrCode = 10037, Description = "no message found" }; + public static readonly JsApiError NotEmptyRequest = new() { Code = 400, ErrCode = 10038, Description = "expected an empty request payload" }; + public static readonly JsApiError NotEnabled = new() { Code = 503, ErrCode = 10076, Description = "JetStream not enabled" }; + public static readonly JsApiError NotEnabledForAccount = new() { Code = 503, ErrCode = 10039, Description = "JetStream not enabled for account" }; + public static readonly JsApiError Pedantic = new() { Code = 400, ErrCode = 10157, Description = "pedantic mode: {err}" }; + public static readonly JsApiError PeerRemap = new() { Code = 503, ErrCode = 10075, Description = "peer remap failed" }; + + // ---- RAFT ---- + public static readonly JsApiError RaftGeneralErr = new() { Code = 500, ErrCode = 10041, Description = "{err}" }; + + // ---- Replicas ---- + public static readonly JsApiError ReplicasCountCannotBeNegative = new() { Code = 400, ErrCode = 10133, Description = "replicas count cannot be negative" }; + public static readonly JsApiError RequiredApiLevel = new() { Code = 412, ErrCode = 10185, Description = "JetStream minimum api level required" }; + + // ---- Restore ---- + public static readonly JsApiError RestoreSubscribeFailed = new() { Code = 500, ErrCode = 10042, Description = "JetStream unable to subscribe to restore snapshot {subject}: {err}" }; + + // ---- Sequence ---- + public static readonly JsApiError SequenceNotFound = new() { Code = 400, ErrCode = 10043, Description = "sequence {seq} not found" }; + public static readonly JsApiError SnapshotDeliverSubjectInvalid = new() { Code = 400, ErrCode = 10015, Description = "deliver subject not valid" }; + + // ---- Source ---- + public static readonly JsApiError SourceConsumerSetupFailed = new() { Code = 500, ErrCode = 10045, Description = "{err}" }; + public static readonly JsApiError SourceDuplicateDetected = new() { Code = 400, ErrCode = 10140, Description = "duplicate source configuration detected" }; + public static readonly JsApiError SourceInvalidStreamName = new() { Code = 400, ErrCode = 10141, Description = "sourced stream name is invalid" }; + public static readonly JsApiError SourceInvalidSubjectFilter = new() { Code = 400, ErrCode = 10145, Description = "source transform source: {err}" }; + public static readonly JsApiError SourceInvalidTransformDestination = new() { Code = 400, ErrCode = 10146, Description = "source transform: {err}" }; + public static readonly JsApiError SourceMaxMessageSizeTooBig = new() { Code = 400, ErrCode = 10046, Description = "stream source must have max message size >= target" }; + public static readonly JsApiError SourceMultipleFiltersNotAllowed = new() { Code = 400, ErrCode = 10144, Description = "source with multiple subject transforms cannot also have a single subject filter" }; + public static readonly JsApiError SourceOverlappingSubjectFilters = new() { Code = 400, ErrCode = 10147, Description = "source filters can not overlap" }; + public static readonly JsApiError SourceWithMsgSchedules = new() { Code = 400, ErrCode = 10187, Description = "stream source can not also schedule messages" }; + + // ---- Storage ---- + public static readonly JsApiError StorageResourcesExceeded = new() { Code = 500, ErrCode = 10047, Description = "insufficient storage resources available" }; + + // ---- Stream ---- + public static readonly JsApiError StreamAssignment = new() { Code = 500, ErrCode = 10048, Description = "{err}" }; + public static readonly JsApiError StreamCreate = new() { Code = 500, ErrCode = 10049, Description = "{err}" }; + public static readonly JsApiError StreamDelete = new() { Code = 500, ErrCode = 10050, Description = "{err}" }; + public static readonly JsApiError StreamDuplicateMessageConflict = new() { Code = 409, ErrCode = 10158, Description = "duplicate message id is in process" }; + public static readonly JsApiError StreamExpectedLastSeqPerSubjectInvalid = new() { Code = 400, ErrCode = 10193, Description = "missing sequence for expected last sequence per subject" }; + public static readonly JsApiError StreamExpectedLastSeqPerSubjectNotReady = new() { Code = 503, ErrCode = 10163, Description = "expected last sequence per subject temporarily unavailable" }; + public static readonly JsApiError StreamExternalApiOverlap = new() { Code = 400, ErrCode = 10021, Description = "stream external api prefix {prefix} must not overlap with {subject}" }; + public static readonly JsApiError StreamExternalDelPrefixOverlaps = new() { Code = 400, ErrCode = 10022, Description = "stream external delivery prefix {prefix} overlaps with stream subject {subject}" }; + public static readonly JsApiError StreamGeneralError = new() { Code = 500, ErrCode = 10051, Description = "{err}" }; + public static readonly JsApiError StreamHeaderExceedsMaximum = new() { Code = 400, ErrCode = 10097, Description = "header size exceeds maximum allowed of 64k" }; + public static readonly JsApiError StreamInfoMaxSubjects = new() { Code = 500, ErrCode = 10117, Description = "subject details would exceed maximum allowed" }; + public static readonly JsApiError StreamInvalidConfig = new() { Code = 500, ErrCode = 10052, Description = "{err}" }; + public static readonly JsApiError StreamInvalid = new() { Code = 500, ErrCode = 10096, Description = "stream not valid" }; + public static readonly JsApiError StreamInvalidExternalDeliverySubj = new() { Code = 400, ErrCode = 10024, Description = "stream external delivery prefix {prefix} must not contain wildcards" }; + public static readonly JsApiError StreamLimits = new() { Code = 500, ErrCode = 10053, Description = "{err}" }; + public static readonly JsApiError StreamMaxBytesRequired = new() { Code = 400, ErrCode = 10113, Description = "account requires a stream config to have max bytes set" }; + public static readonly JsApiError StreamMaxStreamBytesExceeded = new() { Code = 400, ErrCode = 10122, Description = "stream max bytes exceeds account limit max stream bytes" }; + public static readonly JsApiError StreamMessageExceedsMaximum = new() { Code = 400, ErrCode = 10054, Description = "message size exceeds maximum allowed" }; + public static readonly JsApiError StreamMinLastSeq = new() { Code = 412, ErrCode = 10180, Description = "min last sequence" }; + public static readonly JsApiError StreamMirrorNotUpdatable = new() { Code = 400, ErrCode = 10055, Description = "stream mirror configuration can not be updated" }; + public static readonly JsApiError StreamMismatch = new() { Code = 400, ErrCode = 10056, Description = "stream name in subject does not match request" }; + public static readonly JsApiError StreamMoveAndScale = new() { Code = 400, ErrCode = 10123, Description = "can not move and scale a stream in a single update" }; + public static readonly JsApiError StreamMoveInProgress = new() { Code = 400, ErrCode = 10124, Description = "stream move already in progress: {msg}" }; + public static readonly JsApiError StreamMoveNotInProgress = new() { Code = 400, ErrCode = 10129, Description = "stream move not in progress" }; + public static readonly JsApiError StreamMsgDeleteFailed = new() { Code = 500, ErrCode = 10057, Description = "{err}" }; + public static readonly JsApiError StreamNameContainsPathSeparators = new() { Code = 400, ErrCode = 10128, Description = "Stream name can not contain path separators" }; + public static readonly JsApiError StreamNameExist = new() { Code = 400, ErrCode = 10058, Description = "stream name already in use with a different configuration" }; + public static readonly JsApiError StreamNameExistRestoreFailed = new() { Code = 400, ErrCode = 10130, Description = "stream name already in use, cannot restore" }; + public static readonly JsApiError StreamNotFound = new() { Code = 404, ErrCode = 10059, Description = "stream not found" }; + public static readonly JsApiError StreamNotMatch = new() { Code = 400, ErrCode = 10060, Description = "expected stream does not match" }; + public static readonly JsApiError StreamOffline = new() { Code = 500, ErrCode = 10118, Description = "stream is offline" }; + public static readonly JsApiError StreamOfflineReason = new() { Code = 500, ErrCode = 10194, Description = "stream is offline: {err}" }; + public static readonly JsApiError StreamPurgeFailed = new() { Code = 500, ErrCode = 10110, Description = "{err}" }; + public static readonly JsApiError StreamReplicasNotSupported = new() { Code = 500, ErrCode = 10074, Description = "replicas > 1 not supported in non-clustered mode" }; + public static readonly JsApiError StreamReplicasNotUpdatable = new() { Code = 400, ErrCode = 10061, Description = "Replicas configuration can not be updated" }; + public static readonly JsApiError StreamRestore = new() { Code = 500, ErrCode = 10062, Description = "restore failed: {err}" }; + public static readonly JsApiError StreamRollupFailed = new() { Code = 500, ErrCode = 10111, Description = "{err}" }; + public static readonly JsApiError StreamSealed = new() { Code = 400, ErrCode = 10109, Description = "invalid operation on sealed stream" }; + public static readonly JsApiError StreamSequenceNotMatch = new() { Code = 503, ErrCode = 10063, Description = "expected stream sequence does not match" }; + public static readonly JsApiError StreamSnapshot = new() { Code = 500, ErrCode = 10064, Description = "snapshot failed: {err}" }; + public static readonly JsApiError StreamStoreFailed = new() { Code = 503, ErrCode = 10077, Description = "{err}" }; + public static readonly JsApiError StreamSubjectOverlap = new() { Code = 400, ErrCode = 10065, Description = "subjects overlap with an existing stream" }; + public static readonly JsApiError StreamTemplateCreate = new() { Code = 500, ErrCode = 10066, Description = "{err}" }; + public static readonly JsApiError StreamTemplateDelete = new() { Code = 500, ErrCode = 10067, Description = "{err}" }; + public static readonly JsApiError StreamTemplateNotFound = new() { Code = 404, ErrCode = 10068, Description = "template not found" }; + public static readonly JsApiError StreamTooManyRequests = new() { Code = 429, ErrCode = 10167, Description = "too many requests" }; + public static readonly JsApiError StreamTransformInvalidDestination = new() { Code = 400, ErrCode = 10156, Description = "stream transform: {err}" }; + public static readonly JsApiError StreamTransformInvalidSource = new() { Code = 400, ErrCode = 10155, Description = "stream transform source: {err}" }; + public static readonly JsApiError StreamUpdate = new() { Code = 500, ErrCode = 10069, Description = "{err}" }; + public static readonly JsApiError StreamWrongLastMsgID = new() { Code = 400, ErrCode = 10070, Description = "wrong last msg ID: {id}" }; + public static readonly JsApiError StreamWrongLastSequenceConstant = new() { Code = 400, ErrCode = 10164, Description = "wrong last sequence" }; + public static readonly JsApiError StreamWrongLastSequence = new() { Code = 400, ErrCode = 10071, Description = "wrong last sequence: {seq}" }; + + // ---- Temp storage ---- + public static readonly JsApiError TempStorageFailed = new() { Code = 500, ErrCode = 10072, Description = "JetStream unable to open temp storage for restore" }; + public static readonly JsApiError TemplateNameNotMatchSubject = new() { Code = 400, ErrCode = 10073, Description = "template name in subject does not match request" }; + + // --------------------------------------------------------------------------- + // Lookup by ErrCode + // --------------------------------------------------------------------------- + + private static readonly Dictionary _byErrCode; + + static JsApiErrors() + { + _byErrCode = new Dictionary(); + foreach (var field in typeof(JsApiErrors).GetFields( + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)) + { + if (field.GetValue(null) is JsApiError e) + _byErrCode.TryAdd(e.ErrCode, e); + } + } + + /// + /// Returns the pre-built for the given err_code, or null if not found. + /// + public static JsApiError? ForErrCode(ushort errCode) => + _byErrCode.TryGetValue(errCode, out var e) ? e : null; + + /// + /// Returns true if the given matches one or more of the supplied err_codes. + /// Mirrors IsNatsErr in server/jetstream_errors.go. + /// + public static bool IsNatsError(JsApiError? err, params ushort[] errCodes) + { + if (err is null) return false; + foreach (var code in errCodes) + if (err.ErrCode == code) return true; + return false; + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamTypes.cs new file mode 100644 index 0000000..5eb4aa5 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamTypes.cs @@ -0,0 +1,293 @@ +// Copyright 2020-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/jetstream.go in the NATS server Go source. + +using System.Text.Json.Serialization; + +namespace ZB.MOM.NatsNet.Server; + +// --------------------------------------------------------------------------- +// JetStreamConfig +// --------------------------------------------------------------------------- + +/// +/// Configuration for the JetStream subsystem. +/// Mirrors JetStreamConfig in server/jetstream.go. +/// +public sealed class JetStreamConfig +{ + /// Maximum size of memory-backed streams. + [JsonPropertyName("max_memory")] + public long MaxMemory { get; set; } + + /// Maximum size of file-backed streams. + [JsonPropertyName("max_storage")] + public long MaxStore { get; set; } + + /// Directory where storage files are stored. + [JsonPropertyName("store_dir")] + public string StoreDir { get; set; } = string.Empty; + + /// How frequently we sync to disk in the background via fsync. + [JsonPropertyName("sync_interval")] + public TimeSpan SyncInterval { get; set; } + + /// If true, flushes are done after every write. + [JsonPropertyName("sync_always")] + public bool SyncAlways { get; set; } + + /// The JetStream domain for this server. + [JsonPropertyName("domain")] + public string Domain { get; set; } = string.Empty; + + /// Whether compression is supported. + [JsonPropertyName("compress_ok")] + public bool CompressOK { get; set; } + + /// Unique tag assigned to this server instance. + [JsonPropertyName("unique_tag")] + public string UniqueTag { get; set; } = string.Empty; + + /// Whether strict JSON parsing is performed. + [JsonPropertyName("strict")] + public bool Strict { get; set; } +} + +// --------------------------------------------------------------------------- +// JetStreamApiStats +// --------------------------------------------------------------------------- + +/// +/// Statistics about the JetStream API usage for this server. +/// Mirrors JetStreamAPIStats in server/jetstream.go. +/// +public sealed class JetStreamApiStats +{ + /// Active API level implemented by this server. + [JsonPropertyName("level")] + public int Level { get; set; } + + /// Total API requests received since server start. + [JsonPropertyName("total")] + public ulong Total { get; set; } + + /// Total API requests that resulted in error responses. + [JsonPropertyName("errors")] + public ulong Errors { get; set; } + + /// Number of API requests currently being served. + [JsonPropertyName("inflight")] + public ulong Inflight { get; set; } +} + +// --------------------------------------------------------------------------- +// JetStreamStats +// --------------------------------------------------------------------------- + +/// +/// Statistics about JetStream for this server. +/// Mirrors JetStreamStats in server/jetstream.go. +/// +public sealed class JetStreamStats +{ + [JsonPropertyName("memory")] public ulong Memory { get; set; } + [JsonPropertyName("storage")] public ulong Store { get; set; } + [JsonPropertyName("reserved_memory")] public ulong ReservedMemory { get; set; } + [JsonPropertyName("reserved_storage")] public ulong ReservedStore { get; set; } + [JsonPropertyName("accounts")] public int Accounts { get; set; } + [JsonPropertyName("ha_assets")] public int HAAssets { get; set; } + [JsonPropertyName("api")] public JetStreamApiStats Api { get; set; } = new(); +} + +// --------------------------------------------------------------------------- +// JetStreamAccountLimits +// --------------------------------------------------------------------------- + +/// +/// Per-account JetStream limits. +/// Mirrors JetStreamAccountLimits in server/jetstream.go. +/// +public sealed class JetStreamAccountLimits +{ + [JsonPropertyName("max_memory")] public long MaxMemory { get; set; } + [JsonPropertyName("max_storage")] public long MaxStore { get; set; } + [JsonPropertyName("max_streams")] public int MaxStreams { get; set; } + [JsonPropertyName("max_consumers")] public int MaxConsumers { get; set; } + [JsonPropertyName("max_ack_pending")] public int MaxAckPending { get; set; } + [JsonPropertyName("memory_max_stream_bytes")] public long MemoryMaxStreamBytes { get; set; } + [JsonPropertyName("storage_max_stream_bytes")] public long StoreMaxStreamBytes { get; set; } + [JsonPropertyName("max_bytes_required")] public bool MaxBytesRequired { get; set; } +} + +// --------------------------------------------------------------------------- +// JetStreamTier +// --------------------------------------------------------------------------- + +/// +/// Per-tier JetStream usage and limits. +/// Mirrors JetStreamTier in server/jetstream.go. +/// +public sealed class JetStreamTier +{ + [JsonPropertyName("memory")] public ulong Memory { get; set; } + [JsonPropertyName("storage")] public ulong Store { get; set; } + [JsonPropertyName("reserved_memory")] public ulong ReservedMemory { get; set; } + [JsonPropertyName("reserved_storage")]public ulong ReservedStore { get; set; } + [JsonPropertyName("streams")] public int Streams { get; set; } + [JsonPropertyName("consumers")] public int Consumers { get; set; } + [JsonPropertyName("limits")] public JetStreamAccountLimits Limits { get; set; } = new(); +} + +// --------------------------------------------------------------------------- +// JetStreamAccountStats +// --------------------------------------------------------------------------- + +/// +/// Current statistics about an account's JetStream usage. +/// Mirrors JetStreamAccountStats in server/jetstream.go. +/// Embeds for totals. +/// +public sealed class JetStreamAccountStats +{ + // Embedded JetStreamTier fields (Go struct embedding) + [JsonPropertyName("memory")] public ulong Memory { get; set; } + [JsonPropertyName("storage")] public ulong Store { get; set; } + [JsonPropertyName("reserved_memory")] public ulong ReservedMemory { get; set; } + [JsonPropertyName("reserved_storage")]public ulong ReservedStore { get; set; } + [JsonPropertyName("streams")] public int Streams { get; set; } + [JsonPropertyName("consumers")] public int Consumers { get; set; } + [JsonPropertyName("limits")] public JetStreamAccountLimits Limits { get; set; } = new(); + + [JsonPropertyName("domain")] public string? Domain { get; set; } + [JsonPropertyName("api")] public JetStreamApiStats Api { get; set; } = new(); + [JsonPropertyName("tiers")] public Dictionary? Tiers { get; set; } +} + +// --------------------------------------------------------------------------- +// Internal JetStream engine types +// --------------------------------------------------------------------------- + +/// +/// The main JetStream engine, one per server. +/// Mirrors jetStream struct in server/jetstream.go. +/// +internal sealed class JetStream +{ + // Atomic counters (use Interlocked for thread-safety) + public long ApiInflight; + public long ApiTotal; + public long ApiErrors; + public long MemReserved; + public long StoreReserved; + public long MemUsed; + public long StoreUsed; + public long QueueLimit; + public int Clustered; // atomic int32 + + private readonly ReaderWriterLockSlim _mu = new(); + + public object? Server { get; set; } // *Server — set at runtime + public JetStreamConfig Config { get; set; } = new(); + public object? Cluster { get; set; } // *jetStreamCluster — session 20+ + public Dictionary Accounts { get; } = new(StringComparer.Ordinal); + public DateTime Started { get; set; } + + // State booleans + public bool MetaRecovering { get; set; } + public bool StandAlone { get; set; } + public bool Oos { get; set; } + public bool ShuttingDown { get; set; } + public int Disabled; // atomic bool (0=false, 1=true) + + public ReaderWriterLockSlim Lock => _mu; +} + +/// +/// Tracks remote per-tier usage for a JetStream account. +/// Mirrors remoteUsage in server/jetstream.go. +/// +internal sealed class RemoteUsage +{ + public Dictionary Tiers { get; } = new(StringComparer.Ordinal); + public ulong Api; + public ulong Err; +} + +/// +/// Per-tier storage accounting (total + local split). +/// Mirrors jsaStorage in server/jetstream.go. +/// +internal sealed class JsaStorage +{ + public JsaUsage Total { get; set; } = new(); + public JsaUsage Local { get; set; } = new(); +} + +/// +/// A JetStream-enabled account, holding streams, limits and usage tracking. +/// Mirrors jsAccount in server/jetstream.go. +/// +internal sealed class JsAccount +{ + private readonly ReaderWriterLockSlim _mu = new(); + + public object? Js { get; set; } // *jetStream + public object? Account { get; set; } // *Account + public string StoreDir { get; set; } = string.Empty; + + // Concurrent inflight map (mirrors sync.Map) + public readonly System.Collections.Concurrent.ConcurrentDictionary Inflight = new(); + + // Streams keyed by stream name + public Dictionary Streams { get; } = new(StringComparer.Ordinal); // *stream + + // Send queue (mirrors *ipQueue[*pubMsg]) + public object? SendQ { get; set; } + + // Atomic sync flag (0=false, 1=true) + public int Sync; + + // Usage/limits (protected by UsageMu) + private readonly ReaderWriterLockSlim _usageMu = new(); + public Dictionary Limits { get; } = new(StringComparer.Ordinal); + public Dictionary Usage { get; } = new(StringComparer.Ordinal); + public Dictionary RUsage { get; } = new(StringComparer.Ordinal); + + public ulong ApiTotal { get; set; } + public ulong ApiErrors { get; set; } + public ulong UsageApi { get; set; } + public ulong UsageErr { get; set; } + public string UpdatesPub { get; set; } = string.Empty; + public object? UpdatesSub { get; set; } // *subscription + public DateTime LUpdate { get; set; } + + public ReaderWriterLockSlim Lock => _mu; + public ReaderWriterLockSlim UsageLock => _usageMu; +} + +/// +/// Memory and store byte usage. +/// Mirrors jsaUsage in server/jetstream.go. +/// +internal sealed class JsaUsage +{ + public long Mem { get; set; } + public long Store { get; set; } +} + +/// +/// Delegate for a function that generates a key-encryption key from a context byte array. +/// Mirrors keyGen in server/jetstream.go. +/// +public delegate byte[] KeyGen(byte[] context); diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamVersioning.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamVersioning.cs new file mode 100644 index 0000000..6a6af5e --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamVersioning.cs @@ -0,0 +1,106 @@ +// Copyright 2024-2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/jetstream_versioning.go in the NATS server Go source. + +namespace ZB.MOM.NatsNet.Server; + +/// +/// JetStream API level versioning constants and helpers. +/// Mirrors server/jetstream_versioning.go. +/// +public static class JetStreamVersioning +{ + /// Maximum supported JetStream API level for this server. + public const int JsApiLevel = 3; + + /// Metadata key that carries the required API level for a stream or consumer asset. + public const string JsRequiredLevelMetadataKey = "_nats.req.level"; + + /// Metadata key that carries the server version that created/updated the asset. + public const string JsServerVersionMetadataKey = "_nats.ver"; + + /// Metadata key that carries the server API level that created/updated the asset. + public const string JsServerLevelMetadataKey = "_nats.level"; + + // ---- API level feature gates ---- + // These document which API level each feature requires. + // They correspond to the requires() calls in setStaticStreamMetadata / setStaticConsumerMetadata. + + /// API level required for per-message TTL and SubjectDeleteMarkerTTL (v2.11). + public const int ApiLevelForTTL = 1; + + /// API level required for consumer PauseUntil (v2.11). + public const int ApiLevelForConsumerPause = 1; + + /// API level required for priority groups (v2.11). + public const int ApiLevelForPriorityGroups = 1; + + /// API level required for counter CRDTs (v2.12). + public const int ApiLevelForCounters = 2; + + /// API level required for atomic batch publishing (v2.12). + public const int ApiLevelForAtomicPublish = 2; + + /// API level required for message scheduling (v2.12). + public const int ApiLevelForMsgSchedules = 2; + + /// API level required for async persist mode (v2.12). + public const int ApiLevelForAsyncPersist = 2; + + // ---- Helper methods ---- + + /// + /// Returns the required API level string from stream or consumer metadata, + /// or an empty string if not set. + /// Mirrors getRequiredApiLevel. + /// + public static string GetRequiredApiLevel(IDictionary? metadata) + { + if (metadata is not null && metadata.TryGetValue(JsRequiredLevelMetadataKey, out var l) && l.Length > 0) + return l; + return string.Empty; + } + + /// + /// Returns whether this server supports the required API level encoded in the asset's metadata. + /// Mirrors supportsRequiredApiLevel. + /// + public static bool SupportsRequiredApiLevel(IDictionary? metadata) + { + var l = GetRequiredApiLevel(metadata); + if (l.Length == 0) return true; + return int.TryParse(l, out var level) && level <= JsApiLevel; + } + + /// + /// Removes dynamic (per-response) versioning fields from metadata. + /// These should never be stored; only added in API responses. + /// Mirrors deleteDynamicMetadata. + /// + public static void DeleteDynamicMetadata(IDictionary metadata) + { + metadata.Remove(JsServerVersionMetadataKey); + metadata.Remove(JsServerLevelMetadataKey); + } + + /// + /// Returns whether a request should be rejected based on the Nats-Required-Api-Level header value. + /// Mirrors errorOnRequiredApiLevel. + /// + public static bool ErrorOnRequiredApiLevel(string? reqApiLevelHeader) + { + if (string.IsNullOrEmpty(reqApiLevelHeader)) return false; + return !int.TryParse(reqApiLevelHeader, out var minLevel) || JsApiLevel < minLevel; + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs index 68506b3..7623d00 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs @@ -208,22 +208,8 @@ public static class CompressionMode // InternalState is now fully defined in Events/EventTypes.cs (session 12). -/// Stub for JetStream state pointer (session 19). -internal sealed class JetStreamState { } - -/// Stub for JetStream config (session 19). -public sealed class JetStreamConfig -{ - public string StoreDir { get; set; } = string.Empty; - public TimeSpan SyncInterval { get; set; } - public bool SyncAlways { get; set; } - public bool Strict { get; set; } - public long MaxMemory { get; set; } - public long MaxStore { get; set; } - public string Domain { get; set; } = string.Empty; - public bool CompressOK { get; set; } - public string UniqueTag { get; set; } = string.Empty; -} +// JetStreamState — replaced by JsAccount in JetStream/JetStreamTypes.cs (session 19). +// JetStreamConfig — replaced by full implementation in JetStream/JetStreamTypes.cs (session 19). // SrvGateway — full class is in Gateway/GatewayTypes.cs (session 16). diff --git a/porting.db b/porting.db index dd7db8a2db7152ce26f4bd7f27420ce40773aa6c..ff6715299639b74be6ff6bcaec152f9e7a384d1e 100644 GIT binary patch delta 27654 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%0A2} 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 I75nf10nC5kRsaA1 diff --git a/reports/current.md b/reports/current.md index ad44671..aff4275 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-26 21:06:51 UTC +Generated: 2026-02-26 21:14:41 UTC ## Modules (12 total) @@ -13,9 +13,9 @@ Generated: 2026-02-26 21:06:51 UTC | Status | Count | |--------|-------| -| complete | 2048 | +| complete | 2422 | | n_a | 77 | -| not_started | 1455 | +| not_started | 1081 | | stub | 93 | ## Unit Tests (3257 total) @@ -36,4 +36,4 @@ Generated: 2026-02-26 21:06:51 UTC ## Overall Progress -**2636/6942 items complete (38.0%)** +**3010/6942 items complete (43.4%)** diff --git a/reports/report_3cffa5b.md b/reports/report_3cffa5b.md new file mode 100644 index 0000000..aff4275 --- /dev/null +++ b/reports/report_3cffa5b.md @@ -0,0 +1,39 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-26 21:14:41 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| complete | 11 | +| not_started | 1 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| complete | 2422 | +| n_a | 77 | +| not_started | 1081 | +| stub | 93 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| complete | 319 | +| n_a | 181 | +| not_started | 2533 | +| stub | 224 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**3010/6942 items complete (43.4%)**