Files
UnrealEngineUWP/Engine/Source/Programs/Horde/HordeServer/Utilities/TickedBackgroundService.cs
Ben Marsh 5abbc95b6e Add missing copyright notices.
[CL 16160939 by Ben Marsh in ue5-main branch]
2021-04-29 15:35:57 -04:00

133 lines
3.7 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using Datadog.Trace;
using EpicGames.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace HordeServer.Utilities
{
/// <summary>
/// Base class for a background service which is ticked on a regular schedule
/// </summary>
public abstract class TickedBackgroundService : BackgroundService
{
/// <summary>
/// Frequency that the service will be ticked
/// </summary>
public TimeSpan? Interval { get; set; }
/// <summary>
/// Logger interface
/// </summary>
private ILogger Logger;
/// <summary>
/// Event which can be signalled to make the background service tick immediately
/// </summary>
private AsyncEvent TickImmediatelyEvent = new AsyncEvent();
/// <summary>
/// Constructor
/// </summary>
/// <param name="Interval">Interval for ticking the service</param>
/// <param name="Logger">Logger for debug messages and errors</param>
public TickedBackgroundService(TimeSpan? Interval, ILogger Logger)
{
this.Interval = Interval;
this.Logger = Logger;
}
/// <summary>
/// Ticks the service immediately
/// </summary>
protected void TickNow()
{
TickImmediatelyEvent.Set();
}
/// <inheritdoc/>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "<Pending>")]
protected sealed override async Task ExecuteAsync(CancellationToken StoppingToken)
{
using (CancellationTask StoppingTask = new CancellationTask(StoppingToken))
{
// Wait a random amount of time before the first iteration
TimeSpan? IntervalCopy = Interval;
if(IntervalCopy.HasValue)
{
TimeSpan Delay = new Random().NextDouble() * IntervalCopy.Value;
await Task.WhenAny(StoppingTask.Task, Task.Delay(Delay));
}
// Enter the main tick loop
Stopwatch Timer = new Stopwatch();
while (!StoppingToken.IsCancellationRequested)
{
// If the tick immediately event is set, reset it
if(TickImmediatelyEvent.IsSet())
{
TickImmediatelyEvent = new AsyncEvent();
}
// Run the tick method
Timer.Restart();
using (Scope Scope = Tracer.Instance.StartActive($"{GetType().Name}.{nameof(TickAsync)}"))
{
try
{
await TickAsync(StoppingToken);
}
catch (Exception Ex)
{
if (StoppingToken.IsCancellationRequested)
{
break;
}
else
{
Logger.LogError(Ex, "Unhandled exception in {ServiceName}", GetType().Name);
}
}
}
// Wait until the next interval has elapsed
IntervalCopy = Interval;
if(IntervalCopy.HasValue)
{
TimeSpan Delay = IntervalCopy.Value - Timer.Elapsed;
if(Delay > TimeSpan.Zero)
{
await Task.WhenAny(Task.Delay(Delay), StoppingTask.Task);
}
}
}
}
}
/// <summary>
/// Abstract tick method to be implemented by derived classes
/// </summary>
/// <param name="StoppingToken">Token signalling that the task should terminate</param>
/// <returns>Async task</returns>
protected abstract Task TickAsync(CancellationToken StoppingToken);
/// <summary>
/// Tick the implemented method from above
/// **Only for use in testing!**
/// </summary>
/// <param name="StoppingToken">Optional token signalling that the task should terminate</param>
/// <returns>Async task</returns>
public Task TickOnlyForTestingAsync(CancellationToken? StoppingToken = null)
{
return TickAsync(StoppingToken ?? new CancellationToken());
}
}
}