// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace EpicGames.Core;
///
/// Base interface for a scheduled event
///
public interface ITicker : IAsyncDisposable
{
///
/// Start the ticker
///
Task StartAsync();
///
/// Stop the ticker
///
Task StopAsync();
}
///
/// Interface representing time and scheduling events which is pluggable during testing. In normal use, the Clock implementation below is used.
///
public interface IClock
{
///
/// Return time expressed as the Coordinated Universal Time (UTC)
///
DateTime UtcNow { get; }
///
/// Time zone for schedules etc...
///
TimeZoneInfo TimeZone { get; }
///
/// Create an event that will trigger after the given time
///
/// Name of the event
/// Time after which the event will trigger
/// Callback for the tick. Returns the time interval until the next tick, or null to cancel the tick.
/// Logger for error messages
/// Handle to the event
ITicker AddTicker(string name, TimeSpan interval, Func> tickAsync, ILogger logger);
///
/// Create a ticker shared between all server processes.
/// Callback can be run inside any available process but will still only be called once per tick.
///
/// Name of the event
/// Time after which the event will trigger
/// Callback for the tick. Returns the time interval until the next tick, or null to cancel the tick.
/// Logger for error messages
/// New ticker instance
ITicker AddSharedTicker(string name, TimeSpan interval, Func tickAsync, ILogger logger);
}
///
/// Placeholder interface for ITicker
///
public sealed class NullTicker : ITicker
{
///
public ValueTask DisposeAsync() => new ValueTask();
///
public Task StartAsync() => Task.CompletedTask;
///
public Task StopAsync() => Task.CompletedTask;
}
///
/// A default implementation of IClock for normal production use
///
public class DefaultClock : IClock
{
///
public DateTime UtcNow => DateTime.UtcNow;
///
public TimeZoneInfo TimeZone => TimeZoneInfo.Local;
///
public ITicker AddTicker(string name, TimeSpan interval, Func> tickAsync, ILogger logger)
{
throw new NotImplementedException("Not available in default implementation");
}
///
public ITicker AddSharedTicker(string name, TimeSpan interval, Func tickAsync, ILogger logger)
{
throw new NotImplementedException("Not available in default implementation");
}
}
///
/// A stub implementation of IClock. Intended for testing to override time.
///
public class StubClock : IClock
{
private DateTime _utcNow = DateTime.UtcNow;
///
public DateTime UtcNow
{
get => _utcNow;
set => _utcNow = value.ToUniversalTime();
}
///
public TimeZoneInfo TimeZone { get; set; } = TimeZoneInfo.Local;
///
public ITicker AddTicker(string name, TimeSpan interval, Func> tickAsync, ILogger logger)
{
throw new NotImplementedException("Not available in stub implementation");
}
///
public ITicker AddSharedTicker(string name, TimeSpan interval, Func tickAsync, ILogger logger)
{
throw new NotImplementedException("Not available in stub implementation");
}
///
/// Advance the time
///
///
public void Advance(TimeSpan delta)
{
_utcNow += delta;
}
}
///
/// Extension methods for
///
public static class ClockExtensions
{
///
/// Create an event that will trigger after the given time
///
/// Clock to schedule the event on
/// Name of the ticker
/// Interval for the callback
/// Trigger callback
/// Logger for any error messages
/// Handle to the event
public static ITicker AddTicker(this IClock clock, string name, TimeSpan interval, Func tickAsync, ILogger logger)
{
async ValueTask WrappedTrigger(CancellationToken token)
{
Stopwatch timer = Stopwatch.StartNew();
await tickAsync(token);
return interval - timer.Elapsed;
}
return clock.AddTicker(name, interval, WrappedTrigger, logger);
}
///
/// Create an event that will trigger after the given time
///
/// Clock to schedule the event on
/// Time after which the event will trigger
/// Callback for the tick. Returns the time interval until the next tick, or null to cancel the tick.
/// Logger for error messages
/// Handle to the event
public static ITicker AddTicker(this IClock clock, TimeSpan interval, Func> tickAsync, ILogger logger)
=> clock.AddTicker(typeof(T).Name, interval, tickAsync, logger);
///
/// Create an event that will trigger after the given time
///
/// Clock to schedule the event on
/// Interval for the callback
/// Trigger callback
/// Logger for any error messages
/// Handle to the event
public static ITicker AddTicker(this IClock clock, TimeSpan interval, Func tickAsync, ILogger logger)
=> clock.AddTicker(typeof(T).Name, interval, tickAsync, logger);
///
/// Create a ticker shared between all server pods
///
/// Clock to schedule the event on
/// Time after which the event will trigger
/// Callback for the tick. Returns the time interval until the next tick, or null to cancel the tick.
/// Logger for error messages
/// New ticker instance
public static ITicker AddSharedTicker(this IClock clock, TimeSpan interval, Func tickAsync, ILogger logger)
=> clock.AddSharedTicker(typeof(T).Name, interval, tickAsync, logger);
}