Skip to content

Commit 41a3e8e

Browse files
committed
attempt 1:1 map without Lua
1 parent ec293ee commit 41a3e8e

File tree

1 file changed

+71
-73
lines changed

1 file changed

+71
-73
lines changed

src/Caching/StackExchangeRedis/src/RedisCache.cs

Lines changed: 71 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -24,52 +24,23 @@ public partial class RedisCache : IDistributedCache, IDisposable
2424
{
2525
// 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
2626
// 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");
5427

5528
private const string AbsoluteExpirationKey = "absexp";
5629
private const string SlidingExpirationKey = "sldexp";
5730
private const string DataKey = "data";
5831

5932
// 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];
6235

6336
private static RedisValue[] GetHashFields(bool getData) => getData
6437
? _hashMembersAbsoluteExpirationSlidingExpirationData
6538
: _hashMembersAbsoluteExpirationSlidingExpiration;
6639

6740
private const long NotPresent = -1;
68-
private static readonly Version ServerVersionWithExtendedSetCommand = new Version(4, 0, 0);
6941

7042
private volatile IDatabase? _cache;
7143
private bool _disposed;
72-
private string _setScript = SetScript;
7344

7445
private readonly RedisCacheOptions _options;
7546
private readonly RedisKey _instancePrefix;
@@ -169,14 +140,53 @@ public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
169140

170141
try
171142
{
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)
174163
{
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+
}
180190
}
181191
catch (Exception ex)
182192
{
@@ -203,14 +213,21 @@ public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOption
203213

204214
try
205215
{
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+
}
214231
}
215232
catch (Exception ex)
216233
{
@@ -219,6 +236,13 @@ public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOption
219236
}
220237
}
221238

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+
222246
/// <inheritdoc />
223247
public void Refresh(string key)
224248
{
@@ -323,36 +347,10 @@ private async ValueTask<IDatabase> ConnectSlowAsync(CancellationToken token)
323347
private void PrepareConnection(IConnectionMultiplexer connection)
324348
{
325349
WriteTimeTicks(ref _lastConnectTicks, DateTimeOffset.UtcNow);
326-
ValidateServerFeatures(connection);
327350
TryRegisterProfiler(connection);
328351
TryAddSuffix(connection);
329352
}
330353

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-
356354
private void TryRegisterProfiler(IConnectionMultiplexer connection)
357355
{
358356
_ = connection ?? throw new InvalidOperationException($"{nameof(connection)} cannot be null.");
@@ -372,7 +370,7 @@ private void TryAddSuffix(IConnectionMultiplexer connection)
372370
}
373371
catch (Exception ex)
374372
{
375-
Log.UnableToAddLibraryNameSuffix(_logger, ex);;
373+
Log.UnableToAddLibraryNameSuffix(_logger, ex); ;
376374
}
377375
}
378376

0 commit comments

Comments
 (0)