save progress
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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 $$;
|
||||
|
||||
@@ -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()
|
||||
""";
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user