Initial commit: Add project files
ci / backend (push) Failing after 12s
ci / frontend (push) Failing after 19s
ci / static (push) Failing after 45s

This commit is contained in:
2026-08-02 05:15:36 +09:00
commit dcd1322d41
636 changed files with 122352 additions and 0 deletions
@@ -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);
}
}