Skip to content

Commit 256d3c1

Browse files
committed
clean up and optimize
1 parent d694cb4 commit 256d3c1

File tree

8 files changed

+148
-81
lines changed

8 files changed

+148
-81
lines changed

src/StackExchange.Redis/Condition.cs

Lines changed: 28 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -285,18 +285,18 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value)
285285
public static Condition SortedSetNotContains(RedisKey key, RedisValue member) => new ExistsCondition(key, RedisType.SortedSet, member, false);
286286

287287
/// <summary>
288-
/// Enforces that the given sorted set contains a member that ist starting with the start-sequence.
288+
/// Enforces that the given sorted set contains a member that starts with the specified prefix.
289289
/// </summary>
290290
/// <param name="key">The key of the sorted set to check.</param>
291-
/// <param name="memberStartSequence">a byte array: the set must contain at least one member, that starts with the byte-sequence.</param>
292-
public static Condition SortedSetStartsWith(RedisKey key, byte[] memberStartSequence) => new StartsWithCondition(key, memberStartSequence, true);
291+
/// <param name="prefix">The sorted set must contain at least one member that starts with the specified prefix.</param>
292+
public static Condition SortedSetContainsStarting(RedisKey key, RedisValue prefix) => new StartsWithCondition(key, prefix, true);
293293

294294
/// <summary>
295-
/// Enforces that the given sorted set does not contain a member that ist starting with the start-sequence.
295+
/// Enforces that the given sorted set does not contain a member that starts with the specified prefix.
296296
/// </summary>
297297
/// <param name="key">The key of the sorted set to check.</param>
298-
/// <param name="memberStartSequence">a byte array: the set must not contain any members, that start with the byte-sequence.</param>
299-
public static Condition SortedSetNotStartsWith(RedisKey key, byte[] memberStartSequence) => new StartsWithCondition(key, memberStartSequence, false);
298+
/// <param name="prefix">The sorted set must not contain at a member that starts with the specified prefix.</param>
299+
public static Condition SortedSetNotContainsStarting(RedisKey key, RedisValue prefix) => new StartsWithCondition(key, prefix, false);
300300

301301
/// <summary>
302302
/// Enforces that the given sorted set member must have the specified score.
@@ -541,35 +541,45 @@ internal sealed class StartsWithCondition : Condition
541541
working with byte arrays should prevent any encoding within this class, that could distort the comparison */
542542

543543
private readonly bool expectedResult;
544-
private readonly RedisValue expectedStartValue;
544+
private readonly RedisValue prefix;
545545
private readonly RedisKey key;
546546

547547
internal override Condition MapKeys(Func<RedisKey, RedisKey> map) =>
548-
new StartsWithCondition(map(key), expectedStartValue, expectedResult);
548+
new StartsWithCondition(map(key), prefix, expectedResult);
549549

550-
public StartsWithCondition(in RedisKey key, in RedisValue expectedStartValue, bool expectedResult)
550+
public StartsWithCondition(in RedisKey key, in RedisValue prefix, bool expectedResult)
551551
{
552552
if (key.IsNull) throw new ArgumentNullException(nameof(key));
553-
if (expectedStartValue.IsNull) throw new ArgumentNullException(nameof(expectedStartValue));
553+
if (prefix.IsNull) throw new ArgumentNullException(nameof(prefix));
554554
this.key = key;
555-
this.expectedStartValue = expectedStartValue; // array with length 0 returns true condition
555+
this.prefix = prefix;
556556
this.expectedResult = expectedResult;
557557
}
558558

559559
public override string ToString() =>
560-
(expectedStartValue.IsNull ? key.ToString() : ((string?)key) + " " + RedisType.SortedSet + " > " + expectedStartValue)
561-
+ (expectedResult ? " starts with" : " does not start with");
560+
$"{key} {nameof(RedisType.SortedSet)} > {(expectedResult ? " member starting " : " no member starting ")} {prefix} + prefix";
562561

563562
internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZRANGEBYLEX);
564563

565564
internal override IEnumerable<Message> CreateMessages(int db, IResultBox? resultBox)
566565
{
567566
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key);
568567

