save progress

This commit is contained in:
StellaOps Bot
2025-12-18 09:10:36 +02:00
parent b4235c134c
commit 28823a8960
169 changed files with 11995 additions and 449 deletions

View File

@@ -0,0 +1,18 @@
namespace StellaOps.Scheduler.Models;
/// <summary>
/// Parsed cursor used for deterministic pagination of scheduler runs.
/// </summary>
public readonly record struct RunListCursor
{
public RunListCursor(DateTimeOffset createdAt, string runId)
{
CreatedAt = Validation.NormalizeTimestamp(createdAt);
RunId = Validation.EnsureId(runId, nameof(runId));
}
public DateTimeOffset CreatedAt { get; }
public string RunId { get; }
}

View File

@@ -1,5 +1,67 @@
-- Scheduler graph jobs schema (Postgres)
-- Legacy compatibility:
-- Earlier schema revisions shipped `scheduler.graph_jobs` as a TEXT/column-based table in `001_initial_schema.sql`.
-- This migration introduces a new JSON-payload based model with a `type` column and will fail on fresh installs
-- unless we either migrate or rename the legacy table first.
DO $$
DECLARE
rec RECORD;
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'scheduler'
AND table_name = 'graph_jobs'
) AND NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'scheduler'
AND table_name = 'graph_jobs'
AND column_name = 'type'
) THEN
-- Rename legacy table so we can create the v2 shape under the canonical name.
ALTER TABLE scheduler.graph_jobs RENAME TO graph_jobs_legacy;
-- Rename legacy constraints to avoid name collisions with the new table (e.g. graph_jobs_pkey).
FOR rec IN
SELECT c.conname
FROM pg_constraint c
JOIN pg_class rel ON rel.oid = c.conrelid
JOIN pg_namespace n ON n.oid = rel.relnamespace
WHERE n.nspname = 'scheduler'
AND rel.relname = 'graph_jobs_legacy'
LOOP
IF rec.conname LIKE 'graph_jobs%' THEN
EXECUTE format(
'ALTER TABLE scheduler.graph_jobs_legacy RENAME CONSTRAINT %I TO %I',
rec.conname,
replace(rec.conname, 'graph_jobs', 'graph_jobs_legacy'));
END IF;
END LOOP;
-- Rename legacy indexes to avoid collisions (idx_graph_jobs_* and graph_jobs_pkey).
FOR rec IN
SELECT indexname
FROM pg_indexes
WHERE schemaname = 'scheduler'
AND tablename = 'graph_jobs_legacy'
LOOP
IF rec.indexname = 'graph_jobs_pkey' THEN
EXECUTE format(
'ALTER INDEX scheduler.%I RENAME TO %I',
rec.indexname,
'graph_jobs_legacy_pkey');
ELSIF rec.indexname LIKE 'idx_graph_jobs%' THEN
EXECUTE format(
'ALTER INDEX scheduler.%I RENAME TO %I',
rec.indexname,
replace(rec.indexname, 'idx_graph_jobs', 'idx_graph_jobs_legacy'));
END IF;
END LOOP;
END IF;
END $$;
DO $$ BEGIN
CREATE TYPE scheduler.graph_job_type AS ENUM ('build', 'overlay');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

View File

@@ -28,7 +28,7 @@ public sealed class DistributedLockRepository : RepositoryBase<SchedulerDataSour
holder_id = EXCLUDED.holder_id,
tenant_id = EXCLUDED.tenant_id,
acquired_at = NOW(),
expires_at = NOW() + EXCLUDED.expires_at - EXCLUDED.acquired_at
expires_at = NOW() + @duration
WHERE scheduler.locks.expires_at < NOW()
""";

View File

@@ -8,6 +8,7 @@ public sealed class RunQueryOptions
public string? ScheduleId { get; init; }
public ImmutableArray<RunState> States { get; init; } = ImmutableArray<RunState>.Empty;
public DateTimeOffset? CreatedAfter { get; init; }
public RunListCursor? Cursor { get; init; }
public bool SortAscending { get; init; } = false;
public int? Limit { get; init; }
}

View File

@@ -100,6 +100,13 @@ LIMIT 1;
filters.Add("created_at > @CreatedAfter");
}
if (options.Cursor is { } cursor)
{
filters.Add(options.SortAscending
? "(created_at, id) > (@CursorCreatedAt, @CursorId)"
: "(created_at, id) < (@CursorCreatedAt, @CursorId)");
}
var order = options.SortAscending ? "created_at ASC, id ASC" : "created_at DESC, id DESC";
var limit = options.Limit.GetValueOrDefault(50);
@@ -117,6 +124,8 @@ LIMIT @Limit;
ScheduleId = options.ScheduleId,
States = options.States.Select(s => s.ToString().ToLowerInvariant()).ToArray(),
CreatedAfter = options.CreatedAfter?.UtcDateTime,
CursorCreatedAt = options.Cursor?.CreatedAt.UtcDateTime,
CursorId = options.Cursor?.RunId,
Limit = limit
});

View File

@@ -3,5 +3,6 @@ namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
public sealed class ScheduleQueryOptions
{
public bool IncludeDisabled { get; init; } = false;
public bool IncludeDeleted { get; init; } = false;
public int? Limit { get; init; }
}

View File

@@ -93,9 +93,14 @@ LIMIT 1;
options ??= new ScheduleQueryOptions();
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
var where = options.IncludeDisabled
? "tenant_id = @TenantId AND deleted_at IS NULL"
: "tenant_id = @TenantId AND deleted_at IS NULL AND enabled = TRUE";
var where = options.IncludeDeleted
? "tenant_id = @TenantId"
: "tenant_id = @TenantId AND deleted_at IS NULL";
if (!options.IncludeDisabled)
{
where += " AND enabled = TRUE";
}
var limit = options.Limit.GetValueOrDefault(200);

View File

@@ -39,6 +39,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<IScheduleRepository, ScheduleRepository>();
services.AddScoped<IImpactSnapshotRepository, ImpactSnapshotRepository>();
services.AddScoped<IPolicyRunJobRepository, PolicyRunJobRepository>();
services.AddScoped<IFailureSignatureRepository, FailureSignatureRepository>();
services.AddSingleton<IRunSummaryService, RunSummaryService>();
return services;
@@ -65,6 +66,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<IJobHistoryRepository, JobHistoryRepository>();
services.AddScoped<IMetricsRepository, MetricsRepository>();
services.AddScoped<IGraphJobRepository, GraphJobRepository>();
services.AddScoped<IFailureSignatureRepository, FailureSignatureRepository>();
return services;
}