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
Policy Lint & Smoke / policy-lint (push) Has been cancelled
release-manifest-verify / verify (push) Has been cancelled
- Implement comprehensive tests for PackRunAttestationService, covering attestation generation, verification, and event emission. - Add tests for SealedInstallEnforcer to validate sealed install requirements and enforcement logic. - Introduce a MonacoLoaderService stub for testing purposes to prevent Monaco workers/styles from loading during Karma runs.
303 lines
9.3 KiB
C#
303 lines
9.3 KiB
C#
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;
|
|
}
|
|
|
|
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,
|
|
string? scheduleId = null,
|
|
int? ratePerSecond = null,
|
|
bool usageOnly = true,
|
|
IReadOnlyDictionary<string, string>? attributes = null,
|
|
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;
|
|
UsageOnly = usageOnly;
|
|
CorrelationId = string.IsNullOrWhiteSpace(correlationId) ? null : correlationId;
|
|
|
|
_imageDigests = new ReadOnlyCollection<string>(NormalizeDigests(imageDigests));
|
|
_attributes = attributes is null
|
|
? EmptyReadOnlyDictionary<string, string>.Instance
|
|
: new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(attributes, StringComparer.Ordinal));
|
|
_surfaceManifests = surfaceManifests is null
|
|
? 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 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 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 string ClaimantConsumer { get; }
|
|
|
|
public int BatchSize { get; }
|
|
|
|
public TimeSpan MinIdleTime { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Minimal pointer to a Surface.FS manifest associated with an image digest.
|
|
/// Kept local to avoid coupling queue contracts to worker assemblies.
|
|
/// </summary>
|
|
public sealed record SurfaceManifestPointer
|
|
{
|
|
public SurfaceManifestPointer(string manifestDigest, string? tenant)
|
|
{
|
|
ManifestDigest = manifestDigest ?? throw new ArgumentNullException(nameof(manifestDigest));
|
|
Tenant = tenant;
|
|
}
|
|
|
|
[JsonPropertyName("manifestDigest")]
|
|
public string ManifestDigest { get; init; }
|
|
|
|
[JsonPropertyName("tenant")]
|
|
[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>
|
|
{
|
|
}
|