Files
IBKRTrader/Core/Workers/WorkerBase.cs
T
RichardandClaude Opus 4.8 ebeb035e92 Initial commit: IBKRTrader
.NET WinForms-Anwendung (Core, Modules/CongressTrading, UI).
Enthaelt .gitignore und settings.example.json als Konfigurationsvorlage.
Echte settings.json mit Zugangsdaten ist bewusst ausgeschlossen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 18:19:47 +02:00

163 lines
6.0 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 Db.BeginWorkerLogAsync(Name, Module);
await ExecuteAsync(ct);
await Db.EndWorkerLogAsync(logId, true);
Info.LastRuntime = DateTime.Now;
Info.Status = WorkerStatus.Idle;
Info.Info = $"OK {Info.LastRuntime:HH:mm:ss}";
}
catch (OperationCanceledException)
{
if (logId > 0)
await Db.EndWorkerLogAsync(logId, false, "Abgebrochen");
Info.Status = WorkerStatus.Stopped;
Info.Info = "Abgebrochen";
}
catch (Exception ex)
{
if (logId > 0)
await Db.EndWorkerLogAsync(logId, false, ex.Message);
Logger.Error(Module, $"Fehler in Worker {Name}: {ex.Message}", ex);
Info.Status = WorkerStatus.Error;
Info.Info = $"Fehler: {ex.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";
}
}