diff --git a/StackExchange.Redis.slnx b/StackExchange.Redis.slnx
new file mode 100644
index 000000000..853429fb3
--- /dev/null
+++ b/StackExchange.Redis.slnx
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md
index 2ae0a91ea..453eeaa58 100644
--- a/docs/ReleaseNotes.md
+++ b/docs/ReleaseNotes.md
@@ -12,6 +12,7 @@ Current package versions:
- Add Redis 8.8 stream negative acknowledgements (`XNACK`) ([#3058 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3058))
- 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))
- 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))
+- Add `ValueCondition` overloads for `SortedSetIncrement`/`SortedSetIncrementAsync`, supporting `ZADD INCR` with existence conditions ([#3071 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3071))
- Recognize Azure Managed Redis (AMR) resources in new Azure clouds ([#3068 by @philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/3068))
## 2.12.14
diff --git a/src/StackExchange.Redis/Enums/SortedSetWhen.cs b/src/StackExchange.Redis/Enums/SortedSetWhen.cs
index 517aaeaa5..6cfa7664a 100644
--- a/src/StackExchange.Redis/Enums/SortedSetWhen.cs
+++ b/src/StackExchange.Redis/Enums/SortedSetWhen.cs
@@ -36,15 +36,6 @@ public enum SortedSetWhen
internal static class SortedSetWhenExtensions
{
- internal static uint CountBits(this SortedSetWhen when)
- {
- uint v = (uint)when;
- v -= (v >> 1) & 0x55555555; // reuse input as temporary
- v = (v & 0x33333333) + ((v >> 2) & 0x33333333); // temp
- uint c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; // count
- return c;
- }
-
internal static SortedSetWhen Parse(When when) => when switch
{
When.Always => SortedSetWhen.Always,
diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs
index 149cd3797..8866f5669 100644
--- a/src/StackExchange.Redis/Interfaces/IDatabase.cs
+++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs
@@ -2101,7 +2101,25 @@ public partial interface IDatabase : IRedis, IDatabaseAsync
/// The flags to use for this operation.
/// The new score of member.
///
+#pragma warning disable RS0027 // conditional overload needs an additional required ValueCondition parameter
double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None);
+#pragma warning restore RS0027
+
+ ///
+ /// Increments the score of member in the sorted set stored at key by increment, when the specified condition is met.
+ /// 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).
+ ///
+ /// The key of the sorted set.
+ /// The member to increment.
+ /// The amount to increment by.
+ /// The condition to increment the element under; only existence conditions are currently supported.
+ /// The flags to use for this operation.
+ /// The new score of member, or when the condition was not met.
+ ///
+ /// Uses ZINCRBY when is , and ZADD INCR for and .
+ ///
+ ///
+ double? SortedSetIncrement(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags);
///
/// Returns the cardinality of the intersection of the sorted sets at .
diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
index af131135f..ae583295f 100644
--- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
+++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
@@ -500,7 +500,12 @@ public partial interface IDatabaseAsync : IRedisAsync
Task SortedSetDecrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None);
///
+#pragma warning disable RS0027 // conditional overload needs an additional required ValueCondition parameter
Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None);
+#pragma warning restore RS0027
+
+ ///
+ Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags);
///
Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None);
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
index cd8171f5a..a880fa672 100644
--- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
@@ -506,6 +506,9 @@ public Task SortedSetDecrementAsync(RedisKey key, RedisValue member, dou
public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetIncrementAsync(ToInner(key), member, value, flags);
+ public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags) =>
+ Inner.SortedSetIncrementAsync(ToInner(key), member, value, when, flags);
+
public Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetIntersectionLengthAsync(ToInner(keys), limit, flags);
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
index 78e3959d6..0c1ed5294 100644
--- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
@@ -491,6 +491,9 @@ public double SortedSetDecrement(RedisKey key, RedisValue member, double value,
public double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetIncrement(ToInner(key), member, value, flags);
+ public double? SortedSetIncrement(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags) =>
+ Inner.SortedSetIncrement(ToInner(key), member, value, when, flags);
+
public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetIntersectionLength(ToInner(keys), limit, flags);
diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
index ab058de62..6b377a056 100644
--- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+StackExchange.Redis.IDatabase.SortedSetIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags) -> double?
+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!
diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs
index cdf4fc9af..e33fd69b4 100644
--- a/src/StackExchange.Redis/RedisDatabase.cs
+++ b/src/StackExchange.Redis/RedisDatabase.cs
@@ -2435,12 +2435,24 @@ public double SortedSetIncrement(RedisKey key, RedisValue member, double value,
return ExecuteSync(msg, ResultProcessor.Double);
}
+ public double? SortedSetIncrement(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags)
+ {
+ var msg = GetSortedSetIncrementMessage(key, member, value, when, flags);
+ return ExecuteSync(msg, ResultProcessor.NullableDouble);
+ }
+
public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None)
{
var msg = Message.Create(Database, flags, RedisCommand.ZINCRBY, key, value, member);
return ExecuteAsync(msg, ResultProcessor.Double);
}
+ public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags)
+ {
+ var msg = GetSortedSetIncrementMessage(key, member, value, when, flags);
+ return ExecuteAsync(msg, ResultProcessor.NullableDouble);
+ }
+
public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None)
{
var msg = GetSortedSetIntersectionLengthMessage(keys, limit, flags);
@@ -4399,33 +4411,7 @@ private Message GetSetIntersectionLengthMessage(RedisKey[] keys, long limit = 0,
}
private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double score, SortedSetWhen when, bool change, CommandFlags flags)
- {
- RedisValue[] arr = new RedisValue[2 + when.CountBits() + (change ? 1 : 0)];
- int index = 0;
- if ((when & SortedSetWhen.NotExists) != 0)
- {
- arr[index++] = RedisLiterals.NX;
- }
- if ((when & SortedSetWhen.Exists) != 0)
- {
- arr[index++] = RedisLiterals.XX;
- }
- if ((when & SortedSetWhen.GreaterThan) != 0)
- {
- arr[index++] = RedisLiterals.GT;
- }
- if ((when & SortedSetWhen.LessThan) != 0)
- {
- arr[index++] = RedisLiterals.LT;
- }
- if (change)
- {
- arr[index++] = RedisLiterals.CH;
- }
- arr[index++] = score;
- arr[index++] = member;
- return Message.Create(Database, flags, RedisCommand.ZADD, key, arr);
- }
+ => new SingleSortedSetAddMessage(Database, flags, key, member, score, when, change, increment: false);
private Message? GetSortedSetAddMessage(RedisKey key, SortedSetEntry[] values, SortedSetWhen when, bool change, CommandFlags flags)
{
@@ -4436,35 +4422,23 @@ private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double s
case 1:
return GetSortedSetAddMessage(key, values[0].element, values[0].score, when, change, flags);
default:
- RedisValue[] arr = new RedisValue[(values.Length * 2) + when.CountBits() + (change ? 1 : 0)];
- int index = 0;
- if ((when & SortedSetWhen.NotExists) != 0)
- {
- arr[index++] = RedisLiterals.NX;
- }
- if ((when & SortedSetWhen.Exists) != 0)
- {
- arr[index++] = RedisLiterals.XX;
- }
- if ((when & SortedSetWhen.GreaterThan) != 0)
- {
- arr[index++] = RedisLiterals.GT;
- }
- if ((when & SortedSetWhen.LessThan) != 0)
- {
- arr[index++] = RedisLiterals.LT;
- }
- if (change)
- {
- arr[index++] = RedisLiterals.CH;
- }
+ return new MultipleSortedSetAddMessage(Database, flags, key, values, when, change);
+ }
+ }
- for (int i = 0; i < values.Length; i++)
- {
- arr[index++] = values[i].score;
- arr[index++] = values[i].element;
- }
- return Message.Create(Database, flags, RedisCommand.ZADD, key, arr);
+ private Message GetSortedSetIncrementMessage(RedisKey key, RedisValue member, double value, ValueCondition when, CommandFlags flags)
+ {
+ switch (when.Kind)
+ {
+ case ValueCondition.ConditionKind.Always:
+ return Message.Create(Database, flags, RedisCommand.ZINCRBY, key, value, member);
+ case ValueCondition.ConditionKind.Exists:
+ return new SingleSortedSetAddMessage(Database, flags, key, member, value, SortedSetWhen.Exists, change: false, increment: true);
+ case ValueCondition.ConditionKind.NotExists:
+ return new SingleSortedSetAddMessage(Database, flags, key, member, value, SortedSetWhen.NotExists, change: false, increment: true);
+ default:
+ when.ThrowInvalidOperation(nameof(SortedSetIncrement));
+ goto case ValueCondition.ConditionKind.Always; // not reached
}
}
diff --git a/src/StackExchange.Redis/SortedSetAddMessage.cs b/src/StackExchange.Redis/SortedSetAddMessage.cs
new file mode 100644
index 000000000..46f8d97bf
--- /dev/null
+++ b/src/StackExchange.Redis/SortedSetAddMessage.cs
@@ -0,0 +1,146 @@
+using System;
+
+namespace StackExchange.Redis;
+
+internal partial class RedisDatabase
+{
+ private abstract class SortedSetAddMessage(
+ int db,
+ CommandFlags flags,
+ in RedisKey key,
+ SortedSetWhen when,
+ bool change,
+ bool increment) : Message.CommandKeyBase(db, flags, RedisCommand.ZADD, key)
+ {
+ private const SortedSetWhen KnownWhen =
+ SortedSetWhen.Exists | SortedSetWhen.GreaterThan | SortedSetWhen.LessThan | SortedSetWhen.NotExists;
+ private const SortedSetWhen Change = (SortedSetWhen)(1 << 30);
+ private const SortedSetWhen Increment = (SortedSetWhen)(1 << 29);
+
+ private readonly SortedSetWhen _when = GetWhen(when, change, increment);
+
+ public override int ArgCount => 1 + GetOptionCount() + (2 * EntryCount);
+
+ protected abstract int EntryCount { get; }
+
+ protected override void WriteImpl(PhysicalConnection physical)
+ {
+ physical.WriteHeader(Command, ArgCount);
+ physical.Write(Key);
+ WriteOptions(physical);
+ WriteEntries(physical);
+ }
+
+ protected abstract void WriteEntries(PhysicalConnection physical);
+
+ private int GetOptionCount()
+ {
+ int count = 0;
+ if ((_when & SortedSetWhen.NotExists) != 0) count++;
+ if ((_when & SortedSetWhen.Exists) != 0) count++;
+ if ((_when & SortedSetWhen.GreaterThan) != 0) count++;
+ if ((_when & SortedSetWhen.LessThan) != 0) count++;
+ if ((_when & Change) != 0) count++;
+ if ((_when & Increment) != 0) count++;
+ return count;
+ }
+
+ private static SortedSetWhen GetWhen(SortedSetWhen when, bool change, bool increment)
+ {
+ if ((when & ~KnownWhen) != 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(when));
+ }
+ if (change) when |= Change;
+ if (increment) when |= Increment;
+ return when;
+ }
+
+ private void WriteOptions(PhysicalConnection physical)
+ {
+ if ((_when & SortedSetWhen.NotExists) != 0)
+ {
+ physical.WriteBulkString("NX"u8);
+ }
+ if ((_when & SortedSetWhen.Exists) != 0)
+ {
+ physical.WriteBulkString("XX"u8);
+ }
+ if ((_when & SortedSetWhen.GreaterThan) != 0)
+ {
+ physical.WriteBulkString("GT"u8);
+ }
+ if ((_when & SortedSetWhen.LessThan) != 0)
+ {
+ physical.WriteBulkString("LT"u8);
+ }
+ if ((_when & Change) != 0)
+ {
+ physical.WriteBulkString("CH"u8);
+ }
+ if ((_when & Increment) != 0)
+ {
+ physical.WriteBulkString("INCR"u8);
+ }
+ }
+ }
+
+ private sealed class SingleSortedSetAddMessage(
+ int db,
+ CommandFlags flags,
+ in RedisKey key,
+ in RedisValue member,
+ double score,
+ SortedSetWhen when,
+ bool change,
+ bool increment) : SortedSetAddMessage(db, flags, key, when, change, increment)
+ {
+ private readonly RedisValue _member = AssertMember(member);
+ private readonly double _score = score;
+
+ protected override int EntryCount => 1;
+
+ protected override void WriteEntries(PhysicalConnection physical)
+ {
+ physical.WriteBulkString(_score);
+ physical.WriteBulkString(_member);
+ }
+
+ private static RedisValue AssertMember(in RedisValue member)
+ {
+ member.AssertNotNull();
+ return member;
+ }
+ }
+
+ private sealed class MultipleSortedSetAddMessage(
+ int db,
+ CommandFlags flags,
+ in RedisKey key,
+ SortedSetEntry[] values,
+ SortedSetWhen when,
+ bool change) : SortedSetAddMessage(db, flags, key, when, change, increment: false)
+ {
+ private readonly SortedSetEntry[] _values = AssertValues(values);
+
+ protected override int EntryCount => _values.Length;
+
+ protected override void WriteEntries(PhysicalConnection physical)
+ {
+ for (int i = 0; i < _values.Length; i++)
+ {
+ physical.WriteBulkString(_values[i].score);
+ physical.WriteBulkString(_values[i].element);
+ }
+ }
+
+ private static SortedSetEntry[] AssertValues(SortedSetEntry[] values)
+ {
+ for (int i = 0; i < values.Length; i++)
+ {
+ values[i].element.AssertNotNull();
+ }
+ return values;
+ }
+ }
+}
diff --git a/src/StackExchange.Redis/VectorSetAddMessage.cs b/src/StackExchange.Redis/VectorSetAddMessage.cs
index 0beb65205..c833ca5d4 100644
--- a/src/StackExchange.Redis/VectorSetAddMessage.cs
+++ b/src/StackExchange.Redis/VectorSetAddMessage.cs
@@ -12,13 +12,14 @@ internal abstract class VectorSetAddMessage(
VectorSetQuantization quantization,
int? buildExplorationFactor,
int? maxConnections,
- bool useCheckAndSet) : Message(db, flags, RedisCommand.VADD)
+ bool useCheckAndSet,
+ bool useFp32) : Message(db, flags, RedisCommand.VADD)
{
- public override int ArgCount => GetArgCount(UseFp32);
+ public override int ArgCount => GetArgCount();
- private int GetArgCount(bool packed)
+ private int GetArgCount()
{
- var count = 2 + GetElementArgCount(packed); // key, element and either "FP32 {vector}" or VALUES {num}"
+ var count = 2 + GetElementArgCount(); // key, element and either "FP32 {vector}" or VALUES {num}"
if (reducedDimensions.HasValue) count += 2; // [REDUCE {dim}]
if (useCheckAndSet) count++; // [CAS]
@@ -35,13 +36,14 @@ private int GetArgCount(bool packed)
return count;
}
- public abstract int GetElementArgCount(bool packed);
+ public abstract int GetElementArgCount();
public abstract int GetAttributeArgCount();
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
=> serverSelectionStrategy.HashSlot(key);
- private static readonly bool CanUseFp32 = BitConverter.IsLittleEndian && CheckFp32();
+ internal static readonly bool CanUseFp32 = BitConverter.IsLittleEndian && CheckFp32();
+ internal bool UseFp32 { get; } = useFp32 & CanUseFp32; // evaluated during .ctor
private static bool CheckFp32() // check endianness with a known value
{
@@ -49,23 +51,11 @@ private static bool CheckFp32() // check endianness with a known value
return MemoryMarshal.Cast("\0\0(B"u8)[0] == 42;
}
-#if DEBUG
- private static int _fp32Disabled;
- internal static bool UseFp32 => CanUseFp32 & Volatile.Read(ref _fp32Disabled) == 0;
- internal static void SuppressFp32() => Interlocked.Increment(ref _fp32Disabled);
- internal static void RestoreFp32() => Interlocked.Decrement(ref _fp32Disabled);
-#else
- internal static bool UseFp32 => CanUseFp32;
- internal static void SuppressFp32() { }
- internal static void RestoreFp32() { }
-#endif
-
- protected abstract void WriteElement(bool packed, PhysicalConnection physical);
+ protected abstract void WriteElement(PhysicalConnection physical);
protected override void WriteImpl(PhysicalConnection physical)
{
- bool packed = UseFp32; // snapshot to avoid race in debug scenarios
- physical.WriteHeader(Command, GetArgCount(packed));
+ physical.WriteHeader(Command, GetArgCount());
physical.Write(key);
if (reducedDimensions.HasValue)
{
@@ -73,7 +63,7 @@ protected override void WriteImpl(PhysicalConnection physical)
physical.WriteBulkString(reducedDimensions.GetValueOrDefault());
}
- WriteElement(packed, physical);
+ WriteElement(physical);
if (useCheckAndSet) physical.WriteBulkString("CAS"u8);
switch (quantization)
@@ -118,7 +108,8 @@ internal sealed class VectorSetAddMemberMessage(
bool useCheckAndSet,
RedisValue element,
ReadOnlyMemory values,
- string? attributesJson) : VectorSetAddMessage(
+ string? attributesJson,
+ bool useFp32) : VectorSetAddMessage(
db,
flags,
key,
@@ -126,19 +117,20 @@ internal sealed class VectorSetAddMemberMessage(
quantization,
buildExplorationFactor,
maxConnections,
- useCheckAndSet)
+ useCheckAndSet,
+ useFp32)
{
private readonly string? _attributesJson = string.IsNullOrWhiteSpace(attributesJson) ? null : attributesJson;
- public override int GetElementArgCount(bool packed)
+ public override int GetElementArgCount()
=> 2 // "FP32 {vector}" or "VALUES {num}"
- + (packed ? 0 : values.Length); // {vector...}"
+ + (UseFp32 ? 0 : values.Length); // {vector...}"
public override int GetAttributeArgCount()
=> _attributesJson is null ? 0 : 2; // [SETATTR {attributes}]
- protected override void WriteElement(bool packed, PhysicalConnection physical)
+ protected override void WriteElement(PhysicalConnection physical)
{
- if (packed)
+ if (UseFp32)
{
physical.WriteBulkString("FP32"u8);
physical.WriteBulkString(MemoryMarshal.AsBytes(values.Span));
diff --git a/src/StackExchange.Redis/VectorSetAddRequest.cs b/src/StackExchange.Redis/VectorSetAddRequest.cs
index 0ae5641c8..1f920da3e 100644
--- a/src/StackExchange.Redis/VectorSetAddRequest.cs
+++ b/src/StackExchange.Redis/VectorSetAddRequest.cs
@@ -38,6 +38,8 @@ public static VectorSetAddRequest Member(
///
public int? ReducedDimensions { get; set; }
+ internal bool UseFp32 { get; set; } = true; // for testing
+
///
/// Quantization type - Int8 (Q8), None (NOQUANT), or Binary (BIN). Default: Int8.
///
@@ -74,6 +76,7 @@ internal override VectorSetAddMessage ToMessage(RedisKey key, int db, CommandFla
UseCheckAndSet,
element,
values,
- attributesJson);
+ attributesJson,
+ UseFp32);
}
}
diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs
index 1bbc418d5..d6b75e079 100644
--- a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs
+++ b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs
@@ -11,7 +11,8 @@ internal abstract class VectorSetSimilaritySearchMessage(
double epsilon,
int searchExplorationFactor,
string? filterExpression,
- int maxFilteringEffort) : Message(db, flags, RedisCommand.VSIM)
+ int maxFilteringEffort,
+ bool useFp32) : Message(db, flags, RedisCommand.VSIM)
{
// For "FP32" and "VALUES" scenarios; in the future we might want other vector sizes / encodings - for
// example, there could be some "FP16" or "FP8" transport that requires a ROM-short or ROM-sbyte from
@@ -27,15 +28,16 @@ internal sealed class VectorSetSimilaritySearchBySingleVectorMessage(
double epsilon,
int searchExplorationFactor,
string? filterExpression,
- int maxFilteringEffort) : VectorSetSimilaritySearchMessage(db, flags, vsimFlags, key, count, epsilon,
- searchExplorationFactor, filterExpression, maxFilteringEffort)
+ int maxFilteringEffort,
+ bool useFp32) : VectorSetSimilaritySearchMessage(db, flags, vsimFlags, key, count, epsilon,
+ searchExplorationFactor, filterExpression, maxFilteringEffort, useFp32)
{
- internal override int GetSearchTargetArgCount(bool packed) =>
- packed ? 2 : 2 + vector.Length; // FP32 {vector} or VALUES {num} {vector}
+ internal override int GetSearchTargetArgCount() =>
+ UseFp32 ? 2 : (2 + vector.Length); // FP32 {vector} or VALUES {num} {vector}
- internal override void WriteSearchTarget(bool packed, PhysicalConnection physical)
+ internal override void WriteSearchTarget(PhysicalConnection physical)
{
- if (packed)
+ if (UseFp32)
{
physical.WriteBulkString("FP32"u8);
physical.WriteBulkString(System.Runtime.InteropServices.MemoryMarshal.AsBytes(vector.Span));
@@ -63,20 +65,21 @@ internal sealed class VectorSetSimilaritySearchByMemberMessage(
double epsilon,
int searchExplorationFactor,
string? filterExpression,
- int maxFilteringEffort) : VectorSetSimilaritySearchMessage(db, flags, vsimFlags, key, count, epsilon,
- searchExplorationFactor, filterExpression, maxFilteringEffort)
+ int maxFilteringEffort,
+ bool useFp32) : VectorSetSimilaritySearchMessage(db, flags, vsimFlags, key, count, epsilon,
+ searchExplorationFactor, filterExpression, maxFilteringEffort, useFp32)
{
- internal override int GetSearchTargetArgCount(bool packed) => 2; // ELE {member}
+ internal override int GetSearchTargetArgCount() => 2; // ELE {member}
- internal override void WriteSearchTarget(bool packed, PhysicalConnection physical)
+ internal override void WriteSearchTarget(PhysicalConnection physical)
{
physical.WriteBulkString("ELE"u8);
physical.WriteBulkString(member);
}
}
- internal abstract int GetSearchTargetArgCount(bool packed);
- internal abstract void WriteSearchTarget(bool packed, PhysicalConnection physical);
+ internal abstract int GetSearchTargetArgCount();
+ internal abstract void WriteSearchTarget(PhysicalConnection physical);
public ResultProcessor?> GetResultProcessor() =>
VectorSetSimilaritySearchProcessor.Instance;
@@ -177,11 +180,11 @@ internal enum VsimFlags
private bool HasFlag(VsimFlags flag) => (vsimFlags & flag) != 0;
- public override int ArgCount => GetArgCount(VectorSetAddMessage.UseFp32);
+ public override int ArgCount => GetArgCount();
- private int GetArgCount(bool packed)
+ private int GetArgCount()
{
- int argCount = 1 + GetSearchTargetArgCount(packed); // {key} and whatever we need for the vector/element portion
+ int argCount = 1 + GetSearchTargetArgCount(); // {key} and whatever we need for the vector/element portion
if (HasFlag(VsimFlags.WithScores)) argCount++; // [WITHSCORES]
if (HasFlag(VsimFlags.WithAttributes)) argCount++; // [WITHATTRIBS]
if (HasFlag(VsimFlags.Count)) argCount += 2; // [COUNT {count}]
@@ -194,17 +197,18 @@ private int GetArgCount(bool packed)
return argCount;
}
+ internal bool UseFp32 { get; } = useFp32 & VectorSetAddMessage.CanUseFp32; // evaluated during .ctor
+
protected override void WriteImpl(PhysicalConnection physical)
{
// snapshot to avoid race in debug scenarios
- bool packed = VectorSetAddMessage.UseFp32;
- physical.WriteHeader(Command, GetArgCount(packed));
+ physical.WriteHeader(Command, GetArgCount());
// Write key
physical.Write(key);
// Write search target: either "ELE {member}" or vector data
- WriteSearchTarget(packed, physical);
+ WriteSearchTarget(physical);
if (HasFlag(VsimFlags.WithScores))
{
diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs
index e0c7933f3..c48004d8f 100644
--- a/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs
+++ b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs
@@ -26,7 +26,8 @@ internal override VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int d
_epsilon,
_searchExplorationFactor,
_filterExpression,
- _maxFilteringEffort);
+ _maxFilteringEffort,
+ UseFp32);
}
private sealed class VectorSetSimilarityVectorSingleSearchRequest(ReadOnlyMemory vector)
@@ -43,9 +44,12 @@ internal override VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int d
_epsilon,
_searchExplorationFactor,
_filterExpression,
- _maxFilteringEffort);
+ _maxFilteringEffort,
+ UseFp32);
}
+ internal bool UseFp32 { get; set; } = true; // for testing
+
// snapshot the values; I don't trust people not to mutate the object behind my back
internal abstract VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int db, CommandFlags flags);
diff --git a/tests/RedisConfigs/.docker/Redis/Dockerfile b/tests/RedisConfigs/.docker/Redis/Dockerfile
index 334a1d7b9..53757b428 100644
--- a/tests/RedisConfigs/.docker/Redis/Dockerfile
+++ b/tests/RedisConfigs/.docker/Redis/Dockerfile
@@ -1,4 +1,4 @@
-ARG CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:8.8-m02
+ARG CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:unstable-24805570909-debian
FROM ${CLIENT_LIBS_TEST_IMAGE}
COPY --from=configs ./Basic /data/Basic/
diff --git a/tests/RedisConfigs/docker-compose.yml b/tests/RedisConfigs/docker-compose.yml
index 59ff825d8..bdcb8625b 100644
--- a/tests/RedisConfigs/docker-compose.yml
+++ b/tests/RedisConfigs/docker-compose.yml
@@ -5,7 +5,7 @@ services:
build:
context: .docker/Redis
args:
- CLIENT_LIBS_TEST_IMAGE: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.8-m02}
+ CLIENT_LIBS_TEST_IMAGE: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:unstable-24805570909-debian}
additional_contexts:
configs: .
platform: linux
diff --git a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs
index ef28ed6e9..f01645cf3 100644
--- a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs
+++ b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs
@@ -38,12 +38,17 @@ public async Task MuxerIsCollected()
var wr = new WeakReference(conn);
conn = null;
- ForceGC();
- await Task.Delay(2000).ForAwait(); // GC is twitchy
- ForceGC();
+ for (int i = 0; i < 5 && wr.IsAlive; i++)
+ {
+ ForceGC();
+ await Task.Delay(2000).ForAwait(); // GC is twitchy
+ ForceGC();
+ }
// should be collectable
Assert.Null(wr.Target);
+ // just to ensure we wrote conn, and to suppress a warning
+ Assert.Null(conn);
// #if DEBUG // this counter only exists in debug
// int after = ConnectionMultiplexer.CollectedWithoutDispose;
diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs
index f117f8c5f..cbdf55e4e 100644
--- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs
+++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs
@@ -851,6 +851,13 @@ public void SortedSetIncrement()
mock.Received().SortedSetIncrement("prefix:key", "member", 1.23, CommandFlags.None);
}
+ [Fact]
+ public void SortedSetIncrement_When()
+ {
+ prefixed.SortedSetIncrement("key", "member", 1.23, ValueCondition.Exists, CommandFlags.None);
+ mock.Received().SortedSetIncrement("prefix:key", "member", 1.23, ValueCondition.Exists, CommandFlags.None);
+ }
+
[Fact]
public void SortedSetIntersectionLength()
{
diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs
index 625eb022d..dafadd836 100644
--- a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs
+++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs
@@ -775,6 +775,13 @@ public async Task SortedSetIncrementAsync()
await mock.Received().SortedSetIncrementAsync("prefix:key", "member", 1.23, CommandFlags.None);
}
+ [Fact]
+ public async Task SortedSetIncrementAsync_When()
+ {
+ await prefixed.SortedSetIncrementAsync("key", "member", 1.23, ValueCondition.Exists, CommandFlags.None);
+ await mock.Received().SortedSetIncrementAsync("prefix:key", "member", 1.23, ValueCondition.Exists, CommandFlags.None);
+ }
+
[Fact]
public async Task SortedSetIntersectionLengthAsync()
{
diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs
index b4ff2091b..776139028 100644
--- a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs
+++ b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs
@@ -22,17 +22,11 @@ public void VectorSetAdd_Fp32()
{
if (BitConverter.IsLittleEndian)
{
- Assert.True(VectorSetAddMessage.UseFp32);
-#if DEBUG // can be suppressed
- VectorSetAddMessage.SuppressFp32();
- Assert.False(VectorSetAddMessage.UseFp32);
- VectorSetAddMessage.RestoreFp32();
- Assert.True(VectorSetAddMessage.UseFp32);
-#endif
+ Assert.True(VectorSetAddMessage.CanUseFp32);
}
else
{
- Assert.False(VectorSetAddMessage.UseFp32);
+ Assert.False(VectorSetAddMessage.CanUseFp32);
}
}
diff --git a/tests/StackExchange.Redis.Tests/LoggerTests.cs b/tests/StackExchange.Redis.Tests/LoggerTests.cs
index 682856baa..81b5b2339 100644
--- a/tests/StackExchange.Redis.Tests/LoggerTests.cs
+++ b/tests/StackExchange.Redis.Tests/LoggerTests.cs
@@ -119,11 +119,21 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
Interlocked.Increment(ref _callCount);
var logLine = $"{_logLevel}> [LogLevel: {logLevel}, EventId: {eventId}]: {formatter?.Invoke(state, exception)}";
- sb.AppendLine(logLine);
+ lock (sb)
+ {
+ sb.AppendLine(logLine);
+ }
+
_output.WriteLine(logLine);
}
public long CallCount => Interlocked.Read(ref _callCount);
- public override string ToString() => sb.ToString();
+ public override string ToString()
+ {
+ lock (sb)
+ {
+ return sb.ToString();
+ }
+ }
}
}
diff --git a/tests/StackExchange.Redis.Tests/SortedSetIncrementUnitTests.cs b/tests/StackExchange.Redis.Tests/SortedSetIncrementUnitTests.cs
new file mode 100644
index 000000000..6ad33dc31
--- /dev/null
+++ b/tests/StackExchange.Redis.Tests/SortedSetIncrementUnitTests.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace StackExchange.Redis.Tests;
+
+public class SortedSetIncrementUnitTests
+{
+ [Theory]
+ [MemberData(nameof(InvalidValueConditions))]
+ public void InvalidValueConditionModesThrow(ValueCondition condition)
+ {
+ var db = new RedisDatabase(null!, 0, null);
+
+ Assert.Throws(() =>
+ db.SortedSetIncrement("key", "member", 1, condition, CommandFlags.None));
+
+ Assert.Throws(() =>
+ {
+ _ = db.SortedSetIncrementAsync("key", "member", 1, condition, CommandFlags.None);
+ });
+ }
+
+ public static IEnumerable