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 ShardInstance Instance { get; }
public ConnectionStatus ConnStatus { get; }
public BirthdayRoleUpdate BirthdayUpdater { get; }
public SelectiveAutoUserDownload UserDownloader { get; }
public DateTimeOffset LastBackgroundRun { get; private set; }
public string CurrentExecutingService { get; private set; }
public int ConnectionScore => ConnStatus.Score;
public ShardBackgroundWorker(ShardInstance instance)
{
Instance = instance;
_workerCanceller = new CancellationTokenSource();
ConnStatus = new ConnectionStatus(instance);
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);
// ConnectionStatus will always run. Its result determines if remaining tasks also this time.
await ConnStatus.OnTick(_workerCanceller.Token).ConfigureAwait(false);
if (!ConnStatus.Stable) continue;
// Execute tasks sequentially
foreach (var service in _workers)
{
CurrentExecutingService = service.GetType().Name;
try
{
if (_workerCanceller.IsCancellationRequested) break;
await service.OnTick(_workerCanceller.Token).ConfigureAwait(false);
}
catch (Exception ex)
{
if (ex is TaskCanceledException)
{
Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} was interrupted by a cancellation request.");
throw;
}
else
{
// TODO webhook log
Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} encountered an exception:\n" + ex.ToString());
}
}
}
CurrentExecutingService = null;
LastBackgroundRun = DateTimeOffset.UtcNow;
}
}
catch (TaskCanceledException) { }
Instance.Log(nameof(WorkerLoop), "Background worker has concluded normally.");
}
}
}