task2: implement batch38 group A consumer lifecycle features
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
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('\\');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user