< Summary

Information
Class: AmbientServices.CallContextCostTracker
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/DefaultImplementation/BasicAmbientCostTracker.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 31
Uncovered lines: 0
Coverable lines: 31
Total lines: 497
Line coverage: 100%
Branch coverage
100%
Covered branches: 4
Total branches: 4
Branch coverage: 100%
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_ChargeCount()100%11100%
get_AccumulatedChargeSum()100%11100%
get_CostChangeCount()100%11100%
get_AccumulatedCostChangeSum()100%11100%
OnChargesAccrued(...)100%11100%
OnOngoingCostChanged(...)100%11100%
Dispose(...)100%44100%
Dispose()100%11100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/DefaultImplementation/BasicAmbientCostTracker.cs

#LineLine coverage
 1#if NET5_0_OR_GREATER
 2
 3using System;
 4using System.Collections.Concurrent;
 5using System.Threading;
 6using System.Threading.Tasks;
 7
 8namespace AmbientServices;
 9
 10/// <summary>
 11/// A basic default implementation of <see cref="IAmbientCostTracker"/> that broadcasts every cost report to registered 
 12/// </summary>
 13/// <remarks>
 14/// <pitch>The zero-configuration, in-process cost tracker used unless overridden.  Each report costs only a sink fan-ou
 15/// <pledge><see cref="IAmbientCostTracker"/></pledge>
 16/// <plan>Entirely stateless except for the sink set (a <see cref="ConcurrentHashSet{T}"/>, so registration is idempoten
 17/// </remarks>
 18[DefaultAmbientService]
 19internal class BasicAmbientCostTracker : IAmbientCostTracker
 20{
 21    private readonly ConcurrentHashSet<IAmbientCostTrackerNotificationSink> _notificationSinks = new();
 22
 23    public BasicAmbientCostTracker()
 24    {
 25    }
 26
 27    /// <summary>
 28    /// Notifies the notification sink that charges have accrued.
 29    /// </summary>
 30    /// <param name="serviceId">An optional service identifier, with empty string indicating the system itself.</param>
 31    /// <param name="customerId">A string identifying the customer.</param>
 32    /// <param name="charge">The charge (in picodollars).</param>
 33    public void OnChargesAccrued(string serviceId, string customerId, long charge)
 34    {
 35        // call all the notification sinks
 36        foreach (IAmbientCostTrackerNotificationSink notificationSink in _notificationSinks)
 37        {
 38            notificationSink.OnChargesAccrued(serviceId, customerId, charge);
 39        }
 40    }
 41    /// <summary>
 42    /// Notifies the notification sink that an ongoing cost has changed.
 43    /// </summary>
 44    /// <param name="serviceId">An optional service identifier, with empty string indicating the system itself.</param>
 45    /// <param name="customerId">A string identifying the customer.</param>
 46    /// <param name="changePerMonth">The change in cost (in picodollars per month).</param>
 47    public void OnOngoingCostChanged(string serviceId, string customerId, long changePerMonth)
 48    {
 49        // call all the notification sinks
 50        foreach (IAmbientCostTrackerNotificationSink notificationSink in _notificationSinks)
 51        {
 52            notificationSink.OnOngoingCostChanged(serviceId, customerId, changePerMonth);
 53        }
 54    }
 55    /// <summary>
 56    /// Registers a cost tracker notification sink with this ambient service profiler.
 57    /// </summary>
 58    /// <param name="sink">An <see cref="IAmbientCostTrackerNotificationSink"/> that will receive notifications as charg
 59    /// <returns>true if the registration was successful, false if the specified sink was already registered.</returns>
 60    public bool RegisterCostTrackerNotificationSink(IAmbientCostTrackerNotificationSink sink)
 61    {
 62        return _notificationSinks.Add(sink);
 63    }
 64    /// <summary>
 65    /// Deregisters a cost tracker notification sink with this ambient service profiler.
 66    /// </summary>
 67    /// <param name="sink">An <see cref="IAmbientCostTrackerNotificationSink"/> that will receive notifications as charg
 68    /// <returns>true if the deregistration was successful, false if the specified sink was not registered.</returns>
 69    public bool DeregisterCostTrackerNotificationSink(IAmbientCostTrackerNotificationSink sink)
 70    {
 71        return _notificationSinks.Remove(sink);
 72    }
 73}
 74
 75/// <summary>
 76/// A class that tracks service profile statistics across multiple call contexts in a process or a single time window.
 77/// </summary>
 78/// <remarks>
 79/// <pitch>The all-contexts cost accumulator: hook it to a cost tracker and it totals every charge and ongoing-cost chan
 80/// <pledge><see cref="IAmbientAccruedChargesAndCostChanges"/></pledge>
 81/// <pledge><see cref="IAmbientCostTrackerNotificationSink"/></pledge>
 82/// <plan>Registers with the <see cref="IAmbientCostTracker"/> at construction and deregisters on disposal (or on the in
 83/// </remarks>
 84internal class ProcessOrSingleTimeWindowCostTracker : IAmbientAccruedChargesAndCostChanges, IAmbientCostTrackerNotificat
 85{
 86    private readonly IAmbientCostTracker _profiler;
 87    private readonly ConcurrentDictionary<string, ChargeAccumulator> _chargeAccumulatorsByService = new();
 88    private readonly ConcurrentDictionary<string, ChargeAccumulator> _chargeAccumulatorsByCustomer = new();
 89    private readonly ConcurrentDictionary<string, CostAccumulator> _costAccumulatorsByService = new();
 90    private readonly ConcurrentDictionary<string, CostAccumulator> _costAccumulatorsByCustomer = new();
 91    private int _chargeCount;       // interlocked
 92    private int _costChangeCount;   // interlocked
 93    private long _totalCharges;     // interlocked
 94    private long _totalCostChange;  // interlocked
 95    private bool _disposedValue;
 96
 97    public string ScopeName { get; }
 98
 99    public int ChargeCount => _chargeCount;
 100    public long AccumulatedChargeSum => _totalCharges;
 101    public int CostChangeCount => _costChangeCount;
 102    public long AccumulatedCostChangeSum => _totalCostChange;
 103
 104    public ProcessOrSingleTimeWindowCostTracker(IAmbientCostTracker metrics, string scopeName)
 105    {
 106        _profiler = metrics;
 107        ScopeName = scopeName;
 108        _profiler.RegisterCostTrackerNotificationSink(this);
 109    }
 110    /// <summary>
 111    /// Notifies the notification sink that charges have accrued.
 112    /// </summary>
 113    /// <param name="serviceId">An optional service identifier, with empty string indicating the system itself.</param>
 114    /// <param name="customerId">A string identifying the customer.</param>
 115    /// <param name="charge">The charge (in picodollars).</param>
 116    public void OnChargesAccrued(string serviceId, string customerId, long charge)
 117    {
 118        // track the charges per service
 119        ChargeAccumulator.Accrue(_chargeAccumulatorsByService, serviceId, charge);
 120        // track the charges per customer
 121        ChargeAccumulator.Accrue(_chargeAccumulatorsByCustomer, customerId, charge);
 122        // track the total cost
 123        Interlocked.Add(ref _totalCharges, charge);
 124        // track the number of charges
 125        Interlocked.Increment(ref _chargeCount);
 126    }
 127    /// <summary>
 128    /// Notifies the notification sink that an ongoing cost has changed.
 129    /// </summary>
 130    /// <param name="serviceId">An optional service identifier, with empty string indicating the system itself.</param>
 131    /// <param name="customerId">A string identifying the customer.</param>
 132    /// <param name="changePerMonth">The change in cost (in picodollars per month).</param>
 133    public void OnOngoingCostChanged(string serviceId, string customerId, long changePerMonth)
 134    {
 135        // track the cost changes per service
 136        CostAccumulator.ChangeCost(_costAccumulatorsByService, serviceId, changePerMonth);
 137        // track the cost changes per customer
 138        CostAccumulator.ChangeCost(_costAccumulatorsByCustomer, customerId, changePerMonth);
 139        // track the total cost
 140        Interlocked.Add(ref _totalCostChange, changePerMonth);
 141        // track the number of charges
 142        Interlocked.Increment(ref _costChangeCount);
 143    }
 144    internal void CloseTracking()
 145    {
 146        _profiler.DeregisterCostTrackerNotificationSink(this);
 147        _disposedValue = true;
 148    }
 149
 150    protected virtual void Dispose(bool disposing)
 151    {
 152        if (!_disposedValue)
 153        {
 154            if (disposing)
 155            {
 156                // TODO: dispose managed state (managed objects)
 157                _profiler.DeregisterCostTrackerNotificationSink(this);
 158            }
 159
 160            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 161            // TODO: set large fields to null
 162            _disposedValue = true;
 163        }
 164    }
 165
 166    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 167    // ~ProcessingDistributionAccumulator()
 168    // {
 169    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 170    //     Dispose(disposing: false);
 171    // }
 172
 173    /// <summary>
 174    /// Disposes of this instance.
 175    /// </summary>
 176    public void Dispose()
 177    {
 178        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 179        Dispose(disposing: true);
 180        GC.SuppressFinalize(this);
 181    }
 182}
 183
 184/// <summary>
 185/// A class that fans cost notifications out to the sinks registered within one call context.
 186/// </summary>
 187/// <remarks>
 188/// <pitch>The fan-out node for one call context's cost stream: call-context cost trackers register here rather than wit
 189/// <pledge><see cref="IAmbientCostTrackerNotificationSink"/></pledge>
 190/// <plan>A <see cref="ConcurrentHashSet{T}"/> of sinks with synchronous fan-out; idempotent registration.</plan>
 191/// </remarks>
 192internal class ScopeOnChargesAccruedDistributor : IAmbientCostTrackerNotificationSink
 193{
 194    private readonly ConcurrentHashSet<IAmbientCostTrackerNotificationSink> _notificationSinks = new();
 195    /// <summary>
 196    /// Notifies the notification sink that charges have accrued.
 197    /// </summary>
 198    /// <param name="serviceId">An optional service identifier, with empty string indicating the system itself.</param>
 199    /// <param name="customerId">A string identifying the customer.</param>
 200    /// <param name="charge">The charge (in picodollars).</param>
 201    public void OnChargesAccrued(string serviceId, string customerId, long charge)
 202    {
 203        foreach (IAmbientCostTrackerNotificationSink notificationSink in _notificationSinks)
 204        {
 205            notificationSink.OnChargesAccrued(serviceId, customerId, charge);
 206        }
 207    }
 208    /// <summary>
 209    /// Notifies the notification sink that an ongoing cost has changed.
 210    /// </summary>
 211    /// <param name="serviceId">An optional service identifier, with empty string indicating the system itself.</param>
 212    /// <param name="customerId">A string identifying the customer.</param>
 213    /// <param name="changePerMonth">The change in cost (in picodollars per month).</param>
 214    public void OnOngoingCostChanged(string serviceId, string customerId, long changePerMonth)
 215    {
 216        foreach (IAmbientCostTrackerNotificationSink notificationSink in _notificationSinks)
 217        {
 218            notificationSink.OnOngoingCostChanged(serviceId, customerId, changePerMonth);
 219        }
 220    }
 221    public bool RegisterSystemSwitchedNotificationSink(IAmbientCostTrackerNotificationSink sink)
 222    {
 223        return _notificationSinks.Add(sink);
 224    }
 225    public bool DeregisterSystemSwitchedNotificationSink(IAmbientCostTrackerNotificationSink sink)
 226    {
 227        return _notificationSinks.Remove(sink);
 228    }
 229}
 230
 231/// <summary>
 232/// A class that tracks service profile statistics for a specific call context.
 233/// </summary>
 234/// <remarks>
 235/// <pitch>The per-request cost accumulator: totals the charges and ongoing-cost changes reported within one call contex
 236/// <pledge><see cref="IAmbientAccruedChargesAndCostChanges"/></pledge>
 237/// <pledge><see cref="IAmbientCostTrackerNotificationSink"/></pledge>
 238/// <plan>Registers with its call context's <see cref="ScopeOnChargesAccruedDistributor"/> at construction and deregiste
 239/// </remarks>
 240internal class CallContextCostTracker : IAmbientAccruedChargesAndCostChanges, IAmbientCostTrackerNotificationSink, IDisp
 241{
 242    private readonly ScopeOnChargesAccruedDistributor _distributor;
 2243    private readonly ConcurrentDictionary<string, ChargeAccumulator> _accumulatorsByService = new();
 2244    private readonly ConcurrentDictionary<string, ChargeAccumulator> _accumulatorsByCustomer = new();
 2245    private readonly ConcurrentDictionary<string, CostAccumulator> _costAccumulatorsByService = new();
 2246    private readonly ConcurrentDictionary<string, CostAccumulator> _costAccumulatorsByCustomer = new();
 247    private int _chargeCount;       // interlocked
 248    private int _costChangeCount;   // interlocked
 249    private long _totalCharges;     // interlocked
 250    private long _totalCostChange;  // interlocked
 251    private bool _disposedValue;
 252
 253    public string ScopeName { get; }
 2254    public int ChargeCount => _chargeCount;
 2255    public long AccumulatedChargeSum => _totalCharges;
 2256    public int CostChangeCount => _costChangeCount;
 2257    public long AccumulatedCostChangeSum => _totalCostChange;
 258
 259    /// <summary>
 260    /// Constructs a CallContextCostTracker.
 261    /// </summary>
 262    /// <param name="distributor">A <see cref="ScopeOnChargesAccruedDistributor"/> to hook into to receive system change
 263    /// <param name="scopeName">The name of the call context being tracked.</param>
 2264    public CallContextCostTracker(ScopeOnChargesAccruedDistributor distributor, string scopeName)
 265    {
 2266        _distributor = distributor;
 2267        ScopeName = scopeName;
 2268        distributor.RegisterSystemSwitchedNotificationSink(this);
 2269    }
 270    /// <summary>
 271    /// Notifies the notification sink that charges have accrued.
 272    /// </summary>
 273    /// <param name="serviceId">An optional service identifier, with empty string indicating the system itself.</param>
 274    /// <param name="customerId">A string identifying the customer.</param>
 275    /// <param name="charge">The charge (in picodollars).</param>
 276    public void OnChargesAccrued(string serviceId, string customerId, long charge)
 277    {
 278        // track the charges per service
 2279        ChargeAccumulator.Accrue(_accumulatorsByService, serviceId, charge);
 280        // track the charges per customer
 2281        ChargeAccumulator.Accrue(_accumulatorsByCustomer, customerId, charge);
 282        // track the total charges
 2283        Interlocked.Add(ref _totalCharges, charge);
 284        // track the number of charges
 2285        Interlocked.Increment(ref _chargeCount);
 2286    }
 287    /// <summary>
 288    /// Notifies the notification sink that an ongoing cost has changed.
 289    /// </summary>
 290    /// <param name="serviceId">An optional service identifier, with empty string indicating the system itself.</param>
 291    /// <param name="customerId">A string identifying the customer.</param>
 292    /// <param name="changePerMonth">The change in cost (in picodollars per month).</param>
 293    public void OnOngoingCostChanged(string serviceId, string customerId, long changePerMonth)
 294    {
 295        // track the cost change per service
 2296        CostAccumulator.ChangeCost(_costAccumulatorsByService, serviceId, changePerMonth);
 297        // track the cost change per customer
 2298        CostAccumulator.ChangeCost(_costAccumulatorsByCustomer, customerId, changePerMonth);
 299        // track the total cost change
 2300        Interlocked.Add(ref _totalCostChange, changePerMonth);
 301        // track the number of charges
 2302        Interlocked.Increment(ref _costChangeCount);
 2303    }
 304
 305    protected virtual void Dispose(bool disposing)
 306    {
 2307        if (!_disposedValue)
 308        {
 2309            if (disposing)
 310            {
 311                // TODO: dispose managed state (managed objects)
 2312                _distributor.DeregisterSystemSwitchedNotificationSink(this);
 313            }
 314
 315            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 316            // TODO: set large fields to null
 2317            _disposedValue = true;
 318        }
 2319    }
 320
 321    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 322    // ~CallContextCostTracker()
 323    // {
 324    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 325    //     Dispose(disposing: false);
 326    // }
 327
 328    /// <summary>
 329    /// Disposes of this instance.
 330    /// </summary>
 331    public void Dispose()
 332    {
 333        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 2334        Dispose(disposing: true);
 2335        GC.SuppressFinalize(this);
 2336    }
 337}
 338
 339/// <summary>
 340/// A class that tracks service profile statistics for a moving time window.
 341/// </summary>
 342/// <remarks>
 343/// <pitch>Periodic cost reporting: every window of the configured size yields a finished <see cref="IAmbientAccruedChar
 344/// <pledge>Each report is delivered once, after its window closes; costs reported near a boundary land in whichever win
 345/// <plan>An <see cref="AmbientEventTimer"/> rotates a <see cref="ProcessOrSingleTimeWindowCostTracker"/> atomically (vi
 346/// </remarks>
 347internal class TimeWindowCostTracker : IDisposable
 348{
 349    private readonly string _scopeNamePrefix;
 350    private readonly AmbientEventTimer _timeWindowRotator;
 351    private ProcessOrSingleTimeWindowCostTracker? _timeWindowCallContextCollector;  // interlocked
 352    private bool _disposedValue;
 353
 354    /// <summary>
 355    /// Constructs a TimeWindowProcessingDistributionTracker.
 356    /// </summary>
 357    /// <param name="metrics">A <see cref="IAmbientCostTracker"/> to hook into to receive processor change events.</para
 358    /// <param name="scopeNamePrefix">A <see cref="TimeSpan"/> indicating the size of the window.</param>
 359    /// <param name="windowPeriod">A <see cref="TimeSpan"/> indicating how often reports are desired.</param>
 360    /// <param name="onWindowComplete">An async delegate that receives a <see cref="IAmbientAccruedChargesAndCostChanges
 361    public TimeWindowCostTracker(IAmbientCostTracker metrics, string scopeNamePrefix, TimeSpan windowPeriod, Func<IAmbie
 362    {
 363        if (onWindowComplete == null) throw new ArgumentNullException(nameof(onWindowComplete), "Time Window Collection 
 364        _scopeNamePrefix = scopeNamePrefix;
 365        using (Rotate(metrics, windowPeriod)) { }
 366        _timeWindowRotator = new AmbientEventTimer(windowPeriod);
 367        _timeWindowRotator.Elapsed +=
 368            async (sender, handler) =>
 369            {
 370                using ProcessOrSingleTimeWindowCostTracker? oldAccumulator = Rotate(metrics, windowPeriod);
 371                if (oldAccumulator != null)
 372                {
 373                    await onWindowComplete(oldAccumulator);
 374                }
 375            };
 376        _timeWindowRotator.AutoReset = true;
 377        _timeWindowRotator.Enabled = true;
 378    }
 379
 380    private ProcessOrSingleTimeWindowCostTracker? Rotate(IAmbientCostTracker metrics, TimeSpan windowPeriod)
 381    {
 382        string windowName = WindowScope.WindowId(AmbientClock.UtcNow, windowPeriod);
 383        string newAccumulatorScopeName = _scopeNamePrefix + windowName + "(" + WindowScope.WindowSize(windowPeriod) + ")
 384        ProcessOrSingleTimeWindowCostTracker newAccumulator = new(metrics, newAccumulatorScopeName);
 385        ProcessOrSingleTimeWindowCostTracker? oldAccumulator = Interlocked.Exchange(ref _timeWindowCallContextCollector,
 386        oldAccumulator?.CloseTracking();
 387        return oldAccumulator;
 388    }
 389
 390    protected virtual void Dispose(bool disposing)
 391    {
 392        if (!_disposedValue)
 393        {
 394            if (disposing)
 395            {
 396                // TODO: dispose managed state (managed objects)
 397                _timeWindowRotator.Dispose();
 398            }
 399
 400            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 401            // TODO: set large fields to null
 402            _disposedValue = true;
 403        }
 404    }
 405
 406    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 407    // ~ScopeProcessingDistributionTracker()
 408    // {
 409    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 410    //     Dispose(disposing: false);
 411    // }
 412
 413    /// <summary>
 414    /// Disposes of this instance.
 415    /// </summary>
 416    public void Dispose()
 417    {
 418        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 419        Dispose(disposing: true);
 420        GC.SuppressFinalize(this);
 421    }
 422}
 423/// <summary>
 424/// A class that accumulates charges for some scope.
 425/// </summary>
 426/// <remarks>
 427/// <pitch>A tiny thread-safe accumulator pairing a charge count with a running charge total, for per-service and per-cu
 428/// <pledge>Construction records the first charge; <see cref="AddCharge"/> atomically bumps the count and adds to the to
 429/// </remarks>
 430public class ChargeAccumulator
 431{
 432    private long _chargeCount;      // interlocked
 433    private long _totalCharges;     // interlocked
 434
 435    /// <summary>
 436    /// Constructs a charge accumulator.
 437    /// </summary>
 438    /// <param name="charge">The initial charge.</param>
 439    public ChargeAccumulator(long charge)
 440    {
 441        _chargeCount = 1;
 442        _totalCharges = charge;
 443    }
 444
 445    internal static void Accrue(ConcurrentDictionary<string, ChargeAccumulator> chargeAccumulators, string key, long cha
 446    {
 447        chargeAccumulators.AddOrUpdate(key, new ChargeAccumulator(charge), (k, v) => { v.AddCharge(charge); return v; })
 448    }
 449
 450    /// <summary>
 451    /// Adds a charge to the accumulator.
 452    /// </summary>
 453    /// <param name="charge">The charge amount (in picodollars).</param>
 454    public void AddCharge(long charge)
 455    {
 456        Interlocked.Increment(ref _chargeCount);
 457        Interlocked.Add(ref _totalCharges, charge);
 458    }
 459}
 460/// <summary>
 461/// A class that accumulates cost for some scope.
 462/// </summary>
 463/// <remarks>
 464/// <pitch>A tiny thread-safe accumulator pairing a change count with a running total of ongoing-cost-rate changes, for 
 465/// <pledge>Construction records the first change; <see cref="AddCostChange"/> atomically bumps the count and adds the (
 466/// </remarks>
 467public class CostAccumulator
 468{
 469    private long _chargeCount;                  // interlocked
 470    private long _totalCostPerMonthChange;     // interlocked
 471
 472    /// <summary>
 473    /// Constructs a cost accumulator.
 474    /// </summary>
 475    /// <param name="costPerMonthChange">The initial change in cost.</param>
 476    public CostAccumulator(long costPerMonthChange)
 477    {
 478        _chargeCount = 1;
 479        _totalCostPerMonthChange = costPerMonthChange;
 480    }
 481
 482    internal static void ChangeCost(ConcurrentDictionary<string, CostAccumulator> chargeAccumulators, string key, long c
 483    {
 484        chargeAccumulators.AddOrUpdate(key, new CostAccumulator(costPerMonthChange), (k, v) => { v.AddCostChange(costPer
 485    }
 486
 487    /// <summary>
 488    /// Adds a cost change to the accumulator.
 489    /// </summary>
 490    /// <param name="costPerMonthChange">The cost change (in picodollars per month).</param>
 491    public void AddCostChange(long costPerMonthChange)
 492    {
 493        Interlocked.Increment(ref _chargeCount);
 494        Interlocked.Add(ref _totalCostPerMonthChange, costPerMonthChange);
 495    }
 496}
 497#endif