R1: Umbau auf Multi-Projekt-Struktur (Vorbild PolytraderSharp) Kurskorrektur: Vorbild ist PolytraderSharp (C#), nicht die veraltete Python-Version. Reiner Strukturumbau, Verhalten unveraendert. - src/IBKRTrader.Core (classlib): Core-Code + UI-Contract (ModuleFormBase, WindowManager) - src/IBKRTrader.Modules.CongressTrading (classlib, referenziert nur Core) - Root: IBKRTrader.App (WinExe) referenziert Core + Modul - tests/IBKRTrader.Tests: Referenzen auf Core + Modul - Neue .slnx; ungenutztes HtmlAgilityPack entfernt - docs/ARCHITECTURE.md: korrigierter Ziel-Stand + R-Phasenplan - Build + 38/38 Tests gruen; App startet Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
173 lines
6.5 KiB
C#
173 lines
6.5 KiB
C#
using IBKRTrader.Core.Database;
|
||
using IBKRTrader.Core.Logging;
|
||
|
||
namespace IBKRTrader.Core.Workers;
|
||
|
||
/// <summary>
|
||
/// Basisklasse für alle periodischen Worker.
|
||
/// – Verwaltet den internen CancellationToken-Lifecycle
|
||
/// – Führt ExecuteAsync() im konfigurierten Interval aus
|
||
/// – Aktualisiert WorkerInfo (LastRuntime, NextRuntime, Status, Info)
|
||
/// – Schreibt Runs in core_worker_log
|
||
/// </summary>
|
||
public abstract class WorkerBase : IWorker
|
||
{
|
||
// ─── Abstrakte Member ─────────────────────────────────────────────────────
|
||
|
||
public abstract string Name { get; }
|
||
public abstract string Module { get; }
|
||
public virtual WorkerType Type => WorkerType.Worker;
|
||
|
||
/// <summary>Intervall zwischen zwei Runs. null = Service (keine Wiederholung).</summary>
|
||
protected abstract TimeSpan? Interval { get; }
|
||
|
||
/// <summary>Führt die eigentliche Arbeit des Workers aus.</summary>
|
||
protected abstract Task ExecuteAsync(CancellationToken ct);
|
||
|
||
// ─── Infrastruktur ────────────────────────────────────────────────────────
|
||
|
||
protected readonly LoggingService Logger;
|
||
protected readonly DatabaseService Db;
|
||
|
||
public WorkerInfo Info { get; } = new();
|
||
|
||
private CancellationTokenSource? _cts;
|
||
private Task? _runLoop;
|
||
private readonly SemaphoreSlim _triggerSemaphore = new(0, 1);
|
||
|
||
protected WorkerBase(LoggingService logger, DatabaseService db)
|
||
{
|
||
Logger = logger;
|
||
Db = db;
|
||
|
||
Info.WorkerName = Name;
|
||
Info.Module = Module;
|
||
Info.Type = Type.ToString();
|
||
// Info.RunEvery wird NICHT hier gesetzt – Interval ist abstract/virtual
|
||
// und die abgeleitete Klasse hat ihre Felder zu diesem Zeitpunkt noch
|
||
// nicht initialisiert (Base-Ctor läuft vor dem Derived-Ctor).
|
||
// → RunEvery wird lazy in StartAsync gesetzt.
|
||
Info.Status = WorkerStatus.Idle;
|
||
Info.Info = "Bereit";
|
||
Info.Active = true;
|
||
}
|
||
|
||
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
||
|
||
public Task StartAsync(CancellationToken externalCt)
|
||
{
|
||
if (_runLoop is { IsCompleted: false }) return Task.CompletedTask;
|
||
|
||
// Lazy: RunEvery erst hier setzen, wenn alle Derived-Felder sicher initialisiert sind
|
||
if (string.IsNullOrEmpty(Info.RunEvery))
|
||
Info.RunEvery = Interval.HasValue ? FormatInterval(Interval.Value) : "Service";
|
||
|
||
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
|
||
_runLoop = Task.Run(() => RunLoopAsync(_cts.Token), _cts.Token);
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
public async Task StopAsync()
|
||
{
|
||
if (_cts == null) return;
|
||
await _cts.CancelAsync();
|
||
if (_runLoop != null)
|
||
await _runLoop.ConfigureAwait(false);
|
||
|
||
Info.Status = WorkerStatus.Stopped;
|
||
Info.Info = "Gestoppt";
|
||
}
|
||
|
||
public async Task TriggerAsync()
|
||
{
|
||
// Gibt das Semaphore frei – der RunLoop führt sofort einen Run aus
|
||
if (_triggerSemaphore.CurrentCount == 0)
|
||
_triggerSemaphore.Release();
|
||
await Task.CompletedTask;
|
||
}
|
||
|
||
// ─── Interner Run-Loop ────────────────────────────────────────────────────
|
||
|
||
private async Task RunLoopAsync(CancellationToken ct)
|
||
{
|
||
Logger.Info(Module, $"Worker gestartet: {Name}");
|
||
|
||
while (!ct.IsCancellationRequested)
|
||
{
|
||
await RunOnceAsync(ct);
|
||
|
||
if (Interval == null) break; // Service: nur einmal
|
||
|
||
var next = DateTime.Now.Add(Interval.Value);
|
||
Info.NextRuntime = next;
|
||
|
||
// Warte auf Interval ODER manuellen Trigger
|
||
var remaining = next - DateTime.Now;
|
||
if (remaining > TimeSpan.Zero)
|
||
{
|
||
try
|
||
{
|
||
await _triggerSemaphore
|
||
.WaitAsync(remaining, ct)
|
||
.ConfigureAwait(false);
|
||
}
|
||
catch (OperationCanceledException) { break; }
|
||
}
|
||
}
|
||
|
||
Logger.Info(Module, $"Worker beendet: {Name}");
|
||
}
|
||
|
||
private async Task RunOnceAsync(CancellationToken ct)
|
||
{
|
||
Info.Status = WorkerStatus.Running;
|
||
Info.Info = "Läuft...";
|
||
long logId = 0;
|
||
|
||
try
|
||
{
|
||
logId = await BeginRunLogAsync();
|
||
await ExecuteAsync(ct);
|
||
await EndRunLogAsync(logId, true);
|
||
|
||
Info.LastRuntime = DateTime.Now;
|
||
Info.Status = WorkerStatus.Idle;
|
||
Info.Info = $"OK – {Info.LastRuntime:HH:mm:ss}";
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
if (logId > 0)
|
||
await EndRunLogAsync(logId, false, "Abgebrochen");
|
||
Info.Status = WorkerStatus.Stopped;
|
||
Info.Info = "Abgebrochen";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
if (logId > 0)
|
||
await EndRunLogAsync(logId, false, ex.Message);
|
||
Logger.Error(Module, $"Fehler in Worker {Name}: {ex.Message}", ex);
|
||
Info.Status = WorkerStatus.Error;
|
||
Info.Info = $"Fehler: {ex.Message}";
|
||
}
|
||
}
|
||
|
||
// ─── DB-Log-Seam (überschreibbar für Unit-Tests) ──────────────────────────
|
||
|
||
/// <summary>Legt den Worker-Log-Eintrag an. Kapselt den DB-Zugriff (testbar).</summary>
|
||
protected virtual Task<long> BeginRunLogAsync()
|
||
=> Db.BeginWorkerLogAsync(Name, Module);
|
||
|
||
/// <summary>Schließt den Worker-Log-Eintrag ab. Kapselt den DB-Zugriff (testbar).</summary>
|
||
protected virtual Task EndRunLogAsync(long logId, bool success, string? message = null)
|
||
=> Db.EndWorkerLogAsync(logId, success, message);
|
||
|
||
// ─── Hilfsmethoden ────────────────────────────────────────────────────────
|
||
|
||
private static string FormatInterval(TimeSpan ts)
|
||
{
|
||
if (ts.TotalDays >= 1) return $"{(int)ts.TotalDays}d";
|
||
if (ts.TotalHours >= 1) return $"{(int)ts.TotalHours}h";
|
||
return $"{(int)ts.TotalMinutes}m";
|
||
}
|
||
}
|