// Go reference: golang/nats-server/server/jetstream_versioning.go // Versioning metadata management for JetStream streams and consumers. // Manages the _nats.req.level, _nats.ver, _nats.level metadata keys. using NATS.Server.JetStream.Models; namespace NATS.Server.JetStream; /// /// JetStream API versioning constants and metadata management. /// Go reference: server/jetstream_versioning.go /// public static class JsVersioning { /// /// Current JetStream API level supported by this server. /// Go: JSApiLevel = 3 (jetstream_versioning.go:20) /// public const int JsApiLevel = 3; /// Server version string. public const string Version = "2.12.0"; /// Metadata key for the minimum required API level to use this asset. public const string RequiredLevelKey = "_nats.req.level"; /// Metadata key for the server version that last modified this asset. public const string ServerVersionKey = "_nats.ver"; /// Metadata key for the server API level that last modified this asset. public const string ServerLevelKey = "_nats.level"; /// /// Returns the required API level string from metadata, or empty if absent. /// Go: getRequiredApiLevel (jetstream_versioning.go:28) /// public static string GetRequiredApiLevel(Dictionary? metadata) { if (metadata != null && metadata.TryGetValue(RequiredLevelKey, out var level) && level.Length > 0) return level; return string.Empty; } /// /// Returns whether the required API level is supported by this server. /// Go: supportsRequiredApiLevel (jetstream_versioning.go:36) /// public static bool SupportsRequiredApiLevel(Dictionary? metadata) { var level = GetRequiredApiLevel(metadata); if (level.Length == 0) return true; if (!int.TryParse(level, out var li)) return false; return li <= JsApiLevel; } /// /// Sets static (stored) versioning metadata on a stream config. /// Clears dynamic fields (server version/level) and sets the required API level. /// Go: setStaticStreamMetadata (jetstream_versioning.go:44) /// public static void SetStaticStreamMetadata(StreamConfig cfg) { if (cfg.Metadata == null) cfg.Metadata = []; else DeleteDynamicMetadata(cfg.Metadata); var requiredApiLevel = 0; void Requires(int level) { if (level > requiredApiLevel) requiredApiLevel = level; } // TTLs require API level 1 (added v2.11) if (cfg.AllowMsgTtl || cfg.SubjectDeleteMarkerTtlMs > 0) Requires(1); // Counter CRDTs require API level 2 (added v2.12) if (cfg.AllowMsgCounter) Requires(2); // Atomic batch publishing requires API level 2 (added v2.12) if (cfg.AllowAtomicPublish) Requires(2); // Message scheduling requires API level 2 (added v2.12) if (cfg.AllowMsgSchedules) Requires(2); // Async persist mode requires API level 2 (added v2.12) if (cfg.PersistMode == PersistMode.Async) Requires(2); cfg.Metadata[RequiredLevelKey] = requiredApiLevel.ToString(); } /// /// Returns a copy of the stream config with dynamic metadata fields added. /// The original config is not modified. /// Go: setDynamicStreamMetadata (jetstream_versioning.go:88) /// public static StreamConfig SetDynamicStreamMetadata(StreamConfig cfg) { // Shallow copy the config var newCfg = ShallowCopyStream(cfg); newCfg.Metadata = []; if (cfg.Metadata != null) { foreach (var (k, v) in cfg.Metadata) newCfg.Metadata[k] = v; } newCfg.Metadata[ServerVersionKey] = Version; newCfg.Metadata[ServerLevelKey] = JsApiLevel.ToString(); return newCfg; } /// /// Copies versioning fields from prevCfg into cfg (for stream update equality checks). /// Removes dynamic fields. If prevCfg has no metadata, removes the key from cfg. /// Go: copyStreamMetadata (jetstream_versioning.go:110) /// public static void CopyStreamMetadata(StreamConfig cfg, StreamConfig? prevCfg) { if (cfg.Metadata != null) DeleteDynamicMetadata(cfg.Metadata); SetOrDeleteInStreamMetadata(cfg, prevCfg, RequiredLevelKey); } /// /// Sets static (stored) versioning metadata on a consumer config. /// Go: setStaticConsumerMetadata (jetstream_versioning.go:136) /// public static void SetStaticConsumerMetadata(ConsumerConfig cfg) { if (cfg.Metadata == null) cfg.Metadata = []; else DeleteDynamicMetadata(cfg.Metadata); var requiredApiLevel = 0; void Requires(int level) { if (level > requiredApiLevel) requiredApiLevel = level; } // PauseUntil (non-zero) requires API level 1 (added v2.11) if (cfg.PauseUntil.HasValue && cfg.PauseUntil.Value != DateTime.MinValue && cfg.PauseUntil.Value != default) Requires(1); // Priority policy / groups / pinned TTL require API level 1 if (cfg.PriorityPolicy != PriorityPolicy.None || cfg.PinnedTtlMs != 0 || cfg.PriorityGroups.Count > 0) Requires(1); cfg.Metadata[RequiredLevelKey] = requiredApiLevel.ToString(); } /// /// Returns a copy of the consumer config with dynamic metadata fields added. /// Go: setDynamicConsumerMetadata (jetstream_versioning.go:164) /// public static ConsumerConfig SetDynamicConsumerMetadata(ConsumerConfig cfg) { var newCfg = ShallowCopyConsumer(cfg); newCfg.Metadata = []; if (cfg.Metadata != null) { foreach (var (k, v) in cfg.Metadata) newCfg.Metadata[k] = v; } newCfg.Metadata[ServerVersionKey] = Version; newCfg.Metadata[ServerLevelKey] = JsApiLevel.ToString(); return newCfg; } /// /// Copies versioning fields from prevCfg into cfg (for consumer update equality checks). /// Removes dynamic fields. /// Go: copyConsumerMetadata (jetstream_versioning.go:198) /// public static void CopyConsumerMetadata(ConsumerConfig cfg, ConsumerConfig? prevCfg) { if (cfg.Metadata != null) DeleteDynamicMetadata(cfg.Metadata); SetOrDeleteInConsumerMetadata(cfg, prevCfg, RequiredLevelKey); } /// /// Removes dynamic metadata fields (server version and level) from a metadata dictionary. /// Go: deleteDynamicMetadata (jetstream_versioning.go:222) /// public static void DeleteDynamicMetadata(Dictionary metadata) { metadata.Remove(ServerVersionKey); metadata.Remove(ServerLevelKey); } // ========================================================================= // Private helpers // ========================================================================= private static void SetOrDeleteInStreamMetadata(StreamConfig cfg, StreamConfig? prevCfg, string key) { if (prevCfg?.Metadata != null && prevCfg.Metadata.TryGetValue(key, out var value)) { cfg.Metadata ??= []; cfg.Metadata[key] = value; return; } if (cfg.Metadata != null) { cfg.Metadata.Remove(key); if (cfg.Metadata.Count == 0) cfg.Metadata = null; } } private static void SetOrDeleteInConsumerMetadata(ConsumerConfig cfg, ConsumerConfig? prevCfg, string key) { if (prevCfg?.Metadata != null && prevCfg.Metadata.TryGetValue(key, out var value)) { cfg.Metadata ??= []; cfg.Metadata[key] = value; return; } if (cfg.Metadata != null) { cfg.Metadata.Remove(key); if (cfg.Metadata.Count == 0) cfg.Metadata = null; } } /// Shallow copy of a StreamConfig for metadata mutation. private static StreamConfig ShallowCopyStream(StreamConfig src) => new() { Name = src.Name, Description = src.Description, Subjects = src.Subjects, MaxMsgs = src.MaxMsgs, MaxBytes = src.MaxBytes, MaxMsgsPer = src.MaxMsgsPer, MaxAgeMs = src.MaxAgeMs, MaxMsgSize = src.MaxMsgSize, MaxConsumers = src.MaxConsumers, DuplicateWindowMs = src.DuplicateWindowMs, Sealed = src.Sealed, DenyDelete = src.DenyDelete, DenyPurge = src.DenyPurge, AllowDirect = src.AllowDirect, AllowMsgTtl = src.AllowMsgTtl, FirstSeq = src.FirstSeq, Retention = src.Retention, Discard = src.Discard, Storage = src.Storage, Replicas = src.Replicas, Mirror = src.Mirror, Source = src.Source, Sources = src.Sources, SubjectTransformSource = src.SubjectTransformSource, SubjectTransformDest = src.SubjectTransformDest, RePublishSource = src.RePublishSource, RePublishDest = src.RePublishDest, RePublishHeadersOnly = src.RePublishHeadersOnly, SubjectDeleteMarkerTtlMs = src.SubjectDeleteMarkerTtlMs, AllowMsgSchedules = src.AllowMsgSchedules, AllowMsgCounter = src.AllowMsgCounter, AllowAtomicPublish = src.AllowAtomicPublish, PersistMode = src.PersistMode, Metadata = src.Metadata, }; /// Shallow copy of a ConsumerConfig for metadata mutation. private static ConsumerConfig ShallowCopyConsumer(ConsumerConfig src) => new() { DurableName = src.DurableName, Ephemeral = src.Ephemeral, FilterSubject = src.FilterSubject, FilterSubjects = src.FilterSubjects, AckPolicy = src.AckPolicy, DeliverPolicy = src.DeliverPolicy, OptStartSeq = src.OptStartSeq, OptStartTimeUtc = src.OptStartTimeUtc, ReplayPolicy = src.ReplayPolicy, AckWaitMs = src.AckWaitMs, MaxDeliver = src.MaxDeliver, MaxAckPending = src.MaxAckPending, Push = src.Push, DeliverSubject = src.DeliverSubject, HeartbeatMs = src.HeartbeatMs, BackOffMs = src.BackOffMs, FlowControl = src.FlowControl, RateLimitBps = src.RateLimitBps, MaxWaiting = src.MaxWaiting, MaxRequestBatch = src.MaxRequestBatch, MaxRequestMaxBytes = src.MaxRequestMaxBytes, MaxRequestExpiresMs = src.MaxRequestExpiresMs, PauseUntil = src.PauseUntil, PriorityPolicy = src.PriorityPolicy, PriorityGroups = src.PriorityGroups, PinnedTtlMs = src.PinnedTtlMs, Metadata = src.Metadata, }; }