Skip to content

Commit 9252c16

Browse files
committed
use CAS/CAD in locking operations
1 parent aaefd70 commit 9252c16

File tree

9 files changed

+90
-27
lines changed

9 files changed

+90
-27
lines changed

src/StackExchange.Redis/Interfaces/IDatabase.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3385,12 +3385,13 @@ IEnumerable<SortedSetEntry> SortedSetScan(
33853385
/// </summary>
33863386
/// <param name="key">The key of the string.</param>
33873387
/// <param name="value">The value to set.</param>
3388+
/// <param name="expiry">The expiry to set.</param>
33883389
/// <param name="when">The condition to enforce.</param>
33893390
/// <param name="flags">The flags to use for this operation.</param>
33903391
/// <remarks>See <seealso href="https://redis.io/commands/delex"/>.</remarks>
33913392
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
33923393
#pragma warning disable RS0027
3393-
bool StringSet(RedisKey key, RedisValue value, ValueCondition when, CommandFlags flags = CommandFlags.None);
3394+
bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None);
33943395
#pragma warning restore RS0027
33953396

33963397
/// <summary>

src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -839,10 +839,10 @@ IAsyncEnumerable<SortedSetEntry> SortedSetScanAsync(
839839
/// <inheritdoc cref="IDatabase.StringSet(RedisKey, RedisValue, TimeSpan?, bool, When, CommandFlags)"/>
840840
Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None);
841841

842-
/// <inheritdoc cref="IDatabase.StringSet(RedisKey, RedisValue, ValueCondition, CommandFlags)"/>
842+
/// <inheritdoc cref="IDatabase.StringSet(RedisKey, RedisValue, TimeSpan?, ValueCondition, CommandFlags)"/>
843843
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
844844
#pragma warning disable RS0027
845-
Task<bool> StringSetAsync(RedisKey key, RedisValue value, ValueCondition when, CommandFlags flags = CommandFlags.None);
845+
Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None);
846846
#pragma warning restore RS0027
847847

848848
/// <inheritdoc cref="IDatabase.StringSet(KeyValuePair{RedisKey, RedisValue}[], When, CommandFlags)"/>

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -777,8 +777,8 @@ public Task<long> StringIncrementAsync(RedisKey key, long value = 1, CommandFlag
777777
public Task<long> StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
778778
Inner.StringLengthAsync(ToInner(key), flags);
779779

780-
public Task<bool> StringSetAsync(RedisKey key, RedisValue value, ValueCondition when, CommandFlags flags = CommandFlags.None)
781-
=> Inner.StringSetAsync(ToInner(key), value, when, flags);
780+
public Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None)
781+
=> Inner.StringSetAsync(ToInner(key), value, expiry, when, flags);
782782

783783
public Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
784784
Inner.StringSetAsync(ToInner(values), when, flags);

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -759,8 +759,8 @@ public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = C
759759
public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) =>
760760
Inner.StringLength(ToInner(key), flags);
761761

762-
public bool StringSet(RedisKey key, RedisValue value, ValueCondition when, CommandFlags flags = CommandFlags.None)
763-
=> Inner.StringSet(ToInner(key), value, when, flags);
762+
public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None)
763+
=> Inner.StringSet(ToInner(key), value, expiry, when, flags);
764764

765765
public bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
766766
Inner.StringSet(ToInner(values), when, flags);

src/StackExchange.Redis/Message.ValueCondition.cs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
namespace StackExchange.Redis;
1+
using System;
2+
3+
namespace StackExchange.Redis;
24

35
internal partial class Message
46
{
57
public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in ValueCondition when)
68
=> new KeyConditionMessage(db, flags, command, key, when);
79

8-
public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in ValueCondition when)
9-
=> new KeyValueConditionMessage(db, flags, command, key, value, when);
10+
public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, TimeSpan? expiry, in ValueCondition when)
11+
=> new KeyValueExpiryConditionMessage(db, flags, command, key, value, expiry, when);
1012

1113
private sealed class KeyConditionMessage(
1214
int db,
@@ -28,25 +30,41 @@ protected override void WriteImpl(PhysicalConnection physical)
2830
}
2931
}
3032

