Skip to content

Commit 2cfdcad

Browse files
blaze6950Mykyta ZotovCopilot
authored
Feature/implement strong consistency rebalance mode (#6)
* feat(extensions): implement strong consistency mode with GetDataAndWaitForIdleAsync method; docs: update overview, public API, architecture, and README to reflect strong consistency mode; test: add integration and unit tests for strong consistency mode functionality * refactor(tests): replace rebalance request calls with idle wait logic * Update tests/SlidingWindowCache.Unit.Tests/Public/WindowCacheExtensionsTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/SlidingWindowCache.Integration.Tests/StrongConsistencyModeTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/SlidingWindowCache.Integration.Tests/StrongConsistencyModeTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * test: update assertion to use IsAssignableFrom for exception type validation * test: remove obsolete cancellation test for GetDataAndWaitForIdleAsync --------- Co-authored-by: Mykyta Zotov <mykyta.zotov@ihsmarkit.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 5374144 commit 2cfdcad

10 files changed

Lines changed: 1075 additions & 27 deletions

File tree

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,33 @@ Canonical guide: `docs/diagnostics.md`.
320320
6. `docs/state-machine.md` — formal state transitions and mutation ownership
321321
7. `docs/actors.md` — actor responsibilities and execution contexts
322322

323+
## Strong Consistency Mode
324+
325+
By default, `GetDataAsync` is **eventually consistent**: data is returned immediately while the cache window converges asynchronously in the background. For scenarios where you need the cache to be fully converged before proceeding, use the `GetDataAndWaitForIdleAsync` extension method:
326+
327+
```csharp
328+
using SlidingWindowCache.Public;
329+
330+
// Returns only after cache has converged to its desired window geometry
331+
var result = await cache.GetDataAndWaitForIdleAsync(
332+
Range.Closed(100, 200),
333+
cancellationToken);
334+
335+
// Cache geometry is now stable — safe to inspect, assert, or rely on
336+
if (result.Range.HasValue)
337+
ProcessData(result.Data);
338+
```
339+
340+
This is a thin composition of `GetDataAsync` followed by `WaitForIdleAsync`. The returned `RangeResult` is identical to what `GetDataAsync` would return.
341+
342+
**When to use:**
343+
- Cold start synchronization: waiting for the initial cache window to be built before proceeding
344+
- Integration testing: asserting on cache geometry after a request
345+
- Any scenario where you want to know the cache has finished rebalancing before moving on
346+
347+
**When NOT to use:**
348+
- Hot paths or rapid sequential requests — each call waits for full rebalance, which includes the debounce delay plus data fetching. For normal usage, the default eventual consistency model is faster.
349+
323350
### Deterministic Testing
324351

325352
`WaitForIdleAsync()` provides race-free synchronization with background operations for tests. Uses "was idle at some point" semantics — does not guarantee still idle after completion. See `docs/invariants.md` (Activity tracking invariants).

docs/architecture.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ Idle detection requires state-based semantics: when the system becomes idle, ALL
197197

198198
**"Was idle" semantics — not "is idle":** `WaitForIdleAsync` completes when the system was idle at some point. It does not guarantee the system is still idle after completion. This is correct for eventual consistency models. Callers requiring stronger guarantees must re-check state after await.
199199

200+
**Opt-in strong consistency mode:** For scenarios that require the cache to be fully converged before proceeding, the `GetDataAndWaitForIdleAsync` extension method on `IWindowCache` composes `GetDataAsync` and `WaitForIdleAsync` into a single call. This provides a convenient strong consistency mode on top of the default eventual consistency model, at the cost of waiting for rebalance to complete. See `README.md` and `docs/components/public-api.md` for usage details.
201+
200202
---
201203

202204
## Single Cache Instance = Single Consumer

docs/components/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The system is easier to reason about when components are grouped by:
1717
### Top-Level Component Roles
1818

1919
- Public facade: `WindowCache<TRange, TData, TDomain>`
20+
- Public extensions: `WindowCacheExtensions` — opt-in strong consistency mode (`GetDataAndWaitForIdleAsync`)
2021
- User Path: assembles requested data and publishes intent
2122
- Intent loop: observes latest intent and runs analytical validation
2223
- Execution: performs debounced, cancellable rebalance work and mutates cache state
@@ -30,7 +31,6 @@ The system is easier to reason about when components are grouped by:
3031
- `docs/components/execution.md`
3132
- `docs/components/state-and-storage.md`
3233
- `docs/components/infrastructure.md`
33-
3434
### Ownership (Conceptual)
3535

3636
`WindowCache` is the composition root. Internals are constructed once and live for the cache lifetime. Disposal cascades through owned components.

docs/components/public-api.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,42 @@ Optional observability interface with 18 event recording methods covering:
109109

110110
> ⚠️ **Critical**: `RebalanceExecutionFailed` is the only event that signals a background exception. Always wire this in production code.
111111
112+
## Extensions
113+
114+
### WindowCacheExtensions
115+
116+
**File**: `src/SlidingWindowCache/Public/WindowCacheExtensions.cs`
117+
118+
**Type**: `static class` (extension methods on `IWindowCache<TRange, TData, TDomain>`)
119+
120+
Provides opt-in strong consistency mode on top of the default eventual consistency model.
121+
122+
#### GetDataAndWaitForIdleAsync
123+
124+
```csharp
125+
ValueTask<RangeResult<TRange, TData>> GetDataAndWaitForIdleAsync<TRange, TData, TDomain>(
126+
this IWindowCache<TRange, TData, TDomain> cache,
127+
Range<TRange> requestedRange,
128+
CancellationToken cancellationToken = default)
129+
```
130+
131+
Composes `GetDataAsync` + `WaitForIdleAsync` into a single call. Returns the same `RangeResult<TRange, TData>` as `GetDataAsync`, but does not complete until the cache has reached an idle state (no pending intent, no executing rebalance).
132+
133+
**When to use:**
134+
- Asserting or inspecting cache geometry after a request (e.g., verifying a rebalance occurred)
135+
- Cold start synchronization before subsequent operations
136+
- Integration tests that require deterministic cache state before making assertions
137+
138+
**When NOT to use:**
139+
- Hot paths — the idle wait adds latency equal to the full rebalance cycle (debounce delay + data fetch + cache update)
140+
- Rapid sequential requests — eliminates debounce and work-avoidance benefits
141+
142+
**Idle semantics**: Inherits "was idle at some point" semantics from `WaitForIdleAsync` (Invariant H.49). Sufficient for all strong consistency use cases.
143+
144+
**Exception propagation**: If `GetDataAsync` throws, `WaitForIdleAsync` is never called. If `WaitForIdleAsync` throws, the `GetDataAsync` result is discarded.
145+
146+
**See**: `README.md` (Strong Consistency Mode section) and `docs/architecture.md` for broader context.
147+
112148
## See Also
113149

114150
- `docs/boundary-handling.md`

docs/glossary.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ WaitForIdleAsync (“Was Idle” Semantics)
108108
- Completes when the system was idle at some point, which is appropriate for tests and convergence checks.
109109
- It does not guarantee the system is still idle after the task completes.
110110

111+
Strong Consistency Mode
112+
- An opt-in mode provided by the `GetDataAndWaitForIdleAsync` extension method on `IWindowCache`.
113+
- Composes `GetDataAsync` (returns data immediately) with `WaitForIdleAsync` (waits for convergence), returning the same `RangeResult` as `GetDataAsync` but only after the cache has reached an idle state.
114+
- Useful for cold start synchronization, integration testing, and any scenario requiring a guarantee that the cache window has converged before proceeding.
115+
- Not recommended for hot paths: adds latency equal to the rebalance execution time (debounce delay + I/O).
116+
- See `README.md` and `docs/components/public-api.md`.
117+
111118
## Storage And Materialization
112119

113120
UserCacheReadMode
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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

Comments
 (0)