569-
#pragma warning disable CS8600, CS8604, SA1117 // expectedStartValue is checked to be not null in Constructor and must be a byte[] because of API-parameters
570-
var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, RedisCommand.ZRANGEBYLEX, key,
571-
CombineBytes(91, (byte[])expectedStartValue.Box()), "+", "LIMIT", "0", "1"); // prepends '[' to startValue for inclusive search in CombineBytes
572-
#pragma warning disable CS8600, CS8604, SA1117
568+
// prepend '[' to prefix for inclusive search
569+
var startValueWithToken = RedisDatabase.GetLexRange(prefix, Exclude.None, isStart: true, Order.Ascending);
570+
571+
var message = ConditionProcessor.CreateMessage(
572+
this,
573+
db,
574+
CommandFlags.None,
575+
RedisCommand.ZRANGEBYLEX,
576+
key,
577+
startValueWithToken,
578+
RedisLiterals.PlusSymbol,
579+
RedisLiterals.LIMIT,
580+
0,
581+
1);
582+
573583
message.SetSource(ConditionProcessor.Default, resultBox);
574584
yield return message;
575585
}
@@ -578,39 +588,9 @@ internal override IEnumerable<Message> CreateMessages(int db, IResultBox? result
578588

579589
internal override bool TryValidate(in RawResult result, out bool value)
580590
{
581-
RedisValue[]? r = result.GetItemsAsValues();
582-
if (result.ItemsCount == 0) value = false; // false, if empty list -> read after end of memberlist / itemsCout > 1 is impossible due to 'LIMIT 0 1'
583-
#pragma warning disable CS8600, CS8604 // warnings on StartsWith can be ignored because of ItemsCount-check in then preceding command!!
584-
else value = r != null && r.Length > 0 && StartsWith((byte[])r[0].Box(), expectedStartValue);
585-
#pragma warning disable CS8600, CS8604
591+
value = result.ItemsCount == 1 && result[0].AsRedisValue().StartsWith(prefix);
586592

587-
#pragma warning disable CS8602 // warning for r[0] can be ignored because of null-check in then same command-line !!
588593
if (!expectedResult) value = !value;
589-
ConnectionMultiplexer.TraceWithoutContext("actual: " + r == null ? "null" : r.Length == 0 ? "empty" : r[0].ToString()
590-
+ "; expected: " + expectedStartValue.ToString()
591-
+ "; wanted: " + (expectedResult ? "StartsWith" : "NotStartWith")
592-
+ "; voting: " + value);
593-
#pragma warning restore CS8602
594-
return true;
595-
}
596-
597-
private static byte[] CombineBytes(byte b1, byte[] a1) // combines b1 and a1 to new array
598-
{
599-
byte[] newArray = new byte[a1.Length + 1];
600-
newArray[0] = b1;
601-
System.Buffer.BlockCopy(a1, 0, newArray, 1, a1.Length);
602-
return newArray;
603-
}
604-
605-
internal bool StartsWith(byte[] result, byte[] searchfor)
606-
{
607-
if (searchfor.Length > result.Length) return false;
608-
609-
for (int i = 0; i < searchfor.Length; i++)
610-
{
611-
if (result[i] != searchfor[i]) return false;
612-
}
613-
614594
return true;
615595
}
616596
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#pragma warning disable SA1403 // single namespace
2+
3+
#if NET5_0_OR_GREATER
4+
// context: https://github.com/StackExchange/StackExchange.Redis/issues/2619
5+
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
6+
#else
7+
// To support { get; init; } properties
8+
using System.ComponentModel;
9+
using System.Text;
10+
11+
namespace System.Runtime.CompilerServices
12+
{
13+
[EditorBrowsable(EditorBrowsableState.Never)]
14+
internal static class IsExternalInit { }
15+
}
16+
#endif
17+
18+
#if !(NETCOREAPP || NETSTANDARD2_1_OR_GREATER)
19+
20+
namespace System.Text
21+
{
22+
internal static class EncodingExtensions
23+
{
24+
public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan<char> source, Span<byte> destination)
25+
{
26+
fixed (byte* bPtr = destination)
27+
{
28+
fixed (char* cPtr = source)
29+
{
30+
return encoding.GetBytes(cPtr, source.Length, bPtr, destination.Length);
31+
}
32+
}
33+
}
34+
}
35+
}
36+
#endif
37+
38+
39+
#pragma warning restore SA1403

