< Summary

Information
Class: MyNet.IO.AutoSave.AutoSaveEngine
Assembly: MyNet.IO
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.IO/AutoSave/AutoSaveEngine.cs
Tag: 323_28699572109
Line coverage
65%
Covered lines: 39
Uncovered lines: 21
Coverable lines: 60
Total lines: 223
Line coverage: 65%
Branch coverage
58%
Covered branches: 14
Total branches: 24
Branch coverage: 58.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%22100%
get_IsRunning()50%22100%
SetInterval(...)100%1133.33%
TriggerSaveAsync(...)100%11100%
Start()50%4483.33%
Stop()66.66%66100%
Cancel()0%620%
Suspend()100%210%
RunLoopAsync()100%3250%
ExecuteSaveAsync()100%2261.53%
Dispose()100%11100%
Dispose(...)50%4485.71%
.ctor(...)100%210%
Dispose()100%210%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.IO/AutoSave/AutoSaveEngine.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="AutoSaveEngine.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Diagnostics.CodeAnalysis;
 9using System.Threading;
 10using System.Threading.Tasks;
 11using Microsoft.Extensions.Logging;
 12using Microsoft.Extensions.Logging.Abstractions;
 13
 14namespace 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
 22public abstract partial class AutoSaveEngine(TimeSpan interval, ILogger? logger = null) : IAutoSaveEngine, IDisposable
 23{
 1224    private readonly ILogger _logger = logger ?? NullLogger<AutoSaveEngine>.Instance;
 1225    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>
 636    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    {
 355        ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(interval, TimeSpan.Zero);
 56
 057        Interval = interval;
 058    }
 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>
 965    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    {
 372        if (_disposed || _enabled)
 073            return;
 74
 375        _enabled = true;
 76
 377        _cts = new();
 378        _loopTask = RunLoopAsync(_cts.Token);
 379    }
 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    {
 1586        if (!_enabled)
 1287            return;
 88
 389        _enabled = false;
 90
 391        _cts?.Cancel();
 392        _cts?.Dispose();
 393        _cts = null;
 394    }
 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>
 099    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>
 0105    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        {
 9115            while (!token.IsCancellationRequested)
 116            {
 6117                await Task.Delay(Interval, token).ConfigureAwait(false);
 118
 6119                await ExecuteSaveAsync(token).ConfigureAwait(false);
 120            }
 3121        }
 0122        catch (OperationCanceledException)
 123        {
 124            // expected
 0125        }
 0126        catch (Exception ex)
 127        {
 0128            LogAutoSaveLoopFailed(ex);
 0129        }
 3130    }
 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    {
 15138        if (!await _executionLock.WaitAsync(0, token).ConfigureAwait(false))
 3139            return;
 140
 141        try
 142        {
 12143            IsSaving = true;
 144
 12145            await SaveCoreAsync(token).ConfigureAwait(false);
 12146        }
 0147        catch (OperationCanceledException)
 148        {
 149            // ignore
 0150        }
 0151        catch (Exception ex)
 152        {
 0153            LogAutoSaveLoopFailed(ex);
 0154        }
 155        finally
 156        {
 12157            IsSaving = false;
 12158            _executionLock.Release();
 159        }
 15160    }
 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    {
 12174        Dispose(true);
 12175        GC.SuppressFinalize(this);
 12176    }
 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    {
 12184        if (_disposed)
 0185            return;
 186
 12187        if (disposing)
 188        {
 12189            Stop();
 12190            _executionLock.Dispose();
 191        }
 192
 12193        _disposed = true;
 12194    }
 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        {
 0209            _engine = engine;
 0210            _engine.Cancel();
 0211            _engine._enabled = false;
 0212        }
 213
 214        /// <summary>
 215        /// Releases the resources used by the <see cref="SuspensionScope"/> and resumes the auto-save engine by setting
 216        /// </summary>
 0217        public void Dispose() => _engine._enabled = true;
 218    }
 219
 220    [LoggerMessage(LogLevel.Error, "Auto-save loop failed.")]
 221    partial void LogAutoSaveLoopFailed(Exception exception);
 222}
 223