Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<PackageVersion Include="System.Threading.Channels" Version="5.0.0" />
<PackageVersion Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
<PackageVersion Include="System.IO.Compression" Version="4.3.0" />
<PackageVersion Include="System.IO.Hashing" Version="9.0.10" />

<!-- For analyzers, tied to the consumer's build SDK; at the moment, that means "us" -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
Expand Down
3 changes: 3 additions & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Current package versions:

## Unreleased

- Support Redis 8.4 CAS/CAD operations (`DIGEST`, and the `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` modifiers on `SET` / `DEL`)
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))

## 2.9.32

- Fix `SSUBSCRIBE` routing during slot migrations ([#2969 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2969))
Expand Down
26 changes: 26 additions & 0 deletions docs/exp/SER002.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Redis 8.4 is currently in preview and may be subject to change.

New features in Redis 8.4 include:

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

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

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

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

```xml
<NoWarn>$(NoWarn);SER002</NoWarn>
```

or more granularly / locally in C#:

``` c#
#pragma warning disable SER002
```
1 change: 1 addition & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" PrivateAssets="all" />
Expand Down
4 changes: 4 additions & 0 deletions src/StackExchange.Redis/Enums/RedisCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ internal enum RedisCommand
DECR,
DECRBY,
DEL,
DELEX,
DIGEST,
DISCARD,
DUMP,

Expand Down Expand Up @@ -299,6 +301,8 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.DECR:
case RedisCommand.DECRBY:
case RedisCommand.DEL:
case RedisCommand.DELEX:
case RedisCommand.DIGEST:
case RedisCommand.EXPIRE:
case RedisCommand.EXPIREAT:
case RedisCommand.FLUSHALL:
Expand Down
3 changes: 3 additions & 0 deletions src/StackExchange.Redis/Experiments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ namespace StackExchange.Redis
internal static class Experiments
{
public const string UrlFormat = "https://stackexchange.github.io/StackExchange.Redis/exp/";

public const string VectorSets = "SER001";
// ReSharper disable once InconsistentNaming
public const string Server_8_4 = "SER002";
}
}

Expand Down
34 changes: 34 additions & 0 deletions src/StackExchange.Redis/Interfaces/IDatabase.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Net;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -3141,6 +3142,16 @@ IEnumerable<SortedSetEntry> SortedSetScan(
/// </remarks>
long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Deletes <paramref name="key"/> if it matches the given <paramref name="when"/> condition.
/// </summary>
/// <param name="key">The key of the string.</param>
/// <param name="when">The condition to enforce.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <remarks>See <seealso href="https://redis.io/commands/delex"/>.</remarks>
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
bool StringDelete(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Decrements the string representing a floating point number stored at key by the specified decrement.
/// If the key does not exist, it is set to 0 before performing the operation.
Expand All @@ -3153,6 +3164,15 @@ IEnumerable<SortedSetEntry> SortedSetScan(
/// <remarks><seealso href="https://redis.io/commands/incrbyfloat"/></remarks>
double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Gets the digest (hash) value of the specified key, represented as a digest equality <see cref="ValueCondition"/>.
/// </summary>
/// <param name="key">The key of the string.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <remarks><seealso href="https://redis.io/commands/digest"/></remarks>
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
ValueCondition? StringDigest(RedisKey key, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Get the value of key. If the key does not exist the special value <see cref="RedisValue.Null"/> is returned.
/// An error is returned if the value stored at key is not a string, because GET only handles string values.
Expand Down Expand Up @@ -3360,6 +3380,20 @@ IEnumerable<SortedSetEntry> SortedSetScan(
/// <remarks><seealso href="https://redis.io/commands/set"/></remarks>
bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Set <paramref name="key"/> to hold the string <paramref name="value"/>, if it matches the given <paramref name="when"/> condition.
/// </summary>
/// <param name="key">The key of the string.</param>
/// <param name="value">The value to set.</param>
/// <param name="expiry">The expiry to set.</param>
/// <param name="when">The condition to enforce.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <remarks>See <seealso href="https://redis.io/commands/delex"/>.</remarks>
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
#pragma warning disable RS0027
bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None);
#pragma warning restore RS0027

/// <summary>
/// Sets the given keys to their respective values.
/// If <see cref="When.NotExists"/> is specified, this will not perform any operation at all even if just a single key already exists.
Expand Down
15 changes: 15 additions & 0 deletions src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Threading.Tasks;

Expand Down Expand Up @@ -768,9 +769,17 @@ IAsyncEnumerable<SortedSetEntry> SortedSetScanAsync(
/// <inheritdoc cref="IDatabase.StringDecrement(RedisKey, long, CommandFlags)"/>
Task<long> StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None);

/// <inheritdoc cref="IDatabase.StringDelete(RedisKey, ValueCondition, CommandFlags)"/>
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
Task<bool> StringDeleteAsync(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None);

/// <inheritdoc cref="IDatabase.StringDecrement(RedisKey, double, CommandFlags)"/>
Task<double> StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None);

/// <inheritdoc cref="IDatabase.StringDigest(RedisKey, CommandFlags)"/>
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
Task<ValueCondition?> StringDigestAsync(RedisKey key, CommandFlags flags = CommandFlags.None);

/// <inheritdoc cref="IDatabase.StringGet(RedisKey, CommandFlags)"/>
Task<RedisValue> StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None);

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

/// <inheritdoc cref="IDatabase.StringSet(RedisKey, RedisValue, TimeSpan?, ValueCondition, CommandFlags)"/>
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None);
#pragma warning restore RS0027

/// <inheritdoc cref="IDatabase.StringSet(KeyValuePair{RedisKey, RedisValue}[], When, CommandFlags)"/>
Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None);

Expand Down
9 changes: 9 additions & 0 deletions src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -726,9 +726,15 @@ public Task<long> StringBitPositionAsync(RedisKey key, bool bit, long start, lon
public Task<long> StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) =>
Inner.StringBitPositionAsync(ToInner(key), bit, start, end, indexType, flags);

public Task<bool> StringDeleteAsync(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None) =>
Inner.StringDeleteAsync(ToInner(key), when, flags);

public Task<double> StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) =>
Inner.StringDecrementAsync(ToInner(key), value, flags);

