using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace BirthdayBot.BackgroundServices { /// /// Handles the execution of periodic background tasks specific to each shard. /// class ShardBackgroundWorker : IDisposable { /// /// The interval, in seconds, in which background tasks are attempted to be run within a shard. /// public const int Interval = 40; private readonly Task _workerTask; private readonly CancellationTokenSource _workerCanceller; private readonly List _workers; private int _tickCount = -1; private ShardInstance Instance { get; } public BirthdayRoleUpdate BirthdayUpdater { get; } public SelectiveAutoUserDownload UserDownloader { get; } public DateTimeOffset LastBackgroundRun { get; private set; } public string? CurrentExecutingService { get; private set; } public ShardBackgroundWorker(ShardInstance instance) { Instance = instance; _workerCanceller = new CancellationTokenSource(); BirthdayUpdater = new BirthdayRoleUpdate(instance); UserDownloader = new SelectiveAutoUserDownload(instance); _workers = new List() { {UserDownloader}, {BirthdayUpdater}, {new DataRetention(instance)}, {new ExternalStatisticsReporting(instance)} }; _workerTask = Task.Factory.StartNew(WorkerLoop, _workerCanceller.Token); } public void Dispose() { _workerCanceller.Cancel(); _workerTask.Wait(5000); if (!_workerTask.IsCompleted) Instance.Log("Dispose", "Warning: Background worker has not yet stopped. Forcing its disposal."); _workerTask.Dispose(); _workerCanceller.Dispose(); } /// /// *The* background task for the shard. /// Executes service tasks and handles errors. /// private async Task WorkerLoop() { LastBackgroundRun = DateTimeOffset.UtcNow; try { while (!_workerCanceller.IsCancellationRequested) { await Task.Delay(Interval * 1000, _workerCanceller.Token).ConfigureAwait(false); // Skip this round of task execution if the client is not connected if (Instance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) continue; // Execute tasks sequentially foreach (var service in _workers) { CurrentExecutingService = service.GetType().Name; try { if (_workerCanceller.IsCancellationRequested) break; _tickCount++; await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false); } catch (Exception ex) when (ex is not TaskCanceledException) { // TODO webhook log Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} encountered an exception:\n" + ex.ToString()); } } CurrentExecutingService = null; LastBackgroundRun = DateTimeOffset.UtcNow; } } catch (TaskCanceledException) { } } } }