up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Scheduler.Queue.Tests")]
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Scheduler.Queue.Tests")]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
internal interface ISchedulerQueueTransportDiagnostics
|
||||
{
|
||||
ValueTask PingAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
internal interface ISchedulerQueueTransportDiagnostics
|
||||
{
|
||||
ValueTask PingAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Nats;
|
||||
|
||||
internal interface INatsSchedulerQueuePayload<TMessage>
|
||||
{
|
||||
string QueueName { get; }
|
||||
|
||||
string GetIdempotencyKey(TMessage message);
|
||||
|
||||
byte[] Serialize(TMessage message);
|
||||
|
||||
TMessage Deserialize(byte[] payload);
|
||||
|
||||
string GetRunId(TMessage message);
|
||||
|
||||
string GetTenantId(TMessage message);
|
||||
|
||||
string? GetScheduleId(TMessage message);
|
||||
|
||||
string? GetSegmentId(TMessage message);
|
||||
|
||||
string? GetCorrelationId(TMessage message);
|
||||
|
||||
IReadOnlyDictionary<string, string>? GetAttributes(TMessage message);
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Nats;
|
||||
|
||||
internal interface INatsSchedulerQueuePayload<TMessage>
|
||||
{
|
||||
string QueueName { get; }
|
||||
|
||||
string GetIdempotencyKey(TMessage message);
|
||||
|
||||
byte[] Serialize(TMessage message);
|
||||
|
||||
TMessage Deserialize(byte[] payload);
|
||||
|
||||
string GetRunId(TMessage message);
|
||||
|
||||
string GetTenantId(TMessage message);
|
||||
|
||||
string? GetScheduleId(TMessage message);
|
||||
|
||||
string? GetSegmentId(TMessage message);
|
||||
|
||||
string? GetCorrelationId(TMessage message);
|
||||
|
||||
IReadOnlyDictionary<string, string>? GetAttributes(TMessage message);
|
||||
}
|
||||
|
||||
@@ -1,66 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NATS.Client.Core;
|
||||
using NATS.Client.JetStream;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Nats;
|
||||
|
||||
internal sealed class NatsSchedulerPlannerQueue
|
||||
: NatsSchedulerQueueBase<PlannerQueueMessage>, ISchedulerPlannerQueue
|
||||
{
|
||||
public NatsSchedulerPlannerQueue(
|
||||
SchedulerQueueOptions queueOptions,
|
||||
SchedulerNatsQueueOptions natsOptions,
|
||||
ILogger<NatsSchedulerPlannerQueue> logger,
|
||||
TimeProvider timeProvider,
|
||||
Func<NatsOpts, CancellationToken, ValueTask<NatsConnection>>? connectionFactory = null)
|
||||
: base(
|
||||
queueOptions,
|
||||
natsOptions,
|
||||
natsOptions.Planner,
|
||||
PlannerPayload.Instance,
|
||||
logger,
|
||||
timeProvider,
|
||||
connectionFactory)
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class PlannerPayload : INatsSchedulerQueuePayload<PlannerQueueMessage>
|
||||
{
|
||||
public static PlannerPayload Instance { get; } = new();
|
||||
|
||||
public string QueueName => "planner";
|
||||
|
||||
public string GetIdempotencyKey(PlannerQueueMessage message)
|
||||
=> message.IdempotencyKey;
|
||||
|
||||
public byte[] Serialize(PlannerQueueMessage message)
|
||||
=> Encoding.UTF8.GetBytes(CanonicalJsonSerializer.Serialize(message));
|
||||
|
||||
public PlannerQueueMessage Deserialize(byte[] payload)
|
||||
=> CanonicalJsonSerializer.Deserialize<PlannerQueueMessage>(Encoding.UTF8.GetString(payload));
|
||||
|
||||
public string GetRunId(PlannerQueueMessage message)
|
||||
=> message.Run.Id;
|
||||
|
||||
public string GetTenantId(PlannerQueueMessage message)
|
||||
=> message.Run.TenantId;
|
||||
|
||||
public string? GetScheduleId(PlannerQueueMessage message)
|
||||
=> message.ScheduleId;
|
||||
|
||||
public string? GetSegmentId(PlannerQueueMessage message)
|
||||
=> null;
|
||||
|
||||
public string? GetCorrelationId(PlannerQueueMessage message)
|
||||
=> message.CorrelationId;
|
||||
|
||||
public IReadOnlyDictionary<string, string>? GetAttributes(PlannerQueueMessage message)
|
||||
=> null;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NATS.Client.Core;
|
||||
using NATS.Client.JetStream;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Nats;
|
||||
|
||||
internal sealed class NatsSchedulerPlannerQueue
|
||||
: NatsSchedulerQueueBase<PlannerQueueMessage>, ISchedulerPlannerQueue
|
||||
{
|
||||
public NatsSchedulerPlannerQueue(
|
||||
SchedulerQueueOptions queueOptions,
|
||||
SchedulerNatsQueueOptions natsOptions,
|
||||
ILogger<NatsSchedulerPlannerQueue> logger,
|
||||
TimeProvider timeProvider,
|
||||
Func<NatsOpts, CancellationToken, ValueTask<NatsConnection>>? connectionFactory = null)
|
||||
: base(
|
||||
queueOptions,
|
||||
natsOptions,
|
||||
natsOptions.Planner,
|
||||
PlannerPayload.Instance,
|
||||
logger,
|
||||
timeProvider,
|
||||
connectionFactory)
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class PlannerPayload : INatsSchedulerQueuePayload<PlannerQueueMessage>
|
||||
{
|
||||
public static PlannerPayload Instance { get; } = new();
|
||||
|
||||
public string QueueName => "planner";
|
||||
|
||||
public string GetIdempotencyKey(PlannerQueueMessage message)
|
||||
=> message.IdempotencyKey;
|
||||
|
||||
public byte[] Serialize(PlannerQueueMessage message)
|
||||
=> Encoding.UTF8.GetBytes(CanonicalJsonSerializer.Serialize(message));
|
||||
|
||||
public PlannerQueueMessage Deserialize(byte[] payload)
|
||||
=> CanonicalJsonSerializer.Deserialize<PlannerQueueMessage>(Encoding.UTF8.GetString(payload));
|
||||
|
||||
public string GetRunId(PlannerQueueMessage message)
|
||||
=> message.Run.Id;
|
||||
|
||||
public string GetTenantId(PlannerQueueMessage message)
|
||||
=> message.Run.TenantId;
|
||||
|
||||
public string? GetScheduleId(PlannerQueueMessage message)
|
||||
=> message.ScheduleId;
|
||||
|
||||
public string? GetSegmentId(PlannerQueueMessage message)
|
||||
=> null;
|
||||
|
||||
public string? GetCorrelationId(PlannerQueueMessage message)
|
||||
=> message.CorrelationId;
|
||||
|
||||
public IReadOnlyDictionary<string, string>? GetAttributes(PlannerQueueMessage message)
|
||||
=> null;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,101 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NATS.Client.JetStream;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Nats;
|
||||
|
||||
internal sealed class NatsSchedulerQueueLease<TMessage> : ISchedulerQueueLease<TMessage>
|
||||
{
|
||||
private readonly NatsSchedulerQueueBase<TMessage> _queue;
|
||||
private int _completed;
|
||||
|
||||
internal NatsSchedulerQueueLease(
|
||||
NatsSchedulerQueueBase<TMessage> queue,
|
||||
NatsJSMsg<byte[]> message,
|
||||
byte[] payload,
|
||||
string idempotencyKey,
|
||||
string runId,
|
||||
string tenantId,
|
||||
string? scheduleId,
|
||||
string? segmentId,
|
||||
string? correlationId,
|
||||
IReadOnlyDictionary<string, string> attributes,
|
||||
TMessage deserialized,
|
||||
int attempt,
|
||||
DateTimeOffset enqueuedAt,
|
||||
DateTimeOffset leaseExpiresAt,
|
||||
string consumer)
|
||||
{
|
||||
_queue = queue;
|
||||
MessageId = message.Metadata?.Sequence.ToString() ?? idempotencyKey;
|
||||
RunId = runId;
|
||||
TenantId = tenantId;
|
||||
ScheduleId = scheduleId;
|
||||
SegmentId = segmentId;
|
||||
CorrelationId = correlationId;
|
||||
Attributes = attributes;
|
||||
Attempt = attempt;
|
||||
EnqueuedAt = enqueuedAt;
|
||||
LeaseExpiresAt = leaseExpiresAt;
|
||||
Consumer = consumer;
|
||||
IdempotencyKey = idempotencyKey;
|
||||
Message = deserialized;
|
||||
_message = message;
|
||||
Payload = payload;
|
||||
}
|
||||
|
||||
private readonly NatsJSMsg<byte[]> _message;
|
||||
|
||||
internal NatsJSMsg<byte[]> RawMessage => _message;
|
||||
|
||||
internal byte[] Payload { get; }
|
||||
|
||||
public string MessageId { get; }
|
||||
|
||||
public string IdempotencyKey { get; }
|
||||
|
||||
public string RunId { get; }
|
||||
|
||||
public string TenantId { get; }
|
||||
|
||||
public string? ScheduleId { get; }
|
||||
|
||||
public string? SegmentId { get; }
|
||||
|
||||
public string? CorrelationId { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> Attributes { get; }
|
||||
|
||||
public TMessage Message { get; }
|
||||
|
||||
public int Attempt { get; private set; }
|
||||
|
||||
public DateTimeOffset EnqueuedAt { get; }
|
||||
|
||||
public DateTimeOffset LeaseExpiresAt { get; private set; }
|
||||
|
||||
public string Consumer { get; }
|
||||
|
||||
public Task AcknowledgeAsync(CancellationToken cancellationToken = default)
|
||||
=> _queue.AcknowledgeAsync(this, cancellationToken);
|
||||
|
||||
public Task RenewAsync(TimeSpan leaseDuration, CancellationToken cancellationToken = default)
|
||||
=> _queue.RenewAsync(this, leaseDuration, cancellationToken);
|
||||
|
||||
public Task ReleaseAsync(SchedulerQueueReleaseDisposition disposition, CancellationToken cancellationToken = default)
|
||||
=> _queue.ReleaseAsync(this, disposition, cancellationToken);
|
||||
|
||||
public Task DeadLetterAsync(string reason, CancellationToken cancellationToken = default)
|
||||
=> _queue.DeadLetterAsync(this, reason, cancellationToken);
|
||||
|
||||
internal bool TryBeginCompletion()
|
||||
=> Interlocked.CompareExchange(ref _completed, 1, 0) == 0;
|
||||
|
||||
internal void RefreshLease(DateTimeOffset expiresAt)
|
||||
=> LeaseExpiresAt = expiresAt;
|
||||
|
||||
internal void IncrementAttempt()
|
||||
=> Attempt++;
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NATS.Client.JetStream;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Nats;
|
||||
|
||||
internal sealed class NatsSchedulerQueueLease<TMessage> : ISchedulerQueueLease<TMessage>
|
||||
{
|
||||
private readonly NatsSchedulerQueueBase<TMessage> _queue;
|
||||
private int _completed;
|
||||
|
||||
internal NatsSchedulerQueueLease(
|
||||
NatsSchedulerQueueBase<TMessage> queue,
|
||||
NatsJSMsg<byte[]> message,
|
||||
byte[] payload,
|
||||
string idempotencyKey,
|
||||
string runId,
|
||||
string tenantId,
|
||||
string? scheduleId,
|
||||
string? segmentId,
|
||||
string? correlationId,
|
||||
IReadOnlyDictionary<string, string> attributes,
|
||||
TMessage deserialized,
|
||||
int attempt,
|
||||
DateTimeOffset enqueuedAt,
|
||||
DateTimeOffset leaseExpiresAt,
|
||||
string consumer)
|
||||
{
|
||||
_queue = queue;
|
||||
MessageId = message.Metadata?.Sequence.ToString() ?? idempotencyKey;
|
||||
RunId = runId;
|
||||
TenantId = tenantId;
|
||||
ScheduleId = scheduleId;
|
||||
SegmentId = segmentId;
|
||||
CorrelationId = correlationId;
|
||||
Attributes = attributes;
|
||||
Attempt = attempt;
|
||||
EnqueuedAt = enqueuedAt;
|
||||
LeaseExpiresAt = leaseExpiresAt;
|
||||
Consumer = consumer;
|
||||
IdempotencyKey = idempotencyKey;
|
||||
Message = deserialized;
|
||||
_message = message;
|
||||
Payload = payload;
|
||||
}
|
||||
|
||||
private readonly NatsJSMsg<byte[]> _message;
|
||||
|
||||
internal NatsJSMsg<byte[]> RawMessage => _message;
|
||||
|
||||
internal byte[] Payload { get; }
|
||||
|
||||
public string MessageId { get; }
|
||||
|
||||
public string IdempotencyKey { get; }
|
||||
|
||||
public string RunId { get; }
|
||||
|
||||
public string TenantId { get; }
|
||||
|
||||
public string? ScheduleId { get; }
|
||||
|
||||
public string? SegmentId { get; }
|
||||
|
||||
public string? CorrelationId { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> Attributes { get; }
|
||||
|
||||
public TMessage Message { get; }
|
||||
|
||||
public int Attempt { get; private set; }
|
||||
|
||||
public DateTimeOffset EnqueuedAt { get; }
|
||||
|
||||
public DateTimeOffset LeaseExpiresAt { get; private set; }
|
||||
|
||||
public string Consumer { get; }
|
||||
|
||||
public Task AcknowledgeAsync(CancellationToken cancellationToken = default)
|
||||
=> _queue.AcknowledgeAsync(this, cancellationToken);
|
||||
|
||||
public Task RenewAsync(TimeSpan leaseDuration, CancellationToken cancellationToken = default)
|
||||
=> _queue.RenewAsync(this, leaseDuration, cancellationToken);
|
||||
|
||||
public Task ReleaseAsync(SchedulerQueueReleaseDisposition disposition, CancellationToken cancellationToken = default)
|
||||
=> _queue.ReleaseAsync(this, disposition, cancellationToken);
|
||||
|
||||
public Task DeadLetterAsync(string reason, CancellationToken cancellationToken = default)
|
||||
=> _queue.DeadLetterAsync(this, reason, cancellationToken);
|
||||
|
||||
internal bool TryBeginCompletion()
|
||||
=> Interlocked.CompareExchange(ref _completed, 1, 0) == 0;
|
||||
|
||||
internal void RefreshLease(DateTimeOffset expiresAt)
|
||||
=> LeaseExpiresAt = expiresAt;
|
||||
|
||||
internal void IncrementAttempt()
|
||||
=> Attempt++;
|
||||
}
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NATS.Client.Core;
|
||||
using NATS.Client.JetStream;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Nats;
|
||||
|
||||
internal sealed class NatsSchedulerRunnerQueue
|
||||
: NatsSchedulerQueueBase<RunnerSegmentQueueMessage>, ISchedulerRunnerQueue
|
||||
{
|
||||
public NatsSchedulerRunnerQueue(
|
||||
SchedulerQueueOptions queueOptions,
|
||||
SchedulerNatsQueueOptions natsOptions,
|
||||
ILogger<NatsSchedulerRunnerQueue> logger,
|
||||
TimeProvider timeProvider,
|
||||
Func<NatsOpts, CancellationToken, ValueTask<NatsConnection>>? connectionFactory = null)
|
||||
: base(
|
||||
queueOptions,
|
||||
natsOptions,
|
||||
natsOptions.Runner,
|
||||
RunnerPayload.Instance,
|
||||
logger,
|
||||
timeProvider,
|
||||
connectionFactory)
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class RunnerPayload : INatsSchedulerQueuePayload<RunnerSegmentQueueMessage>
|
||||
{
|
||||
public static RunnerPayload Instance { get; } = new();
|
||||
|
||||
public string QueueName => "runner";
|
||||
|
||||
public string GetIdempotencyKey(RunnerSegmentQueueMessage message)
|
||||
=> message.IdempotencyKey;
|
||||
|
||||
public byte[] Serialize(RunnerSegmentQueueMessage message)
|
||||
=> Encoding.UTF8.GetBytes(CanonicalJsonSerializer.Serialize(message));
|
||||
|
||||
public RunnerSegmentQueueMessage Deserialize(byte[] payload)
|
||||
=> CanonicalJsonSerializer.Deserialize<RunnerSegmentQueueMessage>(Encoding.UTF8.GetString(payload));
|
||||
|
||||
public string GetRunId(RunnerSegmentQueueMessage message)
|
||||
=> message.RunId;
|
||||
|
||||
public string GetTenantId(RunnerSegmentQueueMessage message)
|
||||
=> message.TenantId;
|
||||
|
||||
public string? GetScheduleId(RunnerSegmentQueueMessage message)
|
||||
=> message.ScheduleId;
|
||||
|
||||
public string? GetSegmentId(RunnerSegmentQueueMessage message)
|
||||
=> message.SegmentId;
|
||||
|
||||
public string? GetCorrelationId(RunnerSegmentQueueMessage message)
|
||||
=> message.CorrelationId;
|
||||
|
||||
public IReadOnlyDictionary<string, string>? GetAttributes(RunnerSegmentQueueMessage message)
|
||||
{
|
||||
if (message.Attributes is null || message.Attributes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return message.Attributes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NATS.Client.Core;
|
||||
using NATS.Client.JetStream;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Nats;
|
||||
|
||||
internal sealed class NatsSchedulerRunnerQueue
|
||||
: NatsSchedulerQueueBase<RunnerSegmentQueueMessage>, ISchedulerRunnerQueue
|
||||
{
|
||||
public NatsSchedulerRunnerQueue(
|
||||
SchedulerQueueOptions queueOptions,
|
||||
SchedulerNatsQueueOptions natsOptions,
|
||||
ILogger<NatsSchedulerRunnerQueue> logger,
|
||||
TimeProvider timeProvider,
|
||||
Func<NatsOpts, CancellationToken, ValueTask<NatsConnection>>? connectionFactory = null)
|
||||
: base(
|
||||
queueOptions,
|
||||
natsOptions,
|
||||
natsOptions.Runner,
|
||||
RunnerPayload.Instance,
|
||||
logger,
|
||||
timeProvider,
|
||||
connectionFactory)
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class RunnerPayload : INatsSchedulerQueuePayload<RunnerSegmentQueueMessage>
|
||||
{
|
||||
public static RunnerPayload Instance { get; } = new();
|
||||
|
||||
public string QueueName => "runner";
|
||||
|
||||
public string GetIdempotencyKey(RunnerSegmentQueueMessage message)
|
||||
=> message.IdempotencyKey;
|
||||
|
||||
public byte[] Serialize(RunnerSegmentQueueMessage message)
|
||||
=> Encoding.UTF8.GetBytes(CanonicalJsonSerializer.Serialize(message));
|
||||
|
||||
public RunnerSegmentQueueMessage Deserialize(byte[] payload)
|
||||
=> CanonicalJsonSerializer.Deserialize<RunnerSegmentQueueMessage>(Encoding.UTF8.GetString(payload));
|
||||
|
||||
public string GetRunId(RunnerSegmentQueueMessage message)
|
||||
=> message.RunId;
|
||||
|
||||
public string GetTenantId(RunnerSegmentQueueMessage message)
|
||||
=> message.TenantId;
|
||||
|
||||
public string? GetScheduleId(RunnerSegmentQueueMessage message)
|
||||
=> message.ScheduleId;
|
||||
|
||||
public string? GetSegmentId(RunnerSegmentQueueMessage message)
|
||||
=> message.SegmentId;
|
||||
|
||||
public string? GetCorrelationId(RunnerSegmentQueueMessage message)
|
||||
=> message.CorrelationId;
|
||||
|
||||
public IReadOnlyDictionary<string, string>? GetAttributes(RunnerSegmentQueueMessage message)
|
||||
{
|
||||
if (message.Attributes is null || message.Attributes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return message.Attributes.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Redis;
|
||||
|
||||
internal interface IRedisSchedulerQueuePayload<TMessage>
|
||||
{
|
||||
string QueueName { get; }
|
||||
|
||||
string GetIdempotencyKey(TMessage message);
|
||||
|
||||
string Serialize(TMessage message);
|
||||
|
||||
TMessage Deserialize(string payload);
|
||||
|
||||
string GetRunId(TMessage message);
|
||||
|
||||
string GetTenantId(TMessage message);
|
||||
|
||||
string? GetScheduleId(TMessage message);
|
||||
|
||||
string? GetSegmentId(TMessage message);
|
||||
|
||||
string? GetCorrelationId(TMessage message);
|
||||
|
||||
IReadOnlyDictionary<string, string>? GetAttributes(TMessage message);
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Redis;
|
||||
|
||||
internal interface IRedisSchedulerQueuePayload<TMessage>
|
||||
{
|
||||
string QueueName { get; }
|
||||
|
||||
string GetIdempotencyKey(TMessage message);
|
||||
|
||||
string Serialize(TMessage message);
|
||||
|
||||
TMessage Deserialize(string payload);
|
||||
|
||||
string GetRunId(TMessage message);
|
||||
|
||||
string GetTenantId(TMessage message);
|
||||
|
||||
string? GetScheduleId(TMessage message);
|
||||
|
||||
string? GetSegmentId(TMessage message);
|
||||
|
||||
string? GetCorrelationId(TMessage message);
|
||||
|
||||
IReadOnlyDictionary<string, string>? GetAttributes(TMessage message);
|
||||
}
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Redis;
|
||||
|
||||
internal sealed class RedisSchedulerPlannerQueue
|
||||
: RedisSchedulerQueueBase<PlannerQueueMessage>, ISchedulerPlannerQueue
|
||||
{
|
||||
public RedisSchedulerPlannerQueue(
|
||||
SchedulerQueueOptions queueOptions,
|
||||
SchedulerRedisQueueOptions redisOptions,
|
||||
ILogger<RedisSchedulerPlannerQueue> logger,
|
||||
TimeProvider timeProvider,
|
||||
Func<ConfigurationOptions, Task<IConnectionMultiplexer>>? connectionFactory = null)
|
||||
: base(
|
||||
queueOptions,
|
||||
redisOptions,
|
||||
redisOptions.Planner,
|
||||
PlannerPayload.Instance,
|
||||
logger,
|
||||
timeProvider,
|
||||
connectionFactory)
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class PlannerPayload : IRedisSchedulerQueuePayload<PlannerQueueMessage>
|
||||
{
|
||||
public static PlannerPayload Instance { get; } = new();
|
||||
|
||||
public string QueueName => "planner";
|
||||
|
||||
public string GetIdempotencyKey(PlannerQueueMessage message)
|
||||
=> message.IdempotencyKey;
|
||||
|
||||
public string Serialize(PlannerQueueMessage message)
|
||||
=> CanonicalJsonSerializer.Serialize(message);
|
||||
|
||||
public PlannerQueueMessage Deserialize(string payload)
|
||||
=> CanonicalJsonSerializer.Deserialize<PlannerQueueMessage>(payload);
|
||||
|
||||
public string GetRunId(PlannerQueueMessage message)
|
||||
=> message.Run.Id;
|
||||
|
||||
public string GetTenantId(PlannerQueueMessage message)
|
||||
=> message.Run.TenantId;
|
||||
|
||||
public string? GetScheduleId(PlannerQueueMessage message)
|
||||
=> message.ScheduleId;
|
||||
|
||||
public string? GetSegmentId(PlannerQueueMessage message)
|
||||
=> null;
|
||||
|
||||
public string? GetCorrelationId(PlannerQueueMessage message)
|
||||
=> message.CorrelationId;
|
||||
|
||||
public IReadOnlyDictionary<string, string>? GetAttributes(PlannerQueueMessage message)
|
||||
=> null;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Redis;
|
||||
|
||||
internal sealed class RedisSchedulerPlannerQueue
|
||||
: RedisSchedulerQueueBase<PlannerQueueMessage>, ISchedulerPlannerQueue
|
||||
{
|
||||
public RedisSchedulerPlannerQueue(
|
||||
SchedulerQueueOptions queueOptions,
|
||||
SchedulerRedisQueueOptions redisOptions,
|
||||
ILogger<RedisSchedulerPlannerQueue> logger,
|
||||
TimeProvider timeProvider,
|
||||
Func<ConfigurationOptions, Task<IConnectionMultiplexer>>? connectionFactory = null)
|
||||
: base(
|
||||
queueOptions,
|
||||
redisOptions,
|
||||
redisOptions.Planner,
|
||||
PlannerPayload.Instance,
|
||||
logger,
|
||||
timeProvider,
|
||||
connectionFactory)
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class PlannerPayload : IRedisSchedulerQueuePayload<PlannerQueueMessage>
|
||||
{
|
||||
public static PlannerPayload Instance { get; } = new();
|
||||
|
||||
public string QueueName => "planner";
|
||||
|
||||
public string GetIdempotencyKey(PlannerQueueMessage message)
|
||||
=> message.IdempotencyKey;
|
||||
|
||||
public string Serialize(PlannerQueueMessage message)
|
||||
=> CanonicalJsonSerializer.Serialize(message);
|
||||
|
||||
public PlannerQueueMessage Deserialize(string payload)
|
||||
=> CanonicalJsonSerializer.Deserialize<PlannerQueueMessage>(payload);
|
||||
|
||||
public string GetRunId(PlannerQueueMessage message)
|
||||
=> message.Run.Id;
|
||||
|
||||
public string GetTenantId(PlannerQueueMessage message)
|
||||
=> message.Run.TenantId;
|
||||
|
||||
public string? GetScheduleId(PlannerQueueMessage message)
|
||||
=> message.ScheduleId;
|
||||
|
||||
public string? GetSegmentId(PlannerQueueMessage message)
|
||||
=> null;
|
||||
|
||||
public string? GetCorrelationId(PlannerQueueMessage message)
|
||||
=> message.CorrelationId;
|
||||
|
||||
public IReadOnlyDictionary<string, string>? GetAttributes(PlannerQueueMessage message)
|
||||
=> null;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,91 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Redis;
|
||||
|
||||
internal sealed class RedisSchedulerQueueLease<TMessage> : ISchedulerQueueLease<TMessage>
|
||||
{
|
||||
private readonly RedisSchedulerQueueBase<TMessage> _queue;
|
||||
private int _completed;
|
||||
|
||||
internal RedisSchedulerQueueLease(
|
||||
RedisSchedulerQueueBase<TMessage> queue,
|
||||
string messageId,
|
||||
string idempotencyKey,
|
||||
string runId,
|
||||
string tenantId,
|
||||
string? scheduleId,
|
||||
string? segmentId,
|
||||
string? correlationId,
|
||||
IReadOnlyDictionary<string, string> attributes,
|
||||
TMessage message,
|
||||
int attempt,
|
||||
DateTimeOffset enqueuedAt,
|
||||
DateTimeOffset leaseExpiresAt,
|
||||
string consumer)
|
||||
{
|
||||
_queue = queue;
|
||||
MessageId = messageId;
|
||||
IdempotencyKey = idempotencyKey;
|
||||
RunId = runId;
|
||||
TenantId = tenantId;
|
||||
ScheduleId = scheduleId;
|
||||
SegmentId = segmentId;
|
||||
CorrelationId = correlationId;
|
||||
Attributes = attributes;
|
||||
Message = message;
|
||||
Attempt = attempt;
|
||||
EnqueuedAt = enqueuedAt;
|
||||
LeaseExpiresAt = leaseExpiresAt;
|
||||
Consumer = consumer;
|
||||
}
|
||||
|
||||
public string MessageId { get; }
|
||||
|
||||
public string IdempotencyKey { get; }
|
||||
|
||||
public string RunId { get; }
|
||||
|
||||
public string TenantId { get; }
|
||||
|
||||
public string? ScheduleId { get; }
|
||||
|
||||
public string? SegmentId { get; }
|
||||
|
||||
public string? CorrelationId { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> Attributes { get; }
|
||||
|
||||
public TMessage Message { get; }
|
||||
|
||||
public int Attempt { get; private set; }
|
||||
|
||||
public DateTimeOffset EnqueuedAt { get; }
|
||||
|
||||
public DateTimeOffset LeaseExpiresAt { get; private set; }
|
||||
|
||||
public string Consumer { get; }
|
||||
|
||||
public Task AcknowledgeAsync(CancellationToken cancellationToken = default)
|
||||
=> _queue.AcknowledgeAsync(this, cancellationToken);
|
||||
|
||||
public Task RenewAsync(TimeSpan leaseDuration, CancellationToken cancellationToken = default)
|
||||
=> _queue.RenewLeaseAsync(this, leaseDuration, cancellationToken);
|
||||
|
||||
public Task ReleaseAsync(SchedulerQueueReleaseDisposition disposition, CancellationToken cancellationToken = default)
|
||||
=> _queue.ReleaseAsync(this, disposition, cancellationToken);
|
||||
|
||||
public Task DeadLetterAsync(string reason, CancellationToken cancellationToken = default)
|
||||
=> _queue.DeadLetterAsync(this, reason, cancellationToken);
|
||||
|
||||
internal bool TryBeginCompletion()
|
||||
=> Interlocked.CompareExchange(ref _completed, 1, 0) == 0;
|
||||
|
||||
internal void RefreshLease(DateTimeOffset expiresAt)
|
||||
=> LeaseExpiresAt = expiresAt;
|
||||
|
||||
internal void IncrementAttempt()
|
||||
=> Attempt++;
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Redis;
|
||||
|
||||
internal sealed class RedisSchedulerQueueLease<TMessage> : ISchedulerQueueLease<TMessage>
|
||||
{
|
||||
private readonly RedisSchedulerQueueBase<TMessage> _queue;
|
||||
private int _completed;
|
||||
|
||||
internal RedisSchedulerQueueLease(
|
||||
RedisSchedulerQueueBase<TMessage> queue,
|
||||
string messageId,
|
||||
string idempotencyKey,
|
||||
string runId,
|
||||
string tenantId,
|
||||
string? scheduleId,
|
||||
string? segmentId,
|
||||
string? correlationId,
|
||||
IReadOnlyDictionary<string, string> attributes,
|
||||
TMessage message,
|
||||
int attempt,
|
||||
DateTimeOffset enqueuedAt,
|
||||
DateTimeOffset leaseExpiresAt,
|
||||
string consumer)
|
||||
{
|
||||
_queue = queue;
|
||||
MessageId = messageId;
|
||||
IdempotencyKey = idempotencyKey;
|
||||
RunId = runId;
|
||||
TenantId = tenantId;
|
||||
ScheduleId = scheduleId;
|
||||
SegmentId = segmentId;
|
||||
CorrelationId = correlationId;
|
||||
Attributes = attributes;
|
||||
Message = message;
|
||||
Attempt = attempt;
|
||||
EnqueuedAt = enqueuedAt;
|
||||
LeaseExpiresAt = leaseExpiresAt;
|
||||
Consumer = consumer;
|
||||
}
|
||||
|
||||
public string MessageId { get; }
|
||||
|
||||
public string IdempotencyKey { get; }
|
||||
|
||||
public string RunId { get; }
|
||||
|
||||
public string TenantId { get; }
|
||||
|
||||
public string? ScheduleId { get; }
|
||||
|
||||
public string? SegmentId { get; }
|
||||
|
||||
public string? CorrelationId { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> Attributes { get; }
|
||||
|
||||
public TMessage Message { get; }
|
||||
|
||||
public int Attempt { get; private set; }
|
||||
|
||||
public DateTimeOffset EnqueuedAt { get; }
|
||||
|
||||
public DateTimeOffset LeaseExpiresAt { get; private set; }
|
||||
|
||||
public string Consumer { get; }
|
||||
|
||||
public Task AcknowledgeAsync(CancellationToken cancellationToken = default)
|
||||
=> _queue.AcknowledgeAsync(this, cancellationToken);
|
||||
|
||||
public Task RenewAsync(TimeSpan leaseDuration, CancellationToken cancellationToken = default)
|
||||
=> _queue.RenewLeaseAsync(this, leaseDuration, cancellationToken);
|
||||
|
||||
public Task ReleaseAsync(SchedulerQueueReleaseDisposition disposition, CancellationToken cancellationToken = default)
|
||||
=> _queue.ReleaseAsync(this, disposition, cancellationToken);
|
||||
|
||||
public Task DeadLetterAsync(string reason, CancellationToken cancellationToken = default)
|
||||
=> _queue.DeadLetterAsync(this, reason, cancellationToken);
|
||||
|
||||
internal bool TryBeginCompletion()
|
||||
=> Interlocked.CompareExchange(ref _completed, 1, 0) == 0;
|
||||
|
||||
internal void RefreshLease(DateTimeOffset expiresAt)
|
||||
=> LeaseExpiresAt = expiresAt;
|
||||
|
||||
internal void IncrementAttempt()
|
||||
=> Attempt++;
|
||||
}
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Redis;
|
||||
|
||||
internal sealed class RedisSchedulerRunnerQueue
|
||||
: RedisSchedulerQueueBase<RunnerSegmentQueueMessage>, ISchedulerRunnerQueue
|
||||
{
|
||||
public RedisSchedulerRunnerQueue(
|
||||
SchedulerQueueOptions queueOptions,
|
||||
SchedulerRedisQueueOptions redisOptions,
|
||||
ILogger<RedisSchedulerRunnerQueue> logger,
|
||||
TimeProvider timeProvider,
|
||||
Func<ConfigurationOptions, Task<IConnectionMultiplexer>>? connectionFactory = null)
|
||||
: base(
|
||||
queueOptions,
|
||||
redisOptions,
|
||||
redisOptions.Runner,
|
||||
RunnerPayload.Instance,
|
||||
logger,
|
||||
timeProvider,
|
||||
connectionFactory)
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class RunnerPayload : IRedisSchedulerQueuePayload<RunnerSegmentQueueMessage>
|
||||
{
|
||||
public static RunnerPayload Instance { get; } = new();
|
||||
|
||||
public string QueueName => "runner";
|
||||
|
||||
public string GetIdempotencyKey(RunnerSegmentQueueMessage message)
|
||||
=> message.IdempotencyKey;
|
||||
|
||||
public string Serialize(RunnerSegmentQueueMessage message)
|
||||
=> CanonicalJsonSerializer.Serialize(message);
|
||||
|
||||
public RunnerSegmentQueueMessage Deserialize(string payload)
|
||||
=> CanonicalJsonSerializer.Deserialize<RunnerSegmentQueueMessage>(payload);
|
||||
|
||||
public string GetRunId(RunnerSegmentQueueMessage message)
|
||||
=> message.RunId;
|
||||
|
||||
public string GetTenantId(RunnerSegmentQueueMessage message)
|
||||
=> message.TenantId;
|
||||
|
||||
public string? GetScheduleId(RunnerSegmentQueueMessage message)
|
||||
=> message.ScheduleId;
|
||||
|
||||
public string? GetSegmentId(RunnerSegmentQueueMessage message)
|
||||
=> message.SegmentId;
|
||||
|
||||
public string? GetCorrelationId(RunnerSegmentQueueMessage message)
|
||||
=> message.CorrelationId;
|
||||
|
||||
public IReadOnlyDictionary<string, string>? GetAttributes(RunnerSegmentQueueMessage message)
|
||||
{
|
||||
if (message.Attributes.Count == 0 && message.ImageDigests.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure digests remain accessible without deserializing the entire payload.
|
||||
var map = new Dictionary<string, string>(message.Attributes, StringComparer.Ordinal);
|
||||
map["imageDigestCount"] = message.ImageDigests.Count.ToString();
|
||||
|
||||
// populate first few digests for quick inspection (bounded)
|
||||
var take = Math.Min(message.ImageDigests.Count, 5);
|
||||
for (var i = 0; i < take; i++)
|
||||
{
|
||||
map[$"digest{i}"] = message.ImageDigests[i];
|
||||
}
|
||||
|
||||
if (message.RatePerSecond.HasValue)
|
||||
{
|
||||
map["ratePerSecond"] = message.RatePerSecond.Value.ToString();
|
||||
}
|
||||
|
||||
map["usageOnly"] = message.UsageOnly ? "true" : "false";
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Redis;
|
||||
|
||||
internal sealed class RedisSchedulerRunnerQueue
|
||||
: RedisSchedulerQueueBase<RunnerSegmentQueueMessage>, ISchedulerRunnerQueue
|
||||
{
|
||||
public RedisSchedulerRunnerQueue(
|
||||
SchedulerQueueOptions queueOptions,
|
||||
SchedulerRedisQueueOptions redisOptions,
|
||||
ILogger<RedisSchedulerRunnerQueue> logger,
|
||||
TimeProvider timeProvider,
|
||||
Func<ConfigurationOptions, Task<IConnectionMultiplexer>>? connectionFactory = null)
|
||||
: base(
|
||||
queueOptions,
|
||||
redisOptions,
|
||||
redisOptions.Runner,
|
||||
RunnerPayload.Instance,
|
||||
logger,
|
||||
timeProvider,
|
||||
connectionFactory)
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class RunnerPayload : IRedisSchedulerQueuePayload<RunnerSegmentQueueMessage>
|
||||
{
|
||||
public static RunnerPayload Instance { get; } = new();
|
||||
|
||||
public string QueueName => "runner";
|
||||
|
||||
public string GetIdempotencyKey(RunnerSegmentQueueMessage message)
|
||||
=> message.IdempotencyKey;
|
||||
|
||||
public string Serialize(RunnerSegmentQueueMessage message)
|
||||
=> CanonicalJsonSerializer.Serialize(message);
|
||||
|
||||
public RunnerSegmentQueueMessage Deserialize(string payload)
|
||||
=> CanonicalJsonSerializer.Deserialize<RunnerSegmentQueueMessage>(payload);
|
||||
|
||||
public string GetRunId(RunnerSegmentQueueMessage message)
|
||||
=> message.RunId;
|
||||
|
||||
public string GetTenantId(RunnerSegmentQueueMessage message)
|
||||
=> message.TenantId;
|
||||
|
||||
public string? GetScheduleId(RunnerSegmentQueueMessage message)
|
||||
=> message.ScheduleId;
|
||||
|
||||
public string? GetSegmentId(RunnerSegmentQueueMessage message)
|
||||
=> message.SegmentId;
|
||||
|
||||
public string? GetCorrelationId(RunnerSegmentQueueMessage message)
|
||||
=> message.CorrelationId;
|
||||
|
||||
public IReadOnlyDictionary<string, string>? GetAttributes(RunnerSegmentQueueMessage message)
|
||||
{
|
||||
if (message.Attributes.Count == 0 && message.ImageDigests.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure digests remain accessible without deserializing the entire payload.
|
||||
var map = new Dictionary<string, string>(message.Attributes, StringComparer.Ordinal);
|
||||
map["imageDigestCount"] = message.ImageDigests.Count.ToString();
|
||||
|
||||
// populate first few digests for quick inspection (bounded)
|
||||
var take = Math.Min(message.ImageDigests.Count, 5);
|
||||
for (var i = 0; i < take; i++)
|
||||
{
|
||||
map[$"digest{i}"] = message.ImageDigests[i];
|
||||
}
|
||||
|
||||
if (message.RatePerSecond.HasValue)
|
||||
{
|
||||
map["ratePerSecond"] = message.RatePerSecond.Value.ToString();
|
||||
}
|
||||
|
||||
map["usageOnly"] = message.UsageOnly ? "true" : "false";
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +1,66 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
public sealed class PlannerQueueMessage
|
||||
{
|
||||
[JsonConstructor]
|
||||
public PlannerQueueMessage(
|
||||
Run run,
|
||||
ImpactSet impactSet,
|
||||
Schedule? schedule = null,
|
||||
string? correlationId = null)
|
||||
{
|
||||
Run = run ?? throw new ArgumentNullException(nameof(run));
|
||||
ImpactSet = impactSet ?? throw new ArgumentNullException(nameof(impactSet));
|
||||
|
||||
if (schedule is not null && string.IsNullOrWhiteSpace(schedule.Id))
|
||||
{
|
||||
throw new ArgumentException("Schedule must have a valid identifier.", nameof(schedule));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(correlationId))
|
||||
{
|
||||
correlationId = correlationId!.Trim();
|
||||
}
|
||||
|
||||
Schedule = schedule;
|
||||
CorrelationId = string.IsNullOrWhiteSpace(correlationId) ? null : correlationId;
|
||||
}
|
||||
|
||||
public Run Run { get; }
|
||||
|
||||
public ImpactSet ImpactSet { get; }
|
||||
|
||||
public Schedule? Schedule { get; }
|
||||
|
||||
public string? CorrelationId { get; }
|
||||
|
||||
public string IdempotencyKey => Run.Id;
|
||||
|
||||
public string TenantId => Run.TenantId;
|
||||
|
||||
public string? ScheduleId => Run.ScheduleId;
|
||||
}
|
||||
|
||||
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
public sealed class PlannerQueueMessage
|
||||
{
|
||||
[JsonConstructor]
|
||||
public PlannerQueueMessage(
|
||||
Run run,
|
||||
ImpactSet impactSet,
|
||||
Schedule? schedule = null,
|
||||
string? correlationId = null)
|
||||
{
|
||||
Run = run ?? throw new ArgumentNullException(nameof(run));
|
||||
ImpactSet = impactSet ?? throw new ArgumentNullException(nameof(impactSet));
|
||||
|
||||
if (schedule is not null && string.IsNullOrWhiteSpace(schedule.Id))
|
||||
{
|
||||
throw new ArgumentException("Schedule must have a valid identifier.", nameof(schedule));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(correlationId))
|
||||
{
|
||||
correlationId = correlationId!.Trim();
|
||||
}
|
||||
|
||||
Schedule = schedule;
|
||||
CorrelationId = string.IsNullOrWhiteSpace(correlationId) ? null : correlationId;
|
||||
}
|
||||
|
||||
public Run Run { get; }
|
||||
|
||||
public ImpactSet ImpactSet { get; }
|
||||
|
||||
public Schedule? Schedule { get; }
|
||||
|
||||
public string? CorrelationId { get; }
|
||||
|
||||
public string IdempotencyKey => Run.Id;
|
||||
|
||||
public string TenantId => Run.TenantId;
|
||||
|
||||
public string? ScheduleId => Run.ScheduleId;
|
||||
}
|
||||
|
||||
public sealed class RunnerSegmentQueueMessage
|
||||
{
|
||||
private readonly ReadOnlyCollection<string> _imageDigests;
|
||||
private readonly IReadOnlyDictionary<string, string> _attributes;
|
||||
private readonly IReadOnlyDictionary<string, SurfaceManifestPointer> _surfaceManifests;
|
||||
|
||||
[JsonConstructor]
|
||||
public RunnerSegmentQueueMessage(
|
||||
string segmentId,
|
||||
string runId,
|
||||
string tenantId,
|
||||
IReadOnlyList<string> imageDigests,
|
||||
|
||||
[JsonConstructor]
|
||||
public RunnerSegmentQueueMessage(
|
||||
string segmentId,
|
||||
string runId,
|
||||
string tenantId,
|
||||
IReadOnlyList<string> imageDigests,
|
||||
string? scheduleId = null,
|
||||
int? ratePerSecond = null,
|
||||
bool usageOnly = true,
|
||||
@@ -68,26 +68,26 @@ public sealed class RunnerSegmentQueueMessage
|
||||
string? correlationId = null,
|
||||
IReadOnlyDictionary<string, SurfaceManifestPointer>? surfaceManifests = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(segmentId))
|
||||
{
|
||||
throw new ArgumentException("Segment identifier must be provided.", nameof(segmentId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(runId))
|
||||
{
|
||||
throw new ArgumentException("Run identifier must be provided.", nameof(runId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
throw new ArgumentException("Tenant identifier must be provided.", nameof(tenantId));
|
||||
}
|
||||
|
||||
SegmentId = segmentId;
|
||||
RunId = runId;
|
||||
TenantId = tenantId;
|
||||
ScheduleId = string.IsNullOrWhiteSpace(scheduleId) ? null : scheduleId;
|
||||
RatePerSecond = ratePerSecond;
|
||||
if (string.IsNullOrWhiteSpace(segmentId))
|
||||
{
|
||||
throw new ArgumentException("Segment identifier must be provided.", nameof(segmentId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(runId))
|
||||
{
|
||||
throw new ArgumentException("Run identifier must be provided.", nameof(runId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
throw new ArgumentException("Tenant identifier must be provided.", nameof(tenantId));
|
||||
}
|
||||
|
||||
SegmentId = segmentId;
|
||||
RunId = runId;
|
||||
TenantId = tenantId;
|
||||
ScheduleId = string.IsNullOrWhiteSpace(scheduleId) ? null : scheduleId;
|
||||
RatePerSecond = ratePerSecond;
|
||||
UsageOnly = usageOnly;
|
||||
CorrelationId = string.IsNullOrWhiteSpace(correlationId) ? null : correlationId;
|
||||
|
||||
@@ -99,121 +99,121 @@ public sealed class RunnerSegmentQueueMessage
|
||||
? EmptyReadOnlyDictionary<string, SurfaceManifestPointer>.Instance
|
||||
: new ReadOnlyDictionary<string, SurfaceManifestPointer>(new Dictionary<string, SurfaceManifestPointer>(surfaceManifests, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
public string SegmentId { get; }
|
||||
|
||||
public string RunId { get; }
|
||||
|
||||
public string TenantId { get; }
|
||||
|
||||
public string? ScheduleId { get; }
|
||||
|
||||
public int? RatePerSecond { get; }
|
||||
|
||||
public bool UsageOnly { get; }
|
||||
|
||||
public string? CorrelationId { get; }
|
||||
|
||||
public IReadOnlyList<string> ImageDigests => _imageDigests;
|
||||
|
||||
|
||||
public string SegmentId { get; }
|
||||
|
||||
public string RunId { get; }
|
||||
|
||||
public string TenantId { get; }
|
||||
|
||||
public string? ScheduleId { get; }
|
||||
|
||||
public int? RatePerSecond { get; }
|
||||
|
||||
public bool UsageOnly { get; }
|
||||
|
||||
public string? CorrelationId { get; }
|
||||
|
||||
public IReadOnlyList<string> ImageDigests => _imageDigests;
|
||||
|
||||
public IReadOnlyDictionary<string, string> Attributes => _attributes;
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public IReadOnlyDictionary<string, SurfaceManifestPointer> SurfaceManifests => _surfaceManifests;
|
||||
|
||||
public string IdempotencyKey => SegmentId;
|
||||
|
||||
private static List<string> NormalizeDigests(IReadOnlyList<string> digests)
|
||||
{
|
||||
if (digests is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(digests));
|
||||
}
|
||||
|
||||
var list = new List<string>();
|
||||
foreach (var digest in digests)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(digest))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
list.Add(digest.Trim());
|
||||
}
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one image digest must be provided.", nameof(digests));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private sealed class EmptyReadOnlyDictionary<TKey, TValue>
|
||||
where TKey : notnull
|
||||
{
|
||||
public static readonly IReadOnlyDictionary<TKey, TValue> Instance =
|
||||
new ReadOnlyDictionary<TKey, TValue>(new Dictionary<TKey, TValue>(0, EqualityComparer<TKey>.Default));
|
||||
}
|
||||
|
||||
public string IdempotencyKey => SegmentId;
|
||||
|
||||
private static List<string> NormalizeDigests(IReadOnlyList<string> digests)
|
||||
{
|
||||
if (digests is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(digests));
|
||||
}
|
||||
|
||||
var list = new List<string>();
|
||||
foreach (var digest in digests)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(digest))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
list.Add(digest.Trim());
|
||||
}
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one image digest must be provided.", nameof(digests));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private sealed class EmptyReadOnlyDictionary<TKey, TValue>
|
||||
where TKey : notnull
|
||||
{
|
||||
public static readonly IReadOnlyDictionary<TKey, TValue> Instance =
|
||||
new ReadOnlyDictionary<TKey, TValue>(new Dictionary<TKey, TValue>(0, EqualityComparer<TKey>.Default));
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct SchedulerQueueEnqueueResult(string MessageId, bool Deduplicated);
|
||||
|
||||
public sealed class SchedulerQueueLeaseRequest
|
||||
{
|
||||
public SchedulerQueueLeaseRequest(string consumer, int batchSize, TimeSpan leaseDuration)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(consumer))
|
||||
{
|
||||
throw new ArgumentException("Consumer identifier must be provided.", nameof(consumer));
|
||||
}
|
||||
|
||||
if (batchSize <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(batchSize), batchSize, "Batch size must be positive.");
|
||||
}
|
||||
|
||||
if (leaseDuration <= TimeSpan.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(leaseDuration), leaseDuration, "Lease duration must be positive.");
|
||||
}
|
||||
|
||||
Consumer = consumer;
|
||||
BatchSize = batchSize;
|
||||
LeaseDuration = leaseDuration;
|
||||
}
|
||||
|
||||
public string Consumer { get; }
|
||||
|
||||
public int BatchSize { get; }
|
||||
|
||||
public TimeSpan LeaseDuration { get; }
|
||||
}
|
||||
|
||||
public sealed class SchedulerQueueClaimOptions
|
||||
{
|
||||
public SchedulerQueueClaimOptions(string claimantConsumer, int batchSize, TimeSpan minIdleTime)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(claimantConsumer))
|
||||
{
|
||||
throw new ArgumentException("Consumer identifier must be provided.", nameof(claimantConsumer));
|
||||
}
|
||||
|
||||
if (batchSize <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(batchSize), batchSize, "Batch size must be positive.");
|
||||
}
|
||||
|
||||
if (minIdleTime < TimeSpan.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minIdleTime), minIdleTime, "Idle time cannot be negative.");
|
||||
}
|
||||
|
||||
ClaimantConsumer = claimantConsumer;
|
||||
BatchSize = batchSize;
|
||||
MinIdleTime = minIdleTime;
|
||||
}
|
||||
|
||||
|
||||
public sealed class SchedulerQueueLeaseRequest
|
||||
{
|
||||
public SchedulerQueueLeaseRequest(string consumer, int batchSize, TimeSpan leaseDuration)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(consumer))
|
||||
{
|
||||
throw new ArgumentException("Consumer identifier must be provided.", nameof(consumer));
|
||||
}
|
||||
|
||||
if (batchSize <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(batchSize), batchSize, "Batch size must be positive.");
|
||||
}
|
||||
|
||||
if (leaseDuration <= TimeSpan.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(leaseDuration), leaseDuration, "Lease duration must be positive.");
|
||||
}
|
||||
|
||||
Consumer = consumer;
|
||||
BatchSize = batchSize;
|
||||
LeaseDuration = leaseDuration;
|
||||
}
|
||||
|
||||
public string Consumer { get; }
|
||||
|
||||
public int BatchSize { get; }
|
||||
|
||||
public TimeSpan LeaseDuration { get; }
|
||||
}
|
||||
|
||||
public sealed class SchedulerQueueClaimOptions
|
||||
{
|
||||
public SchedulerQueueClaimOptions(string claimantConsumer, int batchSize, TimeSpan minIdleTime)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(claimantConsumer))
|
||||
{
|
||||
throw new ArgumentException("Consumer identifier must be provided.", nameof(claimantConsumer));
|
||||
}
|
||||
|
||||
if (batchSize <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(batchSize), batchSize, "Batch size must be positive.");
|
||||
}
|
||||
|
||||
if (minIdleTime < TimeSpan.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minIdleTime), minIdleTime, "Idle time cannot be negative.");
|
||||
}
|
||||
|
||||
ClaimantConsumer = claimantConsumer;
|
||||
BatchSize = batchSize;
|
||||
MinIdleTime = minIdleTime;
|
||||
}
|
||||
|
||||
public string ClaimantConsumer { get; }
|
||||
|
||||
public int BatchSize { get; }
|
||||
@@ -240,63 +240,63 @@ public sealed record SurfaceManifestPointer
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Tenant { get; init; }
|
||||
}
|
||||
|
||||
public enum SchedulerQueueReleaseDisposition
|
||||
{
|
||||
Retry,
|
||||
Abandon
|
||||
}
|
||||
|
||||
public interface ISchedulerQueue<TMessage>
|
||||
{
|
||||
ValueTask<SchedulerQueueEnqueueResult> EnqueueAsync(TMessage message, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<IReadOnlyList<ISchedulerQueueLease<TMessage>>> LeaseAsync(SchedulerQueueLeaseRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<IReadOnlyList<ISchedulerQueueLease<TMessage>>> ClaimExpiredAsync(SchedulerQueueClaimOptions options, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface ISchedulerQueueLease<out TMessage>
|
||||
{
|
||||
string MessageId { get; }
|
||||
|
||||
int Attempt { get; }
|
||||
|
||||
DateTimeOffset EnqueuedAt { get; }
|
||||
|
||||
DateTimeOffset LeaseExpiresAt { get; }
|
||||
|
||||
string Consumer { get; }
|
||||
|
||||
string TenantId { get; }
|
||||
|
||||
string RunId { get; }
|
||||
|
||||
string? ScheduleId { get; }
|
||||
|
||||
string? SegmentId { get; }
|
||||
|
||||
string? CorrelationId { get; }
|
||||
|
||||
string IdempotencyKey { get; }
|
||||
|
||||
IReadOnlyDictionary<string, string> Attributes { get; }
|
||||
|
||||
TMessage Message { get; }
|
||||
|
||||
Task AcknowledgeAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task RenewAsync(TimeSpan leaseDuration, CancellationToken cancellationToken = default);
|
||||
|
||||
Task ReleaseAsync(SchedulerQueueReleaseDisposition disposition, CancellationToken cancellationToken = default);
|
||||
|
||||
Task DeadLetterAsync(string reason, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface ISchedulerPlannerQueue : ISchedulerQueue<PlannerQueueMessage>
|
||||
{
|
||||
}
|
||||
|
||||
public interface ISchedulerRunnerQueue : ISchedulerQueue<RunnerSegmentQueueMessage>
|
||||
{
|
||||
}
|
||||
|
||||
public enum SchedulerQueueReleaseDisposition
|
||||
{
|
||||
Retry,
|
||||
Abandon
|
||||
}
|
||||
|
||||
public interface ISchedulerQueue<TMessage>
|
||||
{
|
||||
ValueTask<SchedulerQueueEnqueueResult> EnqueueAsync(TMessage message, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<IReadOnlyList<ISchedulerQueueLease<TMessage>>> LeaseAsync(SchedulerQueueLeaseRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<IReadOnlyList<ISchedulerQueueLease<TMessage>>> ClaimExpiredAsync(SchedulerQueueClaimOptions options, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface ISchedulerQueueLease<out TMessage>
|
||||
{
|
||||
string MessageId { get; }
|
||||
|
||||
int Attempt { get; }
|
||||
|
||||
DateTimeOffset EnqueuedAt { get; }
|
||||
|
||||
DateTimeOffset LeaseExpiresAt { get; }
|
||||
|
||||
string Consumer { get; }
|
||||
|
||||
string TenantId { get; }
|
||||
|
||||
string RunId { get; }
|
||||
|
||||
string? ScheduleId { get; }
|
||||
|
||||
string? SegmentId { get; }
|
||||
|
||||
string? CorrelationId { get; }
|
||||
|
||||
string IdempotencyKey { get; }
|
||||
|
||||
IReadOnlyDictionary<string, string> Attributes { get; }
|
||||
|
||||
TMessage Message { get; }
|
||||
|
||||
Task AcknowledgeAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task RenewAsync(TimeSpan leaseDuration, CancellationToken cancellationToken = default);
|
||||
|
||||
Task ReleaseAsync(SchedulerQueueReleaseDisposition disposition, CancellationToken cancellationToken = default);
|
||||
|
||||
Task DeadLetterAsync(string reason, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface ISchedulerPlannerQueue : ISchedulerQueue<PlannerQueueMessage>
|
||||
{
|
||||
}
|
||||
|
||||
public interface ISchedulerRunnerQueue : ISchedulerQueue<RunnerSegmentQueueMessage>
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
internal static class SchedulerQueueFields
|
||||
{
|
||||
public const string Payload = "payload";
|
||||
public const string Attempt = "attempt";
|
||||
public const string EnqueuedAt = "enqueuedAt";
|
||||
public const string IdempotencyKey = "idempotency";
|
||||
public const string RunId = "runId";
|
||||
public const string TenantId = "tenantId";
|
||||
public const string ScheduleId = "scheduleId";
|
||||
public const string SegmentId = "segmentId";
|
||||
public const string QueueKind = "queueKind";
|
||||
public const string CorrelationId = "correlationId";
|
||||
public const string AttributePrefix = "attr:";
|
||||
}
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
internal static class SchedulerQueueFields
|
||||
{
|
||||
public const string Payload = "payload";
|
||||
public const string Attempt = "attempt";
|
||||
public const string EnqueuedAt = "enqueuedAt";
|
||||
public const string IdempotencyKey = "idempotency";
|
||||
public const string RunId = "runId";
|
||||
public const string TenantId = "tenantId";
|
||||
public const string ScheduleId = "scheduleId";
|
||||
public const string SegmentId = "segmentId";
|
||||
public const string QueueKind = "queueKind";
|
||||
public const string CorrelationId = "correlationId";
|
||||
public const string AttributePrefix = "attr:";
|
||||
}
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
public sealed class SchedulerQueueHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly ISchedulerPlannerQueue _plannerQueue;
|
||||
private readonly ISchedulerRunnerQueue _runnerQueue;
|
||||
private readonly ILogger<SchedulerQueueHealthCheck> _logger;
|
||||
|
||||
public SchedulerQueueHealthCheck(
|
||||
ISchedulerPlannerQueue plannerQueue,
|
||||
ISchedulerRunnerQueue runnerQueue,
|
||||
ILogger<SchedulerQueueHealthCheck> logger)
|
||||
{
|
||||
_plannerQueue = plannerQueue ?? throw new ArgumentNullException(nameof(plannerQueue));
|
||||
_runnerQueue = runnerQueue ?? throw new ArgumentNullException(nameof(runnerQueue));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var failures = new List<string>();
|
||||
|
||||
if (!await ProbeAsync(_plannerQueue, "planner", cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
failures.Add("planner transport unreachable");
|
||||
}
|
||||
|
||||
if (!await ProbeAsync(_runnerQueue, "runner", cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
failures.Add("runner transport unreachable");
|
||||
}
|
||||
|
||||
if (failures.Count == 0)
|
||||
{
|
||||
return HealthCheckResult.Healthy("Scheduler queues reachable.");
|
||||
}
|
||||
|
||||
var description = string.Join("; ", failures);
|
||||
return new HealthCheckResult(
|
||||
context.Registration.FailureStatus,
|
||||
description);
|
||||
}
|
||||
|
||||
private async Task<bool> ProbeAsync(object queue, string label, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (queue is ISchedulerQueueTransportDiagnostics diagnostics)
|
||||
{
|
||||
await diagnostics.PingAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Scheduler {Label} queue transport ping failed.", label);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
public sealed class SchedulerQueueHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly ISchedulerPlannerQueue _plannerQueue;
|
||||
private readonly ISchedulerRunnerQueue _runnerQueue;
|
||||
private readonly ILogger<SchedulerQueueHealthCheck> _logger;
|
||||
|
||||
public SchedulerQueueHealthCheck(
|
||||
ISchedulerPlannerQueue plannerQueue,
|
||||
ISchedulerRunnerQueue runnerQueue,
|
||||
ILogger<SchedulerQueueHealthCheck> logger)
|
||||
{
|
||||
_plannerQueue = plannerQueue ?? throw new ArgumentNullException(nameof(plannerQueue));
|
||||
_runnerQueue = runnerQueue ?? throw new ArgumentNullException(nameof(runnerQueue));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var failures = new List<string>();
|
||||
|
||||
if (!await ProbeAsync(_plannerQueue, "planner", cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
failures.Add("planner transport unreachable");
|
||||
}
|
||||
|
||||
if (!await ProbeAsync(_runnerQueue, "runner", cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
failures.Add("runner transport unreachable");
|
||||
}
|
||||
|
||||
if (failures.Count == 0)
|
||||
{
|
||||
return HealthCheckResult.Healthy("Scheduler queues reachable.");
|
||||
}
|
||||
|
||||
var description = string.Join("; ", failures);
|
||||
return new HealthCheckResult(
|
||||
context.Registration.FailureStatus,
|
||||
description);
|
||||
}
|
||||
|
||||
private async Task<bool> ProbeAsync(object queue, string label, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (queue is ISchedulerQueueTransportDiagnostics diagnostics)
|
||||
{
|
||||
await diagnostics.PingAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Scheduler {Label} queue transport ping failed.", label);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
public static class SchedulerQueueMetrics
|
||||
{
|
||||
private const string TransportTagName = "transport";
|
||||
private const string QueueTagName = "queue";
|
||||
|
||||
private static readonly Meter Meter = new("StellaOps.Scheduler.Queue");
|
||||
private const string TransportTagName = "transport";
|
||||
private const string QueueTagName = "queue";
|
||||
|
||||
private static readonly Meter Meter = new("StellaOps.Scheduler.Queue");
|
||||
private static readonly Counter<long> EnqueuedCounter = Meter.CreateCounter<long>("scheduler_queue_enqueued_total");
|
||||
private static readonly Counter<long> DeduplicatedCounter = Meter.CreateCounter<long>("scheduler_queue_deduplicated_total");
|
||||
private static readonly Counter<long> AckCounter = Meter.CreateCounter<long>("scheduler_queue_ack_total");
|
||||
@@ -43,16 +43,16 @@ public static class SchedulerQueueMetrics
|
||||
|
||||
public static void RecordEnqueued(string transport, string queue)
|
||||
=> EnqueuedCounter.Add(1, BuildTags(transport, queue));
|
||||
|
||||
public static void RecordDeduplicated(string transport, string queue)
|
||||
=> DeduplicatedCounter.Add(1, BuildTags(transport, queue));
|
||||
|
||||
public static void RecordAck(string transport, string queue)
|
||||
=> AckCounter.Add(1, BuildTags(transport, queue));
|
||||
|
||||
public static void RecordRetry(string transport, string queue)
|
||||
=> RetryCounter.Add(1, BuildTags(transport, queue));
|
||||
|
||||
|
||||
public static void RecordDeduplicated(string transport, string queue)
|
||||
=> DeduplicatedCounter.Add(1, BuildTags(transport, queue));
|
||||
|
||||
public static void RecordAck(string transport, string queue)
|
||||
=> AckCounter.Add(1, BuildTags(transport, queue));
|
||||
|
||||
public static void RecordRetry(string transport, string queue)
|
||||
=> RetryCounter.Add(1, BuildTags(transport, queue));
|
||||
|
||||
public static void RecordDeadLetter(string transport, string queue)
|
||||
=> DeadLetterCounter.Add(1, BuildTags(transport, queue));
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
public sealed class SchedulerQueueOptions
|
||||
{
|
||||
public SchedulerQueueTransportKind Kind { get; set; } = SchedulerQueueTransportKind.Redis;
|
||||
@@ -30,56 +30,56 @@ public sealed class SchedulerQueueOptions
|
||||
/// Base retry delay used when a message is released for retry.
|
||||
/// </summary>
|
||||
public TimeSpan RetryInitialBackoff { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Cap applied to the retry delay when exponential backoff is used.
|
||||
/// </summary>
|
||||
public TimeSpan RetryMaxBackoff { get; set; } = TimeSpan.FromMinutes(1);
|
||||
}
|
||||
|
||||
public sealed class SchedulerRedisQueueOptions
|
||||
{
|
||||
public string? ConnectionString { get; set; }
|
||||
|
||||
public int? Database { get; set; }
|
||||
|
||||
public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
public RedisSchedulerStreamOptions Planner { get; set; } = RedisSchedulerStreamOptions.ForPlanner();
|
||||
|
||||
public RedisSchedulerStreamOptions Runner { get; set; } = RedisSchedulerStreamOptions.ForRunner();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Cap applied to the retry delay when exponential backoff is used.
|
||||
/// </summary>
|
||||
public TimeSpan RetryMaxBackoff { get; set; } = TimeSpan.FromMinutes(1);
|
||||
}
|
||||
|
||||
public sealed class SchedulerRedisQueueOptions
|
||||
{
|
||||
public string? ConnectionString { get; set; }
|
||||
|
||||
public int? Database { get; set; }
|
||||
|
||||
public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
public RedisSchedulerStreamOptions Planner { get; set; } = RedisSchedulerStreamOptions.ForPlanner();
|
||||
|
||||
public RedisSchedulerStreamOptions Runner { get; set; } = RedisSchedulerStreamOptions.ForRunner();
|
||||
}
|
||||
|
||||
public sealed class RedisSchedulerStreamOptions
|
||||
{
|
||||
public string Stream { get; set; } = string.Empty;
|
||||
|
||||
public string ConsumerGroup { get; set; } = string.Empty;
|
||||
|
||||
public string DeadLetterStream { get; set; } = string.Empty;
|
||||
|
||||
public string IdempotencyKeyPrefix { get; set; } = string.Empty;
|
||||
|
||||
public TimeSpan IdempotencyWindow { get; set; } = TimeSpan.FromHours(12);
|
||||
|
||||
public int? ApproximateMaxLength { get; set; }
|
||||
|
||||
public static RedisSchedulerStreamOptions ForPlanner()
|
||||
=> new()
|
||||
{
|
||||
Stream = "scheduler:planner",
|
||||
ConsumerGroup = "scheduler-planners",
|
||||
DeadLetterStream = "scheduler:planner:dead",
|
||||
IdempotencyKeyPrefix = "scheduler:planner:idemp:"
|
||||
};
|
||||
|
||||
public static RedisSchedulerStreamOptions ForRunner()
|
||||
=> new()
|
||||
{
|
||||
Stream = "scheduler:runner",
|
||||
ConsumerGroup = "scheduler-runners",
|
||||
DeadLetterStream = "scheduler:runner:dead",
|
||||
IdempotencyKeyPrefix = "scheduler:runner:idemp:"
|
||||
public string Stream { get; set; } = string.Empty;
|
||||
|
||||
public string ConsumerGroup { get; set; } = string.Empty;
|
||||
|
||||
public string DeadLetterStream { get; set; } = string.Empty;
|
||||
|
||||
public string IdempotencyKeyPrefix { get; set; } = string.Empty;
|
||||
|
||||
public TimeSpan IdempotencyWindow { get; set; } = TimeSpan.FromHours(12);
|
||||
|
||||
public int? ApproximateMaxLength { get; set; }
|
||||
|
||||
public static RedisSchedulerStreamOptions ForPlanner()
|
||||
=> new()
|
||||
{
|
||||
Stream = "scheduler:planner",
|
||||
ConsumerGroup = "scheduler-planners",
|
||||
DeadLetterStream = "scheduler:planner:dead",
|
||||
IdempotencyKeyPrefix = "scheduler:planner:idemp:"
|
||||
};
|
||||
|
||||
public static RedisSchedulerStreamOptions ForRunner()
|
||||
=> new()
|
||||
{
|
||||
Stream = "scheduler:runner",
|
||||
ConsumerGroup = "scheduler-runners",
|
||||
DeadLetterStream = "scheduler:runner:dead",
|
||||
IdempotencyKeyPrefix = "scheduler:runner:idemp:"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -10,26 +10,26 @@ using StellaOps.Scheduler.Queue.Redis;
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
public static class SchedulerQueueServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddSchedulerQueues(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string sectionName = "scheduler:queue")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
var options = new SchedulerQueueOptions();
|
||||
configuration.GetSection(sectionName).Bind(options);
|
||||
|
||||
{
|
||||
public static IServiceCollection AddSchedulerQueues(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string sectionName = "scheduler:queue")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
var options = new SchedulerQueueOptions();
|
||||
configuration.GetSection(sectionName).Bind(options);
|
||||
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
services.AddSingleton(options);
|
||||
|
||||
services.AddSingleton<ISchedulerPlannerQueue>(sp =>
|
||||
{
|
||||
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||
var timeProvider = sp.GetService<TimeProvider>() ?? TimeProvider.System;
|
||||
|
||||
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||
var timeProvider = sp.GetService<TimeProvider>() ?? TimeProvider.System;
|
||||
|
||||
return options.Kind switch
|
||||
{
|
||||
SchedulerQueueTransportKind.Redis => new RedisSchedulerPlannerQueue(
|
||||
@@ -47,10 +47,10 @@ public static class SchedulerQueueServiceCollectionExtensions
|
||||
});
|
||||
|
||||
services.AddSingleton<ISchedulerRunnerQueue>(sp =>
|
||||
{
|
||||
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||
var timeProvider = sp.GetService<TimeProvider>() ?? TimeProvider.System;
|
||||
|
||||
{
|
||||
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||
var timeProvider = sp.GetService<TimeProvider>() ?? TimeProvider.System;
|
||||
|
||||
return options.Kind switch
|
||||
{
|
||||
SchedulerQueueTransportKind.Redis => new RedisSchedulerRunnerQueue(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
/// <summary>
|
||||
/// Transport backends supported by the scheduler queue abstraction.
|
||||
/// </summary>
|
||||
public enum SchedulerQueueTransportKind
|
||||
{
|
||||
Redis = 0,
|
||||
Nats = 1,
|
||||
}
|
||||
namespace StellaOps.Scheduler.Queue;
|
||||
|
||||
/// <summary>
|
||||
/// Transport backends supported by the scheduler queue abstraction.
|
||||
/// </summary>
|
||||
public enum SchedulerQueueTransportKind
|
||||
{
|
||||
Redis = 0,
|
||||
Nats = 1,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user