public Task<ValueCondition?> StringDigestAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
Inner.StringDigestAsync(ToInner(key), flags);

public Task<long> StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) =>
Inner.StringDecrementAsync(ToInner(key), value, flags);

Expand Down Expand Up @@ -771,6 +777,9 @@ public Task<long> StringIncrementAsync(RedisKey key, long value = 1, CommandFlag
public Task<long> StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
Inner.StringLengthAsync(ToInner(key), flags);

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -708,9 +708,15 @@ public long StringBitPosition(RedisKey key, bool bit, long start, long end, Comm
public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) =>
Inner.StringBitPosition(ToInner(key), bit, start, end, indexType, flags);

public bool StringDelete(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None) =>
Inner.StringDelete(ToInner(key), when, flags);

public double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) =>
Inner.StringDecrement(ToInner(key), value, flags);

public ValueCondition? StringDigest(RedisKey key, CommandFlags flags = CommandFlags.None) =>
Inner.StringDigest(ToInner(key), flags);

public long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) =>
Inner.StringDecrement(ToInner(key), value, flags);

Expand Down Expand Up @@ -753,6 +759,9 @@ public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = C
public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) =>
Inner.StringLength(ToInner(key), flags);

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

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

Expand Down
71 changes: 71 additions & 0 deletions src/StackExchange.Redis/Message.ValueCondition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;

namespace StackExchange.Redis;

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

public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, TimeSpan? expiry, in ValueCondition when)
=> new KeyValueExpiryConditionMessage(db, flags, command, key, value, expiry, when);

private sealed class KeyConditionMessage(
int db,
CommandFlags flags,
RedisCommand command,
in RedisKey key,
in ValueCondition when)
: CommandKeyBase(db, flags, command, key)
{
private readonly ValueCondition _when = when;

public override int ArgCount => 1 + _when.TokenCount;

protected override void WriteImpl(PhysicalConnection physical)
{
physical.WriteHeader(Command, ArgCount);
physical.Write(Key);
_when.WriteTo(physical);
}
}

