Files
IBKRTrader/src/IBKRTrader.Core/Workers/WorkerBase.cs
T
Richard c20538c8d8 @
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>
@
2026-07-27 20:26:25 +02:00

173 lines
6.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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";
}
}