Skip to content

Commit 94e0090

Browse files
Support 8.4 CAS/CAD (IF*) operations (#2978)
* Completely untested start for CAD/CAD * unit tests * propose the actual API * DIGEST integration tests * compensate for leading-zero oddness * apply fixes for server hash format changes * use CAS/CAD in locking operations * release notes * remove mitigations for receiving under-length digests from the server (now fixed at server) * add zero-length digest test * Update src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs Co-authored-by: Philo <[email protected]> --------- Co-authored-by: Philo <[email protected]>
1 parent 416daff commit 94e0090

22 files changed

+1077
-9
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<PackageVersion Include="System.Threading.Channels" Version="5.0.0" />
99
<PackageVersion Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
1010
<PackageVersion Include="System.IO.Compression" Version="4.3.0" />
11+
<PackageVersion Include="System.IO.Hashing" Version="9.0.10" />
1112

1213
<!-- For analyzers, tied to the consumer's build SDK; at the moment, that means "us" -->
1314
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />

docs/ReleaseNotes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Current package versions:
88

99
## Unreleased
1010

11+
- Support Redis 8.4 CAS/CAD operations (`DIGEST`, and the `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` modifiers on `SET` / `DEL`)
12+
via the new `ValueCondition` abstraction, and use CAS/CAD operations for `Lock*` APIs when possible ([#2978 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2978))
1113
- Support `XREADGROUP CLAIM` ([#2972 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2972))
1214
- Support `MSETEX` (Redis 8.4.0) for multi-key operations with expiration ([#2977 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2977))
1315

docs/exp/SER002.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ Redis 8.4 is currently in preview and may be subject to change.
22

33
New features in Redis 8.4 include:
44

5-
- [`MSETEX`](https://github.com/redis/redis/pull/14434) for setting multiple strings with expiry
5+
- [`MSETEX`](https://github.com/redis/redis/pull/14434) for setting multiple strings with expiry
66
- [`XREADGROUP ... CLAIM`](https://github.com/redis/redis/pull/14402) for simplifed stream consumption
77
- [`SET ... {IFEQ|IFNE|IFDEQ|IFDNE}`, `DELEX` and `DIGEST`](https://github.com/redis/redis/pull/14434) for checked (CAS/CAD) string operations
88

99
The corresponding library feature must also be considered subject to change:
1010

1111
1. Existing bindings may cease working correctly if the underlying server API changes.
1212
2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time
13-
or run-time breaks.
13+
or run-time breaks.
1414

1515
While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress
1616
this warning by adding the following to your `csproj` file:

src/Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<GenerateDocumentationFile>true</GenerateDocumentationFile>
55
<IsPackable>true</IsPackable>
66
<CheckEolTargetFramework>false</CheckEolTargetFramework>
7+
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
78
</PropertyGroup>
89
<ItemGroup>
910
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" PrivateAssets="all" />

src/StackExchange.Redis/Enums/RedisCommand.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ internal enum RedisCommand
3030
DECR,
3131
DECRBY,
3232
DEL,
33+
DELEX,
34+
DIGEST,
3335
DISCARD,
3436
DUMP,
3537

@@ -300,6 +302,8 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
300302
case RedisCommand.DECR:
301303
case RedisCommand.DECRBY:
302304
case RedisCommand.DEL:
305+
case RedisCommand.DELEX:
306+
case RedisCommand.DIGEST:
303307
case RedisCommand.EXPIRE:
304308
case RedisCommand.EXPIREAT:
305309
case RedisCommand.FLUSHALL:

src/StackExchange.Redis/Experiments.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace StackExchange.Redis
88
internal static class Experiments
99
{
1010
public const string UrlFormat = "https://stackexchange.github.io/StackExchange.Redis/exp/";
11+
1112
public const string VectorSets = "SER001";
1213
// ReSharper disable once InconsistentNaming
1314
public const string Server_8_4 = "SER002";

src/StackExchange.Redis/Interfaces/IDatabase.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.ComponentModel;
4+
using System.Diagnostics.CodeAnalysis;
45
using System.Net;
56

67
// ReSharper disable once CheckNamespace
@@ -3174,6 +3175,16 @@ IEnumerable<SortedSetEntry> SortedSetScan(
31743175
/// </remarks>
31753176
long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None);
31763177

3178+
/// <summary>
3179+
/// Deletes <paramref name="key"/> if it matches the given <paramref name="when"/> condition.
3180+
/// </summary>
3181+
/// <param name="key">The key of the string.</param>
3182+
/// <param name="when">The condition to enforce.</param>
3183+
/// <param name="flags">The flags to use for this operation.</param>
3184+
/// <remarks>See <seealso href="https://redis.io/commands/delex"/>.</remarks>
3185+
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
3186+
bool StringDelete(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None);
3187+
31773188
/// <summary>
31783189
/// Decrements the string representing a floating point number stored at key by the specified decrement.
31793190
/// If the key does not exist, it is set to 0 before performing the operation.
@@ -3186,6 +3197,15 @@ IEnumerable<SortedSetEntry> SortedSetScan(
31863197
/// <remarks><seealso href="https://redis.io/commands/incrbyfloat"/></remarks>
31873198
double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None);
31883199

3200+
/// <summary>
3201+
/// Gets the digest (hash) value of the specified key, represented as a digest equality <see cref="ValueCondition"/>.
3202+
/// </summary>
3203+
/// <param name="key">The key of the string.</param>
3204+
/// <param name="flags">The flags to use for this operation.</param>
3205+
/// <remarks><seealso href="https://redis.io/commands/digest"/></remarks>
3206+
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
3207+
ValueCondition? StringDigest(RedisKey key, CommandFlags flags = CommandFlags.None);
3208+
31893209
/// <summary>
31903210
/// Get the value of key. If the key does not exist the special value <see cref="RedisValue.Null"/> is returned.
31913211
/// An error is returned if the value stored at key is not a string, because GET only handles string values.
@@ -3393,6 +3413,20 @@ IEnumerable<SortedSetEntry> SortedSetScan(
33933413
/// <remarks><seealso href="https://redis.io/commands/set"/></remarks>
33943414
bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None);
33953415

3416+
/// <summary>
3417+
/// Set <paramref name="key"/> to hold the string <paramref name="value"/>, if it matches the given <paramref name="when"/> condition.
3418+
/// </summary>
3419+
/// <param name="key">The key of the string.</param>
3420+
/// <param name="value">The value to set.</param>
3421+
/// <param name="expiry">The expiry to set.</param>
3422+
/// <param name="when">The condition to enforce.</param>
3423+
/// <param name="flags">The flags to use for this operation.</param>
3424+
/// <remarks>See <seealso href="https://redis.io/commands/delex"/>.</remarks>
3425+
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
3426+
#pragma warning disable RS0027
3427+
bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None);
3428+
#pragma warning restore RS0027
3429+
33963430
/// <summary>
33973431
/// Sets the given keys to their respective values.
33983432
/// If <see cref="When.NotExists"/> is specified, this will not perform any operation at all even if just a single key already exists.

src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.ComponentModel;
4+
using System.Diagnostics.CodeAnalysis;
45
using System.Net;
56
using System.Threading.Tasks;
67

@@ -771,9 +772,17 @@ IAsyncEnumerable<SortedSetEntry> SortedSetScanAsync(
771772
/// <inheritdoc cref="IDatabase.StringDecrement(RedisKey, long, CommandFlags)"/>
772773
Task<long> StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None);
773774

775+
/// <inheritdoc cref="IDatabase.StringDelete(RedisKey, ValueCondition, CommandFlags)"/>
776+
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
777+
Task<bool> StringDeleteAsync(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None);
778+
774779
/// <inheritdoc cref="IDatabase.StringDecrement(RedisKey, double, CommandFlags)"/>
775780
Task<double> StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None);
776781

782+
/// <inheritdoc cref="IDatabase.StringDigest(RedisKey, CommandFlags)"/>
783+
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
784+
Task<ValueCondition?> StringDigestAsync(RedisKey key, CommandFlags flags = CommandFlags.None);
785+
777786
/// <inheritdoc cref="IDatabase.StringGet(RedisKey, CommandFlags)"/>
778787
Task<RedisValue> StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None);
779788

@@ -833,6 +842,12 @@ IAsyncEnumerable<SortedSetEntry> SortedSetScanAsync(
833842
/// <inheritdoc cref="IDatabase.StringSet(RedisKey, RedisValue, TimeSpan?, bool, When, CommandFlags)"/>
834843
Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None);
835844

845+
/// <inheritdoc cref="IDatabase.StringSet(RedisKey, RedisValue, TimeSpan?, ValueCondition, CommandFlags)"/>
846+
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
847+
#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
848+
Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None);
849+
#pragma warning restore RS0027
850+
836851
/// <inheritdoc cref="IDatabase.StringSet(KeyValuePair{RedisKey, RedisValue}[], When, CommandFlags)"/>
837852
Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when, CommandFlags flags);
838853

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,9 +732,15 @@ public Task<long> StringBitPositionAsync(RedisKey key, bool bit, long start, lon
732732
public Task<long> StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) =>
733733
Inner.StringBitPositionAsync(ToInner(key), bit, start, end, indexType, flags);
734734

735+
public Task<bool> StringDeleteAsync(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None) =>
736+
Inner.StringDeleteAsync(ToInner(key), when, flags);
737+
735738
public Task<double> StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) =>
736739
Inner.StringDecrementAsync(ToInner(key), value, flags);
737740

741+
public Task<ValueCondition?> StringDigestAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
742+
Inner.StringDigestAsync(ToInner(key), flags);
743+
738744
public Task<long> StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) =>
739745
Inner.StringDecrementAsync(ToInner(key), value, flags);
740746

@@ -777,6 +783,9 @@ public Task<long> StringIncrementAsync(RedisKey key, long value = 1, CommandFlag
777783
public Task<long> StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
778784
Inner.StringLengthAsync(ToInner(key), flags);
779785

786+
public Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None)
787+
=> Inner.StringSetAsync(ToInner(key), value, expiry, when, flags);
788+
780789
public Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
781790
Inner.StringSetAsync(ToInner(values), when, flags);
782791

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,9 +714,15 @@ public long StringBitPosition(RedisKey key, bool bit, long start, long end, Comm
714714
public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) =>
715715
Inner.StringBitPosition(ToInner(key), bit, start, end, indexType, flags);
716716

717+
public bool StringDelete(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None) =>
718+
Inner.StringDelete(ToInner(key), when, flags);
719+
717720
public double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) =>
718721
Inner.StringDecrement(ToInner(key), value, flags);
719722

723+
public ValueCondition? StringDigest(RedisKey key, CommandFlags flags = CommandFlags.None) =>
724+
Inner.StringDigest(ToInner(key), flags);
725+
720726
public long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) =>
721727
Inner.StringDecrement(ToInner(key), value, flags);
722728

@@ -759,6 +765,9 @@ public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = C
759765
public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) =>
760766
Inner.StringLength(ToInner(key), flags);
761767

768+
public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None)
769+
=> Inner.StringSet(ToInner(key), value, expiry, when, flags);
770+
762771
public bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
763772
Inner.StringSet(ToInner(values), when, flags);
764773

0 commit comments

Comments
 (0)