31-
private sealed class KeyValueConditionMessage(
33+
private sealed class KeyValueExpiryConditionMessage(
3234
int db,
3335
CommandFlags flags,
3436
RedisCommand command,
3537
in RedisKey key,
3638
in RedisValue value,
39+
TimeSpan? expiry,
3740
in ValueCondition when)
3841
: CommandKeyBase(db, flags, command, key)
3942
{
4043
private readonly RedisValue _value = value;
4144
private readonly ValueCondition _when = when;
45+
private readonly TimeSpan? _expiry = expiry == TimeSpan.MaxValue ? null : expiry;
4246

43-
public override int ArgCount => 2 + _when.TokenCount;
47+
public override int ArgCount => 2 + _when.TokenCount + (_expiry is null ? 0 : 2);
4448

4549
protected override void WriteImpl(PhysicalConnection physical)
4650
{
4751
physical.WriteHeader(Command, ArgCount);
4852
physical.Write(Key);
4953
physical.WriteBulkString(_value);
54+
if (_expiry.HasValue)
55+
{
56+
var ms = (long)_expiry.GetValueOrDefault().TotalMilliseconds;
57+
if ((ms % 1000) == 0)
58+
{
59+
physical.WriteBulkString("EX"u8);
60+
physical.WriteBulkString(ms / 1000);
61+
}
62+
else
63+
{
64+
physical.WriteBulkString("PX"u8);
65+
physical.WriteBulkString(ms);
66+
}
67+
}
5068
_when.WriteTo(physical);
5169
}
5270
}

src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#nullable enable
2+
StackExchange.Redis.RedisFeatures.DeleteWithValueCheck.get -> bool
3+
StackExchange.Redis.RedisFeatures.SetWithValueCheck.get -> bool
24
[SER002]override StackExchange.Redis.ValueCondition.Equals(object? obj) -> bool
35
[SER002]override StackExchange.Redis.ValueCondition.GetHashCode() -> int
46
[SER002]override StackExchange.Redis.ValueCondition.ToString() -> string!
57
[SER002]StackExchange.Redis.IDatabase.StringDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
68
[SER002]StackExchange.Redis.IDatabase.StringDigest(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ValueCondition?
7-
[SER002]StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
9+
[SER002]StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
810
[SER002]StackExchange.Redis.IDatabaseAsync.StringDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<bool>!
911
[SER002]StackExchange.Redis.IDatabaseAsync.StringDigestAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.ValueCondition?>!
10-
[SER002]StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<bool>!
12+
[SER002]StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<bool>!
1113
[SER002]StackExchange.Redis.ValueCondition
1214
[SER002]StackExchange.Redis.ValueCondition.AsDigest() -> StackExchange.Redis.ValueCondition
1315
[SER002]StackExchange.Redis.ValueCondition.Value.get -> StackExchange.Redis.RedisValue

src/StackExchange.Redis/RedisDatabase.Strings.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Runtime.CompilerServices;
1+
using System;
2+
using System.Runtime.CompilerServices;
23
using System.Threading.Tasks;
34

45
namespace StackExchange.Redis;
@@ -47,31 +48,31 @@ private Message GetStringDeleteMessage(in RedisKey key, in ValueCondition when,
4748
return ExecuteAsync(msg, ResultProcessor.Digest);
4849
}
4950

50-
public Task<bool> StringSetAsync(RedisKey key, RedisValue value, ValueCondition when, CommandFlags flags = CommandFlags.None)
51+
public Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None)
5152
{
52-
var msg = GetStringSetMessage(key, value, when, flags);
53+
var msg = GetStringSetMessage(key, value, expiry, when, flags);
5354
return ExecuteAsync(msg, ResultProcessor.Boolean);
5455
}
5556

56-
public bool StringSet(RedisKey key, RedisValue value, ValueCondition when, CommandFlags flags = CommandFlags.None)
57+
public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None)
5758
{
58-
var msg = GetStringSetMessage(key, value, when, flags);
59+
var msg = GetStringSetMessage(key, value, expiry, when, flags);
5960
return ExecuteSync(msg, ResultProcessor.Boolean);
6061
}
6162

