feat(api): Implement Console Export Client and Models
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
- Added ConsoleExportClient for managing export requests and responses. - Introduced ConsoleExportRequest and ConsoleExportResponse models. - Implemented methods for creating and retrieving exports with appropriate headers. feat(crypto): Add Software SM2/SM3 Cryptography Provider - Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography. - Added support for signing and verification using SM2 algorithm. - Included hashing functionality with SM3 algorithm. - Configured options for loading keys from files and environment gate checks. test(crypto): Add unit tests for SmSoftCryptoProvider - Created comprehensive tests for signing, verifying, and hashing functionalities. - Ensured correct behavior for key management and error handling. feat(api): Enhance Console Export Models - Expanded ConsoleExport models to include detailed status and event types. - Added support for various export formats and notification options. test(time): Implement TimeAnchorPolicyService tests - Developed tests for TimeAnchorPolicyService to validate time anchors. - Covered scenarios for anchor validation, drift calculation, and policy enforcement.
This commit is contained in:
@@ -85,6 +85,11 @@ if (storageSection.Exists())
|
||||
builder.Services.AddSchedulerPostgresStorage(storageSection);
|
||||
builder.Services.AddScoped<IGraphJobRepository, GraphJobRepository>();
|
||||
builder.Services.AddSingleton<IGraphJobStore, PostgresGraphJobStore>();
|
||||
builder.Services.AddScoped<IScheduleRepository, ScheduleRepository>();
|
||||
builder.Services.AddScoped<IRunRepository, RunRepository>();
|
||||
builder.Services.AddSingleton<IRunSummaryService, RunSummaryService>();
|
||||
builder.Services.AddScoped<IImpactSnapshotRepository, ImpactSnapshotRepository>();
|
||||
builder.Services.AddScoped<IPolicyRunJobRepository, PolicyRunJobRepository>();
|
||||
builder.Services.AddSingleton<IPolicyRunService, PolicyRunService>();
|
||||
builder.Services.AddSingleton<IPolicySimulationMetricsProvider, PolicySimulationMetricsProvider>();
|
||||
builder.Services.AddSingleton<IPolicySimulationMetricsRecorder>(static sp => (IPolicySimulationMetricsRecorder)sp.GetRequiredService<IPolicySimulationMetricsProvider>());
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace StellaOps.Scheduler.Models;
|
||||
|
||||
public interface IRunSummaryService
|
||||
{
|
||||
Task<RunSummaryProjection> ProjectAsync(Run run, CancellationToken cancellationToken = default);
|
||||
Task<RunSummaryProjection?> GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<RunSummaryProjection>> ListAsync(string tenantId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Temporary compatibility stub to allow transition away from MongoDB driver.
|
||||
namespace MongoDB.Driver
|
||||
{
|
||||
public interface IClientSessionHandle { }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scheduler.Models;
|
||||
|
||||
public sealed record RunSummarySnapshot(string RunId, DateTimeOffset CompletedAt, RunState State, int Deltas);
|
||||
|
||||
public sealed record RunSummaryCounters(
|
||||
int Total,
|
||||
int Planning,
|
||||
int Queued,
|
||||
int Running,
|
||||
int Completed,
|
||||
int Error,
|
||||
int Cancelled,
|
||||
int TotalDeltas,
|
||||
int TotalNewCriticals,
|
||||
int TotalNewHigh,
|
||||
int TotalNewMedium,
|
||||
int TotalNewLow);
|
||||
|
||||
public sealed record RunSummaryProjection(
|
||||
string TenantId,
|
||||
string ScheduleId,
|
||||
DateTimeOffset UpdatedAt,
|
||||
string? LastRunId,
|
||||
ImmutableArray<RunSummarySnapshot> RecentRuns,
|
||||
RunSummaryCounters Counters);
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres;
|
||||
|
||||
internal static class CanonicalJsonSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public static string Serialize<T>(T value) => JsonSerializer.Serialize(value, Options);
|
||||
|
||||
public static T? Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json, Options);
|
||||
|
||||
public static JsonSerializerOptions Settings => Options;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
-- Scheduler Schema Migration 003: Runs, Impact Snapshots, Policy Run Jobs
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE scheduler.run_state AS ENUM ('planning','queued','running','completed','error','cancelled');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE scheduler.policy_run_status AS ENUM ('pending','submitted','retrying','failed','completed','cancelled');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scheduler.runs (
|
||||
id TEXT NOT NULL,
|
||||
tenant_id TEXT NOT NULL,
|
||||
schedule_id TEXT,
|
||||
trigger JSONB NOT NULL,
|
||||
state scheduler.run_state NOT NULL,
|
||||
stats JSONB NOT NULL,
|
||||
reason JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
started_at TIMESTAMPTZ,
|
||||
finished_at TIMESTAMPTZ,
|
||||
error TEXT,
|
||||
deltas JSONB NOT NULL,
|
||||
retry_of TEXT,
|
||||
schema_version TEXT,
|
||||
PRIMARY KEY (tenant_id, id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_runs_state ON scheduler.runs(state);
|
||||
CREATE INDEX IF NOT EXISTS idx_runs_schedule ON scheduler.runs(tenant_id, schedule_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_runs_created ON scheduler.runs(created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scheduler.impact_snapshots (
|
||||
snapshot_id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
run_id TEXT,
|
||||
impact JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_impact_snapshots_run ON scheduler.impact_snapshots(run_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scheduler.policy_run_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
policy_id TEXT NOT NULL,
|
||||
policy_version INT,
|
||||
mode TEXT NOT NULL,
|
||||
priority INT NOT NULL,
|
||||
priority_rank INT NOT NULL,
|
||||
run_id TEXT,
|
||||
requested_by TEXT,
|
||||
correlation_id TEXT,
|
||||
metadata JSONB,
|
||||
inputs JSONB NOT NULL,
|
||||
queued_at TIMESTAMPTZ,
|
||||
status scheduler.policy_run_status NOT NULL,
|
||||
attempt_count INT NOT NULL,
|
||||
last_attempt_at TIMESTAMPTZ,
|
||||
last_error TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
available_at TIMESTAMPTZ NOT NULL,
|
||||
submitted_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
lease_owner TEXT,
|
||||
lease_expires_at TIMESTAMPTZ,
|
||||
cancellation_requested BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
cancellation_requested_at TIMESTAMPTZ,
|
||||
cancellation_reason TEXT,
|
||||
cancelled_at TIMESTAMPTZ,
|
||||
schema_version TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_run_jobs_tenant ON scheduler.policy_run_jobs(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_run_jobs_status ON scheduler.policy_run_jobs(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_run_jobs_run ON scheduler.policy_run_jobs(run_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_run_jobs_policy ON scheduler.policy_run_jobs(tenant_id, policy_id);
|
||||
@@ -88,7 +88,10 @@ public sealed class GraphJobRepository : IGraphJobRepository
|
||||
Status = (short?)status,
|
||||
Limit = limit
|
||||
});
|
||||
return rows.Select(r => CanonicalJsonSerializer.Deserialize<GraphBuildJob>(r)).ToArray();
|
||||
return rows
|
||||
.Select(r => CanonicalJsonSerializer.Deserialize<GraphBuildJob>(r))
|
||||
.Where(r => r is not null)!
|
||||
.ToArray()!;
|
||||
}
|
||||
|
||||
public async ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken)
|
||||
@@ -108,7 +111,10 @@ public sealed class GraphJobRepository : IGraphJobRepository
|
||||
Status = (short?)status,
|
||||
Limit = limit
|
||||
});
|
||||
return rows.Select(r => CanonicalJsonSerializer.Deserialize<GraphOverlayJob>(r)).ToArray();
|
||||
return rows
|
||||
.Select(r => CanonicalJsonSerializer.Deserialize<GraphOverlayJob>(r))
|
||||
.Where(r => r is not null)!
|
||||
.ToArray()!;
|
||||
}
|
||||
|
||||
public ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IImpactSnapshotRepository
|
||||
{
|
||||
Task UpsertAsync(ImpactSet snapshot, CancellationToken cancellationToken = default);
|
||||
Task<ImpactSet?> GetBySnapshotIdAsync(string snapshotId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IPolicyRunJobRepository
|
||||
{
|
||||
Task<PolicyRunJob?> GetAsync(string tenantId, string jobId, CancellationToken cancellationToken = default);
|
||||
Task<PolicyRunJob?> GetByRunIdAsync(string tenantId, string runId, CancellationToken cancellationToken = default);
|
||||
Task InsertAsync(PolicyRunJob job, CancellationToken cancellationToken = default);
|
||||
Task<long> CountAsync(string tenantId, PolicyRunMode mode, IReadOnlyCollection<PolicyRunJobStatus> statuses, CancellationToken cancellationToken = default);
|
||||
Task<PolicyRunJob?> LeaseAsync(string leaseOwner, DateTimeOffset now, TimeSpan leaseDuration, int maxAttempts, CancellationToken cancellationToken = default);
|
||||
Task<bool> ReplaceAsync(PolicyRunJob job, string? expectedLeaseOwner = null, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<PolicyRunJob>> ListAsync(string tenantId, string? policyId = null, PolicyRunMode? mode = null, IReadOnlyCollection<PolicyRunJobStatus>? statuses = null, DateTimeOffset? queuedAfter = null, int limit = 50, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IRunRepository
|
||||
{
|
||||
Task InsertAsync(Run run, CancellationToken cancellationToken = default);
|
||||
Task<bool> UpdateAsync(Run run, CancellationToken cancellationToken = default);
|
||||
Task<Run?> GetAsync(string tenantId, string runId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Run>> ListAsync(string tenantId, RunQueryOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Run>> ListByStateAsync(RunState state, int limit = 50, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IScheduleRepository
|
||||
{
|
||||
Task UpsertAsync(Schedule schedule, CancellationToken cancellationToken = default);
|
||||
Task<Schedule?> GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Schedule>> ListAsync(string tenantId, ScheduleQueryOptions? options = null, CancellationToken cancellationToken = default);
|
||||
Task<bool> SoftDeleteAsync(string tenantId, string scheduleId, string deletedBy, DateTimeOffset deletedAt, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Connections;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class ImpactSnapshotRepository : IImpactSnapshotRepository
|
||||
{
|
||||
private readonly SchedulerDataSource _dataSource;
|
||||
private readonly JsonSerializerOptions _serializer = CanonicalJsonSerializer.Settings;
|
||||
|
||||
public ImpactSnapshotRepository(SchedulerDataSource dataSource)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(ImpactSet snapshot, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
var tenantId = snapshot.Selector?.TenantId ?? string.Empty;
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO scheduler.impact_snapshots (snapshot_id, tenant_id, impact, created_at)
|
||||
VALUES (@SnapshotId, @TenantId, @Impact, NOW())
|
||||
ON CONFLICT (snapshot_id) DO UPDATE SET impact = EXCLUDED.impact;
|
||||
""";
|
||||
|
||||
await conn.ExecuteAsync(sql, new
|
||||
{
|
||||
SnapshotId = snapshot.SnapshotId ?? $"impact::{Guid.NewGuid():N}",
|
||||
TenantId = tenantId,
|
||||
Impact = JsonSerializer.Serialize(snapshot, _serializer)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<ImpactSet?> GetBySnapshotIdAsync(string snapshotId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(snapshotId);
|
||||
await using var conn = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
SELECT impact
|
||||
FROM scheduler.impact_snapshots
|
||||
WHERE snapshot_id = @SnapshotId
|
||||
LIMIT 1;
|
||||
""";
|
||||
|
||||
var json = await conn.ExecuteScalarAsync<string?>(sql, new { SnapshotId = snapshotId });
|
||||
return json is null ? null : JsonSerializer.Deserialize<ImpactSet>(json, _serializer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Connections;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class PolicyRunJobRepository : IPolicyRunJobRepository
|
||||
{
|
||||
private readonly SchedulerDataSource _dataSource;
|
||||
private readonly JsonSerializerOptions _serializer = CanonicalJsonSerializer.Settings;
|
||||
|
||||
public PolicyRunJobRepository(SchedulerDataSource dataSource)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
public async Task<PolicyRunJob?> GetAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(jobId);
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
|
||||
const string sql = "SELECT * FROM scheduler.policy_run_jobs WHERE tenant_id = @TenantId AND id = @Id LIMIT 1;";
|
||||
var row = await conn.QuerySingleOrDefaultAsync(sql, new { TenantId = tenantId, Id = jobId });
|
||||
return row is null ? null : Map(row);
|
||||
}
|
||||
|
||||
public async Task<PolicyRunJob?> GetByRunIdAsync(string tenantId, string runId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
|
||||
const string sql = "SELECT * FROM scheduler.policy_run_jobs WHERE tenant_id = @TenantId AND run_id = @RunId LIMIT 1;";
|
||||
var row = await conn.QuerySingleOrDefaultAsync(sql, new { TenantId = tenantId, RunId = runId });
|
||||
return row is null ? null : Map(row);
|
||||
}
|
||||
|
||||
public async Task InsertAsync(PolicyRunJob job, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(job.TenantId, "writer", cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO scheduler.policy_run_jobs (
|
||||
id, tenant_id, policy_id, policy_version, mode, priority, priority_rank, run_id, requested_by, correlation_id,
|
||||
metadata, inputs, queued_at, status, attempt_count, last_attempt_at, last_error,
|
||||
created_at, updated_at, available_at, submitted_at, completed_at, lease_owner, lease_expires_at,
|
||||
cancellation_requested, cancellation_requested_at, cancellation_reason, cancelled_at, schema_version)
|
||||
VALUES (
|
||||
@Id, @TenantId, @PolicyId, @PolicyVersion, @Mode, @Priority, @PriorityRank, @RunId, @RequestedBy, @CorrelationId,
|
||||
@Metadata, @Inputs, @QueuedAt, @Status, @AttemptCount, @LastAttemptAt, @LastError,
|
||||
@CreatedAt, @UpdatedAt, @AvailableAt, @SubmittedAt, @CompletedAt, @LeaseOwner, @LeaseExpiresAt,
|
||||
@CancellationRequested, @CancellationRequestedAt, @CancellationReason, @CancelledAt, @SchemaVersion)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
""";
|
||||
|
||||
await conn.ExecuteAsync(sql, MapParams(job));
|
||||
}
|
||||
|
||||
public async Task<long> CountAsync(string tenantId, PolicyRunMode mode, IReadOnlyCollection<PolicyRunJobStatus> statuses, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
|
||||
const string sql = """
|
||||
SELECT COUNT(*) FROM scheduler.policy_run_jobs
|
||||
WHERE tenant_id = @TenantId AND mode = @Mode AND status = ANY(@Statuses);
|
||||
""";
|
||||
return await conn.ExecuteScalarAsync<long>(sql, new
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Mode = mode.ToString().ToLowerInvariant(),
|
||||
Statuses = statuses.Select(s => s.ToString().ToLowerInvariant()).ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<PolicyRunJob?> LeaseAsync(string leaseOwner, DateTimeOffset now, TimeSpan leaseDuration, int maxAttempts, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(leaseOwner);
|
||||
await using var conn = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
WITH candidate AS (
|
||||
SELECT *
|
||||
FROM scheduler.policy_run_jobs
|
||||
WHERE status IN ('pending','retrying')
|
||||
ORDER BY available_at ASC, priority_rank DESC, created_at ASC
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT 1
|
||||
)
|
||||
UPDATE scheduler.policy_run_jobs j
|
||||
SET lease_owner = @LeaseOwner,
|
||||
lease_expires_at = @LeaseExpires,
|
||||
attempt_count = j.attempt_count + 1,
|
||||
last_attempt_at = @Now,
|
||||
status = CASE WHEN j.status = 'pending' THEN 'submitted' ELSE 'retrying' END,
|
||||
updated_at = @Now
|
||||
FROM candidate c
|
||||
WHERE j.id = c.id
|
||||
AND j.attempt_count < @MaxAttempts
|
||||
RETURNING j.*;
|
||||
""";
|
||||
|
||||
var row = await conn.QuerySingleOrDefaultAsync(sql, new
|
||||
{
|
||||
LeaseOwner = leaseOwner,
|
||||
LeaseExpires = now.Add(leaseDuration),
|
||||
Now = now,
|
||||
MaxAttempts = maxAttempts
|
||||
});
|
||||
|
||||
return row is null ? null : Map(row);
|
||||
}
|
||||
|
||||
public async Task<bool> ReplaceAsync(PolicyRunJob job, string? expectedLeaseOwner = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(job.TenantId, "writer", cancellationToken);
|
||||
|
||||
var matchLease = string.IsNullOrWhiteSpace(expectedLeaseOwner)
|
||||
? ""
|
||||
: "AND lease_owner = @ExpectedLeaseOwner";
|
||||
|
||||
var sql = $"""
|
||||
UPDATE scheduler.policy_run_jobs
|
||||
SET policy_version = @PolicyVersion,
|
||||
status = @Status,
|
||||
attempt_count = @AttemptCount,
|
||||
last_attempt_at = @LastAttemptAt,
|
||||
last_error = @LastError,
|
||||
available_at = @AvailableAt,
|
||||
submitted_at = @SubmittedAt,
|
||||
completed_at = @CompletedAt,
|
||||
lease_owner = @LeaseOwner,
|
||||
lease_expires_at = @LeaseExpiresAt,
|
||||
cancellation_requested = @CancellationRequested,
|
||||
cancellation_requested_at = @CancellationRequestedAt,
|
||||
cancellation_reason = @CancellationReason,
|
||||
cancelled_at = @CancelledAt,
|
||||
updated_at = @UpdatedAt,
|
||||
run_id = @RunId
|
||||
WHERE id = @Id {matchLease};
|
||||
""";
|
||||
|
||||
var affected = await conn.ExecuteAsync(sql, MapParams(job, expectedLeaseOwner));
|
||||
return affected > 0;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PolicyRunJob>> ListAsync(
|
||||
string tenantId,
|
||||
string? policyId = null,
|
||||
PolicyRunMode? mode = null,
|
||||
IReadOnlyCollection<PolicyRunJobStatus>? statuses = null,
|
||||
DateTimeOffset? queuedAfter = null,
|
||||
int limit = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
|
||||
|
||||
var filters = new List<string> { "tenant_id = @TenantId" };
|
||||
if (!string.IsNullOrWhiteSpace(policyId)) filters.Add("policy_id = @PolicyId");
|
||||
if (mode is not null) filters.Add("mode = @Mode");
|
||||
if (statuses is not null && statuses.Count > 0) filters.Add("status = ANY(@Statuses)");
|
||||
if (queuedAfter is not null) filters.Add("queued_at > @QueuedAfter");
|
||||
|
||||
var sql = $"""
|
||||
SELECT *
|
||||
FROM scheduler.policy_run_jobs
|
||||
WHERE {string.Join(" AND ", filters)}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT @Limit;
|
||||
""";
|
||||
|
||||
var rows = await conn.QueryAsync(sql, new
|
||||
{
|
||||
TenantId = tenantId,
|
||||
PolicyId = policyId,
|
||||
Mode = mode?.ToString().ToLowerInvariant(),
|
||||
Statuses = statuses?.Select(s => s.ToString().ToLowerInvariant()).ToArray(),
|
||||
QueuedAfter = queuedAfter,
|
||||
Limit = limit
|
||||
});
|
||||
|
||||
return rows.Select(Map).ToList();
|
||||
}
|
||||
|
||||
private object MapParams(PolicyRunJob job, string? expectedLeaseOwner = null) => new
|
||||
{
|
||||
job.Id,
|
||||
job.TenantId,
|
||||
job.PolicyId,
|
||||
job.PolicyVersion,
|
||||
Mode = job.Mode.ToString().ToLowerInvariant(),
|
||||
Priority = (int)job.Priority,
|
||||
job.PriorityRank,
|
||||
job.RunId,
|
||||
job.RequestedBy,
|
||||
job.CorrelationId,
|
||||
Metadata = job.Metadata is null ? null : JsonSerializer.Serialize(job.Metadata, _serializer),
|
||||
Inputs = JsonSerializer.Serialize(job.Inputs, _serializer),
|
||||
job.QueuedAt,
|
||||
Status = job.Status.ToString().ToLowerInvariant(),
|
||||
job.AttemptCount,
|
||||
job.LastAttemptAt,
|
||||
job.LastError,
|
||||
job.CreatedAt,
|
||||
job.UpdatedAt,
|
||||
job.AvailableAt,
|
||||
job.SubmittedAt,
|
||||
job.CompletedAt,
|
||||
job.LeaseOwner,
|
||||
job.LeaseExpiresAt,
|
||||
job.CancellationRequested,
|
||||
job.CancellationRequestedAt,
|
||||
job.CancellationReason,
|
||||
job.CancelledAt,
|
||||
job.SchemaVersion,
|
||||
ExpectedLeaseOwner = expectedLeaseOwner
|
||||
};
|
||||
|
||||
private PolicyRunJob Map(dynamic row)
|
||||
{
|
||||
var metadata = row.metadata is null
|
||||
? null
|
||||
: JsonSerializer.Deserialize<ImmutableSortedDictionary<string, string>>((string)row.metadata, _serializer);
|
||||
|
||||
var inputs = JsonSerializer.Deserialize<PolicyRunInputs>((string)row.inputs, _serializer)!;
|
||||
|
||||
return new PolicyRunJob(
|
||||
(string?)row.schema_version ?? SchedulerSchemaVersions.PolicyRunJob,
|
||||
(string)row.id,
|
||||
(string)row.tenant_id,
|
||||
(string)row.policy_id,
|
||||
(int?)row.policy_version,
|
||||
Enum.Parse<PolicyRunMode>((string)row.mode, true),
|
||||
(PolicyRunPriority)row.priority,
|
||||
(int)row.priority_rank,
|
||||
(string?)row.run_id,
|
||||
(string?)row.requested_by,
|
||||
(string?)row.correlation_id,
|
||||
metadata,
|
||||
inputs,
|
||||
row.queued_at is null ? null : DateTime.SpecifyKind(row.queued_at, DateTimeKind.Utc),
|
||||
Enum.Parse<PolicyRunJobStatus>((string)row.status, true),
|
||||
(int)row.attempt_count,
|
||||
row.last_attempt_at is null ? null : DateTime.SpecifyKind(row.last_attempt_at, DateTimeKind.Utc),
|
||||
(string?)row.last_error,
|
||||
DateTime.SpecifyKind(row.created_at, DateTimeKind.Utc),
|
||||
DateTime.SpecifyKind(row.updated_at, DateTimeKind.Utc),
|
||||
DateTime.SpecifyKind(row.available_at, DateTimeKind.Utc),
|
||||
row.submitted_at is null ? null : DateTime.SpecifyKind(row.submitted_at, DateTimeKind.Utc),
|
||||
row.completed_at is null ? null : DateTime.SpecifyKind(row.completed_at, DateTimeKind.Utc),
|
||||
(string?)row.lease_owner,
|
||||
row.lease_expires_at is null ? null : DateTime.SpecifyKind(row.lease_expires_at, DateTimeKind.Utc),
|
||||
(bool)row.cancellation_requested,
|
||||
row.cancellation_requested_at is null ? null : DateTime.SpecifyKind(row.cancellation_requested_at, DateTimeKind.Utc),
|
||||
(string?)row.cancellation_reason,
|
||||
row.cancelled_at is null ? null : DateTime.SpecifyKind(row.cancelled_at, DateTimeKind.Utc));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class RunQueryOptions
|
||||
{
|
||||
public string? ScheduleId { get; init; }
|
||||
public ImmutableArray<RunState> States { get; init; } = ImmutableArray<RunState>.Empty;
|
||||
public DateTimeOffset? CreatedAfter { get; init; }
|
||||
public bool SortAscending { get; init; } = false;
|
||||
public int? Limit { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Connections;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class RunRepository : IRunRepository
|
||||
{
|
||||
private readonly SchedulerDataSource _dataSource;
|
||||
private readonly JsonSerializerOptions _serializer = CanonicalJsonSerializer.Settings;
|
||||
|
||||
public RunRepository(SchedulerDataSource dataSource)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
public async Task InsertAsync(Run run, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(run);
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(run.TenantId, "writer", cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO scheduler.runs (
|
||||
id, tenant_id, schedule_id, trigger, state, stats, reason, created_at, started_at, finished_at,
|
||||
error, deltas, retry_of, schema_version)
|
||||
VALUES (@Id, @TenantId, @ScheduleId, @Trigger, @State, @Stats, @Reason, @CreatedAt, @StartedAt, @FinishedAt,
|
||||
@Error, @Deltas, @RetryOf, @SchemaVersion)
|
||||
ON CONFLICT (tenant_id, id) DO NOTHING;
|
||||
""";
|
||||
|
||||
var payload = MapParams(run);
|
||||
await conn.ExecuteAsync(new CommandDefinition(sql, payload, cancellationToken: cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(Run run, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(run);
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(run.TenantId, "writer", cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
UPDATE scheduler.runs
|
||||
SET state = @State,
|
||||
stats = @Stats,
|
||||
reason = @Reason,
|
||||
started_at = @StartedAt,
|
||||
finished_at = @FinishedAt,
|
||||
error = @Error,
|
||||
deltas = @Deltas,
|
||||
retry_of = @RetryOf,
|
||||
schema_version = @SchemaVersion
|
||||
WHERE tenant_id = @TenantId AND id = @Id;
|
||||
""";
|
||||
|
||||
var payload = MapParams(run);
|
||||
var affected = await conn.ExecuteAsync(new CommandDefinition(sql, payload, cancellationToken: cancellationToken));
|
||||
return affected > 0;
|
||||
}
|
||||
|
||||
public async Task<Run?> GetAsync(string tenantId, string runId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(runId);
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
SELECT *
|
||||
FROM scheduler.runs
|
||||
WHERE tenant_id = @TenantId AND id = @RunId
|
||||
LIMIT 1;
|
||||
""";
|
||||
|
||||
var row = await conn.QuerySingleOrDefaultAsync(sql, new { TenantId = tenantId, RunId = runId });
|
||||
return row is null ? null : MapRun(row);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Run>> ListAsync(string tenantId, RunQueryOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
options ??= new RunQueryOptions();
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
|
||||
|
||||
var filters = new List<string> { "tenant_id = @TenantId" };
|
||||
if (!string.IsNullOrWhiteSpace(options.ScheduleId))
|
||||
{
|
||||
filters.Add("schedule_id = @ScheduleId");
|
||||
}
|
||||
|
||||
if (!options.States.IsDefaultOrEmpty)
|
||||
{
|
||||
filters.Add("state = ANY(@States)");
|
||||
}
|
||||
|
||||
if (options.CreatedAfter is { } after)
|
||||
{
|
||||
filters.Add("created_at > @CreatedAfter");
|
||||
}
|
||||
|
||||
var order = options.SortAscending ? "created_at ASC, id ASC" : "created_at DESC, id DESC";
|
||||
var limit = options.Limit.GetValueOrDefault(50);
|
||||
|
||||
var sql = $"""
|
||||
SELECT *
|
||||
FROM scheduler.runs
|
||||
WHERE {string.Join(" AND ", filters)}
|
||||
ORDER BY {order}
|
||||
LIMIT @Limit;
|
||||
""";
|
||||
|
||||
var rows = await conn.QueryAsync(sql, new
|
||||
{
|
||||
TenantId = tenantId,
|
||||
ScheduleId = options.ScheduleId,
|
||||
States = options.States.Select(s => s.ToString().ToLowerInvariant()).ToArray(),
|
||||
CreatedAfter = options.CreatedAfter?.UtcDateTime,
|
||||
Limit = limit
|
||||
});
|
||||
|
||||
return rows.Select(MapRun).ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Run>> ListByStateAsync(RunState state, int limit = 50, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var conn = await _dataSource.OpenSystemConnectionAsync(cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
SELECT *
|
||||
FROM scheduler.runs
|
||||
WHERE state = @State
|
||||
ORDER BY created_at ASC
|
||||
LIMIT @Limit;
|
||||
""";
|
||||
|
||||
var rows = await conn.QueryAsync(sql, new { State = state.ToString().ToLowerInvariant(), Limit = limit });
|
||||
return rows.Select(MapRun).ToList();
|
||||
}
|
||||
|
||||
private object MapParams(Run run) => new
|
||||
{
|
||||
run.Id,
|
||||
run.TenantId,
|
||||
run.ScheduleId,
|
||||
Trigger = Serialize(run.Trigger),
|
||||
State = run.State.ToString().ToLowerInvariant(),
|
||||
Stats = Serialize(run.Stats),
|
||||
Reason = Serialize(run.Reason),
|
||||
run.CreatedAt,
|
||||
run.StartedAt,
|
||||
run.FinishedAt,
|
||||
run.Error,
|
||||
Deltas = Serialize(run.Deltas),
|
||||
run.RetryOf,
|
||||
run.SchemaVersion
|
||||
};
|
||||
|
||||
private Run MapRun(dynamic row)
|
||||
{
|
||||
var trigger = Deserialize<RunTrigger>(row.trigger);
|
||||
var state = Enum.Parse<RunState>(row.state, true);
|
||||
var stats = Deserialize<RunStats>(row.stats);
|
||||
var reason = Deserialize<RunReason>(row.reason);
|
||||
var deltas = Deserialize<IEnumerable<DeltaSummary>>(row.deltas) ?? Enumerable.Empty<DeltaSummary>();
|
||||
|
||||
return new Run(
|
||||
(string)row.id,
|
||||
(string)row.tenant_id,
|
||||
trigger,
|
||||
state,
|
||||
stats,
|
||||
reason,
|
||||
(string?)row.schedule_id,
|
||||
DateTime.SpecifyKind(row.created_at, DateTimeKind.Utc),
|
||||
row.started_at is null ? null : DateTime.SpecifyKind(row.started_at, DateTimeKind.Utc),
|
||||
row.finished_at is null ? null : DateTime.SpecifyKind(row.finished_at, DateTimeKind.Utc),
|
||||
(string?)row.error,
|
||||
deltas,
|
||||
(string?)row.retry_of,
|
||||
(string?)row.schema_version);
|
||||
}
|
||||
|
||||
private string Serialize<T>(T value) =>
|
||||
JsonSerializer.Serialize(value, _serializer);
|
||||
|
||||
private T? Deserialize<T>(string json) =>
|
||||
JsonSerializer.Deserialize<T>(json, _serializer);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class RunSummaryService : IRunSummaryService
|
||||
{
|
||||
private readonly ConcurrentDictionary<(string TenantId, string ScheduleId), RunSummaryProjection> _projections = new();
|
||||
|
||||
public Task<RunSummaryProjection> ProjectAsync(Run run, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(run);
|
||||
var scheduleId = run.ScheduleId ?? string.Empty;
|
||||
var updatedAt = run.FinishedAt ?? run.StartedAt ?? run.CreatedAt;
|
||||
|
||||
var counters = new RunSummaryCounters(
|
||||
Total: 1,
|
||||
Planning: run.State == RunState.Planning ? 1 : 0,
|
||||
Queued: run.State == RunState.Queued ? 1 : 0,
|
||||
Running: run.State == RunState.Running ? 1 : 0,
|
||||
Completed: run.State == RunState.Completed ? 1 : 0,
|
||||
Error: run.State == RunState.Error ? 1 : 0,
|
||||
Cancelled: run.State == RunState.Cancelled ? 1 : 0,
|
||||
TotalDeltas: run.Stats.Deltas,
|
||||
TotalNewCriticals: run.Stats.NewCriticals,
|
||||
TotalNewHigh: run.Stats.NewHigh,
|
||||
TotalNewMedium: run.Stats.NewMedium,
|
||||
TotalNewLow: run.Stats.NewLow);
|
||||
|
||||
var projection = new RunSummaryProjection(
|
||||
run.TenantId,
|
||||
scheduleId,
|
||||
updatedAt,
|
||||
run.Id,
|
||||
ImmutableArray<RunSummarySnapshot>.Empty,
|
||||
counters);
|
||||
|
||||
_projections[(run.TenantId, scheduleId)] = projection;
|
||||
return Task.FromResult(projection);
|
||||
}
|
||||
|
||||
public Task<RunSummaryProjection?> GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_projections.TryGetValue((tenantId, scheduleId), out var projection);
|
||||
return Task.FromResult<RunSummaryProjection?>(projection);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<RunSummaryProjection>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var results = _projections.Values
|
||||
.Where(p => string.Equals(p.TenantId, tenantId, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<RunSummaryProjection>>(results);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class ScheduleQueryOptions
|
||||
{
|
||||
public bool IncludeDisabled { get; init; } = false;
|
||||
public int? Limit { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Connections;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class ScheduleRepository : IScheduleRepository
|
||||
{
|
||||
private readonly SchedulerDataSource _dataSource;
|
||||
private readonly JsonSerializerOptions _serializer = CanonicalJsonSerializer.Settings;
|
||||
|
||||
public ScheduleRepository(SchedulerDataSource dataSource)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(Schedule schedule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(schedule);
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(schedule.TenantId, "writer", cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO scheduler.schedules (
|
||||
id, tenant_id, name, description, enabled, cron_expression, timezone, mode,
|
||||
selection, only_if, notify, limits, subscribers, created_at, created_by,
|
||||
updated_at, updated_by, deleted_at, deleted_by, schema_version)
|
||||
VALUES (
|
||||
@Id, @TenantId, @Name, @Description, @Enabled, @CronExpression, @Timezone, @Mode,
|
||||
@Selection, @OnlyIf, @Notify, @Limits, @Subscribers, @CreatedAt, @CreatedBy,
|
||||
@UpdatedAt, @UpdatedBy, NULL, NULL, @SchemaVersion)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
description = EXCLUDED.description,
|
||||
enabled = EXCLUDED.enabled,
|
||||
cron_expression = EXCLUDED.cron_expression,
|
||||
timezone = EXCLUDED.timezone,
|
||||
mode = EXCLUDED.mode,
|
||||
selection = EXCLUDED.selection,
|
||||
only_if = EXCLUDED.only_if,
|
||||
notify = EXCLUDED.notify,
|
||||
limits = EXCLUDED.limits,
|
||||
subscribers = EXCLUDED.subscribers,
|
||||
updated_at = EXCLUDED.updated_at,
|
||||
updated_by = EXCLUDED.updated_by,
|
||||
schema_version = EXCLUDED.schema_version,
|
||||
deleted_at = NULL,
|
||||
deleted_by = NULL;
|
||||
""";
|
||||
|
||||
await conn.ExecuteAsync(sql, new
|
||||
{
|
||||
schedule.Id,
|
||||
schedule.TenantId,
|
||||
schedule.Name,
|
||||
Description = (string?)null,
|
||||
schedule.Enabled,
|
||||
schedule.CronExpression,
|
||||
schedule.Timezone,
|
||||
Mode = schedule.Mode.ToString().ToLowerInvariant(),
|
||||
Selection = JsonSerializer.Serialize(schedule.Selection, _serializer),
|
||||
OnlyIf = JsonSerializer.Serialize(schedule.OnlyIf, _serializer),
|
||||
Notify = JsonSerializer.Serialize(schedule.Notify, _serializer),
|
||||
Limits = JsonSerializer.Serialize(schedule.Limits, _serializer),
|
||||
Subscribers = schedule.Subscribers.ToArray(),
|
||||
schedule.CreatedAt,
|
||||
schedule.CreatedBy,
|
||||
schedule.UpdatedAt,
|
||||
schedule.UpdatedBy,
|
||||
schedule.SchemaVersion
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<Schedule?> GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(scheduleId);
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
SELECT *
|
||||
FROM scheduler.schedules
|
||||
WHERE tenant_id = @TenantId AND id = @ScheduleId AND deleted_at IS NULL
|
||||
LIMIT 1;
|
||||
""";
|
||||
|
||||
var row = await conn.QuerySingleOrDefaultAsync(sql, new { TenantId = tenantId, ScheduleId = scheduleId });
|
||||
return row is null ? null : Map(row);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Schedule>> ListAsync(string tenantId, ScheduleQueryOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
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 limit = options.Limit.GetValueOrDefault(200);
|
||||
|
||||
var sql = $"""
|
||||
SELECT *
|
||||
FROM scheduler.schedules
|
||||
WHERE {where}
|
||||
ORDER BY name ASC
|
||||
LIMIT @Limit;
|
||||
""";
|
||||
|
||||
var rows = await conn.QueryAsync(sql, new { TenantId = tenantId, Limit = limit });
|
||||
return rows.Select(Map).ToList();
|
||||
}
|
||||
|
||||
public async Task<bool> SoftDeleteAsync(string tenantId, string scheduleId, string deletedBy, DateTimeOffset deletedAt, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
UPDATE scheduler.schedules
|
||||
SET deleted_at = @DeletedAt, deleted_by = @DeletedBy
|
||||
WHERE tenant_id = @TenantId AND id = @ScheduleId AND deleted_at IS NULL;
|
||||
""";
|
||||
|
||||
var affected = await conn.ExecuteAsync(sql, new
|
||||
{
|
||||
TenantId = tenantId,
|
||||
ScheduleId = scheduleId,
|
||||
DeletedBy = deletedBy,
|
||||
DeletedAt = deletedAt
|
||||
});
|
||||
return affected > 0;
|
||||
}
|
||||
|
||||
private Schedule Map(dynamic row)
|
||||
{
|
||||
return new Schedule(
|
||||
(string)row.id,
|
||||
(string)row.tenant_id,
|
||||
(string)row.name,
|
||||
(bool)row.enabled,
|
||||
(string)row.cron_expression,
|
||||
(string)row.timezone,
|
||||
Enum.Parse<ScheduleMode>((string)row.mode, true),
|
||||
JsonSerializer.Deserialize<Selector>((string)row.selection, _serializer)!,
|
||||
JsonSerializer.Deserialize<ScheduleOnlyIf>((string)row.only_if, _serializer)!,
|
||||
JsonSerializer.Deserialize<ScheduleNotify>((string)row.notify, _serializer)!,
|
||||
JsonSerializer.Deserialize<ScheduleLimits>((string)row.limits, _serializer)!,
|
||||
JsonSerializer.Deserialize<System.Collections.Immutable.ImmutableArray<string>>((string)row.subscribers, _serializer),
|
||||
DateTime.SpecifyKind(row.created_at, DateTimeKind.Utc),
|
||||
(string)row.created_by,
|
||||
DateTime.SpecifyKind(row.updated_at, DateTimeKind.Utc),
|
||||
(string)row.updated_by,
|
||||
(string?)row.schema_version);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Infrastructure.Postgres;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres;
|
||||
@@ -34,6 +35,11 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IJobHistoryRepository, JobHistoryRepository>();
|
||||
services.AddScoped<IMetricsRepository, MetricsRepository>();
|
||||
services.AddScoped<IGraphJobRepository, GraphJobRepository>();
|
||||
services.AddScoped<IRunRepository, RunRepository>();
|
||||
services.AddScoped<IScheduleRepository, ScheduleRepository>();
|
||||
services.AddScoped<IImpactSnapshotRepository, ImpactSnapshotRepository>();
|
||||
services.AddScoped<IPolicyRunJobRepository, PolicyRunJobRepository>();
|
||||
services.AddSingleton<IRunSummaryService, RunSummaryService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
using StellaOps.Scheduler.Worker.Events;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
|
||||
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
|
||||
namespace StellaOps.Scheduler.Worker.Graph;
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Graph.Cartographer;
|
||||
using StellaOps.Scheduler.Worker.Graph.Scheduler;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
|
||||
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
|
||||
namespace StellaOps.Scheduler.Worker.Graph;
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Graph.Cartographer;
|
||||
using StellaOps.Scheduler.Worker.Graph.Scheduler;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
|
||||
namespace StellaOps.Scheduler.Worker.Planning;
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
|
||||
namespace StellaOps.Scheduler.Worker.Policy;
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.WebService.GraphJobs;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Repositories;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Repositories;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Repositories;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Repositories;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Services;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Repositories.Tests.Services;
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.WebService.PolicySimulations;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Graph;
|
||||
using StellaOps.Scheduler.Worker.Graph.Cartographer;
|
||||
using StellaOps.Scheduler.Worker.Graph.Scheduler;
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Graph;
|
||||
using StellaOps.Scheduler.Worker.Graph.Cartographer;
|
||||
using StellaOps.Scheduler.Worker.Graph.Scheduler;
|
||||
|
||||
@@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Projections;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Projections;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Planning;
|
||||
|
||||
@@ -6,7 +6,7 @@ using MongoDB.Driver;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Policy;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
|
||||
@@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
using StellaOps.Scheduler.Worker.Policy;
|
||||
|
||||
@@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Projections;
|
||||
using StellaOps.Scheduler.Worker.Events;
|
||||
|
||||
Reference in New Issue
Block a user