src/StackExchange.Redis/Hacks.cs

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1951,5 +1951,3 @@ StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.R
19511951
StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue field, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue>!
19521952
StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.DateTime expiry, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue>!
19531953
StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue>!
1954-
static StackExchange.Redis.Condition.SortedSetStartsWith(StackExchange.Redis.RedisKey key, byte[]! memberStartSequence) -> StackExchange.Redis.Condition!
1955-
static StackExchange.Redis.Condition.SortedSetNotStartsWith(StackExchange.Redis.RedisKey key, byte[]! memberStartSequence) -> StackExchange.Redis.Condition!
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
#nullable enable
1+
#nullable enable
2+
StackExchange.Redis.RedisValue.CopyTo(System.Span<byte> destination) -> int
3+
StackExchange.Redis.RedisValue.GetByteCount() -> int
4+
StackExchange.Redis.RedisValue.GetLongByteCount() -> long
5+
static StackExchange.Redis.Condition.SortedSetContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition!
6+
static StackExchange.Redis.Condition.SortedSetNotContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition!

src/StackExchange.Redis/RedisDatabase.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3962,21 +3962,23 @@ private Message GetSortedSetMultiPopMessage(RedisKey[] keys, Order order, long c
39623962
return tran;
39633963
}
39643964

3965-
private static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool isStart, Order order)
3965+
internal static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool isStart, Order order)
39663966
{
3967-
if (value.IsNull)
3967+
if (value.IsNull) // open search
39683968
{
39693969
if (order == Order.Ascending) return isStart ? RedisLiterals.MinusSymbol : RedisLiterals.PlusSymbol;
39703970

3971-
return isStart ? RedisLiterals.PlusSymbol : RedisLiterals.MinusSymbol; // 24.01.2024: when descending order: Plus and Minus have to be reversed
3971+
return isStart ? RedisLiterals.PlusSymbol : RedisLiterals.MinusSymbol; // when descending order: Plus and Minus have to be reversed
39723972
}
39733973

3974-
byte[] orig = value!;
3974+
var srcLength = value.GetByteCount();
3975+
Debug.Assert(srcLength >= 0);
39753976

3976-
byte[] result = new byte[orig.Length + 1];
3977+
byte[] result = new byte[srcLength + 1];
39773978
// no defaults here; must always explicitly specify [ / (
39783979
result[0] = (exclude & (isStart ? Exclude.Start : Exclude.Stop)) == 0 ? (byte)'[' : (byte)'(';
3979-
Buffer.BlockCopy(orig, 0, result, 1, orig.Length);
3980+
int written = value.CopyTo(result.AsSpan(1));
3981+
Debug.Assert(written == srcLength, "predicted/actual length mismatch");
39803982
return result;
39813983
}
39823984

src/StackExchange.Redis/RedisValue.cs

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Buffers;
33
using System.Buffers.Text;
44
using System.ComponentModel;
5+
using System.Diagnostics;
56
using System.IO;
67
using System.Linq;
78
using System.Reflection;
@@ -838,23 +839,78 @@ private static string ToHex(ReadOnlySpan<byte> src)
838839

