using ZB.MOM.NatsNet.Server.Internal.DataStructures; namespace ZB.MOM.NatsNet.Server; internal sealed partial class NatsConsumer { internal const int DefaultMaxAckPending = 1000; internal static readonly TimeSpan DefaultAckWait = TimeSpan.FromSeconds(30); internal static readonly TimeSpan DefaultDeleteWait = TimeSpan.FromSeconds(5); internal static readonly TimeSpan DefaultPinnedTtl = TimeSpan.FromMinutes(2); internal static JsApiError? SetConsumerConfigDefaults( ConsumerConfig config, StreamConfig streamConfig, JetStreamAccountLimits? selectedLimits, bool pedantic) { ArgumentNullException.ThrowIfNull(config); ArgumentNullException.ThrowIfNull(streamConfig); var streamReplicas = Math.Max(1, streamConfig.Replicas); if (config.MaxDeliver is 0 or < -1) { if (pedantic && config.MaxDeliver < -1) return JsApiErrors.NewJSPedanticError(new InvalidOperationException("max_deliver must be set to -1")); config.MaxDeliver = -1; } if (config.MaxWaiting < 0) { if (pedantic) return JsApiErrors.NewJSPedanticError(new InvalidOperationException("max_waiting must not be negative")); config.MaxWaiting = 0; } if (config.MaxAckPending < -1) { if (pedantic) return JsApiErrors.NewJSPedanticError(new InvalidOperationException("max_ack_pending must be set to -1")); config.MaxAckPending = -1; } if (config.MaxRequestBatch < 0) { if (pedantic) return JsApiErrors.NewJSPedanticError(new InvalidOperationException("max_batch must not be negative")); config.MaxRequestBatch = 0; } if (config.MaxRequestExpires < TimeSpan.Zero) { if (pedantic) return JsApiErrors.NewJSPedanticError(new InvalidOperationException("max_expires must not be negative")); config.MaxRequestExpires = TimeSpan.Zero; } if (config.MaxRequestMaxBytes < 0) { if (pedantic) return JsApiErrors.NewJSPedanticError(new InvalidOperationException("max_bytes must not be negative")); config.MaxRequestMaxBytes = 0; } if (config.Heartbeat < TimeSpan.Zero) { if (pedantic) return JsApiErrors.NewJSPedanticError(new InvalidOperationException("idle_heartbeat must not be negative")); config.Heartbeat = TimeSpan.Zero; } if (config.InactiveThreshold < TimeSpan.Zero) { if (pedantic) return JsApiErrors.NewJSPedanticError(new InvalidOperationException("inactive_threshold must not be negative")); config.InactiveThreshold = TimeSpan.Zero; } if (config.PinnedTTL < TimeSpan.Zero) { if (pedantic) return JsApiErrors.NewJSPedanticError(new InvalidOperationException("priority_timeout must not be negative")); config.PinnedTTL = TimeSpan.Zero; } if (config.AckWait == TimeSpan.Zero) config.AckWait = DefaultAckWait; if (config.MaxAckPending == 0 && config.AckPolicy != AckPolicy.AckNone) { config.MaxAckPending = selectedLimits?.MaxAckPending > 0 ? selectedLimits.MaxAckPending : DefaultMaxAckPending; } if (config.InactiveThreshold == TimeSpan.Zero && string.IsNullOrWhiteSpace(config.Durable)) config.InactiveThreshold = DefaultDeleteWait; if (config.PinnedTTL == TimeSpan.Zero && config.PriorityPolicy == PriorityPolicy.PriorityPinnedClient) config.PinnedTTL = DefaultPinnedTtl; if (config.Replicas == 0 || config.Replicas > streamReplicas) config.Replicas = streamReplicas; if (!string.IsNullOrWhiteSpace(config.Name) && string.IsNullOrWhiteSpace(config.Durable)) config.Durable = config.Name; return null; } internal static JsApiError? CheckConsumerCfg( ConsumerConfig config, StreamConfig streamConfig, JetStreamAccountLimits? selectedLimits, bool isRecovering) { ArgumentNullException.ThrowIfNull(config); ArgumentNullException.ThrowIfNull(streamConfig); var streamReplicas = Math.Max(1, streamConfig.Replicas); if (!string.IsNullOrWhiteSpace(config.Durable) && !string.IsNullOrWhiteSpace(config.Name) && !string.Equals(config.Durable, config.Name, StringComparison.Ordinal)) { return JsApiErrors.NewJSConsumerCreateDurableAndNameMismatchError(); } if (HasPathSeparators(config.Durable) || HasPathSeparators(config.Name)) return JsApiErrors.NewJSConsumerNameContainsPathSeparatorsError(); if (config.Replicas > streamReplicas) return JsApiErrors.NewJSConsumerReplicasExceedsStreamError(); if (!Enum.IsDefined(config.AckPolicy)) return JsApiErrors.NewJSConsumerAckPolicyInvalidError(); if (!Enum.IsDefined(config.ReplayPolicy)) return JsApiErrors.NewJSConsumerReplayPolicyInvalidError(); if (!Enum.IsDefined(config.DeliverPolicy)) return JsApiErrors.NewJSConsumerInvalidPolicyError(new InvalidOperationException("deliver policy invalid")); if (config.FilterSubjects is { Length: > 0 } && !string.IsNullOrWhiteSpace(config.FilterSubject)) return JsApiErrors.NewJSConsumerDuplicateFilterSubjectsError(); var filters = config.FilterSubjects is { Length: > 0 } ? SubjectTokens.Subjects(config.FilterSubjects) : (string.IsNullOrWhiteSpace(config.FilterSubject) ? [] : [config.FilterSubject]); for (var i = 0; i < filters.Length; i++) { if (string.IsNullOrWhiteSpace(filters[i])) return JsApiErrors.NewJSConsumerEmptyFilterError(); if (!SubscriptionIndex.IsValidSubject(filters[i])) return JsApiErrors.NewJSConsumerFilterNotSubsetError(); for (var j = i + 1; j < filters.Length; j++) { if (SubscriptionIndex.SubjectsCollide(filters[i], filters[j])) return JsApiErrors.NewJSConsumerOverlappingSubjectFiltersError(); } } var isPush = !string.IsNullOrWhiteSpace(config.DeliverSubject); if (isPush) { if (!SubscriptionIndex.IsValidSubject(config.DeliverSubject!)) return JsApiErrors.NewJSConsumerInvalidDeliverSubjectError(); if (SubscriptionIndex.SubjectHasWildcard(config.DeliverSubject!)) return JsApiErrors.NewJSConsumerDeliverToWildcardsError(); if (config.MaxWaiting > 0) return JsApiErrors.NewJSConsumerPushMaxWaitingError(); } else { if (config.RateLimit > 0) return JsApiErrors.NewJSConsumerPullWithRateLimitError(); } if (config.MaxAckPending > 0 && selectedLimits?.MaxAckPending > 0 && config.MaxAckPending > selectedLimits.MaxAckPending) return JsApiErrors.NewJSConsumerMaxPendingAckExcessError(selectedLimits.MaxAckPending); if (streamConfig.Retention == RetentionPolicy.WorkQueuePolicy && config.AckPolicy != AckPolicy.AckExplicit) return JsApiErrors.NewJSConsumerWQRequiresExplicitAckError(); if (config.Direct) { if (isPush) return JsApiErrors.NewJSConsumerDirectRequiresPushError(); if (!string.IsNullOrWhiteSpace(config.Durable)) return JsApiErrors.NewJSConsumerDirectRequiresEphemeralError(); } _ = isRecovering; return null; } internal void UpdateInactiveThreshold(ConsumerConfig config) { ArgumentNullException.ThrowIfNull(config); _mu.EnterWriteLock(); try { _deleteThreshold = config.InactiveThreshold > TimeSpan.Zero ? config.InactiveThreshold : DefaultDeleteWait; Config.InactiveThreshold = _deleteThreshold; } finally { _mu.ExitWriteLock(); } } internal void UpdatePauseState(ConsumerConfig config, DateTime? nowUtc = null) { ArgumentNullException.ThrowIfNull(config); var now = nowUtc ?? DateTime.UtcNow; _mu.EnterWriteLock(); try { Config.PauseUntil = config.PauseUntil; _isPaused = config.PauseUntil.HasValue && config.PauseUntil.Value > now; } finally { _mu.ExitWriteLock(); } } internal ConsumerAssignment? ConsumerAssignment() { _mu.EnterReadLock(); try { return _assignment; } finally { _mu.ExitReadLock(); } } internal void SetConsumerAssignment(ConsumerAssignment? assignment) { _mu.EnterWriteLock(); try { _assignment = assignment; } finally { _mu.ExitWriteLock(); } } private static bool HasPathSeparators(string? value) { if (string.IsNullOrWhiteSpace(value)) return false; return value.Contains('/') || value.Contains('\\'); } }