Skip to content

Commit 85a0900

Browse files
authored
Fix RedisCache update that breaks usage against older redis servers (#38927)
1 parent e47af40 commit 85a0900

File tree

1 file changed

+43
-5
lines changed

1 file changed

+43
-5
lines changed

src/Caching/StackExchangeRedis/src/RedisCache.cs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ namespace Microsoft.Extensions.Caching.StackExchangeRedis
1616
/// </summary>
1717
public class RedisCache : IDistributedCache, IDisposable
1818
{
19+
// -- Explanation of why two kinds of SetScript are used --
20+
// * Redis 2.0 had HSET key field value for setting individual hash fields,
21+
// and HMSET key field value [field value ...] for setting multiple hash fields (against the same key).
22+
// * Redis 4.0 added HSET key field value [field value ...] and deprecated HMSET.
23+
//
24+
// On Redis versions that don't have the newer HSET variant, we use SetScriptPreExtendedSetCommand
25+
// which uses the (now deprecated) HMSET.
26+
1927
// KEYS[1] = = key
2028
// ARGV[1] = absolute-expiration - ticks as long (-1 for none)
2129
// ARGV[2] = sliding-expiration - ticks as long (-1 for none)
@@ -28,14 +36,22 @@ public class RedisCache : IDistributedCache, IDisposable
2836
redis.call('EXPIRE', KEYS[1], ARGV[3])
2937
end
3038
return 1");
39+
private const string SetScriptPreExtendedSetCommand = (@"
40+
redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])
41+
if ARGV[3] ~= '-1' then
42+
redis.call('EXPIRE', KEYS[1], ARGV[3])
43+
end
44+
return 1");
3145
private const string AbsoluteExpirationKey = "absexp";
3246
private const string SlidingExpirationKey = "sldexp";
3347
private const string DataKey = "data";
3448
private const long NotPresent = -1;
49+
private static readonly Version ServerVersionWithExtendedSetCommand = new Version(4, 0, 0);
3550

3651
private volatile IConnectionMultiplexer _connection;
3752
private IDatabase _cache;
3853
private bool _disposed;
54+
private string _setScript = SetScript;
3955

4056
private readonly RedisCacheOptions _options;
4157
private readonly string _instance;
@@ -107,7 +123,7 @@ public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
107123

108124
var absoluteExpiration = GetAbsoluteExpiration(creationTime, options);
109125

110-
var result = _cache.ScriptEvaluate(SetScript, new RedisKey[] { _instance + key },
126+
var result = _cache.ScriptEvaluate(_setScript, new RedisKey[] { _instance + key },
111127
new RedisValue[]
112128
{
113129
absoluteExpiration?.Ticks ?? NotPresent,
@@ -143,7 +159,7 @@ public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
143159

144160
var absoluteExpiration = GetAbsoluteExpiration(creationTime, options);
145161

146-
await _cache.ScriptEvaluateAsync(SetScript, new RedisKey[] { _instance + key },
162+
await _cache.ScriptEvaluateAsync(_setScript, new RedisKey[] { _instance + key },
147163
new RedisValue[]
148164
{
149165
absoluteExpiration?.Ticks ?? NotPresent,
@@ -206,7 +222,7 @@ private void Connect()
206222
_connection = _options.ConnectionMultiplexerFactory().GetAwaiter().GetResult();
207223
}
208224

209-
TryRegisterProfiler();
225+
PrepareConnection();
210226
_cache = _connection.GetDatabase();
211227
}
212228
}
@@ -247,7 +263,7 @@ private void Connect()
247263
_connection = await _options.ConnectionMultiplexerFactory();
248264
}
249265

250-
TryRegisterProfiler();
266+
PrepareConnection();
251267
_cache = _connection.GetDatabase();
252268
}
253269
}
@@ -257,9 +273,31 @@ private void Connect()
257273
}
258274
}
259275

276+
private void PrepareConnection()
277+
{
278+
ValidateServerFeatures();
279+
TryRegisterProfiler();
280+
}
281+
282+
private void ValidateServerFeatures()
283+
{
284+
_ = _connection ?? throw new InvalidOperationException($"{nameof(_connection)} cannot be null.");
285+
286+
foreach (var endPoint in _connection.GetEndPoints())
287+
{
288+
if (_connection.GetServer(endPoint).Version < ServerVersionWithExtendedSetCommand)
289+
{
290+
_setScript = SetScriptPreExtendedSetCommand;
291+
return;
292+
}
293+
}
294+
}
295+
260296
private void TryRegisterProfiler()
261297
{
262-
if (_connection != null && _options.ProfilingSession != null)
298+
_ = _connection ?? throw new InvalidOperationException($"{nameof(_connection)} cannot be null.");
299+
300+
if (_options.ProfilingSession != null)
263301
{
264302
_connection.RegisterProfiler(_options.ProfilingSession);
265303
}

0 commit comments

Comments
 (0)