< Summary

Information
Class: AmbientServices.StatusChecker
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/Status/StatusChecker.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 21
Uncovered lines: 0
Coverable lines: 21
Total lines: 220
Line coverage: 100%
Branch coverage
92%
Covered branches: 13
Total branches: 14
Branch coverage: 92.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_LatestResults()100%11100%
get_History()100%11100%
BeginStop()100%11100%
FinishStop()100%11100%
SetLatestResults(...)91.67%1212100%
GetStatus(...)100%11100%
Dispose(...)100%22100%
Dispose()100%11100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/Status/StatusChecker.cs

#LineLine coverage
 1using AmbientServices.Utilities;
 2using System;
 3using System.Collections.Concurrent;
 4using System.Collections.Generic;
 5using System.Threading;
 6using System.Threading.Tasks;
 7
 8namespace AmbientServices;
 9
 10/// <summary>
 11/// A abstract base class containing logic to compute status results for a particular part of the system.
 12/// Any non-abstract derivative of this class with an empty constructor will be automatically instantiated by the system
 13/// Derived classes do not have to be immutable, but they must be threadsafe.
 14/// StatusChecker is disposable because derived classes often contain things like mutexes and timers that require dispos
 15/// </summary>
 16/// <remarks>
 17/// <pitch>
 18/// The base unit of the status system: derive from this to report the health of one target system when the status is pa
 19/// When the status must be actively and periodically tested, derive from <see cref="StatusAuditor"/> instead, which lay
 20/// </pitch>
 21/// <pledge>
 22/// A checker is permanently bound to the single target system named at construction: results recorded through <see cref
 23/// <see cref="GetStatus"/> may be called from multiple threads simultaneously, must never throw (failures are converted
 24/// The owning <see cref="Status"/> drives the shutdown lifecycle in phases across all checkers — <see cref="BeginStop"/
 25/// </pledge>
 26/// <plan>
 27/// The latest results are held by interlocked exchange inside a private tracker, so publishing is lock-free and readers
 28/// </plan>
 29/// <pin><see cref="StatusResults"/></pin>
 30/// </remarks>
 31public abstract class StatusChecker : IDisposable
 32{
 33    private readonly StatusResultsTracker _resultsTracker;
 34    private bool _disposedValue;
 35
 36    /// <summary>
 37    /// Constructs a <see cref="StatusChecker"/>.
 38    /// </summary>
 39    /// <param name="targetSystem">The name of the target system (if any).</param>
 40    /// <remarks>
 41    /// Target system names are concatenated with ancestor and descendant nodes and used to aggregate errors from the sa
 42    /// Targets with a leading slash character indicate that the system is a shared system and may have status results m
 43    /// Shared targets are not concatenated to the targets indicated by ancestor nodes, and their parents are ignored du
 44    /// Defaults to null, but should almost always be set to a non-empty string.
 45    /// Null should only be used to indicate that this node is not related to any specific target system, which would pr
 46    /// </remarks>
 247    internal protected StatusChecker(string targetSystem)
 48    {
 249        TargetSystem = targetSystem;
 250        _resultsTracker = new StatusResultsTracker(StatusResults.GetPendingResults(null, targetSystem));
 251    }
 52
 53    /// <summary>
 54    /// Gets the name of the target system.
 55    /// </summary>
 56    public string TargetSystem { get; }
 57    /// <summary>
 58    /// Gets the latest status results.
 59    /// </summary>
 260    public StatusResults LatestResults => _resultsTracker.LatestResults;
 61    /// <summary>
 62    /// Gets an enumeration of previous <see cref="StatusResults"/>s.
 63    /// Empty if no such test results are available or applicable.
 64    /// Note that the history here is limited to a set time span and a set number of entries (see settings).
 65    /// </summary>
 266    public virtual IEnumerable<StatusResults> History => _resultsTracker.History;
 67
 68    /// <summary>
 69    /// Starts stopping any asynchronous activity.
 70    /// </summary>
 71    internal protected virtual ValueTask BeginStop()
 72    {
 273        return default;
 74    }
 75    /// <summary>
 76    /// Finishes stopping any asynchronous activity;
 77    /// </summary>
 78    internal protected virtual ValueTask FinishStop()
 79    {
 280        return default;
 81    }
 82
 83    /// <summary>
 84    /// Sets the latest results.
 85    /// </summary>
 86    /// <param name="newResults">The new <see cref="StatusResults"/>.  Note that null results will not be stored.</param
 87    internal protected void SetLatestResults(StatusResults newResults)
 88    {
 289        if (newResults != null)
 90        {
 291            if (!string.Equals(TargetSystem, newResults.TargetSystem, StringComparison.Ordinal)) throw new ArgumentExcep
 292            if (newResults.Report != null) Status.Logger.Filter("Results", newResults.Report.Alert?.Rating < StatusRatin
 293            _resultsTracker.SetLatestResults(newResults);
 94        }
 295    }
 96
 97    /// <summary>
 98    /// Gets whether or not this status node is applicable and should be included in the list of statuses for this machi
 99    /// </summary>
 100    internal protected abstract bool Applicable { get; }
 101    /// <summary>
 102    /// Computes the current status, returning a <see cref="StatusResults"/> containing information about the status.
 103    /// </summary>
 104    /// <remarks>
 105    /// Note that this function may be called on multiple threads simultaneously.
 106    /// Unlike <see cref="StatusAuditor.Audit(StatusResultsBuilder, CancellationToken)"/>, this method returns <see cref
 107    /// The reason for this is that <see cref="StatusAuditor"/> runs on a timer and generates status data on the fly eac
 108    /// This function is only called once when the status system is started and then again whenever <see cref="Status.Re
 109    /// As a result, some <see cref="StatusChecker"/> implementations may build a <see cref="StatusResults"/> during ini
 110    /// The default implementation simply returns <see cref="LatestResults"/>.
 111    /// Any exceptions should be caught and converted into meaningful <see cref="StatusResults"/>.
 112    /// Results should always be recorded using <see cref="SetLatestResults"/>.
 113    /// </remarks>
 114    /// <param name="cancel">A <see cref="CancellationToken"/> to cancel the operation before it finishes.</param>
 115    public virtual ValueTask<StatusResults> GetStatus(CancellationToken cancel = default)
 116    {
 117        // POSSIBLE BREAKING CHANGE: maybe it would be good to have a public function that
 118        // catches exceptions and handles them properly and also always saves results using SetLatestResults?
 2119        return TaskUtilities.ValueTaskFromResult(LatestResults);
 120    }
 121    /// <summary>
 122    /// Disposes of the instance.
 123    /// </summary>
 124    /// <param name="disposing">Whether the instance is being disposed (as opposed to finalized).</param>
 125    protected virtual void Dispose(bool disposing)
 126    {
 2127        if (!_disposedValue)
 128        {
 2129            if (disposing)
 130            {
 131                // TODO: dispose managed state (managed objects)
 132            }
 133
 134            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 135            // TODO: set large fields to null
 2136            _disposedValue = true;
 137        }
 2138    }
 139
 140    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 141    // ~StatusChecker()
 142    // {
 143    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 144    //     Dispose(disposing: false);
 145    // }
 146    /// <summary>
 147    /// Disposes of the instance.
 148    /// </summary>
 149    public void Dispose()
 150    {
 151        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 2152        Dispose(disposing: true);
 2153        GC.SuppressFinalize(this);
 2154    }
 155}
 156
 157/// <summary>
 158/// A class used to track the status results from a configured top-level status checker.
 159/// </summary>
 160internal class StatusResultsTracker
 161{
 162    private readonly ConcurrentQueue<StatusResults> _statusResultsHistory = new();
 163    private StatusResults _statusResults;                           // interlocked
 164
 165    public StatusResultsTracker(StatusResults pendingStatusResults)
 166    {
 167        _statusResults = pendingStatusResults;
 168    }
 169
 170    /// <summary>
 171    /// Gets the latest status results.
 172    /// </summary>
 173    public StatusResults LatestResults => _statusResults;
 174    /// <summary>
 175    /// Gets an enumeration of previous <see cref="StatusResults"/>s.
 176    /// Null or empty if no such test results are available or applicable.
 177    /// Note that previous ratings are limited to a set time span and a set number of entries (see settings).
 178    /// </summary>
 179    public IEnumerable<StatusResults> History => _statusResultsHistory;
 180
 181    /// <summary>
 182    /// Adds the specified results as the latest results, moving the previous results to the history.
 183    /// </summary>
 184    /// <param name="newResults">The new latest results.</param>
 185    /// <remarks>
 186    /// Note that the new results will replace the old results and the old results will briefly disappear before being p
 187    /// </remarks>
 188    public void SetLatestResults(StatusResults newResults)
 189    {
 190        StatusResults oldResults = Interlocked.Exchange(ref _statusResults, newResults);
 191        if (oldResults != null)
 192        {
 193            ConcurrentQueue<StatusResults> history = _statusResultsHistory;
 194            history.Enqueue(oldResults);
 195            // NOTE that there is a race here that might remove too many previous entries--this should be rare and not c
 196            TruncateQueue(_statusResultsHistory);
 197        }
 198    }
 199    private static readonly IAmbientSetting<int> _StatusResultsRetentionMinutes = AmbientSettings.GetAmbientSetting<int>
 200    private static readonly IAmbientSetting<int> _StatusResultsRetentionEntries = AmbientSettings.GetAmbientSetting<int>
 201    private static void TruncateQueue(ConcurrentQueue<StatusResults> queueToTruncate)
 202    {
 203        StatusResults? oldRatingResults;
 204        // only keep entries newer than that set retention period
 205        while (!queueToTruncate.IsEmpty)
 206        {
 207            // nothing in the queue?
 208            if (!queueToTruncate.TryPeek(out oldRatingResults)) break;
 209            DateTime time = (oldRatingResults.Report == null) ? oldRatingResults.Time : oldRatingResults.Report.AuditSta
 210            // first item in the queue is newer than the retention cutoff?
 211            if (time >= AmbientClock.UtcNow.AddMinutes(-_StatusResultsRetentionMinutes.Value)) break;
 212            queueToTruncate.TryDequeue(out oldRatingResults);
 213        }
 214        // only keep up to the configured number of values
 215        while (queueToTruncate.Count > _StatusResultsRetentionEntries.Value)
 216        {
 217            queueToTruncate.TryDequeue(out oldRatingResults);
 218        }
 219    }
 220}