| | | 1 | | // ----------------------------------------------------------------------- |
| | | 2 | | // <copyright file="AutoSaveEngine.cs" company="Stéphane ANDRE"> |
| | | 3 | | // Copyright (c) Stéphane ANDRE. All rights reserved. |
| | | 4 | | // </copyright> |
| | | 5 | | // ----------------------------------------------------------------------- |
| | | 6 | | |
| | | 7 | | using System; |
| | | 8 | | using System.Diagnostics.CodeAnalysis; |
| | | 9 | | using System.Threading; |
| | | 10 | | using System.Threading.Tasks; |
| | | 11 | | using Microsoft.Extensions.Logging; |
| | | 12 | | using Microsoft.Extensions.Logging.Abstractions; |
| | | 13 | | |
| | | 14 | | namespace MyNet.IO.AutoSave; |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Base class for implementing an auto-save engine that periodically saves data at a specified interval. |
| | | 18 | | /// </summary> |
| | | 19 | | /// <param name="interval">The interval at which the auto-save engine should save data.</param> |
| | | 20 | | /// <param name="logger">An optional logger for logging messages and exceptions.</param> |
| | | 21 | | [SuppressMessage("Performance", "CA1823:Avoid unused private fields", Justification = "Used by LoggerMessage source gene |
| | | 22 | | public abstract partial class AutoSaveEngine(TimeSpan interval, ILogger? logger = null) : IAutoSaveEngine, IDisposable |
| | | 23 | | { |
| | 12 | 24 | | private readonly ILogger _logger = logger ?? NullLogger<AutoSaveEngine>.Instance; |
| | 12 | 25 | | private readonly SemaphoreSlim _executionLock = new(1, 1); |
| | | 26 | | |
| | | 27 | | private CancellationTokenSource? _cts; |
| | | 28 | | private Task? _loopTask; |
| | | 29 | | |
| | | 30 | | private bool _enabled; |
| | | 31 | | private bool _disposed; |
| | | 32 | | |
| | | 33 | | /// <summary> |
| | | 34 | | /// Gets a value indicating whether the auto-save engine is currently running. This property returns <c>true</c> if |
| | | 35 | | /// </summary> |
| | 6 | 36 | | public bool IsRunning => _loopTask is { IsCompleted: false }; |
| | | 37 | | |
| | | 38 | | /// <summary> |
| | | 39 | | /// Gets a value indicating whether the auto-save engine is currently performing a save operation. This property is |
| | | 40 | | /// </summary> |
| | | 41 | | public bool IsSaving { get; private set; } |
| | | 42 | | |
| | | 43 | | /// <summary> |
| | | 44 | | /// Gets the interval at which the auto-save engine saves data. This property is initialized with the value provided |
| | | 45 | | /// </summary> |
| | | 46 | | public TimeSpan Interval { get; private set; } = interval; |
| | | 47 | | |
| | | 48 | | /// <summary> |
| | | 49 | | /// Sets the interval at which the auto-save engine saves data. The provided interval must be greater than zero; oth |
| | | 50 | | /// </summary> |
| | | 51 | | /// <param name="interval">The new interval at which the auto-save engine should save data.</param> |
| | | 52 | | /// <exception cref="ArgumentOutOfRangeException">Thrown if the provided interval is less than or equal to zero.</ex |
| | | 53 | | public void SetInterval(TimeSpan interval) |
| | | 54 | | { |
| | 3 | 55 | | ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(interval, TimeSpan.Zero); |
| | | 56 | | |
| | 0 | 57 | | Interval = interval; |
| | 0 | 58 | | } |
| | | 59 | | |
| | | 60 | | /// <summary> |
| | | 61 | | /// Triggers an immediate save operation, allowing consumers to request a save outside of the regular interval. This |
| | | 62 | | /// </summary> |
| | | 63 | | /// <param name="ct">A cancellation token that can be used to cancel the save operation.</param> |
| | | 64 | | /// <returns>A task that represents the asynchronous save operation.</returns> |
| | 9 | 65 | | public Task TriggerSaveAsync(CancellationToken ct = default) => ExecuteSaveAsync(ct); |
| | | 66 | | |
| | | 67 | | /// <summary> |
| | | 68 | | /// Starts the auto-save engine, initiating the periodic save loop. If the engine is already running or has been dis |
| | | 69 | | /// </summary> |
| | | 70 | | public void Start() |
| | | 71 | | { |
| | 3 | 72 | | if (_disposed || _enabled) |
| | 0 | 73 | | return; |
| | | 74 | | |
| | 3 | 75 | | _enabled = true; |
| | | 76 | | |
| | 3 | 77 | | _cts = new(); |
| | 3 | 78 | | _loopTask = RunLoopAsync(_cts.Token); |
| | 3 | 79 | | } |
| | | 80 | | |
| | | 81 | | /// <summary> |
| | | 82 | | /// Stops the auto-save engine, signaling it to cease periodic save operations. If the engine is not currently runni |
| | | 83 | | /// </summary> |
| | | 84 | | public void Stop() |
| | | 85 | | { |
| | 15 | 86 | | if (!_enabled) |
| | 12 | 87 | | return; |
| | | 88 | | |
| | 3 | 89 | | _enabled = false; |
| | | 90 | | |
| | 3 | 91 | | _cts?.Cancel(); |
| | 3 | 92 | | _cts?.Dispose(); |
| | 3 | 93 | | _cts = null; |
| | 3 | 94 | | } |
| | | 95 | | |
| | | 96 | | /// <summary> |
| | | 97 | | /// Cancels the current save operation if it is in progress. This method can be used to interrupt an ongoing save op |
| | | 98 | | /// </summary> |
| | 0 | 99 | | public void Cancel() => _cts?.Cancel(); |
| | | 100 | | |
| | | 101 | | /// <summary> |
| | | 102 | | /// Suspends the auto-save engine, preventing it from performing save operations until the returned <see cref="IDisp |
| | | 103 | | /// </summary> |
| | | 104 | | /// <returns>An <see cref="IDisposable"/> that, when disposed, will resume the auto-save engine.</returns> |
| | 0 | 105 | | public IDisposable Suspend() => new SuspensionScope(this); |
| | | 106 | | |
| | | 107 | | /// <summary> |
| | | 108 | | /// Runs the main loop of the auto-save engine, which periodically executes save operations based on the configured |
| | | 109 | | /// </summary> |
| | | 110 | | /// <param name="token">A cancellation token that can be used to cancel the loop.</param> |
| | | 111 | | private async Task RunLoopAsync(CancellationToken token) |
| | | 112 | | { |
| | | 113 | | try |
| | | 114 | | { |
| | 9 | 115 | | while (!token.IsCancellationRequested) |
| | | 116 | | { |
| | 6 | 117 | | await Task.Delay(Interval, token).ConfigureAwait(false); |
| | | 118 | | |
| | 6 | 119 | | await ExecuteSaveAsync(token).ConfigureAwait(false); |
| | | 120 | | } |
| | 3 | 121 | | } |
| | 0 | 122 | | catch (OperationCanceledException) |
| | | 123 | | { |
| | | 124 | | // expected |
| | 0 | 125 | | } |
| | 0 | 126 | | catch (Exception ex) |
| | | 127 | | { |
| | 0 | 128 | | LogAutoSaveLoopFailed(ex); |
| | 0 | 129 | | } |
| | 3 | 130 | | } |
| | | 131 | | |
| | | 132 | | /// <summary> |
| | | 133 | | /// Executes the save operation, ensuring that only one save can occur at a time by using a semaphore to control acc |
| | | 134 | | /// </summary> |
| | | 135 | | /// <param name="token">A cancellation token that can be used to cancel the save operation.</param> |
| | | 136 | | private async Task ExecuteSaveAsync(CancellationToken token) |
| | | 137 | | { |
| | 15 | 138 | | if (!await _executionLock.WaitAsync(0, token).ConfigureAwait(false)) |
| | 3 | 139 | | return; |
| | | 140 | | |
| | | 141 | | try |
| | | 142 | | { |
| | 12 | 143 | | IsSaving = true; |
| | | 144 | | |
| | 12 | 145 | | await SaveCoreAsync(token).ConfigureAwait(false); |
| | 12 | 146 | | } |
| | 0 | 147 | | catch (OperationCanceledException) |
| | | 148 | | { |
| | | 149 | | // ignore |
| | 0 | 150 | | } |
| | 0 | 151 | | catch (Exception ex) |
| | | 152 | | { |
| | 0 | 153 | | LogAutoSaveLoopFailed(ex); |
| | 0 | 154 | | } |
| | | 155 | | finally |
| | | 156 | | { |
| | 12 | 157 | | IsSaving = false; |
| | 12 | 158 | | _executionLock.Release(); |
| | | 159 | | } |
| | 15 | 160 | | } |
| | | 161 | | |
| | | 162 | | /// <summary> |
| | | 163 | | /// When implemented in a derived class, performs the actual save operation. This method is called by the ExecuteSav |
| | | 164 | | /// </summary> |
| | | 165 | | /// <param name="cancellationToken">A cancellation token that can be used to cancel the save operation.</param> |
| | | 166 | | /// <returns>A task that represents the asynchronous save operation.</returns> |
| | | 167 | | protected abstract Task SaveCoreAsync(CancellationToken cancellationToken); |
| | | 168 | | |
| | | 169 | | /// <summary> |
| | | 170 | | /// Releases all resources used by the auto-save engine. This method stops the engine if it is currently running and |
| | | 171 | | /// </summary> |
| | | 172 | | public void Dispose() |
| | | 173 | | { |
| | 12 | 174 | | Dispose(true); |
| | 12 | 175 | | GC.SuppressFinalize(this); |
| | 12 | 176 | | } |
| | | 177 | | |
| | | 178 | | /// <summary> |
| | | 179 | | /// Releases the unmanaged resources used by the auto-save engine and optionally releases the managed resources. If |
| | | 180 | | /// </summary> |
| | | 181 | | /// <param name="disposing">A boolean value indicating whether the method is being called from the Dispose method (t |
| | | 182 | | protected virtual void Dispose(bool disposing) |
| | | 183 | | { |
| | 12 | 184 | | if (_disposed) |
| | 0 | 185 | | return; |
| | | 186 | | |
| | 12 | 187 | | if (disposing) |
| | | 188 | | { |
| | 12 | 189 | | Stop(); |
| | 12 | 190 | | _executionLock.Dispose(); |
| | | 191 | | } |
| | | 192 | | |
| | 12 | 193 | | _disposed = true; |
| | 12 | 194 | | } |
| | | 195 | | |
| | | 196 | | /// <summary> |
| | | 197 | | /// Represents a scope that temporarily suspends the auto-save engine when created and resumes it when disposed. Thi |
| | | 198 | | /// </summary> |
| | | 199 | | private sealed class SuspensionScope : IDisposable |
| | | 200 | | { |
| | | 201 | | private readonly AutoSaveEngine _engine; |
| | | 202 | | |
| | | 203 | | /// <summary> |
| | | 204 | | /// Initializes a new instance of the <see cref="SuspensionScope"/> class, which suspends the auto-save engine b |
| | | 205 | | /// </summary> |
| | | 206 | | /// <param name="engine">The auto-save engine to be suspended.</param> |
| | | 207 | | public SuspensionScope(AutoSaveEngine engine) |
| | | 208 | | { |
| | 0 | 209 | | _engine = engine; |
| | 0 | 210 | | _engine.Cancel(); |
| | 0 | 211 | | _engine._enabled = false; |
| | 0 | 212 | | } |
| | | 213 | | |
| | | 214 | | /// <summary> |
| | | 215 | | /// Releases the resources used by the <see cref="SuspensionScope"/> and resumes the auto-save engine by setting |
| | | 216 | | /// </summary> |
| | 0 | 217 | | public void Dispose() => _engine._enabled = true; |
| | | 218 | | } |
| | | 219 | | |
| | | 220 | | [LoggerMessage(LogLevel.Error, "Auto-save loop failed.")] |
| | | 221 | | partial void LogAutoSaveLoopFailed(Exception exception); |
| | | 222 | | } |
| | | 223 | | |