@@ -24,52 +24,23 @@ public partial class RedisCache : IDistributedCache, IDisposable
24
24
{
25
25
// Note that the "force reconnect" pattern as described https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection#using-forcereconnect-with-stackexchangeredis
26
26
// can be enabled via the "Microsoft.AspNetCore.Caching.StackExchangeRedis.UseForceReconnect" app-context switch
27
- //
28
- // -- Explanation of why two kinds of SetScript are used --
29
- // * Redis 2.0 had HSET key field value for setting individual hash fields,
30
- // and HMSET key field value [field value ...] for setting multiple hash fields (against the same key).
31
- // * Redis 4.0 added HSET key field value [field value ...] and deprecated HMSET.
32
- //
33
- // On Redis versions that don't have the newer HSET variant, we use SetScriptPreExtendedSetCommand
34
- // which uses the (now deprecated) HMSET.
35
-
36
- // KEYS[1] = = key
37
- // ARGV[1] = absolute-expiration - ticks as long (-1 for none)
38
- // ARGV[2] = sliding-expiration - ticks as long (-1 for none)
39
- // ARGV[3] = relative-expiration (long, in seconds, -1 for none) - Min(absolute-expiration - Now, sliding-expiration)
40
- // ARGV[4] = data - byte[]
41
- // this order should not change LUA script depends on it
42
- private const string SetScript = ( @"
43
- redis.call('HSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])
44
- if ARGV[3] ~= '-1' then
45
- redis.call('EXPIRE', KEYS[1], ARGV[3])
46
- end
47
- return 1" ) ;
48
- private const string SetScriptPreExtendedSetCommand = ( @"
49
- redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])
50
- if ARGV[3] ~= '-1' then
51
- redis.call('EXPIRE', KEYS[1], ARGV[3])
52
- end
53
- return 1" ) ;
54
27
55
28
private const string AbsoluteExpirationKey = "absexp" ;
56
29
private const string SlidingExpirationKey = "sldexp" ;
57
30
private const string DataKey = "data" ;
58
31
59
32
// combined keys - same hash keys fetched constantly; avoid allocating an array each time
60
- private static readonly RedisValue [ ] _hashMembersAbsoluteExpirationSlidingExpirationData = new RedisValue [ ] { AbsoluteExpirationKey , SlidingExpirationKey , DataKey } ;
61
- private static readonly RedisValue [ ] _hashMembersAbsoluteExpirationSlidingExpiration = new RedisValue [ ] { AbsoluteExpirationKey , SlidingExpirationKey } ;
33
+ private static readonly RedisValue [ ] _hashMembersAbsoluteExpirationSlidingExpirationData = [ AbsoluteExpirationKey , SlidingExpirationKey , DataKey ] ;
34
+ private static readonly RedisValue [ ] _hashMembersAbsoluteExpirationSlidingExpiration = [ AbsoluteExpirationKey , SlidingExpirationKey ] ;
62
35
63
36
private static RedisValue [ ] GetHashFields ( bool getData ) => getData
64
37
? _hashMembersAbsoluteExpirationSlidingExpirationData
65
38
: _hashMembersAbsoluteExpirationSlidingExpiration ;
66
39
67
40
private const long NotPresent = - 1 ;
68
- private static readonly Version ServerVersionWithExtendedSetCommand = new Version ( 4 , 0 , 0 ) ;
69
41
70
42
private volatile IDatabase ? _cache ;
71
43
private bool _disposed ;
72
- private string _setScript = SetScript ;
73
44
74
45
private readonly RedisCacheOptions _options ;
75
46
private readonly RedisKey _instancePrefix ;
@@ -169,14 +140,53 @@ public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
169
140
170
141
try
171
142
{
172
- cache . ScriptEvaluate ( _setScript , new RedisKey [ ] { _instancePrefix . Append ( key ) } ,
173
- new RedisValue [ ]
143
+ var prefixedKey = _instancePrefix . Append ( key ) ;
144
+ var ttl = GetExpirationInSeconds ( creationTime , absoluteExpiration , options ) ;
145
+ var fields = GetHashFields ( value , absoluteExpiration , options . SlidingExpiration ) ;
146
+
147
+ if ( ttl is null )
148
+ {
149
+ cache . HashSet ( prefixedKey , fields ) ;
150
+ }
151
+ else
152
+ {
153
+ // use the batch API to pipeline the two commands and wait synchronously;
154
+ // SE.Redis reuses the async API shape for this scenario
155
+ var batch = cache . CreateBatch ( ) ;
156
+ var setFields = batch . HashSetAsync ( prefixedKey , fields ) ;
157
+ var setTtl = batch . KeyExpireAsync ( prefixedKey , TimeSpan . FromSeconds ( ttl . GetValueOrDefault ( ) ) ) ;
158
+ batch . Execute ( ) ; // synchronous wait-for-all
159
+
160
+ // we *expect* that they are both complete; if not, something is *already*
161
+ // horribly wrong, so: we'll assert that
162
+ if ( setFields . IsCompleted && setTtl . IsCompleted )
174
163
{
175
- absoluteExpiration ? . Ticks ?? NotPresent ,
176
- options . SlidingExpiration ? . Ticks ?? NotPresent ,
177
- GetExpirationInSeconds ( creationTime , absoluteExpiration , options ) ?? NotPresent ,
178
- value
179
- } ) ;
164
+ // can check synchronously without adding a sync-over-async
165
+ if ( setFields . IsFaulted && setTtl . IsFaulted )
166
+ {
167
+ // both faulted? look at ttl so not "unobserved", and
168
+ // use the error from the fiels as the primary fault
169
+ try
170
+ {
171
+ setTtl . GetAwaiter ( ) . GetResult ( ) ;
172
+ }
173
+ catch { } // this is a deliberate swallow; we know setFields is also doomed
174
+ setFields . GetAwaiter ( ) . GetResult ( ) ;
175
+ }
176
+ else
177
+ {
178
+ // at most one faulted; just check the results the simple way
179
+ // (emphasis: they're already complete)
180
+ setFields . GetAwaiter ( ) . GetResult ( ) ;
181
+ setTtl . GetAwaiter ( ) . GetResult ( ) ;
182
+ }
183
+ }
184
+ else
185
+ {
186
+ // something weird happened; we do not want to add a sync-over-async
187
+ throw new InvalidOperationException ( "Batch did not complete setting cache entry" ) ;
188
+ }
189
+ }
180
190
}
181
191
catch ( Exception ex )
182
192
{
@@ -203,14 +213,21 @@ public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOption
203
213
204
214
try
205
215
{
206
- await cache . ScriptEvaluateAsync ( _setScript , new RedisKey [ ] { _instancePrefix . Append ( key ) } ,
207
- new RedisValue [ ]
208
- {
209
- absoluteExpiration ? . Ticks ?? NotPresent ,
210
- options . SlidingExpiration ? . Ticks ?? NotPresent ,
211
- GetExpirationInSeconds ( creationTime , absoluteExpiration , options ) ?? NotPresent ,
212
- value
213
- } ) . ConfigureAwait ( false ) ;
216
+ var prefixedKey = _instancePrefix . Append ( key ) ;
217
+ var ttl = GetExpirationInSeconds ( creationTime , absoluteExpiration , options ) ;
218
+ var fields = GetHashFields ( value , absoluteExpiration , options . SlidingExpiration ) ;
219
+
220
+ if ( ttl is null )
221
+ {
222
+ await cache . HashSetAsync ( prefixedKey , fields ) . ConfigureAwait ( false ) ;
223
+ }
224
+ else
225
+ {
226
+ await Task . WhenAll (
227
+ cache . HashSetAsync ( prefixedKey , fields ) ,
228
+ cache . KeyExpireAsync ( prefixedKey , TimeSpan . FromSeconds ( ttl . GetValueOrDefault ( ) ) )
229
+ ) . ConfigureAwait ( false ) ;
230
+ }
214
231
}
215
232
catch ( Exception ex )
216
233
{
@@ -219,6 +236,13 @@ public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOption
219
236
}
220
237
}
221
238
239
+ private static HashEntry [ ] GetHashFields ( RedisValue value , DateTimeOffset ? absoluteExpiration , TimeSpan ? slidingExpiration )
240
+ => [
241
+ new HashEntry ( AbsoluteExpirationKey , absoluteExpiration ? . Ticks ?? NotPresent ) ,
242
+ new HashEntry ( SlidingExpirationKey , slidingExpiration ? . Ticks ?? NotPresent ) ,
243
+ new HashEntry ( DataKey , value )
244
+ ] ;
245
+
222
246
/// <inheritdoc />
223
247
public void Refresh ( string key )
224
248
{
@@ -323,36 +347,10 @@ private async ValueTask<IDatabase> ConnectSlowAsync(CancellationToken token)
323
347
private void PrepareConnection ( IConnectionMultiplexer connection )
324
348
{
325
349
WriteTimeTicks ( ref _lastConnectTicks , DateTimeOffset . UtcNow ) ;
326
- ValidateServerFeatures ( connection ) ;
327
350
TryRegisterProfiler ( connection ) ;
328
351
TryAddSuffix ( connection ) ;
329
352
}
330
353
331
- private void ValidateServerFeatures ( IConnectionMultiplexer connection )
332
- {
333
- _ = connection ?? throw new InvalidOperationException ( $ "{ nameof ( connection ) } cannot be null.") ;
334
-
335
- try
336
- {
337
- foreach ( var endPoint in connection . GetEndPoints ( ) )
338
- {
339
- if ( connection . GetServer ( endPoint ) . Version < ServerVersionWithExtendedSetCommand )
340
- {
341
- _setScript = SetScriptPreExtendedSetCommand ;
342
- return ;
343
- }
344
- }
345
- }
346
- catch ( NotSupportedException ex )
347
- {
348
- Log . CouldNotDetermineServerVersion ( _logger , ex ) ;
349
-
350
- // The GetServer call may not be supported with some configurations, in which
351
- // case let's also fall back to using the older command.
352
- _setScript = SetScriptPreExtendedSetCommand ;
353
- }
354
- }
355
-
356
354
private void TryRegisterProfiler ( IConnectionMultiplexer connection )
357
355
{
358
356
_ = connection ?? throw new InvalidOperationException ( $ "{ nameof ( connection ) } cannot be null.") ;
@@ -372,7 +370,7 @@ private void TryAddSuffix(IConnectionMultiplexer connection)
372
370
}
373
371
catch ( Exception ex )
374
372
{
375
- Log . UnableToAddLibraryNameSuffix ( _logger , ex ) ; ;
373
+ Log . UnableToAddLibraryNameSuffix ( _logger , ex ) ; ;
376
374
}
377
375
}
378
376
0 commit comments