< Summary

Information
Class: AmbientServices.StatusResultsTracker
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/Status/StatusChecker.cs
Tag: 332_35464845198
Line coverage
95%
Covered lines: 21
Uncovered lines: 1
Coverable lines: 22
Total lines: 220
Line coverage: 95.4%
Branch coverage
83%
Covered branches: 10
Total branches: 12
Branch coverage: 83.3%
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%
SetLatestResults(...)100%22100%
.cctor()100%11100%
TruncateQueue(...)80%101087.5%

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>
 47    internal protected StatusChecker(string targetSystem)
 48    {
 49        TargetSystem = targetSystem;
 50        _resultsTracker = new StatusResultsTracker(StatusResults.GetPendingResults(null, targetSystem));
 51    }
 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>
 60    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>
 66    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    {
 73        return default;
 74    }
 75    /// <summary>
 76    /// Finishes stopping any asynchronous activity;
 77    /// </summary>
 78    internal protected virtual ValueTask FinishStop()
 79    {
 80        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    {
 89        if (newResults != null)
 90        {
 91            if (!string.Equals(TargetSystem, newResults.TargetSystem, StringComparison.Ordinal)) throw new ArgumentExcep
 92            if (newResults.Report != null) Status.Logger.Filter("Results", newResults.Report.Alert?.Rating < StatusRatin
 93            _resultsTracker.SetLatestResults(newResults);
 94        }
 95    }
 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?
 119        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    {
 127        if (!_disposedValue)
 128        {
 129            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
 136            _disposedValue = true;
 137        }
 138    }
 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
 152        Dispose(disposing: true);
 153        GC.SuppressFinalize(this);
 154    }
 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{
 2162    private readonly ConcurrentQueue<StatusResults> _statusResultsHistory = new();
 163    private StatusResults _statusResults;                           // interlocked
 164
 2165    public StatusResultsTracker(StatusResults pendingStatusResults)
 166    {
 2167        _statusResults = pendingStatusResults;
 2168    }
 169
 170    /// <summary>
 171    /// Gets the latest status results.
 172    /// </summary>
 2173    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>
 2179    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    {
 2190        StatusResults oldResults = Interlocked.Exchange(ref _statusResults, newResults);
 2191        if (oldResults != null)
 192        {
 2193            ConcurrentQueue<StatusResults> history = _statusResultsHistory;
 2194            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
 2196            TruncateQueue(_statusResultsHistory);
 197        }
 2198    }
 2199    private static readonly IAmbientSetting<int> _StatusResultsRetentionMinutes = AmbientSettings.GetAmbientSetting<int>
 2200    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
 2205        while (!queueToTruncate.IsEmpty)
 206        {
 207            // nothing in the queue?
 2208            if (!queueToTruncate.TryPeek(out oldRatingResults)) break;
 2209            DateTime time = (oldRatingResults.Report == null) ? oldRatingResults.Time : oldRatingResults.Report.AuditSta
 210            // first item in the queue is newer than the retention cutoff?
 2211            if (time >= AmbientClock.UtcNow.AddMinutes(-_StatusResultsRetentionMinutes.Value)) break;
 0212            queueToTruncate.TryDequeue(out oldRatingResults);
 213        }
 214        // only keep up to the configured number of values
 2215        while (queueToTruncate.Count > _StatusResultsRetentionEntries.Value)
 216        {
 2217            queueToTruncate.TryDequeue(out oldRatingResults);
 218        }
 2219    }
 220}