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>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user