Skip to content

Commit 86e68d5

Browse files
committed
- reduce scope+context to single type
- use Me() for key in tests
1 parent 76b3548 commit 86e68d5

3 files changed

Lines changed: 152 additions & 124 deletions

File tree

src/StackExchange.Redis/RedisCancellationExtensions.cs

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ namespace StackExchange.Redis
88
/// </summary>
99
public static class RedisCancellationExtensions
1010
{
11-
private static readonly AsyncLocal<CancellationContext?> _context = new();
12-
1311
/// <summary>
1412
/// Sets an ambient cancellation token that will be used for all Redis operations
1513
/// in the current async context until the returned scope is disposed.
@@ -27,9 +25,7 @@ public static class RedisCancellationExtensions
2725
/// </code>
2826
/// </example>
2927
public static IDisposable WithCancellation(this IRedisAsync redis, CancellationToken cancellationToken)
30-
{
31-
return new CancellationScope(cancellationToken, null);
32-
}
28+
=> new CancellationScope(cancellationToken, null);
3329

3430
/// <summary>
3531
/// Sets an ambient timeout that will be used for all Redis operations
@@ -47,9 +43,7 @@ public static IDisposable WithCancellation(this IRedisAsync redis, CancellationT
4743
/// </code>
4844
/// </example>
4945
public static IDisposable WithTimeout(this IRedisAsync redis, TimeSpan timeout)
50-
{
51-
return new CancellationScope(default, timeout);
52-
}
46+
=> new CancellationScope(CancellationToken.None, timeout);
5347

5448
/// <summary>
5549
/// Sets both an ambient cancellation token and timeout that will be used for all Redis operations
@@ -71,66 +65,35 @@ public static IDisposable WithCancellationAndTimeout(
7165
this IRedisAsync redis,
7266
CancellationToken cancellationToken,
7367
TimeSpan timeout)
74-
{
75-
return new CancellationScope(cancellationToken, timeout);
76-
}
68+
=> new CancellationScope(cancellationToken, timeout);
7769

7870
/// <summary>
7971
/// Gets the effective cancellation token for the current async context,
8072
/// combining any ambient cancellation token and timeout.
8173
/// </summary>
8274
/// <returns>The effective cancellation token, or CancellationToken.None if no ambient context is set.</returns>
8375
internal static CancellationToken GetEffectiveCancellationToken()
84-
{
85-
var context = _context.Value;
86-
return context?.GetEffectiveToken() ?? default;
87-
}
76+
=> _context.Value?.Token ?? CancellationToken.None;
8877

8978
/// <summary>
9079
/// Gets the current cancellation context for diagnostic purposes.
9180
/// </summary>
9281
/// <returns>The current context, or null if no ambient context is set.</returns>
93-
internal static CancellationContext? GetCurrentContext() => _context.Value;
82+
internal static object? GetCurrentScope() => _context.Value;
9483

95-
/// <summary>
96-
/// Represents the cancellation context for Redis operations.
97-
/// </summary>
98-
internal record CancellationContext(CancellationToken Token, TimeSpan? Timeout)
99-
{
100-
/// <summary>
101-
/// Gets the effective cancellation token, combining the explicit token with any timeout.
102-
/// </summary>
103-
/// <returns>A cancellation token that will be cancelled when either the explicit token is cancelled or the timeout expires.</returns>
104-
public CancellationToken GetEffectiveToken()
105-
{
106-
if (!Timeout.HasValue) return Token;
107-
108-
var timeoutSource = new CancellationTokenSource(Timeout.Value);
109-
return Token.CanBeCanceled
110-
? CancellationTokenSource.CreateLinkedTokenSource(Token, timeoutSource.Token).Token
111-
: timeoutSource.Token;
112-
}
113-
114-
/// <summary>
115-
/// Gets a string representation of this context for debugging.
116-
/// </summary>
117-
public override string ToString()
118-
{
119-
var parts = new System.Collections.Generic.List<string>();
120-
if (Token.CanBeCanceled) parts.Add($"Token: {(Token.IsCancellationRequested ? "Cancelled" : "Active")}");
121-
if (Timeout.HasValue) parts.Add($"Timeout: {Timeout.Value.TotalMilliseconds}ms");
122-
return parts.Count > 0 ? string.Join(", ", parts) : "None";
123-
}
124-
}
84+
private static readonly AsyncLocal<CancellationScope?> _context = new();
12585

12686
/// <summary>
12787
/// A disposable scope that manages the ambient cancellation context.
12888
/// </summary>
12989
private sealed class CancellationScope : IDisposable
13090
{
131-
private readonly CancellationContext? _previous;
91+
private readonly CancellationTokenSource? _ownedSource;
92+
private readonly CancellationScope? _previous;
13293
private bool _disposed;
13394

95+
public CancellationToken Token { get; }
96+
13497
/// <summary>
13598
/// Creates a new cancellation scope with the specified token and timeout.
13699
/// </summary>
@@ -139,7 +102,42 @@ private sealed class CancellationScope : IDisposable
139102
public CancellationScope(CancellationToken cancellationToken, TimeSpan? timeout)
140103
{
141104
_previous = _context.Value;
142-
_context.Value = new CancellationContext(cancellationToken, timeout);
105+
if (timeout.HasValue)
106+
{
107+
// has a timeout
108+
if (cancellationToken.CanBeCanceled)
109+
{
110+
// need both timeout and cancellation; but we can avoid some layers if
111+
// we're already doomed
112+
if (cancellationToken.IsCancellationRequested)
113+
{
114+
Token = cancellationToken;
115+
}
116+
else
117+
{
118+
_ownedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
119+
_ownedSource.CancelAfter(timeout.GetValueOrDefault());
120+
Token = _ownedSource.Token;
121+
}
122+
}
123+
else
124+
{
125+
// just a timeout
126+
_ownedSource = new CancellationTokenSource(timeout.GetValueOrDefault());
127+
Token = _ownedSource.Token;
128+
}
129+
}
130+
else if (cancellationToken.CanBeCanceled)
131+
{
132+
// nice and simple, just a CT
133+
Token = cancellationToken;
134+
}
135+
else
136+
{
137+
Token = CancellationToken.None;
138+
}
139+
140+
_context.Value = this;
143141
}
144142

145143
/// <summary>
@@ -149,8 +147,13 @@ public void Dispose()
149147
{
150148
if (!_disposed)
151149
{
152-
_context.Value = _previous;
153150
_disposed = true;
151+
if (ReferenceEquals(_context.Value, this))
152+
{
153+
// reinstate the previous context
154+
_context.Value = _previous;
155+
}
156+
_ownedSource?.Dispose();
154157
}
155158
}
156159
}

0 commit comments

Comments
 (0)