62-
private Message GetStringSetMessage(in RedisKey key, in RedisValue value, in ValueCondition when, CommandFlags flags, [CallerMemberName] string? operation = null)
63+
private Message GetStringSetMessage(in RedisKey key, in RedisValue value, TimeSpan? expiry, in ValueCondition when, CommandFlags flags, [CallerMemberName] string? operation = null)
6364
{
6465
switch (when.Kind)
6566
{
6667
case ValueCondition.ConditionKind.Exists:
6768
case ValueCondition.ConditionKind.NotExists:
6869
case ValueCondition.ConditionKind.Always:
69-
return GetStringSetMessage(key, value, when: when.AsWhen(), flags: flags);
70+
return GetStringSetMessage(key, value, expiry: expiry, when: when.AsWhen(), flags: flags);
7071
case ValueCondition.ConditionKind.ValueEquals:
7172
case ValueCondition.ConditionKind.ValueNotEquals:
7273
case ValueCondition.ConditionKind.DigestEquals:
7374
case ValueCondition.ConditionKind.DigestNotEquals:
74-
return Message.Create(Database, flags, RedisCommand.SET, key, value, when);
75+
return Message.Create(Database, flags, RedisCommand.SET, key, value, expiry, when);
7576
default:
7677
when.ThrowInvalidOperation(operation);
7778
goto case ValueCondition.ConditionKind.Always; // not reached

src/StackExchange.Redis/RedisDatabase.cs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.Generic;
44
using System.Diagnostics;
55
using System.Net;
6+
using System.Runtime.CompilerServices;
67
using System.Threading.Tasks;
78
using Pipelines.Sockets.Unofficial.Arenas;
89

@@ -1770,18 +1771,33 @@ public Task ListTrimAsync(RedisKey key, long start, long stop, CommandFlags flag
17701771

17711772
public bool LockExtend(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None)
17721773
{
1773-
if (value.IsNull) throw new ArgumentNullException(nameof(value));
1774-
var tran = GetLockExtendTransaction(key, value, expiry);
1774+
var msg = TryGetLockExtendMessage(key, value, expiry, flags, out var server);
1775+
if (msg is not null) return ExecuteSync(msg, ResultProcessor.Boolean, server);
17751776

1777+
var tran = GetLockExtendTransaction(key, value, expiry);
17761778
if (tran != null) return tran.Execute(flags);
17771779

17781780
// without transactions (twemproxy etc), we can't enforce the "value" part
17791781
return KeyExpire(key, expiry, flags);
17801782
}
17811783

1782-
public Task<bool> LockExtendAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None)
1784+
private Message? TryGetLockExtendMessage(in RedisKey key, in RedisValue value, TimeSpan expiry, CommandFlags flags, out ServerEndPoint? server, [CallerMemberName] string? caller = null)
17831785
{
17841786
if (value.IsNull) throw new ArgumentNullException(nameof(value));
1787+
1788+
// note that lock tokens are expected to be small, so: we'll use IFEQ rather than IFDEQ, for reliability
1789+
// note possible future extension:[P]EXPIRE ... IF* https://github.com/redis/redis/issues/14505
1790+
var features = GetFeatures(key, flags, RedisCommand.SET, out server);
1791+
return features.SetWithValueCheck
1792+
? GetStringSetMessage(key, value, expiry, ValueCondition.Equal(value), flags, caller) // use check-and-set
1793+
: null;
1794+
}
1795+
1796+
public Task<bool> LockExtendAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None)
1797+
{
1798+
var msg = TryGetLockExtendMessage(key, value, expiry, flags, out var server);
1799+
if (msg is not null) return ExecuteAsync(msg, ResultProcessor.Boolean, server);
1800+
17851801
var tran = GetLockExtendTransaction(key, value, expiry);
17861802
if (tran != null) return tran.ExecuteAsync(flags);
17871803

@@ -1801,17 +1817,32 @@ public Task<RedisValue> LockQueryAsync(RedisKey key, CommandFlags flags = Comman
18011817

18021818
public bool LockRelease(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
18031819
{
1804-
if (value.IsNull) throw new ArgumentNullException(nameof(value));
1820+
var msg = TryGetLockReleaseMessage(key, value, flags, out var server);
1821+
if (msg is not null) return ExecuteSync(msg, ResultProcessor.Boolean, server);
1822+
18051823
var tran = GetLockReleaseTransaction(key, value);
18061824
if (tran != null) return tran.Execute(flags);
18071825

18081826
// without transactions (twemproxy etc), we can't enforce the "value" part
18091827
return KeyDelete(key, flags);
18101828
}
18111829

1812-
public Task<bool> LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
1830+
private Message? TryGetLockReleaseMessage(in RedisKey key, in RedisValue value, CommandFlags flags, out ServerEndPoint? server, [CallerMemberName] string? caller = null)
18131831
{
18141832
if (value.IsNull) throw new ArgumentNullException(nameof(value));
1833+
1834+
// note that lock tokens are expected to be small, so: we'll use IFEQ rather than IFDEQ, for reliability
1835+
var features = GetFeatures(key, flags, RedisCommand.SET, out server);
1836+
return features.DeleteWithValueCheck
1837+
? GetStringDeleteMessage(key, ValueCondition.Equal(value), flags, caller) // use check-and-delete
1838+
: null;
1839+
}
1840+
1841+
public Task<bool> LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
1842+
{
1843+
var msg = TryGetLockReleaseMessage(key, value, flags, out var server);
1844+
if (msg is not null) return ExecuteAsync(msg, ResultProcessor.Boolean, server);
1845+
18151846
var tran = GetLockReleaseTransaction(key, value);
18161847
if (tran != null) return tran.ExecuteAsync(flags);
18171848

src/StackExchange.Redis/RedisFeatures.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,16 @@ public RedisFeatures(Version version)
285285
/// </summary>
286286
public bool Resp3 => Version.IsAtLeast(v6_0_0);
287287

288+
/// <summary>
289+
/// Are the <c>IF*</c> modifiers on <see href="https://redis.io/commands/set/">SET</see> available?
290+
/// </summary>
291+
public bool SetWithValueCheck => Version.IsAtLeast(v8_4_0_rc1);
292+
293+
/// <summary>
294+
/// Are the <c>IF*</c> modifiers on <see href="https://redis.io/commands/del/">DEL</see> available?
295+
/// </summary>
296+
public bool DeleteWithValueCheck => Version.IsAtLeast(v8_4_0_rc1);
297+
288298
#pragma warning restore 1629 // Documentation text should end with a period.
289299

290300
/// <summary>

0 commit comments

Comments
 (0)