Skip to content

Commit 89270ea

Browse files
authored
Support ZADD INCR (#3071)
* Support ZADD INCR; fix #3069 * PR number in release note * use high bits of enum for CH/INCR * LoggerTests: synchronize over the SB (CI race condition, rare) * CI stability: `VectorSetAdd_WithEverything` - disambiguate test key * VADD: clarify where CI is failing * CI: stabilize GC test * avoid m02 * more tools to inlvestigate weird VADD break on CI * unused directive * unused extern * more VADD investigation * move FP32 logic to the request level * giving up for now; logging separately * note ticket number
1 parent 006cfaf commit 89270ea

27 files changed

Lines changed: 646 additions & 180 deletions

StackExchange.Redis.slnx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<Solution>
2+
<Folder Name="/eng/">
3+
<Project Path="eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj" />
4+
</Folder>
5+
<Folder Name="/Solution Items/">
6+
<File Path=".editorconfig" />
7+
<File Path=".github/workflows/CI.yml" />
8+
<File Path=".github/workflows/codeql.yml" />
9+
<File Path="appveyor.yml" />
10+
<File Path="build.cmd" />
11+
<File Path="Build.csproj" />
12+
<File Path="build.ps1" />
13+
<File Path="Directory.Build.props" />
14+
<File Path="Directory.Build.targets" />
15+
<File Path="Directory.Packages.props" />
16+
<File Path="docs/ReleaseNotes.md" />
17+
<File Path="global.json" />
18+
<File Path="NuGet.Config" />
19+
<File Path="README.md" />
20+
<File Path="Shared.ruleset" />
21+
<File Path="tests/RedisConfigs/.docker/Redis/Dockerfile" />
22+
<File Path="tests/RedisConfigs/docker-compose.yml" />
23+
<File Path="version.json" />
24+
</Folder>
25+
<Folder Name="/src/">
26+
<File Path="src/Directory.Build.props" />
27+
<Project Path="src/RESPite/RESPite.csproj" />
28+
<Project Path="src/StackExchange.Redis/StackExchange.Redis.csproj" />
29+
</Folder>
30+
<Folder Name="/tests/">
31+
<File Path="tests/.editorconfig" />
32+
<File Path="tests/Directory.Build.targets" />
33+
<Project Path="tests/BasicTest/BasicTest.csproj" />
34+
<Project Path="tests/BasicTestBaseline/BasicTestBaseline.csproj" />
35+
<Project Path="tests/ConsoleTest/ConsoleTest.csproj" />
36+
<Project Path="tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj" />
37+
<Project Path="tests/RESPite.Tests/RESPite.Tests.csproj" />
38+
<Project Path="tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj" />
39+
<Project Path="tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj" />
40+
</Folder>
41+
<Folder Name="/tests/RedisConfigs/">
42+
<File Path="tests/RedisConfigs/cli-master.cmd" />
43+
<File Path="tests/RedisConfigs/cli-secure.cmd" />
44+
<File Path="tests/RedisConfigs/cli-slave.cmd" />
45+
<File Path="tests/RedisConfigs/docker-compose.yml" />
46+
<File Path="tests/RedisConfigs/Dockerfile" />
47+
<File Path="tests/RedisConfigs/start-all.cmd" />
48+
<File Path="tests/RedisConfigs/start-all.sh" />
49+
<File Path="tests/RedisConfigs/start-basic.cmd" />
50+
<File Path="tests/RedisConfigs/start-basic.sh" />
51+
<File Path="tests/RedisConfigs/start-cluster.cmd" />
52+
<File Path="tests/RedisConfigs/start-sentinel.cmd" />
53+
<File Path="tests/RedisConfigs/wsl2.md" />
54+
</Folder>
55+
<Folder Name="/tests/RedisConfigs/Basic/">
56+
<File Path="tests/RedisConfigs/Basic/primary-6379.conf" />
57+
<File Path="tests/RedisConfigs/Basic/replica-6380.conf" />
58+
<File Path="tests/RedisConfigs/Basic/secure-6381.conf" />
59+
<File Path="tests/RedisConfigs/Basic/tls-ciphers-6384.conf" />
60+
</Folder>
61+
<Folder Name="/tests/RedisConfigs/Cluster/">
62+
<File Path="tests/RedisConfigs/Cluster/cluster-7000.conf" />
63+
<File Path="tests/RedisConfigs/Cluster/cluster-7001.conf" />
64+
<File Path="tests/RedisConfigs/Cluster/cluster-7002.conf" />
65+
<File Path="tests/RedisConfigs/Cluster/cluster-7003.conf" />
66+
<File Path="tests/RedisConfigs/Cluster/cluster-7004.conf" />
67+
<File Path="tests/RedisConfigs/Cluster/cluster-7005.conf" />
68+
<File Path="tests/RedisConfigs/Cluster/nodes-7000.conf" />
69+
<File Path="tests/RedisConfigs/Cluster/nodes-7001.conf" />
70+
<File Path="tests/RedisConfigs/Cluster/nodes-7002.conf" />
71+
<File Path="tests/RedisConfigs/Cluster/nodes-7003.conf" />
72+
<File Path="tests/RedisConfigs/Cluster/nodes-7004.conf" />
73+
<File Path="tests/RedisConfigs/Cluster/nodes-7005.conf" />
74+
</Folder>
75+
<Folder Name="/tests/RedisConfigs/Docker/">
76+
<File Path="tests/RedisConfigs/Docker/docker-entrypoint.sh" />
77+
<File Path="tests/RedisConfigs/Docker/supervisord.conf" />
78+
</Folder>
79+
<Folder Name="/tests/RedisConfigs/Failover/">
80+
<File Path="tests/RedisConfigs/Failover/primary-6382.conf" />
81+
<File Path="tests/RedisConfigs/Failover/replica-6383.conf" />
82+
</Folder>
83+
<Folder Name="/tests/RedisConfigs/Sentinel/">
84+
<File Path="tests/RedisConfigs/Sentinel/redis-7010.conf" />
85+
<File Path="tests/RedisConfigs/Sentinel/redis-7011.conf" />
86+
<File Path="tests/RedisConfigs/Sentinel/sentinel-26379.conf" />
87+
<File Path="tests/RedisConfigs/Sentinel/sentinel-26380.conf" />
88+
<File Path="tests/RedisConfigs/Sentinel/sentinel-26381.conf" />
89+
</Folder>
90+
<Folder Name="/toys/">
91+
<Project Path="toys/KestrelRedisServer/KestrelRedisServer.csproj" />
92+
<Project Path="toys/OpBench/OpBench.csproj" />
93+
<Project Path="toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj" />
94+
<Project Path="toys/TestConsole/TestConsole.csproj" />
95+
<Project Path="toys/TestConsoleBaseline/TestConsoleBaseline.csproj" />
96+
</Folder>
97+
<Project Path=".github/.github.csproj" />
98+
<Project Path="docs/docs.csproj" />
99+
</Solution>

docs/ReleaseNotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Current package versions:
1212
- Add Redis 8.8 stream negative acknowledgements (`XNACK`) ([#3058 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3058))
1313
- Update experimental `GCRA` APIs and wire protocol terminology from "requests" to "tokens", to match server change ([#3051 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3051))
1414
- Add experimental `Aggregate.Count` support for sorted-set combination operations against Redis 8.8 ([#3059 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3059))
15+
- Add `ValueCondition` overloads for `SortedSetIncrement`/`SortedSetIncrementAsync`, supporting `ZADD INCR` with existence conditions ([#3071 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3071))
1516
- Recognize Azure Managed Redis (AMR) resources in new Azure clouds ([#3068 by @philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/3068))
1617

1718
## 2.12.14

src/StackExchange.Redis/Enums/SortedSetWhen.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,6 @@ public enum SortedSetWhen
3636

3737
internal static class SortedSetWhenExtensions
3838
{
39-
internal static uint CountBits(this SortedSetWhen when)
40-
{
41-
uint v = (uint)when;
42-
v -= (v >> 1) & 0x55555555; // reuse input as temporary
43-
v = (v & 0x33333333) + ((v >> 2) & 0x33333333); // temp
44-
uint c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; // count
45-
return c;
46-
}
47-
4839
internal static SortedSetWhen Parse(When when) => when switch
4940
{
5041
When.Always => SortedSetWhen.Always,

src/StackExchange.Redis/Interfaces/IDatabase.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2101,7 +2101,25 @@ public partial interface IDatabase : IRedis, IDatabaseAsync
21012101
/// <param name="flags">The flags to use for this operation.</param>
21022102
/// <returns>The new score of member.</returns>
21032103
/// <remarks><seealso href="https://redis.io/commands/zincrby"/></remarks>
2104+
#pragma warning disable RS0027 // conditional overload needs an additional required ValueCondition parameter
21042105
double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None);
2106+
#pragma warning restore RS0027
2107+
2108+
/// <summary>
2109+
/// Increments the score of member in the sorted set stored at key by increment, when the specified condition is met.
2110+
/// If member does not exist in the sorted set and the condition permits it, it is added with increment as its score (as if its previous score was 0.0).
2111+
/// </summary>
2112+
/// <param name="key">The key of the sorted set.</param>
2113+
/// <param name="member">The member to increment.</param>
2114+
/// <param name="value">The amount to increment by.</param>
2115+
/// <param name="when">The condition to increment the element under; only existence conditions are currently supported.</param>
2116+
/// <param name="flags">The flags to use for this operation.</param>
2117+
/// <returns>The new score of member, or <see langword="null"/> when the condition was not met.</returns>
2118+
/// <remarks>
2119+
/// <para>Uses <c>ZINCRBY</c> when <paramref name="when"/> is <see cref="ValueCondition.Always"/>, and <c>ZADD INCR</c> for <see cref="ValueCondition.Exists"/> and <see cref="ValueCondition.NotExists"/>.</para>
2120+
/// <para><seealso href="https://redis.io/commands/zadd"/></para>
2121+
/// </remarks>
2122+
double? SortedSetIncrement(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags);
21052123

21062124
/// <summary>
21072125
/// Returns the cardinality of the intersection of the sorted sets at <paramref name="keys"/>.

src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,12 @@ public partial interface IDatabaseAsync : IRedisAsync
500500
Task<double> SortedSetDecrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None);
501501

502502
/// <inheritdoc cref="IDatabase.SortedSetIncrement(RedisKey, RedisValue, double, CommandFlags)"/>
503+
#pragma warning disable RS0027 // conditional overload needs an additional required ValueCondition parameter
503504
Task<double> SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None);
505+
#pragma warning restore RS0027
506+
507+
/// <inheritdoc cref="IDatabase.SortedSetIncrement(RedisKey, RedisValue, double, ValueCondition, CommandFlags)"/>
508+
Task<double?> SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags);
504509

505510
/// <inheritdoc cref="IDatabase.SortedSetIntersectionLength(RedisKey[], long, CommandFlags)"/>
506511
Task<long> SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None);

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,9 @@ public Task<double> SortedSetDecrementAsync(RedisKey key, RedisValue member, dou
506506
public Task<double> SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) =>
507507
Inner.SortedSetIncrementAsync(ToInner(key), member, value, flags);
508508

509+
public Task<double?> SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags) =>
510+
Inner.SortedSetIncrementAsync(ToInner(key), member, value, when, flags);
511+
509512
public Task<long> SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) =>
510513
Inner.SortedSetIntersectionLengthAsync(ToInner(keys), limit, flags);
511514

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,9 @@ public double SortedSetDecrement(RedisKey key, RedisValue member, double value,
491491
public double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) =>
492492
Inner.SortedSetIncrement(ToInner(key), member, value, flags);
493493

494+
public double? SortedSetIncrement(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags) =>
495+
Inner.SortedSetIncrement(ToInner(key), member, value, when, flags);
496+
494497
public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) =>
495498
Inner.SortedSetIntersectionLength(ToInner(keys), limit, flags);
496499

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
StackExchange.Redis.IDatabase.SortedSetIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags) -> double?
3+
StackExchange.Redis.IDatabaseAsync.SortedSetIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task<double?>!

src/StackExchange.Redis/RedisDatabase.cs

Lines changed: 29 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2435,12 +2435,24 @@ public double SortedSetIncrement(RedisKey key, RedisValue member, double value,
24352435
return ExecuteSync(msg, ResultProcessor.Double);
24362436
}
24372437

2438+
public double? SortedSetIncrement(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags)
2439+
{
2440+
var msg = GetSortedSetIncrementMessage(key, member, value, when, flags);
2441+
return ExecuteSync(msg, ResultProcessor.NullableDouble);
2442+
}
2443+
24382444
public Task<double> SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None)
24392445
{
24402446
var msg = Message.Create(Database, flags, RedisCommand.ZINCRBY, key, value, member);
24412447
return ExecuteAsync(msg, ResultProcessor.Double);
24422448
}
24432449

2450+
public Task<double?> SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags)
2451+
{
2452+
var msg = GetSortedSetIncrementMessage(key, member, value, when, flags);
2453+
return ExecuteAsync(msg, ResultProcessor.NullableDouble);
2454+
}
2455+
24442456
public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None)
24452457
{
24462458
var msg = GetSortedSetIntersectionLengthMessage(keys, limit, flags);
@@ -4399,33 +4411,7 @@ private Message GetSetIntersectionLengthMessage(RedisKey[] keys, long limit = 0,
43994411
}
44004412

44014413
private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double score, SortedSetWhen when, bool change, CommandFlags flags)
4402-
{
4403-
RedisValue[] arr = new RedisValue[2 + when.CountBits() + (change ? 1 : 0)];
4404-
int index = 0;
4405-
if ((when & SortedSetWhen.NotExists) != 0)
4406-
{
4407-
arr[index++] = RedisLiterals.NX;
4408-
}
4409-
if ((when & SortedSetWhen.Exists) != 0)
4410-
{
4411-
arr[index++] = RedisLiterals.XX;
4412-
}
4413-
if ((when & SortedSetWhen.GreaterThan) != 0)
4414-
{
4415-
arr[index++] = RedisLiterals.GT;
4416-
}
4417-
if ((when & SortedSetWhen.LessThan) != 0)
4418-
{
4419-
arr[index++] = RedisLiterals.LT;
4420-
}
4421-
if (change)
4422-
{
4423-
arr[index++] = RedisLiterals.CH;
4424-
}
4425-
arr[index++] = score;
4426-
arr[index++] = member;
4427-
return Message.Create(Database, flags, RedisCommand.ZADD, key, arr);
4428-
}
4414+
=> new SingleSortedSetAddMessage(Database, flags, key, member, score, when, change, increment: false);
44294415

44304416
private Message? GetSortedSetAddMessage(RedisKey key, SortedSetEntry[] values, SortedSetWhen when, bool change, CommandFlags flags)
44314417
{
@@ -4436,35 +4422,23 @@ private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double s
44364422
case 1:
44374423
return GetSortedSetAddMessage(key, values[0].element, values[0].score, when, change, flags);
44384424
default:
4439-
RedisValue[] arr = new RedisValue[(values.Length * 2) + when.CountBits() + (change ? 1 : 0)];
4440-
int index = 0;
4441-
if ((when & SortedSetWhen.NotExists) != 0)
4442-
{
4443-
arr[index++] = RedisLiterals.NX;
4444-
}
4445-
if ((when & SortedSetWhen.Exists) != 0)
4446-
{
4447-
arr[index++] = RedisLiterals.XX;
4448-
}
4449-
if ((when & SortedSetWhen.GreaterThan) != 0)
4450-
{
4451-
arr[index++] = RedisLiterals.GT;
4452-
}
4453-
if ((when & SortedSetWhen.LessThan) != 0)
4454-
{
4455-
arr[index++] = RedisLiterals.LT;
4456-
}
4457-
if (change)
4458-
{
4459-
arr[index++] = RedisLiterals.CH;
4460-
}
4425+
return new MultipleSortedSetAddMessage(Database, flags, key, values, when, change);
4426+
}
4427+
}
44614428

4462-
for (int i = 0; i < values.Length; i++)
4463-
{
4464-
arr[index++] = values[i].score;
4465-
arr[index++] = values[i].element;
4466-
}
4467-
return Message.Create(Database, flags, RedisCommand.ZADD, key, arr);
4429+
private Message GetSortedSetIncrementMessage(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags)
4430+
{
4431+
switch (when.Kind)
4432+
{
4433+
case ValueCondition.ConditionKind.Always:
4434+
return Message.Create(Database, flags, RedisCommand.ZINCRBY, key, value, member);
4435+
case ValueCondition.ConditionKind.Exists:
4436+
return new SingleSortedSetAddMessage(Database, flags, key, member, value, SortedSetWhen.Exists, change: false, increment: true);
4437+
case ValueCondition.ConditionKind.NotExists:
4438+
return new SingleSortedSetAddMessage(Database, flags, key, member, value, SortedSetWhen.NotExists, change: false, increment: true);
4439+
default:
4440+
when.ThrowInvalidOperation(nameof(SortedSetIncrement));
4441+
goto case ValueCondition.ConditionKind.Always; // not reached
44684442
}
44694443
}
44704444

0 commit comments

Comments
 (0)