// 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); }