839840
return value._memory.ToArray();
840841
case StorageType.Int64:
841-
Span<byte> span = stackalloc byte[Format.MaxInt64TextLen + 2];
842-
int len = PhysicalConnection.WriteRaw(span, value.OverlappedValueInt64, false, 0);
843-
arr = new byte[len - 2]; // don't need the CRLF
844-
span.Slice(0, arr.Length).CopyTo(arr);
845-
return arr;
842+
Debug.Assert(Format.MaxInt64TextLen <= 24);
843+
Span<byte> span = stackalloc byte[24];
844+
int len = Format.FormatInt64(value.OverlappedValueInt64, span);
845+
return span.Slice(0, len).ToArray();
846846
case StorageType.UInt64:
847-
// we know it is a huge value - just jump straight to Utf8Formatter
848-
span = stackalloc byte[Format.MaxInt64TextLen];
847+
Debug.Assert(Format.MaxInt64TextLen <= 24);
848+
span = stackalloc byte[24];
849849
len = Format.FormatUInt64(value.OverlappedValueUInt64, span);
850-
arr = new byte[len];
851-
span.Slice(0, len).CopyTo(arr);
852-
return arr;
850+
return span.Slice(0, len).ToArray();
851+
case StorageType.Double:
852+
span = stackalloc byte[128];
853+
len = Format.FormatDouble(value.OverlappedValueDouble, span);
854+
return span.Slice(0, len).ToArray();
855+
case StorageType.String:
856+
return Encoding.UTF8.GetBytes((string)value._objectOrSentinel!);
853857
}
854858
// fallback: stringify and encode
855859
return Encoding.UTF8.GetBytes((string)value!);
856860
}
857861

862+
/// <summary>
863+
/// Gets the length of the value in bytes.
864+
/// </summary>
865+
public int GetByteCount()
866+
{
867+
switch (Type)
868+
{
869+
case StorageType.Null: return 0;
870+
case StorageType.Raw: return _memory.Length;
871+
case StorageType.String: return Encoding.UTF8.GetByteCount((string)_objectOrSentinel!);
872+
case StorageType.Int64: return Format.MeasureInt64(OverlappedValueInt64);
873+
case StorageType.UInt64: return Format.MeasureUInt64(OverlappedValueUInt64);
874+
case StorageType.Double: return Format.MeasureDouble(OverlappedValueDouble);
875+
default: return ThrowUnableToMeasure();
876+
}
877+
}
878+
879+
private int ThrowUnableToMeasure() => throw new InvalidOperationException("Unable to compute length of type: " + Type);
880+
881+
/// <summary>
882+
/// Gets the length of the value in bytes.
883+
/// </summary>
884+
/* right now, we only support int lengths, but adding this now so that
885+
there are no surprises if/when we add support for discontiguous buffers */
886+
public long GetLongByteCount() => GetByteCount();
887+
888+
/// <summary>
889+
/// Copy the value as bytes to the provided <paramref name="destination"/>.
890+
/// </summary>
891+
public int CopyTo(Span<byte> destination)
892+
{
893+
switch (Type)
894+
{
895+
case StorageType.Null:
896+
return 0;
897+
case StorageType.Raw:
898+
var srcBytes = _memory.Span;
899+
srcBytes.CopyTo(destination);
900+
return srcBytes.Length;
901+
case StorageType.String:
902+
return Encoding.UTF8.GetBytes(((string)_objectOrSentinel!).AsSpan(), destination);
903+
case StorageType.Int64:
904+
return Format.FormatInt64(OverlappedValueInt64, destination);
905+
case StorageType.UInt64:
906+
return Format.FormatUInt64(OverlappedValueUInt64, destination);
907+
case StorageType.Double:
908+
return Format.FormatDouble(OverlappedValueDouble, destination);
909+
default:
910+
return ThrowUnableToMeasure();
911+
}
912+
}
913+
858914
/// <summary>
859915
/// Converts a <see cref="RedisValue"/> to a <see cref="ReadOnlyMemory{T}"/>.
860916
/// </summary>

tests/StackExchange.Redis.Tests/TransactionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,7 @@ public async Task BasicTranWithSortedSetStartsWithCondition(bool demandKeyExists
832832
Assert.Equal(keyExists, db.SortedSetScore(key2, member).HasValue);
833833

834834
var tran = db.CreateTransaction();
835-
var cond = tran.AddCondition(demandKeyExists ? Condition.SortedSetStartsWith(key2, startWith) : Condition.SortedSetNotStartsWith(key2, startWith));
835+
var cond = tran.AddCondition(demandKeyExists ? Condition.SortedSetContainsStarting(key2, startWith) : Condition.SortedSetNotContainsStarting(key2, startWith));
836836
var incr = tran.StringIncrementAsync(key);
837837
var exec = tran.ExecuteAsync();
838838
var get = db.StringGet(key);

0 commit comments

Comments
 (0)