|
@@ -122,6 +122,56 @@ public class ModuleRebuildServiceTests
|
|
|
Assert.Equal(ModuleRebuildStatus.Queued, result.Body.Status);
|
|
Assert.Equal(ModuleRebuildStatus.Queued, result.Body.Status);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 夜间兜底任务在非执行机上永远领不到(<c>ClaimNextQueuedAsync</c> 的白名单),
|
|
|
|
|
+ /// 若它还能挡住人工入队,页面「数据重算」按钮就被永久卡死。人工请求必须能取代它。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public async Task ManualEnqueue_SupersedesQueuedAutoJob()
|
|
|
|
|
+ {
|
|
|
|
|
+ var store = new MemoryJobStore();
|
|
|
|
|
+ var svc = new ModuleRebuildService(store, new ModuleRebuildQueue(), new AlwaysOnModuleRebuildCapability());
|
|
|
|
|
+ var nightly = await svc.EnqueueAsync("S2", 9, 1, null, "AUTO_NIGHTLY");
|
|
|
|
|
+
|
|
|
|
|
+ var manual = await svc.EnqueueAsync("S2", 9, 1, 7);
|
|
|
|
|
+
|
|
|
|
|
+ Assert.Equal(202, manual.StatusCode);
|
|
|
|
|
+ Assert.NotEqual(nightly.Body.JobId, manual.Body.JobId);
|
|
|
|
|
+ var superseded = store.Rows.Single(x => x.Id == nightly.Body.JobId);
|
|
|
|
|
+ Assert.Equal(ModuleRebuildStatus.Cancelled, superseded.Status);
|
|
|
|
|
+ Assert.Contains("AUTO_NIGHTLY", superseded.ErrorMessage);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>已经开跑的任务不得被掀掉:取代只针对 QUEUED。</summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public async Task ManualEnqueue_DoesNotSupersedeRunningAutoJob()
|
|
|
|
|
+ {
|
|
|
|
|
+ var store = new MemoryJobStore();
|
|
|
|
|
+ var svc = new ModuleRebuildService(store, new ModuleRebuildQueue(), new AlwaysOnModuleRebuildCapability());
|
|
|
|
|
+ await svc.EnqueueAsync("S2", 9, 1, null, "AUTO_NIGHTLY");
|
|
|
|
|
+ var running = await store.ClaimNextQueuedAsync(new[] { "S2" });
|
|
|
|
|
+
|
|
|
|
|
+ var manual = await svc.EnqueueAsync("S2", 9, 1, 7);
|
|
|
|
|
+
|
|
|
|
|
+ Assert.Equal(409, manual.StatusCode);
|
|
|
|
|
+ Assert.Equal(ModuleRebuildStatus.Running, store.Rows.Single(x => x.Id == running.Id).Status);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>取代资格只给人工请求;自动触发器之间仍按原样互斥,避免夜间批次自相残杀。</summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public async Task AutoEnqueue_DoesNotSupersedeQueuedAutoJob()
|
|
|
|
|
+ {
|
|
|
|
|
+ var store = new MemoryJobStore();
|
|
|
|
|
+ var svc = new ModuleRebuildService(store, new ModuleRebuildQueue(), new ConfigurableCapability { AutoMinIntervalHours = 0 });
|
|
|
|
|
+ var nightly = await svc.EnqueueAsync("S2", 9, 1, null, "AUTO_NIGHTLY");
|
|
|
|
|
+
|
|
|
|
|
+ var auto = await svc.EnqueueAsync("S2", 9, 1, null, "AUTO");
|
|
|
|
|
+
|
|
|
|
|
+ Assert.Equal(409, auto.StatusCode);
|
|
|
|
|
+ Assert.Equal(nightly.Body.JobId, auto.Body.JobId);
|
|
|
|
|
+ Assert.Equal(ModuleRebuildStatus.Queued, store.Rows.Single(x => x.Id == nightly.Body.JobId).Status);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
[Fact]
|
|
[Fact]
|
|
|
public void S1Service_DelegatesToUnifiedPath()
|
|
public void S1Service_DelegatesToUnifiedPath()
|
|
|
{
|
|
{
|
|
@@ -620,6 +670,20 @@ public class ModuleRebuildServiceTests
|
|
|
.OrderBy(x => x.Id).FirstOrDefault());
|
|
.OrderBy(x => x.Id).FirstOrDefault());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ public Task<bool> TrySupersedeQueuedAsync(long jobId, string reason, CancellationToken ct = default)
|
|
|
|
|
+ {
|
|
|
|
|
+ lock (_gate)
|
|
|
|
|
+ {
|
|
|
|
|
+ var row = Rows.FirstOrDefault(x => x.Id == jobId && x.Status == ModuleRebuildStatus.Queued);
|
|
|
|
|
+ if (row == null) return Task.FromResult(false);
|
|
|
|
|
+ row.Status = ModuleRebuildStatus.Cancelled;
|
|
|
|
|
+ row.CurrentStage = ModuleRebuildStages.Cancelled;
|
|
|
|
|
+ row.FinishedAt = DateTime.Now;
|
|
|
|
|
+ row.ErrorMessage = reason;
|
|
|
|
|
+ return Task.FromResult(true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public Task<AdoModuleDashboardRebuildJob> GetByIdAsync(string moduleCode, long id, long tenantId, long factoryId, CancellationToken ct = default)
|
|
public Task<AdoModuleDashboardRebuildJob> GetByIdAsync(string moduleCode, long id, long tenantId, long factoryId, CancellationToken ct = default)
|
|
|
{
|
|
{
|
|
|
lock (_gate)
|
|
lock (_gate)
|