// // Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later. // using Microsoft.Extensions.Logging; using StellaOps.HybridLogicalClock; using StellaOps.Scheduler.Persistence.Postgres.Models; using StellaOps.Scheduler.Persistence.Postgres.Repositories; namespace StellaOps.Scheduler.Queue.Hlc; /// /// Implementation of HLC-ordered scheduler job dequeuing. /// public sealed class HlcSchedulerDequeueService : IHlcSchedulerDequeueService { private readonly ISchedulerLogRepository _logRepository; private readonly ILogger _logger; /// /// Creates a new HLC scheduler dequeue service. /// public HlcSchedulerDequeueService( ISchedulerLogRepository logRepository, ILogger logger) { _logRepository = logRepository ?? throw new ArgumentNullException(nameof(logRepository)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// public async Task DequeueAsync( string tenantId, int limit, string? partitionKey = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(limit); var entries = await _logRepository.GetByHlcOrderAsync( tenantId, partitionKey, limit, cancellationToken).ConfigureAwait(false); // Get total count for pagination info var totalCount = await _logRepository.CountByHlcRangeAsync( tenantId, startTHlc: null, endTHlc: null, partitionKey, cancellationToken).ConfigureAwait(false); _logger.LogDebug( "Dequeued {Count} of {Total} entries in HLC order. TenantId={TenantId}, PartitionKey={PartitionKey}", entries.Count, totalCount, tenantId, partitionKey ?? "(all)"); return new SchedulerHlcDequeueResult( entries, totalCount, RangeStartHlc: null, RangeEndHlc: null); } /// public async Task DequeueByRangeAsync( string tenantId, HlcTimestamp? startHlc, HlcTimestamp? endHlc, int limit, string? partitionKey = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(limit); var startTHlc = startHlc?.ToSortableString(); var endTHlc = endHlc?.ToSortableString(); var entries = await _logRepository.GetByHlcRangeAsync( tenantId, startTHlc, endTHlc, limit, partitionKey, cancellationToken).ConfigureAwait(false); var totalCount = await _logRepository.CountByHlcRangeAsync( tenantId, startTHlc, endTHlc, partitionKey, cancellationToken).ConfigureAwait(false); _logger.LogDebug( "Dequeued {Count} of {Total} entries in HLC range [{Start}, {End}]. TenantId={TenantId}", entries.Count, totalCount, startTHlc ?? "(unbounded)", endTHlc ?? "(unbounded)", tenantId); return new SchedulerHlcDequeueResult( entries, totalCount, startHlc, endHlc); } /// public async Task DequeueAfterAsync( string tenantId, HlcTimestamp afterHlc, int limit, string? partitionKey = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(limit); var afterTHlc = afterHlc.ToSortableString(); var entries = await _logRepository.GetAfterHlcAsync( tenantId, afterTHlc, limit, partitionKey, cancellationToken).ConfigureAwait(false); // Count remaining entries after cursor var totalCount = await _logRepository.CountByHlcRangeAsync( tenantId, afterTHlc, endTHlc: null, partitionKey, cancellationToken).ConfigureAwait(false); _logger.LogDebug( "Dequeued {Count} entries after HLC {AfterHlc}. TenantId={TenantId}, PartitionKey={PartitionKey}", entries.Count, afterTHlc, tenantId, partitionKey ?? "(all)"); return new SchedulerHlcDequeueResult( entries, totalCount, afterHlc, RangeEndHlc: null); } /// public async Task GetByJobIdAsync( string tenantId, Guid jobId, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); var entry = await _logRepository.GetByJobIdAsync(jobId, cancellationToken).ConfigureAwait(false); // Verify tenant isolation if (entry is not null && !string.Equals(entry.TenantId, tenantId, StringComparison.Ordinal)) { _logger.LogWarning( "Job {JobId} found but belongs to different tenant. RequestedTenant={RequestedTenant}, ActualTenant={ActualTenant}", jobId, tenantId, entry.TenantId); return null; } return entry; } }