feat: Add Go module and workspace test fixtures
- Created expected JSON files for Go modules and workspaces. - Added go.mod and go.sum files for example projects. - Implemented private module structure with expected JSON output. - Introduced vendored dependencies with corresponding expected JSON. - Developed PostgresGraphJobStore for managing graph jobs. - Established SQL migration scripts for graph jobs schema. - Implemented GraphJobRepository for CRUD operations on graph jobs. - Created IGraphJobRepository interface for repository abstraction. - Added unit tests for GraphJobRepository to ensure functionality.
This commit is contained in:
@@ -1,244 +1,244 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Graph;
|
||||
using StellaOps.Scheduler.Worker.Graph.Cartographer;
|
||||
using StellaOps.Scheduler.Worker.Graph.Scheduler;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class GraphBuildExecutionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenGraphDisabled()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerBuildClient();
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = false
|
||||
}
|
||||
});
|
||||
var service = new GraphBuildExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphBuildExecutionService>.Instance);
|
||||
|
||||
var job = CreateGraphJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphBuildExecutionResultType.Skipped, result.Type);
|
||||
Assert.Equal("graph_processing_disabled", result.Reason);
|
||||
Assert.Equal(0, repository.ReplaceCalls);
|
||||
Assert.Equal(0, cartographer.CallCount);
|
||||
Assert.Empty(completion.Notifications);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_CompletesJob_OnSuccess()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerBuildClient
|
||||
{
|
||||
Result = new CartographerBuildResult(
|
||||
GraphJobStatus.Completed,
|
||||
CartographerJobId: "carto-1",
|
||||
GraphSnapshotId: "graph_snap",
|
||||
ResultUri: "oras://graph/result",
|
||||
Error: null)
|
||||
};
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true,
|
||||
MaxAttempts = 2,
|
||||
RetryBackoff = TimeSpan.FromMilliseconds(10)
|
||||
}
|
||||
});
|
||||
var service = new GraphBuildExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphBuildExecutionService>.Instance);
|
||||
|
||||
var job = CreateGraphJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphBuildExecutionResultType.Completed, result.Type);
|
||||
Assert.Single(completion.Notifications);
|
||||
var notification = completion.Notifications[0];
|
||||
Assert.Equal(job.Id, notification.JobId);
|
||||
Assert.Equal("Build", notification.JobType);
|
||||
Assert.Equal(GraphJobStatus.Completed, notification.Status);
|
||||
Assert.Equal("oras://graph/result", notification.ResultUri);
|
||||
Assert.Equal("graph_snap", notification.GraphSnapshotId);
|
||||
Assert.Null(notification.Error);
|
||||
Assert.Equal(1, cartographer.CallCount);
|
||||
Assert.True(repository.ReplaceCalls >= 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Fails_AfterMaxAttempts()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerBuildClient
|
||||
{
|
||||
ExceptionToThrow = new InvalidOperationException("network")
|
||||
};
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true,
|
||||
MaxAttempts = 2,
|
||||
RetryBackoff = TimeSpan.FromMilliseconds(1)
|
||||
}
|
||||
});
|
||||
var service = new GraphBuildExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphBuildExecutionService>.Instance);
|
||||
|
||||
var job = CreateGraphJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphBuildExecutionResultType.Failed, result.Type);
|
||||
Assert.Equal(2, cartographer.CallCount);
|
||||
Assert.Single(completion.Notifications);
|
||||
Assert.Equal(GraphJobStatus.Failed, completion.Notifications[0].Status);
|
||||
Assert.Equal("network", completion.Notifications[0].Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenConcurrencyConflict()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository
|
||||
{
|
||||
ShouldReplaceSucceed = false
|
||||
};
|
||||
var cartographer = new StubCartographerBuildClient();
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true
|
||||
}
|
||||
});
|
||||
var service = new GraphBuildExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphBuildExecutionService>.Instance);
|
||||
|
||||
var job = CreateGraphJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphBuildExecutionResultType.Skipped, result.Type);
|
||||
Assert.Equal("concurrency_conflict", result.Reason);
|
||||
Assert.Equal(0, cartographer.CallCount);
|
||||
Assert.Empty(completion.Notifications);
|
||||
}
|
||||
|
||||
private static GraphBuildJob CreateGraphJob() => new(
|
||||
id: "gbj_1",
|
||||
tenantId: "tenant-alpha",
|
||||
sbomId: "sbom-1",
|
||||
sbomVersionId: "sbom-1-v1",
|
||||
sbomDigest: "sha256:" + new string('a', 64),
|
||||
status: GraphJobStatus.Pending,
|
||||
trigger: GraphBuildJobTrigger.SbomVersion,
|
||||
createdAt: DateTimeOffset.UtcNow,
|
||||
attempts: 0,
|
||||
metadata: Array.Empty<KeyValuePair<string, string>>());
|
||||
|
||||
private sealed class RecordingGraphJobRepository : IGraphJobRepository
|
||||
{
|
||||
public int ReplaceCalls { get; private set; }
|
||||
|
||||
public bool ShouldReplaceSucceed { get; set; } = true;
|
||||
|
||||
public Task<bool> TryReplaceAsync(GraphBuildJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!ShouldReplaceSucceed)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
ReplaceCalls++;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<GraphBuildJob> ReplaceAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphOverlayJob> ReplaceAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task InsertAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task InsertAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphBuildJob?> GetBuildJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphOverlayJob?> GetOverlayJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<bool> TryReplaceOverlayAsync(GraphOverlayJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private sealed class StubCartographerBuildClient : ICartographerBuildClient
|
||||
{
|
||||
public CartographerBuildResult Result { get; set; } = new(GraphJobStatus.Completed, null, null, null, null);
|
||||
|
||||
public Exception? ExceptionToThrow { get; set; }
|
||||
|
||||
public int CallCount { get; private set; }
|
||||
|
||||
public Task<CartographerBuildResult> StartBuildAsync(GraphBuildJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
CallCount++;
|
||||
|
||||
if (ExceptionToThrow is not null)
|
||||
{
|
||||
throw ExceptionToThrow;
|
||||
}
|
||||
|
||||
return Task.FromResult(Result);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingCompletionClient : IGraphJobCompletionClient
|
||||
{
|
||||
public List<GraphJobCompletionRequestDto> Notifications { get; } = new();
|
||||
|
||||
public Task NotifyAsync(GraphJobCompletionRequestDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
Notifications.Add(request);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class GraphBuildExecutionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenGraphDisabled()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerBuildClient();
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = false
|
||||
}
|
||||
});
|
||||
var service = new GraphBuildExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphBuildExecutionService>.Instance);
|
||||
|
||||
var job = CreateGraphJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphBuildExecutionResultType.Skipped, result.Type);
|
||||
Assert.Equal("graph_processing_disabled", result.Reason);
|
||||
Assert.Equal(0, repository.ReplaceCalls);
|
||||
Assert.Equal(0, cartographer.CallCount);
|
||||
Assert.Empty(completion.Notifications);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_CompletesJob_OnSuccess()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerBuildClient
|
||||
{
|
||||
Result = new CartographerBuildResult(
|
||||
GraphJobStatus.Completed,
|
||||
CartographerJobId: "carto-1",
|
||||
GraphSnapshotId: "graph_snap",
|
||||
ResultUri: "oras://graph/result",
|
||||
Error: null)
|
||||
};
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true,
|
||||
MaxAttempts = 2,
|
||||
RetryBackoff = TimeSpan.FromMilliseconds(10)
|
||||
}
|
||||
});
|
||||
var service = new GraphBuildExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphBuildExecutionService>.Instance);
|
||||
|
||||
var job = CreateGraphJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphBuildExecutionResultType.Completed, result.Type);
|
||||
Assert.Single(completion.Notifications);
|
||||
var notification = completion.Notifications[0];
|
||||
Assert.Equal(job.Id, notification.JobId);
|
||||
Assert.Equal("Build", notification.JobType);
|
||||
Assert.Equal(GraphJobStatus.Completed, notification.Status);
|
||||
Assert.Equal("oras://graph/result", notification.ResultUri);
|
||||
Assert.Equal("graph_snap", notification.GraphSnapshotId);
|
||||
Assert.Null(notification.Error);
|
||||
Assert.Equal(1, cartographer.CallCount);
|
||||
Assert.True(repository.ReplaceCalls >= 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Fails_AfterMaxAttempts()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerBuildClient
|
||||
{
|
||||
ExceptionToThrow = new InvalidOperationException("network")
|
||||
};
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true,
|
||||
MaxAttempts = 2,
|
||||
RetryBackoff = TimeSpan.FromMilliseconds(1)
|
||||
}
|
||||
});
|
||||
var service = new GraphBuildExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphBuildExecutionService>.Instance);
|
||||
|
||||
var job = CreateGraphJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphBuildExecutionResultType.Failed, result.Type);
|
||||
Assert.Equal(2, cartographer.CallCount);
|
||||
Assert.Single(completion.Notifications);
|
||||
Assert.Equal(GraphJobStatus.Failed, completion.Notifications[0].Status);
|
||||
Assert.Equal("network", completion.Notifications[0].Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenConcurrencyConflict()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository
|
||||
{
|
||||
ShouldReplaceSucceed = false
|
||||
};
|
||||
var cartographer = new StubCartographerBuildClient();
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true
|
||||
}
|
||||
});
|
||||
var service = new GraphBuildExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphBuildExecutionService>.Instance);
|
||||
|
||||
var job = CreateGraphJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphBuildExecutionResultType.Skipped, result.Type);
|
||||
Assert.Equal("concurrency_conflict", result.Reason);
|
||||
Assert.Equal(0, cartographer.CallCount);
|
||||
Assert.Empty(completion.Notifications);
|
||||
}
|
||||
|
||||
private static GraphBuildJob CreateGraphJob() => new(
|
||||
id: "gbj_1",
|
||||
tenantId: "tenant-alpha",
|
||||
sbomId: "sbom-1",
|
||||
sbomVersionId: "sbom-1-v1",
|
||||
sbomDigest: "sha256:" + new string('a', 64),
|
||||
status: GraphJobStatus.Pending,
|
||||
trigger: GraphBuildJobTrigger.SbomVersion,
|
||||
createdAt: DateTimeOffset.UtcNow,
|
||||
attempts: 0,
|
||||
metadata: Array.Empty<KeyValuePair<string, string>>());
|
||||
|
||||
private sealed class RecordingGraphJobRepository : IGraphJobRepository
|
||||
{
|
||||
public int ReplaceCalls { get; private set; }
|
||||
|
||||
public bool ShouldReplaceSucceed { get; set; } = true;
|
||||
|
||||
public Task<bool> TryReplaceAsync(GraphBuildJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!ShouldReplaceSucceed)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
ReplaceCalls++;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<GraphBuildJob> ReplaceAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphOverlayJob> ReplaceAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task InsertAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task InsertAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphBuildJob?> GetBuildJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphOverlayJob?> GetOverlayJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<bool> TryReplaceOverlayAsync(GraphOverlayJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private sealed class StubCartographerBuildClient : ICartographerBuildClient
|
||||
{
|
||||
public CartographerBuildResult Result { get; set; } = new(GraphJobStatus.Completed, null, null, null, null);
|
||||
|
||||
public Exception? ExceptionToThrow { get; set; }
|
||||
|
||||
public int CallCount { get; private set; }
|
||||
|
||||
public Task<CartographerBuildResult> StartBuildAsync(GraphBuildJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
CallCount++;
|
||||
|
||||
if (ExceptionToThrow is not null)
|
||||
{
|
||||
throw ExceptionToThrow;
|
||||
}
|
||||
|
||||
return Task.FromResult(Result);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingCompletionClient : IGraphJobCompletionClient
|
||||
{
|
||||
public List<GraphJobCompletionRequestDto> Notifications { get; } = new();
|
||||
|
||||
public Task NotifyAsync(GraphJobCompletionRequestDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
Notifications.Add(request);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,238 +1,238 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
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.Worker.Graph;
|
||||
using StellaOps.Scheduler.Worker.Graph.Cartographer;
|
||||
using StellaOps.Scheduler.Worker.Graph.Scheduler;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class GraphOverlayExecutionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenGraphDisabled()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerOverlayClient();
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = false
|
||||
}
|
||||
});
|
||||
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
|
||||
|
||||
var job = CreateOverlayJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphOverlayExecutionResultType.Skipped, result.Type);
|
||||
Assert.Equal("graph_processing_disabled", result.Reason);
|
||||
Assert.Empty(completion.Notifications);
|
||||
Assert.Equal(0, cartographer.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_CompletesJob_OnSuccess()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerOverlayClient
|
||||
{
|
||||
Result = new CartographerOverlayResult(
|
||||
GraphJobStatus.Completed,
|
||||
GraphSnapshotId: "graph_snap_2",
|
||||
ResultUri: "oras://graph/overlay",
|
||||
Error: null)
|
||||
};
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true,
|
||||
MaxAttempts = 2,
|
||||
RetryBackoff = TimeSpan.FromMilliseconds(5)
|
||||
}
|
||||
});
|
||||
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
|
||||
|
||||
var job = CreateOverlayJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphOverlayExecutionResultType.Completed, result.Type);
|
||||
Assert.Single(completion.Notifications);
|
||||
var notification = completion.Notifications[0];
|
||||
Assert.Equal("Overlay", notification.JobType);
|
||||
Assert.Equal(GraphJobStatus.Completed, notification.Status);
|
||||
Assert.Equal("oras://graph/overlay", notification.ResultUri);
|
||||
Assert.Equal("graph_snap_2", notification.GraphSnapshotId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Fails_AfterRetries()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerOverlayClient
|
||||
{
|
||||
ExceptionToThrow = new InvalidOperationException("overlay failed")
|
||||
};
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true,
|
||||
MaxAttempts = 2,
|
||||
RetryBackoff = TimeSpan.FromMilliseconds(1)
|
||||
}
|
||||
});
|
||||
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
|
||||
|
||||
var job = CreateOverlayJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphOverlayExecutionResultType.Failed, result.Type);
|
||||
Assert.Single(completion.Notifications);
|
||||
Assert.Equal(GraphJobStatus.Failed, completion.Notifications[0].Status);
|
||||
Assert.Equal("overlay failed", completion.Notifications[0].Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenConcurrencyConflict()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository
|
||||
{
|
||||
ShouldReplaceSucceed = false
|
||||
};
|
||||
var cartographer = new StubCartographerOverlayClient();
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true
|
||||
}
|
||||
});
|
||||
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
|
||||
|
||||
var job = CreateOverlayJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphOverlayExecutionResultType.Skipped, result.Type);
|
||||
Assert.Equal("concurrency_conflict", result.Reason);
|
||||
Assert.Empty(completion.Notifications);
|
||||
Assert.Equal(0, cartographer.CallCount);
|
||||
}
|
||||
|
||||
private static GraphOverlayJob CreateOverlayJob() => new(
|
||||
id: "goj_1",
|
||||
tenantId: "tenant-alpha",
|
||||
graphSnapshotId: "snap-1",
|
||||
overlayKind: GraphOverlayKind.Policy,
|
||||
overlayKey: "policy@1",
|
||||
status: GraphJobStatus.Pending,
|
||||
trigger: GraphOverlayJobTrigger.Policy,
|
||||
createdAt: DateTimeOffset.UtcNow,
|
||||
subjects: Array.Empty<string>(),
|
||||
attempts: 0,
|
||||
metadata: Array.Empty<KeyValuePair<string, string>>());
|
||||
|
||||
private sealed class RecordingGraphJobRepository : IGraphJobRepository
|
||||
{
|
||||
public bool ShouldReplaceSucceed { get; set; } = true;
|
||||
|
||||
public int RunningReplacements { get; private set; }
|
||||
|
||||
public Task<bool> TryReplaceOverlayAsync(GraphOverlayJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!ShouldReplaceSucceed)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
RunningReplacements++;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<bool> TryReplaceAsync(GraphBuildJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphBuildJob> ReplaceAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphOverlayJob> ReplaceAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task InsertAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task InsertAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphBuildJob?> GetBuildJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphOverlayJob?> GetOverlayJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private sealed class StubCartographerOverlayClient : ICartographerOverlayClient
|
||||
{
|
||||
public CartographerOverlayResult Result { get; set; } = new(GraphJobStatus.Completed, null, null, null);
|
||||
|
||||
public Exception? ExceptionToThrow { get; set; }
|
||||
|
||||
public int CallCount { get; private set; }
|
||||
|
||||
public Task<CartographerOverlayResult> StartOverlayAsync(GraphOverlayJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
CallCount++;
|
||||
|
||||
if (ExceptionToThrow is not null)
|
||||
{
|
||||
throw ExceptionToThrow;
|
||||
}
|
||||
|
||||
return Task.FromResult(Result);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingCompletionClient : IGraphJobCompletionClient
|
||||
{
|
||||
public List<GraphJobCompletionRequestDto> Notifications { get; } = new();
|
||||
|
||||
public Task NotifyAsync(GraphJobCompletionRequestDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
Notifications.Add(request);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class GraphOverlayExecutionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenGraphDisabled()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerOverlayClient();
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = false
|
||||
}
|
||||
});
|
||||
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
|
||||
|
||||
var job = CreateOverlayJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphOverlayExecutionResultType.Skipped, result.Type);
|
||||
Assert.Equal("graph_processing_disabled", result.Reason);
|
||||
Assert.Empty(completion.Notifications);
|
||||
Assert.Equal(0, cartographer.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_CompletesJob_OnSuccess()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerOverlayClient
|
||||
{
|
||||
Result = new CartographerOverlayResult(
|
||||
GraphJobStatus.Completed,
|
||||
GraphSnapshotId: "graph_snap_2",
|
||||
ResultUri: "oras://graph/overlay",
|
||||
Error: null)
|
||||
};
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true,
|
||||
MaxAttempts = 2,
|
||||
RetryBackoff = TimeSpan.FromMilliseconds(5)
|
||||
}
|
||||
});
|
||||
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
|
||||
|
||||
var job = CreateOverlayJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphOverlayExecutionResultType.Completed, result.Type);
|
||||
Assert.Single(completion.Notifications);
|
||||
var notification = completion.Notifications[0];
|
||||
Assert.Equal("Overlay", notification.JobType);
|
||||
Assert.Equal(GraphJobStatus.Completed, notification.Status);
|
||||
Assert.Equal("oras://graph/overlay", notification.ResultUri);
|
||||
Assert.Equal("graph_snap_2", notification.GraphSnapshotId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Fails_AfterRetries()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
var cartographer = new StubCartographerOverlayClient
|
||||
{
|
||||
ExceptionToThrow = new InvalidOperationException("overlay failed")
|
||||
};
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true,
|
||||
MaxAttempts = 2,
|
||||
RetryBackoff = TimeSpan.FromMilliseconds(1)
|
||||
}
|
||||
});
|
||||
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
|
||||
|
||||
var job = CreateOverlayJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphOverlayExecutionResultType.Failed, result.Type);
|
||||
Assert.Single(completion.Notifications);
|
||||
Assert.Equal(GraphJobStatus.Failed, completion.Notifications[0].Status);
|
||||
Assert.Equal("overlay failed", completion.Notifications[0].Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenConcurrencyConflict()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository
|
||||
{
|
||||
ShouldReplaceSucceed = false
|
||||
};
|
||||
var cartographer = new StubCartographerOverlayClient();
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
{
|
||||
Enabled = true
|
||||
}
|
||||
});
|
||||
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
|
||||
|
||||
var job = CreateOverlayJob();
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(GraphOverlayExecutionResultType.Skipped, result.Type);
|
||||
Assert.Equal("concurrency_conflict", result.Reason);
|
||||
Assert.Empty(completion.Notifications);
|
||||
Assert.Equal(0, cartographer.CallCount);
|
||||
}
|
||||
|
||||
private static GraphOverlayJob CreateOverlayJob() => new(
|
||||
id: "goj_1",
|
||||
tenantId: "tenant-alpha",
|
||||
graphSnapshotId: "snap-1",
|
||||
overlayKind: GraphOverlayKind.Policy,
|
||||
overlayKey: "policy@1",
|
||||
status: GraphJobStatus.Pending,
|
||||
trigger: GraphOverlayJobTrigger.Policy,
|
||||
createdAt: DateTimeOffset.UtcNow,
|
||||
subjects: Array.Empty<string>(),
|
||||
attempts: 0,
|
||||
metadata: Array.Empty<KeyValuePair<string, string>>());
|
||||
|
||||
private sealed class RecordingGraphJobRepository : IGraphJobRepository
|
||||
{
|
||||
public bool ShouldReplaceSucceed { get; set; } = true;
|
||||
|
||||
public int RunningReplacements { get; private set; }
|
||||
|
||||
public Task<bool> TryReplaceOverlayAsync(GraphOverlayJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!ShouldReplaceSucceed)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
RunningReplacements++;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<bool> TryReplaceAsync(GraphBuildJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphBuildJob> ReplaceAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphOverlayJob> ReplaceAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task InsertAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task InsertAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphBuildJob?> GetBuildJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GraphOverlayJob?> GetOverlayJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private sealed class StubCartographerOverlayClient : ICartographerOverlayClient
|
||||
{
|
||||
public CartographerOverlayResult Result { get; set; } = new(GraphJobStatus.Completed, null, null, null);
|
||||
|
||||
public Exception? ExceptionToThrow { get; set; }
|
||||
|
||||
public int CallCount { get; private set; }
|
||||
|
||||
public Task<CartographerOverlayResult> StartOverlayAsync(GraphOverlayJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
CallCount++;
|
||||
|
||||
if (ExceptionToThrow is not null)
|
||||
{
|
||||
throw ExceptionToThrow;
|
||||
}
|
||||
|
||||
return Task.FromResult(Result);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingCompletionClient : IGraphJobCompletionClient
|
||||
{
|
||||
public List<GraphJobCompletionRequestDto> Notifications { get; } = new();
|
||||
|
||||
public Task NotifyAsync(GraphJobCompletionRequestDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
Notifications.Add(request);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Projections;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Services;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Projections;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
using StellaOps.Scheduler.Worker.Planning;
|
||||
|
||||
@@ -5,9 +5,9 @@ using MongoDB.Driver;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Projections;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Services;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Projections;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Planning;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
|
||||
@@ -6,7 +6,7 @@ using MongoDB.Driver;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Policy;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
|
||||
@@ -1,80 +1,80 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
using StellaOps.Scheduler.Worker.Policy;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class PolicyRunExecutionServiceTests
|
||||
{
|
||||
private static readonly SchedulerWorkerOptions WorkerOptions = new()
|
||||
{
|
||||
Policy =
|
||||
{
|
||||
Dispatch =
|
||||
{
|
||||
LeaseOwner = "test-dispatch",
|
||||
BatchSize = 1,
|
||||
LeaseDuration = TimeSpan.FromMinutes(1),
|
||||
IdleDelay = TimeSpan.FromMilliseconds(10),
|
||||
MaxAttempts = 2,
|
||||
RetryBackoff = TimeSpan.FromSeconds(30)
|
||||
},
|
||||
Api =
|
||||
{
|
||||
BaseAddress = new Uri("https://policy.example.com"),
|
||||
RunsPath = "/api/policy/policies/{policyId}/runs",
|
||||
SimulatePath = "/api/policy/policies/{policyId}/simulate"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_CancelsJob_WhenCancellationRequested()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
var client = new StubPolicyRunClient();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
|
||||
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
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.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
using StellaOps.Scheduler.Worker.Policy;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class PolicyRunExecutionServiceTests
|
||||
{
|
||||
private static readonly SchedulerWorkerOptions WorkerOptions = new()
|
||||
{
|
||||
Policy =
|
||||
{
|
||||
Dispatch =
|
||||
{
|
||||
LeaseOwner = "test-dispatch",
|
||||
BatchSize = 1,
|
||||
LeaseDuration = TimeSpan.FromMinutes(1),
|
||||
IdleDelay = TimeSpan.FromMilliseconds(10),
|
||||
MaxAttempts = 2,
|
||||
RetryBackoff = TimeSpan.FromSeconds(30)
|
||||
},
|
||||
Api =
|
||||
{
|
||||
BaseAddress = new Uri("https://policy.example.com"),
|
||||
RunsPath = "/api/policy/policies/{policyId}/runs",
|
||||
SimulatePath = "/api/policy/policies/{policyId}/simulate"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_CancelsJob_WhenCancellationRequested()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
var client = new StubPolicyRunClient();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
|
||||
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var targeting = new StubPolicyRunTargetingService
|
||||
{
|
||||
OnEnsureTargets = job => PolicyRunTargetingResult.Unchanged(job)
|
||||
};
|
||||
var webhook = new RecordingPolicySimulationWebhookClient();
|
||||
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, webhook, NullLogger<PolicyRunExecutionService>.Instance);
|
||||
|
||||
var job = CreateJob(status: PolicyRunJobStatus.Dispatching) with
|
||||
{
|
||||
CancellationRequested = true,
|
||||
LeaseOwner = "test-dispatch",
|
||||
LeaseExpiresAt = timeProvider.GetUtcNow().AddMinutes(1)
|
||||
};
|
||||
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(PolicyRunExecutionResultType.Cancelled, result.Type);
|
||||
Assert.Equal(PolicyRunJobStatus.Cancelled, result.UpdatedJob.Status);
|
||||
|
||||
var job = CreateJob(status: PolicyRunJobStatus.Dispatching) with
|
||||
{
|
||||
CancellationRequested = true,
|
||||
LeaseOwner = "test-dispatch",
|
||||
LeaseExpiresAt = timeProvider.GetUtcNow().AddMinutes(1)
|
||||
};
|
||||
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(PolicyRunExecutionResultType.Cancelled, result.Type);
|
||||
Assert.Equal(PolicyRunJobStatus.Cancelled, result.UpdatedJob.Status);
|
||||
Assert.True(repository.ReplaceCalled);
|
||||
Assert.Equal("test-dispatch", repository.ExpectedLeaseOwner);
|
||||
Assert.Single(webhook.Payloads);
|
||||
Assert.Equal("cancelled", webhook.Payloads[0].Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SubmitsJob_OnSuccess()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SubmitsJob_OnSuccess()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
var client = new StubPolicyRunClient
|
||||
{
|
||||
Result = PolicyRunSubmissionResult.Succeeded("run:P-7:2025", DateTimeOffset.Parse("2025-10-28T10:01:00Z"))
|
||||
@@ -88,33 +88,33 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
};
|
||||
var webhook = new RecordingPolicySimulationWebhookClient();
|
||||
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, webhook, NullLogger<PolicyRunExecutionService>.Instance);
|
||||
|
||||
var job = CreateJob(status: PolicyRunJobStatus.Dispatching) with
|
||||
{
|
||||
LeaseOwner = "test-dispatch",
|
||||
LeaseExpiresAt = timeProvider.GetUtcNow().AddMinutes(1)
|
||||
};
|
||||
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(PolicyRunExecutionResultType.Submitted, result.Type);
|
||||
|
||||
var job = CreateJob(status: PolicyRunJobStatus.Dispatching) with
|
||||
{
|
||||
LeaseOwner = "test-dispatch",
|
||||
LeaseExpiresAt = timeProvider.GetUtcNow().AddMinutes(1)
|
||||
};
|
||||
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(PolicyRunExecutionResultType.Submitted, result.Type);
|
||||
Assert.Equal(PolicyRunJobStatus.Submitted, result.UpdatedJob.Status);
|
||||
Assert.Equal("run:P-7:2025", result.UpdatedJob.RunId);
|
||||
Assert.Equal(job.AttemptCount + 1, result.UpdatedJob.AttemptCount);
|
||||
Assert.Null(result.UpdatedJob.LastError);
|
||||
Assert.True(repository.ReplaceCalled);
|
||||
Assert.Empty(webhook.Payloads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_RetriesJob_OnFailure()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
var client = new StubPolicyRunClient
|
||||
{
|
||||
Result = PolicyRunSubmissionResult.Failed("timeout")
|
||||
};
|
||||
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_RetriesJob_OnFailure()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
var client = new StubPolicyRunClient
|
||||
{
|
||||
Result = PolicyRunSubmissionResult.Failed("timeout")
|
||||
};
|
||||
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
|
||||
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var targeting = new StubPolicyRunTargetingService
|
||||
@@ -123,35 +123,35 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
};
|
||||
var webhook = new RecordingPolicySimulationWebhookClient();
|
||||
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, webhook, NullLogger<PolicyRunExecutionService>.Instance);
|
||||
|
||||
var job = CreateJob(status: PolicyRunJobStatus.Dispatching) with
|
||||
{
|
||||
LeaseOwner = "test-dispatch",
|
||||
LeaseExpiresAt = timeProvider.GetUtcNow().AddMinutes(1)
|
||||
};
|
||||
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(PolicyRunExecutionResultType.Retrying, result.Type);
|
||||
Assert.Equal(PolicyRunJobStatus.Pending, result.UpdatedJob.Status);
|
||||
|
||||
var job = CreateJob(status: PolicyRunJobStatus.Dispatching) with
|
||||
{
|
||||
LeaseOwner = "test-dispatch",
|
||||
LeaseExpiresAt = timeProvider.GetUtcNow().AddMinutes(1)
|
||||
};
|
||||
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(PolicyRunExecutionResultType.Retrying, result.Type);
|
||||
Assert.Equal(PolicyRunJobStatus.Pending, result.UpdatedJob.Status);
|
||||
Assert.Equal(job.AttemptCount + 1, result.UpdatedJob.AttemptCount);
|
||||
Assert.Equal("timeout", result.UpdatedJob.LastError);
|
||||
Assert.True(result.UpdatedJob.AvailableAt > job.AvailableAt);
|
||||
Assert.Empty(webhook.Payloads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_MarksJobFailed_WhenAttemptsExceeded()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
var client = new StubPolicyRunClient
|
||||
{
|
||||
Result = PolicyRunSubmissionResult.Failed("bad request")
|
||||
};
|
||||
var optionsValue = CloneOptions();
|
||||
optionsValue.Policy.Dispatch.MaxAttempts = 1;
|
||||
var options = Microsoft.Extensions.Options.Options.Create(optionsValue);
|
||||
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_MarksJobFailed_WhenAttemptsExceeded()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
var client = new StubPolicyRunClient
|
||||
{
|
||||
Result = PolicyRunSubmissionResult.Failed("bad request")
|
||||
};
|
||||
var optionsValue = CloneOptions();
|
||||
optionsValue.Policy.Dispatch.MaxAttempts = 1;
|
||||
var options = Microsoft.Extensions.Options.Options.Create(optionsValue);
|
||||
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var targeting = new StubPolicyRunTargetingService
|
||||
{
|
||||
@@ -159,13 +159,13 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
};
|
||||
var webhook = new RecordingPolicySimulationWebhookClient();
|
||||
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, webhook, NullLogger<PolicyRunExecutionService>.Instance);
|
||||
|
||||
var job = CreateJob(status: PolicyRunJobStatus.Dispatching, attemptCount: 0) with
|
||||
{
|
||||
LeaseOwner = "test-dispatch",
|
||||
LeaseExpiresAt = timeProvider.GetUtcNow().AddMinutes(1)
|
||||
};
|
||||
|
||||
|
||||
var job = CreateJob(status: PolicyRunJobStatus.Dispatching, attemptCount: 0) with
|
||||
{
|
||||
LeaseOwner = "test-dispatch",
|
||||
LeaseExpiresAt = timeProvider.GetUtcNow().AddMinutes(1)
|
||||
};
|
||||
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(PolicyRunExecutionResultType.Failed, result.Type);
|
||||
@@ -173,100 +173,100 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
Assert.Equal("bad request", result.UpdatedJob.LastError);
|
||||
Assert.Single(webhook.Payloads);
|
||||
Assert.Equal("failed", webhook.Payloads[0].Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_NoWork_CompletesJob()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
var client = new StubPolicyRunClient();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
|
||||
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_NoWork_CompletesJob()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
var client = new StubPolicyRunClient();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
|
||||
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
var targeting = new StubPolicyRunTargetingService
|
||||
{
|
||||
OnEnsureTargets = job => PolicyRunTargetingResult.NoWork(job, "empty")
|
||||
};
|
||||
var webhook = new RecordingPolicySimulationWebhookClient();
|
||||
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, webhook, NullLogger<PolicyRunExecutionService>.Instance);
|
||||
|
||||
var job = CreateJob(status: PolicyRunJobStatus.Dispatching, inputs: PolicyRunInputs.Empty) with
|
||||
{
|
||||
LeaseOwner = "test-dispatch",
|
||||
LeaseExpiresAt = timeProvider.GetUtcNow().AddMinutes(1)
|
||||
};
|
||||
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
|
||||
var job = CreateJob(status: PolicyRunJobStatus.Dispatching, inputs: PolicyRunInputs.Empty) with
|
||||
{
|
||||
LeaseOwner = "test-dispatch",
|
||||
LeaseExpiresAt = timeProvider.GetUtcNow().AddMinutes(1)
|
||||
};
|
||||
|
||||
var result = await service.ExecuteAsync(job, CancellationToken.None);
|
||||
|
||||
Assert.Equal(PolicyRunExecutionResultType.NoOp, result.Type);
|
||||
Assert.Equal(PolicyRunJobStatus.Completed, result.UpdatedJob.Status);
|
||||
Assert.True(repository.ReplaceCalled);
|
||||
Assert.Equal("test-dispatch", repository.ExpectedLeaseOwner);
|
||||
Assert.Single(webhook.Payloads);
|
||||
Assert.Equal("succeeded", webhook.Payloads[0].Result);
|
||||
}
|
||||
|
||||
private static PolicyRunJob CreateJob(PolicyRunJobStatus status, int attemptCount = 0, PolicyRunInputs? inputs = null)
|
||||
{
|
||||
var resolvedInputs = inputs ?? new PolicyRunInputs(sbomSet: new[] { "sbom:S-42" }, captureExplain: true);
|
||||
var metadata = ImmutableSortedDictionary.Create<string, string>(StringComparer.Ordinal);
|
||||
return new PolicyRunJob(
|
||||
SchemaVersion: SchedulerSchemaVersions.PolicyRunJob,
|
||||
Id: "job_1",
|
||||
TenantId: "tenant-alpha",
|
||||
PolicyId: "P-7",
|
||||
PolicyVersion: 4,
|
||||
Mode: PolicyRunMode.Incremental,
|
||||
Priority: PolicyRunPriority.Normal,
|
||||
PriorityRank: -1,
|
||||
RunId: "run:P-7:2025",
|
||||
RequestedBy: "user:cli",
|
||||
CorrelationId: "corr-1",
|
||||
Metadata: metadata,
|
||||
Inputs: resolvedInputs,
|
||||
QueuedAt: DateTimeOffset.Parse("2025-10-28T09:59:00Z"),
|
||||
Status: status,
|
||||
AttemptCount: attemptCount,
|
||||
LastAttemptAt: null,
|
||||
LastError: null,
|
||||
CreatedAt: DateTimeOffset.Parse("2025-10-28T09:58:00Z"),
|
||||
UpdatedAt: DateTimeOffset.Parse("2025-10-28T09:58:00Z"),
|
||||
AvailableAt: DateTimeOffset.Parse("2025-10-28T09:59:00Z"),
|
||||
SubmittedAt: null,
|
||||
CompletedAt: null,
|
||||
LeaseOwner: null,
|
||||
LeaseExpiresAt: null,
|
||||
CancellationRequested: false,
|
||||
CancellationRequestedAt: null,
|
||||
CancellationReason: null,
|
||||
CancelledAt: null);
|
||||
}
|
||||
|
||||
private static SchedulerWorkerOptions CloneOptions()
|
||||
{
|
||||
return new SchedulerWorkerOptions
|
||||
{
|
||||
Policy = new SchedulerWorkerOptions.PolicyOptions
|
||||
{
|
||||
Enabled = WorkerOptions.Policy.Enabled,
|
||||
Dispatch = new SchedulerWorkerOptions.PolicyOptions.DispatchOptions
|
||||
{
|
||||
LeaseOwner = WorkerOptions.Policy.Dispatch.LeaseOwner,
|
||||
BatchSize = WorkerOptions.Policy.Dispatch.BatchSize,
|
||||
LeaseDuration = WorkerOptions.Policy.Dispatch.LeaseDuration,
|
||||
IdleDelay = WorkerOptions.Policy.Dispatch.IdleDelay,
|
||||
MaxAttempts = WorkerOptions.Policy.Dispatch.MaxAttempts,
|
||||
RetryBackoff = WorkerOptions.Policy.Dispatch.RetryBackoff
|
||||
},
|
||||
Api = new SchedulerWorkerOptions.PolicyOptions.ApiOptions
|
||||
{
|
||||
BaseAddress = WorkerOptions.Policy.Api.BaseAddress,
|
||||
RunsPath = WorkerOptions.Policy.Api.RunsPath,
|
||||
SimulatePath = WorkerOptions.Policy.Api.SimulatePath,
|
||||
TenantHeader = WorkerOptions.Policy.Api.TenantHeader,
|
||||
IdempotencyHeader = WorkerOptions.Policy.Api.IdempotencyHeader,
|
||||
RequestTimeout = WorkerOptions.Policy.Api.RequestTimeout
|
||||
},
|
||||
}
|
||||
|
||||
private static PolicyRunJob CreateJob(PolicyRunJobStatus status, int attemptCount = 0, PolicyRunInputs? inputs = null)
|
||||
{
|
||||
var resolvedInputs = inputs ?? new PolicyRunInputs(sbomSet: new[] { "sbom:S-42" }, captureExplain: true);
|
||||
var metadata = ImmutableSortedDictionary.Create<string, string>(StringComparer.Ordinal);
|
||||
return new PolicyRunJob(
|
||||
SchemaVersion: SchedulerSchemaVersions.PolicyRunJob,
|
||||
Id: "job_1",
|
||||
TenantId: "tenant-alpha",
|
||||
PolicyId: "P-7",
|
||||
PolicyVersion: 4,
|
||||
Mode: PolicyRunMode.Incremental,
|
||||
Priority: PolicyRunPriority.Normal,
|
||||
PriorityRank: -1,
|
||||
RunId: "run:P-7:2025",
|
||||
RequestedBy: "user:cli",
|
||||
CorrelationId: "corr-1",
|
||||
Metadata: metadata,
|
||||
Inputs: resolvedInputs,
|
||||
QueuedAt: DateTimeOffset.Parse("2025-10-28T09:59:00Z"),
|
||||
Status: status,
|
||||
AttemptCount: attemptCount,
|
||||
LastAttemptAt: null,
|
||||
LastError: null,
|
||||
CreatedAt: DateTimeOffset.Parse("2025-10-28T09:58:00Z"),
|
||||
UpdatedAt: DateTimeOffset.Parse("2025-10-28T09:58:00Z"),
|
||||
AvailableAt: DateTimeOffset.Parse("2025-10-28T09:59:00Z"),
|
||||
SubmittedAt: null,
|
||||
CompletedAt: null,
|
||||
LeaseOwner: null,
|
||||
LeaseExpiresAt: null,
|
||||
CancellationRequested: false,
|
||||
CancellationRequestedAt: null,
|
||||
CancellationReason: null,
|
||||
CancelledAt: null);
|
||||
}
|
||||
|
||||
private static SchedulerWorkerOptions CloneOptions()
|
||||
{
|
||||
return new SchedulerWorkerOptions
|
||||
{
|
||||
Policy = new SchedulerWorkerOptions.PolicyOptions
|
||||
{
|
||||
Enabled = WorkerOptions.Policy.Enabled,
|
||||
Dispatch = new SchedulerWorkerOptions.PolicyOptions.DispatchOptions
|
||||
{
|
||||
LeaseOwner = WorkerOptions.Policy.Dispatch.LeaseOwner,
|
||||
BatchSize = WorkerOptions.Policy.Dispatch.BatchSize,
|
||||
LeaseDuration = WorkerOptions.Policy.Dispatch.LeaseDuration,
|
||||
IdleDelay = WorkerOptions.Policy.Dispatch.IdleDelay,
|
||||
MaxAttempts = WorkerOptions.Policy.Dispatch.MaxAttempts,
|
||||
RetryBackoff = WorkerOptions.Policy.Dispatch.RetryBackoff
|
||||
},
|
||||
Api = new SchedulerWorkerOptions.PolicyOptions.ApiOptions
|
||||
{
|
||||
BaseAddress = WorkerOptions.Policy.Api.BaseAddress,
|
||||
RunsPath = WorkerOptions.Policy.Api.RunsPath,
|
||||
SimulatePath = WorkerOptions.Policy.Api.SimulatePath,
|
||||
TenantHeader = WorkerOptions.Policy.Api.TenantHeader,
|
||||
IdempotencyHeader = WorkerOptions.Policy.Api.IdempotencyHeader,
|
||||
RequestTimeout = WorkerOptions.Policy.Api.RequestTimeout
|
||||
},
|
||||
Targeting = new SchedulerWorkerOptions.PolicyOptions.TargetingOptions
|
||||
{
|
||||
Enabled = WorkerOptions.Policy.Targeting.Enabled,
|
||||
@@ -284,15 +284,15 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class StubPolicyRunTargetingService : IPolicyRunTargetingService
|
||||
{
|
||||
public Func<PolicyRunJob, PolicyRunTargetingResult>? OnEnsureTargets { get; set; }
|
||||
|
||||
public Task<PolicyRunTargetingResult> EnsureTargetsAsync(PolicyRunJob job, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(OnEnsureTargets?.Invoke(job) ?? PolicyRunTargetingResult.Unchanged(job));
|
||||
}
|
||||
|
||||
|
||||
private sealed class StubPolicyRunTargetingService : IPolicyRunTargetingService
|
||||
{
|
||||
public Func<PolicyRunJob, PolicyRunTargetingResult>? OnEnsureTargets { get; set; }
|
||||
|
||||
public Task<PolicyRunTargetingResult> EnsureTargetsAsync(PolicyRunJob job, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(OnEnsureTargets?.Invoke(job) ?? PolicyRunTargetingResult.Unchanged(job));
|
||||
}
|
||||
|
||||
private sealed class RecordingPolicySimulationWebhookClient : IPolicySimulationWebhookClient
|
||||
{
|
||||
public List<PolicySimulationWebhookPayload> Payloads { get; } = new();
|
||||
@@ -306,13 +306,13 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
|
||||
private sealed class RecordingPolicyRunJobRepository : IPolicyRunJobRepository
|
||||
{
|
||||
public bool ReplaceCalled { get; private set; }
|
||||
public string? ExpectedLeaseOwner { get; private set; }
|
||||
public PolicyRunJob? LastJob { get; private set; }
|
||||
|
||||
public Task<PolicyRunJob?> GetAsync(string tenantId, string jobId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<PolicyRunJob?>(null);
|
||||
|
||||
public bool ReplaceCalled { get; private set; }
|
||||
public string? ExpectedLeaseOwner { get; private set; }
|
||||
public PolicyRunJob? LastJob { get; private set; }
|
||||
|
||||
public Task<PolicyRunJob?> GetAsync(string tenantId, string jobId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<PolicyRunJob?>(null);
|
||||
|
||||
public Task<PolicyRunJob?> GetByRunIdAsync(string tenantId, string runId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<PolicyRunJob?>(null);
|
||||
|
||||
@@ -327,38 +327,38 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
|
||||
public Task<PolicyRunJob?> LeaseAsync(string leaseOwner, DateTimeOffset now, TimeSpan leaseDuration, int maxAttempts, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<PolicyRunJob?>(null);
|
||||
|
||||
public Task<bool> ReplaceAsync(PolicyRunJob job, string? expectedLeaseOwner = null, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ReplaceCalled = true;
|
||||
ExpectedLeaseOwner = expectedLeaseOwner;
|
||||
LastJob = job;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<PolicyRunJob>> ListAsync(string tenantId, string? policyId = null, PolicyRunMode? mode = null, IReadOnlyCollection<PolicyRunJobStatus>? statuses = null, DateTimeOffset? queuedAfter = null, int limit = 50, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<IReadOnlyList<PolicyRunJob>>(Array.Empty<PolicyRunJob>());
|
||||
}
|
||||
|
||||
private sealed class StubPolicyRunClient : IPolicyRunClient
|
||||
{
|
||||
public PolicyRunSubmissionResult Result { get; set; } = PolicyRunSubmissionResult.Succeeded(null, null);
|
||||
|
||||
public Task<PolicyRunSubmissionResult> SubmitAsync(PolicyRunJob job, PolicyRunRequest request, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(Result);
|
||||
}
|
||||
|
||||
private sealed class TestTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now;
|
||||
|
||||
public TestTimeProvider(DateTimeOffset now)
|
||||
{
|
||||
_now = now;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
|
||||
public void Advance(TimeSpan delta) => _now = _now.Add(delta);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> ReplaceAsync(PolicyRunJob job, string? expectedLeaseOwner = null, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ReplaceCalled = true;
|
||||
ExpectedLeaseOwner = expectedLeaseOwner;
|
||||
LastJob = job;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<PolicyRunJob>> ListAsync(string tenantId, string? policyId = null, PolicyRunMode? mode = null, IReadOnlyCollection<PolicyRunJobStatus>? statuses = null, DateTimeOffset? queuedAfter = null, int limit = 50, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<IReadOnlyList<PolicyRunJob>>(Array.Empty<PolicyRunJob>());
|
||||
}
|
||||
|
||||
private sealed class StubPolicyRunClient : IPolicyRunClient
|
||||
{
|
||||
public PolicyRunSubmissionResult Result { get; set; } = PolicyRunSubmissionResult.Succeeded(null, null);
|
||||
|
||||
public Task<PolicyRunSubmissionResult> SubmitAsync(PolicyRunJob job, PolicyRunRequest request, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(Result);
|
||||
}
|
||||
|
||||
private sealed class TestTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now;
|
||||
|
||||
public TestTimeProvider(DateTimeOffset now)
|
||||
{
|
||||
_now = now;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
|
||||
public void Advance(TimeSpan delta) => _now = _now.Add(delta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Services;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Projections;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Projections;
|
||||
using StellaOps.Scheduler.Worker.Events;
|
||||
using StellaOps.Scheduler.Worker.Execution;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
|
||||
Reference in New Issue
Block a user