Budgetende, Stufe 3: Nachholen nur im Spielraum des Tages, keine Kappung bei 500 (#716)
- TaskSchedule.OccurrencesBetween zaehlt, wie oft eine Aufgabe bis zu einem Zeitpunkt noch laeuft, einschliesslich eines jetzt faelligen Laufs. - BudgetHeadroom (ILateRunGate): Ein mitgenommener oder nachzuholender Lauf startet nur, wenn er in den Spielraum passt: Grenze minus Verbrauch minus Reservierung minus Bedarf der restlichen geplanten Laeufe bis Mitternacht minus 10 % Reserve. Das gilt je Agent und je Instanz, fuer Kosten und Tokens. Anderes Spaetwerk zaehlt nicht als Bedarf, sonst blockierten sich mitgenommene Aufgaben gegenseitig. - Der Scanner fragt den Spielraum nur fuer Spaetwerk und protokolliert, wenn es warten muss. Die Engine gibt ihren BudgetGuard heraus, damit Scanner und Engine dieselben Reservierungen sehen. - Ersetzt den pauschalen 25-%-Anteil aus dem Konzept (dort nachgezogen): Ein fester Anteil ist an vollen Tagen zu viel, an ruhigen zu wenig. Drei weitere stille Kappungen bei 500 beseitigt (ListAllAsync, ungekappt): Kostenprognose (Aufgaben ueber 500 fehlten), Waisenabgleich beim Import und task_find aus #674, wo Dubletten unter aelteren Aufgaben mit niedriger Prioritaet unsichtbar wurden. Tests: BudgetHeadroomTests (9), Scanner-Pforte (1), Dublette hinter 500 Aufgaben (1). 1.257 gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8a83a67fb4
commit
9cf9a2cfc2
@@ -238,6 +238,42 @@ public sealed class BudgetMissTests : IDisposable
|
||||
task.DeferredUntil.ShouldBeNull("a shutdown keeps the old 30-minute retry");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Late_work_waits_while_the_headroom_gate_says_no()
|
||||
{
|
||||
await SeedOneTime("t-yesterday");
|
||||
var dispatcher = new BudgetDispatcher { HasBudget = false };
|
||||
var gate = new FixedGate { Room = false };
|
||||
var scanner = new TaskScanner(_repo, dispatcher, NullLoggerFactory.Instance,
|
||||
clock: _clock, lateRunGate: gate);
|
||||
await scanner.ScanOnceAsync(default);
|
||||
|
||||
MoveTo(NextLocalMidnightUtc().AddMinutes(1));
|
||||
await SeedOneTime("t-today");
|
||||
dispatcher.HasBudget = true;
|
||||
dispatcher.Started.Clear();
|
||||
|
||||
await scanner.ScanOnceAsync(default);
|
||||
dispatcher.Started.ShouldBe(["t-today"], "regular work never asks the gate");
|
||||
gate.Asked.ShouldBe(["t-yesterday"]);
|
||||
|
||||
gate.Room = true;
|
||||
await scanner.ScanOnceAsync(default);
|
||||
dispatcher.Started.ShouldBe(["t-today", "t-yesterday"]);
|
||||
}
|
||||
|
||||
private sealed class FixedGate : ILateRunGate
|
||||
{
|
||||
public bool Room { get; set; }
|
||||
public List<string> Asked { get; } = [];
|
||||
|
||||
public Task<bool> HasRoomAsync(TaskItem lateTask, IReadOnlyList<TaskItem> runnable, DateTime nowUtc, CancellationToken ct)
|
||||
{
|
||||
Asked.Add(lateTask.Id);
|
||||
return Task.FromResult(Room);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════ test double ═══════════════════
|
||||
|
||||
private sealed class BudgetDispatcher : ITaskDispatcher
|
||||
|
||||
Reference in New Issue
Block a user