Weboberflaeche (#531): X-Instanz an der einen fetch-Stelle, Instanzauswahl in der Kopfzeile, Verwaltungsansicht mit Anlegen/Starten/Anhalten/Fenster, Herkunft der Ereignisse, Sicherungs- und Einstellungsansicht. 402/409/502 werden eigen behandelt. Sicherungen im Manager (#530): Auflisten, Anlegen, Wiederherstellen, Loeschen. Das Zurueckspielen haelt die Instanz vorher an und startet sie danach wieder. Geordnetes Ende (Konzept 9.8). Zwei Befunde waren der Anlass: Der Manager konnte eine Instanz MIT FENSTER nie geordnet beenden. Control.ShutdownRequested war nur in HeadlessRun verdrahtet, nicht in der Avalonia-Anwendung - und ein Fenster ist die Vorgabe. Die Bitte wurde mit 202 beantwortet, niemand hoerte zu, und der Manager schoss den Prozess nach Ablauf seiner Frist jedes Mal ab. InstanceSupervisor.StopAsync hielt Socket-Schweigen fuer einen Todesnachweis und meldete "beendet", ohne den Prozess anzufassen. Bei einer verstummten Instanz ist der Socket von vornherein still - der Neustart haette einen zweiten Prozess auf dieselbe state.db gesetzt. Genau der Schaden, gegen den #528 gebaut ist. - ShutdownCoordinator: erst die Tuer schliessen, dann auf offene Arbeiten warten, Frist zwei Minuten. Vorher brach TaskScanner.DisposeAsync mitten in ProcessAsync ab, sodass die Aufgabe bis zum Lease-Ablauf auf "running" stehenblieb. - GET /api/instanz/puls hinter dem Socket-Gate: Phase und offene Arbeiten. Weiter gefragt statt gemeldet - ein Puls, den die Instanz von sich aus schickt, fehlt genau dann, wenn sie haengt. - Eskalationsleiter: drei Durchgaenge ohne Antwort, einmal um ein Ende bitten, nach 30s beenden und den Vollzug abwarten, gemeldet als Absturz. - EnsureDeadAsync vor jedem Start. Weiteres aus dem Review: - .gitignore verankert: "Instances/" passte auch auf src/ClawdDotNet.Core/Instances/. InstanceEndpoint.cs und InstanceLayout.cs waren dadurch nie eingecheckt. - Das WebUI-Token stand im Protokoll des Managers. Konsole ja, Datei nein. - Ein Klick auf "Instanz anlegen" loeste requestSubmit() zweimal aus. - [Obsolete] auf WebServerPort wieder entfernt: sechs Warnungen, die niemand abstellen konnte, solange E-M5 nicht fertig ist. - Die Wiederherstellung bricht nicht mehr ab, wenn der Browser weggeht. - Entwicklungspfade zum Instanzmanager aus Segmenten statt mit Rueckstrichen, und der Port kommt aus manager.json statt fest aus der Zeile. Offen und gemeldet: --managed hebelt die Lizenzpruefung aus (#535). 806 Tests gruen, 0 Warnungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -89,12 +89,41 @@ public sealed class TaskScanner : IAsyncDisposable
|
||||
/// </summary>
|
||||
public bool HasStopped => _loop is { IsCompleted: true };
|
||||
|
||||
// ─── Geordnetes Ende (#528) ───
|
||||
|
||||
private int _openRuns;
|
||||
private int _accepting = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Wie viele Aufgaben gerade tatsächlich laufen — also beansprucht und noch nicht
|
||||
/// abgeschlossen.
|
||||
/// </summary>
|
||||
public int OpenCount => Volatile.Read(ref _openRuns);
|
||||
|
||||
/// <summary>Beansprucht der Scanner noch neue Aufgaben?</summary>
|
||||
public bool IsAcceptingWork => Volatile.Read(ref _accepting) == 1;
|
||||
|
||||
/// <summary>
|
||||
/// Schließt die Tür: kein neuer Takt, keine neue Beanspruchung.
|
||||
///
|
||||
/// <para><b>Warum das nicht dasselbe ist wie <see cref="DisposeAsync"/>.</b> Dispose
|
||||
/// bricht den laufenden Takt über sein Token ab — mitten in <c>ProcessAsync</c>, also
|
||||
/// nachdem eine Aufgabe beansprucht wurde und bevor <c>CompleteClaimAsync</c> sie
|
||||
/// wieder freigibt. Sie steht danach auf <c>running</c> und bleibt dort bis zum
|
||||
/// Ablauf des Lease. Wer geordnet enden will, schließt erst hier die Tür, wartet auf
|
||||
/// <see cref="OpenCount"/> gleich null und ruft Dispose erst dann.</para>
|
||||
/// </summary>
|
||||
public void StopAccepting() => Volatile.Write(ref _accepting, 0);
|
||||
|
||||
private async Task RunLoopAsync(CancellationToken ct)
|
||||
{
|
||||
// PeriodicTimer über den TimeProvider — im Test steuerbar, im Betrieb driftfrei.
|
||||
using var timer = new PeriodicTimer(_tick, _clock);
|
||||
while (await timer.WaitForNextTickAsync(ct))
|
||||
{
|
||||
if (!IsAcceptingWork)
|
||||
continue; // Die Tuer ist zu. Der Takt laeuft leer, bis Dispose ihn beendet.
|
||||
|
||||
try
|
||||
{
|
||||
await ScanOnceAsync(ct);
|
||||
@@ -113,6 +142,9 @@ public sealed class TaskScanner : IAsyncDisposable
|
||||
/// </summary>
|
||||
public async Task<int> ScanOnceAsync(CancellationToken ct)
|
||||
{
|
||||
if (!IsAcceptingWork)
|
||||
return 0;
|
||||
|
||||
var now = _clock.GetUtcNow().UtcDateTime;
|
||||
var leaseCutoff = now - _lease;
|
||||
|
||||
@@ -167,6 +199,9 @@ public sealed class TaskScanner : IAsyncDisposable
|
||||
/// </summary>
|
||||
public async Task<bool> RunTaskNowAsync(string taskId, CancellationToken ct)
|
||||
{
|
||||
if (!IsAcceptingWork)
|
||||
return false;
|
||||
|
||||
var task = await _repo.GetAsync(taskId, ct);
|
||||
if (task is null)
|
||||
return false;
|
||||
@@ -181,6 +216,23 @@ public sealed class TaskScanner : IAsyncDisposable
|
||||
}
|
||||
|
||||
private async Task ProcessAsync(TaskItem task, string token, CancellationToken ct)
|
||||
{
|
||||
// Ab hier ist die Aufgabe beansprucht und steht auf `running`. Wer das
|
||||
// Herunterfahren abwartet, wartet genau auf diesen Zaehler - denn erst
|
||||
// CompleteClaimAsync weiter unten gibt sie wieder frei.
|
||||
Interlocked.Increment(ref _openRuns);
|
||||
|
||||
try
|
||||
{
|
||||
await ProcessCoreAsync(task, token, ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref _openRuns);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessCoreAsync(TaskItem task, string token, CancellationToken ct)
|
||||
{
|
||||
bool ok;
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user