using IBKRTrader.Core.Database; using IBKRTrader.Core.Logging; namespace IBKRTrader.Core.Workers; /// /// 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 /// public abstract class WorkerBase : IWorker { // ─── Abstrakte Member ───────────────────────────────────────────────────── public abstract string Name { get; } public abstract string Module { get; } public virtual WorkerType Type => WorkerType.Worker; /// Intervall zwischen zwei Runs. null = Service (keine Wiederholung). protected abstract TimeSpan? Interval { get; } /// Führt die eigentliche Arbeit des Workers aus. 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"; } }