//
// 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;
}
}