private sealed class KeyValueExpiryConditionMessage(
int db,
CommandFlags flags,
RedisCommand command,
in RedisKey key,
in RedisValue value,
TimeSpan? expiry,
in ValueCondition when)
: CommandKeyBase(db, flags, command, key)
{
private readonly RedisValue _value = value;
private readonly ValueCondition _when = when;
private readonly TimeSpan? _expiry = expiry == TimeSpan.MaxValue ? null : expiry;

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

protected override void WriteImpl(PhysicalConnection physical)
{
physical.WriteHeader(Command, ArgCount);
physical.Write(Key);
physical.WriteBulkString(_value);
if (_expiry.HasValue)
{
var ms = (long)_expiry.GetValueOrDefault().TotalMilliseconds;
if ((ms % 1000) == 0)
{
physical.WriteBulkString("EX"u8);
physical.WriteBulkString(ms / 1000);
}
else
{
physical.WriteBulkString("PX"u8);
physical.WriteBulkString(ms);
}
}
_when.WriteTo(physical);
}
}
}
2 changes: 1 addition & 1 deletion src/StackExchange.Redis/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ protected override void WriteImpl(PhysicalConnection physical)
public ILogger Log => log;
}

internal abstract class Message : ICompletable
internal abstract partial class Message : ICompletable
{
public readonly int Db;

Expand Down
27 changes: 27 additions & 0 deletions src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,28 @@
#nullable enable
StackExchange.Redis.RedisFeatures.DeleteWithValueCheck.get -> bool
StackExchange.Redis.RedisFeatures.SetWithValueCheck.get -> bool
[SER002]override StackExchange.Redis.ValueCondition.Equals(object? obj) -> bool
[SER002]override StackExchange.Redis.ValueCondition.GetHashCode() -> int
[SER002]override StackExchange.Redis.ValueCondition.ToString() -> string!
[SER002]StackExchange.Redis.IDatabase.StringDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
[SER002]StackExchange.Redis.IDatabase.StringDigest(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ValueCondition?
[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
[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>!
[SER002]StackExchange.Redis.IDatabaseAsync.StringDigestAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.ValueCondition?>!
[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>!
[SER002]StackExchange.Redis.ValueCondition
[SER002]StackExchange.Redis.ValueCondition.AsDigest() -> StackExchange.Redis.ValueCondition
[SER002]StackExchange.Redis.ValueCondition.Value.get -> StackExchange.Redis.RedisValue
[SER002]StackExchange.Redis.ValueCondition.ValueCondition() -> void
[SER002]static StackExchange.Redis.ValueCondition.Always.get -> StackExchange.Redis.ValueCondition
[SER002]static StackExchange.Redis.ValueCondition.CalculateDigest(System.ReadOnlySpan<byte> value) -> StackExchange.Redis.ValueCondition
[SER002]static StackExchange.Redis.ValueCondition.DigestEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition
[SER002]static StackExchange.Redis.ValueCondition.DigestNotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition
[SER002]static StackExchange.Redis.ValueCondition.Equal(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition
[SER002]static StackExchange.Redis.ValueCondition.Exists.get -> StackExchange.Redis.ValueCondition
[SER002]static StackExchange.Redis.ValueCondition.implicit operator StackExchange.Redis.ValueCondition(StackExchange.Redis.When when) -> StackExchange.Redis.ValueCondition
[SER002]static StackExchange.Redis.ValueCondition.NotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition
[SER002]static StackExchange.Redis.ValueCondition.NotExists.get -> StackExchange.Redis.ValueCondition
[SER002]static StackExchange.Redis.ValueCondition.operator !(in StackExchange.Redis.ValueCondition value) -> StackExchange.Redis.ValueCondition
[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan<byte> digest) -> StackExchange.Redis.ValueCondition
[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan<char> digest) -> StackExchange.Redis.ValueCondition
Loading
Loading