Initial commit: Add project files
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
using Hangfire;
|
||||
using Hangfire.Common;
|
||||
using Hangfire.States;
|
||||
using KArtSell.BuildingBlocks.Time;
|
||||
using KArtSell.Modules.ModelOperations.Application;
|
||||
using KArtSell.Modules.ModelOperations.Domain;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace KArtSell.Modules.ModelOperations.Scheduling;
|
||||
|
||||
public sealed class ModelOperationsDispatcherJob(
|
||||
IModelScheduleRepository schedules,
|
||||
IBackgroundJobClient jobs,
|
||||
IClock clock,
|
||||
ScheduleOccurrencePlanner occurrencePlanner,
|
||||
ILogger<ModelOperationsDispatcherJob> logger)
|
||||
{
|
||||
[Queue("q-control")]
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 840)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
var now = clock.UtcNow;
|
||||
var leaseOwner = $"dispatcher:{Environment.MachineName}:{Guid.NewGuid():N}";
|
||||
var due = await schedules.AcquireDueAsync(now, leaseOwner, TimeSpan.FromMinutes(14), 50, CancellationToken.None);
|
||||
|
||||
foreach (var item in due)
|
||||
{
|
||||
try
|
||||
{
|
||||
var job = Job.FromExpression<ScheduledModelOperationJob>(handler => handler.ExecuteAsync(
|
||||
item.ScheduleId,
|
||||
item.OperationCode,
|
||||
item.ScopeKey,
|
||||
item.AutomationMode,
|
||||
item.IdempotencyKey));
|
||||
var backgroundJobId = jobs.Create(job, new EnqueuedState(item.Queue));
|
||||
|
||||
var nextDueAt = occurrencePlanner.GetNextDueAt(item.ScheduledFor, item.Cadence, item.CatchUpPolicy, item.MaxCatchUp, now);
|
||||
await schedules.MarkDispatchedAsync(
|
||||
item.ScheduleId,
|
||||
leaseOwner,
|
||||
backgroundJobId,
|
||||
now,
|
||||
nextDueAt,
|
||||
CancellationToken.None);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to dispatch model operation {OperationCode} for {ScopeKey}.", item.OperationCode, item.ScopeKey);
|
||||
await schedules.ReleaseAsync(
|
||||
item.ScheduleId,
|
||||
leaseOwner,
|
||||
"DISPATCH_FAILED",
|
||||
now,
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Hangfire;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace KArtSell.Modules.ModelOperations.Scheduling;
|
||||
|
||||
public static class ModelOperationsScheduler
|
||||
{
|
||||
public const string DispatcherJobId = "model-operations-dispatcher-v1";
|
||||
|
||||
public static void RegisterModelOperationsSchedules(
|
||||
this IServiceProvider services,
|
||||
bool dispatcherEnabled,
|
||||
string dispatcherCron)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(dispatcherCron);
|
||||
|
||||
var manager = services.GetRequiredService<IRecurringJobManager>();
|
||||
if (!dispatcherEnabled)
|
||||
{
|
||||
manager.RemoveIfExists(DispatcherJobId);
|
||||
return;
|
||||
}
|
||||
|
||||
manager.AddOrUpdate<ModelOperationsDispatcherJob>(
|
||||
DispatcherJobId,
|
||||
job => job.ExecuteAsync(),
|
||||
dispatcherCron,
|
||||
new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Hangfire;
|
||||
using KArtSell.Modules.ModelOperations.Application;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace KArtSell.Modules.ModelOperations.Scheduling;
|
||||
|
||||
public sealed class ScheduledModelOperationJob(
|
||||
IModelOperationRequestService service,
|
||||
ILogger<ScheduledModelOperationJob> logger)
|
||||
{
|
||||
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
|
||||
public async Task ExecuteAsync(
|
||||
Guid scheduleId,
|
||||
string operationCode,
|
||||
string scopeKey,
|
||||
string automationMode,
|
||||
string idempotencyKey)
|
||||
{
|
||||
var correlationId = $"model-operation:{operationCode}:{Guid.NewGuid():N}";
|
||||
var request = await service.RequestAsync(
|
||||
scheduleId,
|
||||
operationCode,
|
||||
scopeKey,
|
||||
automationMode,
|
||||
idempotencyKey,
|
||||
correlationId,
|
||||
CancellationToken.None);
|
||||
|
||||
if (request is null)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Model operation {OperationCode} for {ScopeKey} was not created because the approved frozen context is unavailable or the idempotency key already exists.",
|
||||
operationCode,
|
||||
scopeKey);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Requested model operation {OperationCode} for {ScopeKey} with model {ModelVersion} and dataset {DatasetId}. No model mutation is performed by this job.",
|
||||
request.OperationCode,
|
||||
request.ScopeKey,
|
||||
request.Context.VersionSet.ModelVersion,
|
||||
request.Context.VersionSet.DatasetId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user