|
| 1 | +using Intervals.NET; |
| 2 | +using Intervals.NET.Domain.Abstractions; |
| 3 | +using SlidingWindowCache.Public.Dto; |
| 4 | + |
| 5 | +namespace SlidingWindowCache.Public; |
| 6 | + |
| 7 | +/// <summary> |
| 8 | +/// Extension methods for <see cref="IWindowCache{TRange, TData, TDomain}"/> providing |
| 9 | +/// opt-in strong consistency mode on top of the default eventual consistency model. |
| 10 | +/// </summary> |
| 11 | +public static class WindowCacheExtensions |
| 12 | +{ |
| 13 | + /// <summary> |
| 14 | + /// Retrieves data for the specified range and waits for the cache to reach an idle |
| 15 | + /// state before returning, providing strong consistency semantics. |
| 16 | + /// </summary> |
| 17 | + /// <typeparam name="TRange"> |
| 18 | + /// The type representing the range boundaries. Must implement <see cref="IComparable{T}"/>. |
| 19 | + /// </typeparam> |
| 20 | + /// <typeparam name="TData"> |
| 21 | + /// The type of data being cached. |
| 22 | + /// </typeparam> |
| 23 | + /// <typeparam name="TDomain"> |
| 24 | + /// The type representing the domain of the ranges. Must implement <see cref="IRangeDomain{TRange}"/>. |
| 25 | + /// </typeparam> |
| 26 | + /// <param name="cache"> |
| 27 | + /// The cache instance to retrieve data from. |
| 28 | + /// </param> |
| 29 | + /// <param name="requestedRange"> |
| 30 | + /// The range for which to retrieve data. |
| 31 | + /// </param> |
| 32 | + /// <param name="cancellationToken"> |
| 33 | + /// A cancellation token to cancel the operation. Passed to both |
| 34 | + /// <see cref="IWindowCache{TRange, TData, TDomain}.GetDataAsync"/> and |
| 35 | + /// <see cref="IWindowCache{TRange, TData, TDomain}.WaitForIdleAsync"/>. |
| 36 | + /// </param> |
| 37 | + /// <returns> |
| 38 | + /// A task that represents the asynchronous operation. The task result contains a |
| 39 | + /// <see cref="RangeResult{TRange, TData}"/> with the actual available range and data, |
| 40 | + /// identical to what <see cref="IWindowCache{TRange, TData, TDomain}.GetDataAsync"/> returns. |
| 41 | + /// The task completes only after the cache has reached an idle state (no pending intent, |
| 42 | + /// no executing rebalance). |
| 43 | + /// </returns> |
| 44 | + /// <remarks> |
| 45 | + /// <para><strong>Default vs. Strong Consistency:</strong></para> |
| 46 | + /// <para> |
| 47 | + /// By default, <see cref="IWindowCache{TRange, TData, TDomain}.GetDataAsync"/> returns data |
| 48 | + /// immediately under an eventual consistency model: the user always receives correct data, |
| 49 | + /// but the cache window may still be converging toward its optimal configuration in the background. |
| 50 | + /// </para> |
| 51 | + /// <para> |
| 52 | + /// This method extends that with a wait: it calls <c>GetDataAsync</c> first (user data returned |
| 53 | + /// immediately from cache or <c>IDataSource</c>), then awaits |
| 54 | + /// <see cref="IWindowCache{TRange, TData, TDomain}.WaitForIdleAsync"/> before returning. |
| 55 | + /// The caller receives the same <see cref="RangeResult{TRange, TData}"/> as <c>GetDataAsync</c> |
| 56 | + /// would return, but the method does not complete until the cache has converged. |
| 57 | + /// </para> |
| 58 | + /// <para><strong>Composition:</strong></para> |
| 59 | + /// <code> |
| 60 | + /// // Equivalent to: |
| 61 | + /// var result = await cache.GetDataAsync(requestedRange, cancellationToken); |
| 62 | + /// await cache.WaitForIdleAsync(cancellationToken); |
| 63 | + /// return result; |
| 64 | + /// </code> |
| 65 | + /// <para><strong>When to Use:</strong></para> |
| 66 | + /// <list type="bullet"> |
| 67 | + /// <item><description> |
| 68 | + /// When the caller needs to assert or inspect the cache geometry after the request |
| 69 | + /// (e.g., verifying that a rebalance occurred or that the window has shifted). |
| 70 | + /// </description></item> |
| 71 | + /// <item><description> |
| 72 | + /// Cold start synchronization: waiting for the initial rebalance to complete before |
| 73 | + /// proceeding with subsequent operations. |
| 74 | + /// </description></item> |
| 75 | + /// <item><description> |
| 76 | + /// Integration tests that need deterministic cache state before making assertions. |
| 77 | + /// </description></item> |
| 78 | + /// </list> |
| 79 | + /// <para><strong>When NOT to Use:</strong></para> |
| 80 | + /// <list type="bullet"> |
| 81 | + /// <item><description> |
| 82 | + /// Hot paths: the idle wait adds latency proportional to the rebalance execution time |
| 83 | + /// (debounce delay + data fetching + cache update). For normal usage, prefer the default |
| 84 | + /// eventual consistency model via <see cref="IWindowCache{TRange, TData, TDomain}.GetDataAsync"/>. |
| 85 | + /// </description></item> |
| 86 | + /// <item><description> |
| 87 | + /// Rapid sequential requests: calling this method back-to-back means each call waits |
| 88 | + /// for the prior rebalance to complete, eliminating the debounce and work-avoidance |
| 89 | + /// benefits of the cache. |
| 90 | + /// </description></item> |
| 91 | + /// </list> |
| 92 | + /// <para><strong>Idle Semantics (Invariant H.49):</strong></para> |
| 93 | + /// <para> |
| 94 | + /// The idle wait uses "was idle at some point" semantics inherited from |
| 95 | + /// <see cref="IWindowCache{TRange, TData, TDomain}.WaitForIdleAsync"/>. This is sufficient |
| 96 | + /// for the strong consistency use cases above: after the await, the cache has converged at |
| 97 | + /// least once since the request. New activity may begin immediately after, but the |
| 98 | + /// cache state observed at the idle point reflects the completed rebalance. |
| 99 | + /// </para> |
| 100 | + /// <para><strong>Exception Propagation:</strong></para> |
| 101 | + /// <list type="bullet"> |
| 102 | + /// <item><description> |
| 103 | + /// If <c>GetDataAsync</c> throws (e.g., <see cref="ObjectDisposedException"/>, |
| 104 | + /// <see cref="OperationCanceledException"/>), the exception propagates immediately and |
| 105 | + /// <c>WaitForIdleAsync</c> is never called. |
| 106 | + /// </description></item> |
| 107 | + /// <item><description> |
| 108 | + /// If <c>WaitForIdleAsync</c> throws (e.g., <see cref="OperationCanceledException"/> via |
| 109 | + /// <paramref name="cancellationToken"/>), the exception propagates. The data returned by |
| 110 | + /// <c>GetDataAsync</c> is discarded. |
| 111 | + /// </description></item> |
| 112 | + /// </list> |
| 113 | + /// <para><strong>Example:</strong></para> |
| 114 | + /// <code> |
| 115 | + /// // Strong consistency: returns only after cache has converged |
| 116 | + /// var result = await cache.GetDataAndWaitForIdleAsync( |
| 117 | + /// Range.Closed(100, 200), |
| 118 | + /// cancellationToken); |
| 119 | + /// |
| 120 | + /// // Cache geometry is now fully converged — safe to inspect or assert |
| 121 | + /// if (result.Range.HasValue) |
| 122 | + /// ProcessData(result.Data); |
| 123 | + /// </code> |
| 124 | + /// </remarks> |
| 125 | + public static async ValueTask<RangeResult<TRange, TData>> GetDataAndWaitForIdleAsync<TRange, TData, TDomain>( |
| 126 | + this IWindowCache<TRange, TData, TDomain> cache, |
| 127 | + Range<TRange> requestedRange, |
| 128 | + CancellationToken cancellationToken = default) |
| 129 | + where TRange : IComparable<TRange> |
| 130 | + where TDomain : IRangeDomain<TRange> |
| 131 | + { |
| 132 | + var result = await cache.GetDataAsync(requestedRange, cancellationToken); |
| 133 | + await cache.WaitForIdleAsync(cancellationToken); |
| 134 | + return result; |
| 135 | + } |
| 136 | +} |
0 commit comments