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
@@ -201,6 +201,70 @@ public static class TaskSchedule
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #716: How many runs the task will still trigger from <paramref name="fromUtc"/> up to
|
||||
/// (excluding) <paramref name="toUtc"/> — including one that is due right now. The
|
||||
/// basis for "how much budget does the rest of the day still need".
|
||||
/// </summary>
|
||||
public static int OccurrencesBetween(TaskItem task, DateTime fromUtc, DateTime toUtc, int cap = 1_440)
|
||||
{
|
||||
fromUtc = DateTime.SpecifyKind(fromUtc, DateTimeKind.Utc);
|
||||
toUtc = DateTime.SpecifyKind(toUtc, DateTimeKind.Utc);
|
||||
if (toUtc <= fromUtc)
|
||||
return 0;
|
||||
|
||||
var dueNow = DueOccurrence(task, fromUtc) is not null ? 1 : 0;
|
||||
|
||||
if (task.When is not { } when)
|
||||
return dueNow; // one-time without a schedule: due until it ran
|
||||
|
||||
switch (when.Kind)
|
||||
{
|
||||
case TaskWhenKind.At:
|
||||
{
|
||||
if (dueNow == 1 || task.LastOccurrence is not null)
|
||||
return dueNow;
|
||||
return TryResolveInstant(when.Value, when.TimeZone, out var at) && at < toUtc ? 1 : 0;
|
||||
}
|
||||
|
||||
case TaskWhenKind.Every:
|
||||
{
|
||||
if (!TryParseInterval(when.Value, out var interval) || interval <= TimeSpan.Zero)
|
||||
return dueNow;
|
||||
|
||||
var next = dueNow == 1
|
||||
? fromUtc + interval
|
||||
: NextOccurrenceUtc(task, fromUtc) ?? toUtc;
|
||||
var count = dueNow;
|
||||
while (count < cap && next < toUtc)
|
||||
{
|
||||
count++;
|
||||
next += interval;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
case TaskWhenKind.Cron:
|
||||
{
|
||||
if (!TryPrepareCron(when, out var cron, out var tz))
|
||||
return 0;
|
||||
|
||||
var count = dueNow;
|
||||
var cursor = fromUtc;
|
||||
while (count < cap && NextCronUtc(cron, tz, cursor) is { } next && next < toUtc)
|
||||
{
|
||||
if (next > fromUtc)
|
||||
count++;
|
||||
cursor = next;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
default:
|
||||
return dueNow;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryPrepareCron(TaskWhen when, out CronExpression cron, out TimeZoneInfo tz)
|
||||
{
|
||||
cron = null!;
|
||||
|
||||
Reference in New Issue
Block a user