167 lines
6.3 KiB
C#
167 lines
6.3 KiB
C#
// <copyright file="HlcSchedulerEnqueueService.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
|
// </copyright>
|
|
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Canonical.Json;
|
|
using StellaOps.HybridLogicalClock;
|
|
using StellaOps.Scheduler.Persistence;
|
|
using StellaOps.Scheduler.Persistence.Postgres.Models;
|
|
using StellaOps.Scheduler.Persistence.Postgres.Repositories;
|
|
|
|
namespace StellaOps.Scheduler.Queue.Hlc;
|
|
|
|
/// <summary>
|
|
/// Implementation of HLC-ordered scheduler job enqueueing with chain linking.
|
|
/// </summary>
|
|
public sealed class HlcSchedulerEnqueueService : IHlcSchedulerEnqueueService
|
|
{
|
|
/// <summary>
|
|
/// Namespace GUID for deterministic job ID generation (v5 UUID style).
|
|
/// </summary>
|
|
private static readonly Guid JobIdNamespace = new("b8a7c6d5-e4f3-42a1-9b0c-1d2e3f4a5b6c");
|
|
|
|
private readonly IHybridLogicalClock _hlc;
|
|
private readonly ISchedulerLogRepository _logRepository;
|
|
private readonly IChainHeadRepository _chainHeadRepository;
|
|
private readonly ILogger<HlcSchedulerEnqueueService> _logger;
|
|
|
|
/// <summary>
|
|
/// Creates a new HLC scheduler enqueue service.
|
|
/// </summary>
|
|
public HlcSchedulerEnqueueService(
|
|
IHybridLogicalClock hlc,
|
|
ISchedulerLogRepository logRepository,
|
|
IChainHeadRepository chainHeadRepository,
|
|
ILogger<HlcSchedulerEnqueueService> logger)
|
|
{
|
|
_hlc = hlc ?? throw new ArgumentNullException(nameof(hlc));
|
|
_logRepository = logRepository ?? throw new ArgumentNullException(nameof(logRepository));
|
|
_chainHeadRepository = chainHeadRepository ?? throw new ArgumentNullException(nameof(chainHeadRepository));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<SchedulerHlcEnqueueResult> EnqueuePlannerAsync(
|
|
string tenantId,
|
|
PlannerQueueMessage message,
|
|
string? partitionKey = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(message);
|
|
return EnqueueAsync(tenantId, message, message.IdempotencyKey, partitionKey, cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<SchedulerHlcEnqueueResult> EnqueueRunnerSegmentAsync(
|
|
string tenantId,
|
|
RunnerSegmentQueueMessage message,
|
|
string? partitionKey = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(message);
|
|
return EnqueueAsync(tenantId, message, message.IdempotencyKey, partitionKey, cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<SchedulerHlcEnqueueResult> EnqueueAsync<T>(
|
|
string tenantId,
|
|
T payload,
|
|
string idempotencyKey,
|
|
string? partitionKey = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
|
ArgumentNullException.ThrowIfNull(payload);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(idempotencyKey);
|
|
|
|
var effectivePartitionKey = partitionKey ?? string.Empty;
|
|
|
|
// 1. Generate deterministic job ID from idempotency key
|
|
var jobId = ComputeDeterministicJobId(idempotencyKey);
|
|
|
|
// 2. Check for existing entry (idempotency)
|
|
if (await _logRepository.ExistsAsync(tenantId, jobId, cancellationToken).ConfigureAwait(false))
|
|
{
|
|
var existing = await _logRepository.GetByJobIdAsync(jobId, cancellationToken).ConfigureAwait(false);
|
|
if (existing is not null)
|
|
{
|
|
_logger.LogDebug(
|
|
"Job already enqueued, returning existing entry. TenantId={TenantId}, JobId={JobId}",
|
|
tenantId,
|
|
jobId);
|
|
|
|
return new SchedulerHlcEnqueueResult(
|
|
HlcTimestamp.Parse(existing.THlc),
|
|
existing.JobId,
|
|
existing.Link,
|
|
Deduplicated: true);
|
|
}
|
|
}
|
|
|
|
// 3. Generate HLC timestamp
|
|
var tHlc = _hlc.Tick();
|
|
|
|
// 4. Compute payload hash
|
|
var payloadHash = SchedulerChainLinking.ComputePayloadHash(payload);
|
|
|
|
// 5. Get previous chain link
|
|
var prevLink = await _chainHeadRepository.GetLastLinkAsync(tenantId, effectivePartitionKey, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
// 6. Compute new chain link
|
|
var link = SchedulerChainLinking.ComputeLink(prevLink, jobId, tHlc, payloadHash);
|
|
|
|
// 7. Insert log entry (atomic with chain head update)
|
|
var entry = new SchedulerLogEntry
|
|
{
|
|
TenantId = tenantId,
|
|
THlc = tHlc.ToSortableString(),
|
|
PartitionKey = effectivePartitionKey,
|
|
JobId = jobId,
|
|
PayloadHash = payloadHash,
|
|
PrevLink = prevLink,
|
|
Link = link
|
|
};
|
|
|
|
await _logRepository.InsertWithChainUpdateAsync(entry, cancellationToken).ConfigureAwait(false);
|
|
|
|
_logger.LogInformation(
|
|
"Job enqueued with HLC ordering. TenantId={TenantId}, JobId={JobId}, THlc={THlc}, Link={Link}",
|
|
tenantId,
|
|
jobId,
|
|
tHlc.ToSortableString(),
|
|
SchedulerChainLinking.ToHex(link));
|
|
|
|
return new SchedulerHlcEnqueueResult(tHlc, jobId, link, Deduplicated: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes a deterministic GUID from the idempotency key using SHA-256.
|
|
/// </summary>
|
|
private static Guid ComputeDeterministicJobId(string idempotencyKey)
|
|
{
|
|
// Use namespace + key pattern similar to UUID v5
|
|
var namespaceBytes = JobIdNamespace.ToByteArray();
|
|
var keyBytes = Encoding.UTF8.GetBytes(idempotencyKey);
|
|
|
|
var combined = new byte[namespaceBytes.Length + keyBytes.Length];
|
|
Buffer.BlockCopy(namespaceBytes, 0, combined, 0, namespaceBytes.Length);
|
|
Buffer.BlockCopy(keyBytes, 0, combined, namespaceBytes.Length, keyBytes.Length);
|
|
|
|
var hash = SHA256.HashData(combined);
|
|
|
|
// Take first 16 bytes for GUID
|
|
var guidBytes = new byte[16];
|
|
Buffer.BlockCopy(hash, 0, guidBytes, 0, 16);
|
|
|
|
// Set version (4) and variant bits for RFC 4122 compliance
|
|
guidBytes[6] = (byte)((guidBytes[6] & 0x0F) | 0x40); // Version 4
|
|
guidBytes[8] = (byte)((guidBytes[8] & 0x3F) | 0x80); // Variant 1
|
|
|
|
return new Guid(guidBytes);
|
|
}
|
|
}
|