Skip to content

Commit 51a7d90

Browse files
mgravellNickCraver
andauthored
Implement RedisValue.Length for all underlying storage kinds (#2370)
* fix #2368 - implement Length() for other encodings (using format layout) - unify format code - switch to C# 11 for u8 strings (needed a few "scoped" modifiers adding) - tests for format and Length * tweak langver * cleanup double format * use 7.0.101 SDK (102 not yet on ubuntu?) * tweak SDK in CI.yml; add CI.yml to sln * We need Redis 6 runtime for tests, so let's grab both * Add release notes --------- Co-authored-by: Nick Craver <[email protected]>
1 parent b94a8cf commit 51a7d90

File tree

13 files changed

+245
-51
lines changed

13 files changed

+245
-51
lines changed

.github/workflows/CI.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
with:
2525
dotnet-version: |
2626
6.0.x
27+
7.0.x
2728
- name: .NET Build
2829
run: dotnet build Build.csproj -c Release /p:CI=true
2930
- name: Start Redis Services (docker-compose)
@@ -56,6 +57,7 @@ jobs:
5657
# with:
5758
# dotnet-version: |
5859
# 6.0.x
60+
# 7.0.x
5961
- name: .NET Build
6062
run: dotnet build Build.csproj -c Release /p:CI=true
6163
- name: Start Redis Services (v3.0.503)

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<PackageProjectUrl>https://stackexchange.github.io/StackExchange.Redis/</PackageProjectUrl>
1616
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1717

18-
<LangVersion>10.0</LangVersion>
18+
<LangVersion>11</LangVersion>
1919
<RepositoryType>git</RepositoryType>
2020
<RepositoryUrl>https://github.com/StackExchange/StackExchange.Redis/</RepositoryUrl>
2121

StackExchange.Redis.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
99
build.cmd = build.cmd
1010
Build.csproj = Build.csproj
1111
build.ps1 = build.ps1
12+
.github\workflows\CI.yml = .github\workflows\CI.yml
1213
Directory.Build.props = Directory.Build.props
1314
Directory.Build.targets = Directory.Build.targets
1415
Directory.Packages.props = Directory.Packages.props

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ init:
66

77
install:
88
- cmd: >-
9-
choco install dotnet-sdk --version 6.0.101
9+
choco install dotnet-sdk --version 7.0.102
1010
1111
cd tests\RedisConfigs\3.0.503
1212

docs/ReleaseNotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Current package versions:
1010

1111
- Fix [#2350](https://github.com/StackExchange/StackExchange.Redis/issues/2350): Properly parse lua script paramters in all cultures ([#2351 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2351))
1212
- Fix [#2362](https://github.com/StackExchange/StackExchange.Redis/issues/2362): Set `RedisConnectionException.FailureType` to `AuthenticationFailure` on all authentication scenarios for better handling ([#2367 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2367))
13+
- Fix [#2368](https://github.com/StackExchange/StackExchange.Redis/issues/2368): Support `RedisValue.Length()` for all storage types ([#2370 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2370))
1314

1415
## 2.6.90
1516

src/StackExchange.Redis/BufferReader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ private bool FetchNextSegment()
4444
return true;
4545
}
4646

47-
public BufferReader(ReadOnlySequence<byte> buffer)
47+
public BufferReader(scoped in ReadOnlySequence<byte> buffer)
4848
{
4949
_buffer = buffer;
5050
_lastSnapshotPosition = buffer.Start;

src/StackExchange.Redis/Format.cs

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ internal static bool TryParseInt64(ReadOnlySpan<byte> s, out long value) =>
174174

175175
internal static bool CouldBeInteger(string s)
176176
{
177-
if (string.IsNullOrEmpty(s) || s.Length > PhysicalConnection.MaxInt64TextLen) return false;
177+
if (string.IsNullOrEmpty(s) || s.Length > Format.MaxInt64TextLen) return false;
178178
bool isSigned = s[0] == '-';
179179
for (int i = isSigned ? 1 : 0; i < s.Length; i++)
180180
{
@@ -185,7 +185,7 @@ internal static bool CouldBeInteger(string s)
185185
}
186186
internal static bool CouldBeInteger(ReadOnlySpan<byte> s)
187187
{
188-
if (s.IsEmpty | s.Length > PhysicalConnection.MaxInt64TextLen) return false;
188+
if (s.IsEmpty | s.Length > Format.MaxInt64TextLen) return false;
189189
bool isSigned = s[0] == '-';
190190
for (int i = isSigned ? 1 : 0; i < s.Length; i++)
191191
{
@@ -355,5 +355,77 @@ internal static unsafe string GetString(ReadOnlySpan<byte> span)
355355
return Encoding.UTF8.GetString(ptr, span.Length);
356356
}
357357
}
358+
359+
[DoesNotReturn]
360+
private static void ThrowFormatFailed() => throw new InvalidOperationException("TryFormat failed");
361+
362+
internal const int
363+
MaxInt32TextLen = 11, // -2,147,483,648 (not including the commas)
364+
MaxInt64TextLen = 20; // -9,223,372,036,854,775,808 (not including the commas)
365+
366+
internal static int MeasureDouble(double value)
367+
{
368+
if (double.IsInfinity(value)) return 4; // +inf / -inf
369+
var s = value.ToString("G17", NumberFormatInfo.InvariantInfo); // this looks inefficient, but is how Utf8Formatter works too, just: more direct
370+
return s.Length;
371+
}
372+
373+
internal static int FormatDouble(double value, Span<byte> destination)
374+
{
375+
if (double.IsInfinity(value))
376+
{
377+
if (double.IsPositiveInfinity(value))
378+
{
379+
if (!"+inf"u8.TryCopyTo(destination)) ThrowFormatFailed();
380+
}
381+
else
382+
{
383+
if (!"-inf"u8.TryCopyTo(destination)) ThrowFormatFailed();
384+
}
385+
return 4;
386+
}
387+
var s = value.ToString("G17", NumberFormatInfo.InvariantInfo); // this looks inefficient, but is how Utf8Formatter works too, just: more direct
388+
if (s.Length > destination.Length) ThrowFormatFailed();
389+
390+
var chars = s.AsSpan();
391+
for (int i = 0; i < chars.Length; i++)
392+
{
393+
destination[i] = (byte)chars[i];
394+
}
395+
return chars.Length;
396+
}
397+
398+
internal static int MeasureInt64(long value)
399+
{
400+
Span<byte> valueSpan = stackalloc byte[MaxInt64TextLen];
401+
return FormatInt64(value, valueSpan);
402+
}
403+
404+
internal static int FormatInt64(long value, Span<byte> destination)
405+
{
406+
if (!Utf8Formatter.TryFormat(value, destination, out var len))
407+
ThrowFormatFailed();
408+
return len;
409+
}
410+
411+
internal static int MeasureUInt64(ulong value)
412+
{
413+
Span<byte> valueSpan = stackalloc byte[MaxInt64TextLen];
414+
return FormatUInt64(value, valueSpan);
415+
}
416+
417+
internal static int FormatUInt64(ulong value, Span<byte> destination)
418+
{
419+
if (!Utf8Formatter.TryFormat(value, destination, out var len))
420+
ThrowFormatFailed();
421+
return len;
422+
}
423+
424+
internal static int FormatInt32(int value, Span<byte> destination)
425+
{
426+
if (!Utf8Formatter.TryFormat(value, destination, out var len))
427+
ThrowFormatFailed();
428+
return len;
429+
}
358430
}
359431
}

src/StackExchange.Redis/PhysicalConnection.cs

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
using System;
1+
using Pipelines.Sockets.Unofficial;
2+
using Pipelines.Sockets.Unofficial.Arenas;
3+
using System;
24
using System.Buffers;
3-
using System.Buffers.Text;
45
using System.Collections.Generic;
56
using System.Diagnostics;
67
using System.IO;
@@ -15,8 +16,6 @@
1516
using System.Text;
1617
using System.Threading;
1718
using System.Threading.Tasks;
18-
using Pipelines.Sockets.Unofficial;
19-
using Pipelines.Sockets.Unofficial.Arenas;
2019

2120
namespace StackExchange.Redis
2221
{
@@ -449,7 +448,7 @@ void add(string lk, string sk, string? v)
449448
add("Outstanding-Responses", "outstanding", GetSentAwaitingResponseCount().ToString());
450449
add("Last-Read", "last-read", (unchecked(now - lastRead) / 1000) + "s ago");
451450
add("Last-Write", "last-write", (unchecked(now - lastWrite) / 1000) + "s ago");
452-
if(unansweredWriteTime != 0) add("Unanswered-Write", "unanswered-write", (unchecked(now - unansweredWriteTime) / 1000) + "s ago");
451+
if (unansweredWriteTime != 0) add("Unanswered-Write", "unanswered-write", (unchecked(now - unansweredWriteTime) / 1000) + "s ago");
453452
add("Keep-Alive", "keep-alive", bridge.ServerEndPoint?.WriteEverySeconds + "s");
454453
add("Previous-Physical-State", "state", oldState.ToString());
455454
add("Manager", "mgr", bridge.Multiplexer.SocketManager?.GetState());
@@ -777,8 +776,7 @@ internal static void WriteBulkString(in RedisValue value, PipeWriter output)
777776

778777
internal void WriteHeader(RedisCommand command, int arguments, CommandBytes commandBytes = default)
779778
{
780-
var bridge = BridgeCouldBeNull;
781-
if (bridge == null) throw new ObjectDisposedException(ToString());
779+
var bridge = BridgeCouldBeNull ?? throw new ObjectDisposedException(ToString());
782780

783781
if (command == RedisCommand.UNKNOWN)
784782
{
@@ -801,7 +799,7 @@ internal void WriteHeader(RedisCommand command, int arguments, CommandBytes comm
801799
// *{argCount}\r\n = 3 + MaxInt32TextLen
802800
// ${cmd-len}\r\n = 3 + MaxInt32TextLen
803801
// {cmd}\r\n = 2 + commandBytes.Length
804-
var span = _ioPipe!.Output.GetSpan(commandBytes.Length + 8 + MaxInt32TextLen + MaxInt32TextLen);
802+
var span = _ioPipe!.Output.GetSpan(commandBytes.Length + 8 + Format.MaxInt32TextLen + Format.MaxInt32TextLen);
805803
span[0] = (byte)'*';
806804

807805
int offset = WriteRaw(span, arguments + 1, offset: 1);
@@ -817,16 +815,12 @@ internal void WriteHeader(RedisCommand command, int arguments, CommandBytes comm
817815
internal static void WriteMultiBulkHeader(PipeWriter output, long count)
818816
{
819817
// *{count}\r\n = 3 + MaxInt32TextLen
820-
var span = output.GetSpan(3 + MaxInt32TextLen);
818+
var span = output.GetSpan(3 + Format.MaxInt32TextLen);
821819
span[0] = (byte)'*';
822820
int offset = WriteRaw(span, count, offset: 1);
823821
output.Advance(offset);
824822
}
825823

826-
internal const int
827-
MaxInt32TextLen = 11, // -2,147,483,648 (not including the commas)
828-
MaxInt64TextLen = 20; // -9,223,372,036,854,775,808 (not including the commas)
829-
830824
[MethodImpl(MethodImplOptions.AggressiveInlining)]
831825
internal static int WriteCrlf(Span<byte> span, int offset)
832826
{
@@ -906,25 +900,16 @@ internal static int WriteRaw(Span<byte> span, long value, bool withLengthPrefix
906900
{
907901
// we're going to write it, but *to the wrong place*
908902
var availableChunk = span.Slice(offset);
909-
if (!Utf8Formatter.TryFormat(value, availableChunk, out int formattedLength))
910-
{
911-
throw new InvalidOperationException("TryFormat failed");
912-
}
903+
var formattedLength = Format.FormatInt64(value, availableChunk);
913904
if (withLengthPrefix)
914905
{
915906
// now we know how large the prefix is: write the prefix, then write the value
916-
if (!Utf8Formatter.TryFormat(formattedLength, availableChunk, out int prefixLength))
917-
{
918-
throw new InvalidOperationException("TryFormat failed");
919-
}
907+
var prefixLength = Format.FormatInt32(formattedLength, availableChunk);
920908
offset += prefixLength;
921909
offset = WriteCrlf(span, offset);
922910

923911
availableChunk = span.Slice(offset);
924-
if (!Utf8Formatter.TryFormat(value, availableChunk, out int finalLength))
925-
{
926-
throw new InvalidOperationException("TryFormat failed");
927-
}
912+
var finalLength = Format.FormatInt64(value, availableChunk);
928913
offset += finalLength;
929914
Debug.Assert(finalLength == formattedLength);
930915
}
@@ -1035,15 +1020,15 @@ private static void WriteUnifiedSpan(PipeWriter writer, ReadOnlySpan<byte> value
10351020
}
10361021
else if (value.Length <= MaxQuickSpanSize)
10371022
{
1038-
var span = writer.GetSpan(5 + MaxInt32TextLen + value.Length);
1023+
var span = writer.GetSpan(5 + Format.MaxInt32TextLen + value.Length);
10391024
span[0] = (byte)'$';
10401025
int bytes = AppendToSpan(span, value, 1);
10411026
writer.Advance(bytes);
10421027
}
10431028
else
10441029
{
10451030
// too big to guarantee can do in a single span
1046-
var span = writer.GetSpan(3 + MaxInt32TextLen);
1031+
var span = writer.GetSpan(3 + Format.MaxInt32TextLen);
10471032
span[0] = (byte)'$';
10481033
int bytes = WriteRaw(span, value.Length, offset: 1);
10491034
writer.Advance(bytes);
@@ -1136,7 +1121,7 @@ internal static void WriteUnifiedPrefixedString(PipeWriter writer, byte[]? prefi
11361121
}
11371122
else
11381123
{
1139-
var span = writer.GetSpan(3 + MaxInt32TextLen);
1124+
var span = writer.GetSpan(3 + Format.MaxInt32TextLen);
11401125
span[0] = (byte)'$';
11411126
int bytes = WriteRaw(span, totalLength, offset: 1);
11421127
writer.Advance(bytes);
@@ -1228,7 +1213,7 @@ private static void WriteUnifiedPrefixedBlob(PipeWriter writer, byte[]? prefix,
12281213
}
12291214
else
12301215
{
1231-
var span = writer.GetSpan(3 + MaxInt32TextLen); // note even with 2 max-len, we're still in same text range
1216+
var span = writer.GetSpan(3 + Format.MaxInt32TextLen); // note even with 2 max-len, we're still in same text range
12321217
span[0] = (byte)'$';
12331218
int bytes = WriteRaw(span, prefix.LongLength + value.LongLength, offset: 1);
12341219
writer.Advance(bytes);
@@ -1249,7 +1234,7 @@ private static void WriteUnifiedInt64(PipeWriter writer, long value)
12491234

12501235
// ${asc-len}\r\n = 3 + MaxInt32TextLen
12511236
// {asc}\r\n = MaxInt64TextLen + 2
1252-
var span = writer.GetSpan(5 + MaxInt32TextLen + MaxInt64TextLen);
1237+
var span = writer.GetSpan(5 + Format.MaxInt32TextLen + Format.MaxInt64TextLen);
12531238

12541239
span[0] = (byte)'$';
12551240
var bytes = WriteRaw(span, value, withLengthPrefix: true, offset: 1);
@@ -1263,11 +1248,10 @@ private static void WriteUnifiedUInt64(PipeWriter writer, ulong value)
12631248

12641249
// ${asc-len}\r\n = 3 + MaxInt32TextLen
12651250
// {asc}\r\n = MaxInt64TextLen + 2
1266-
var span = writer.GetSpan(5 + MaxInt32TextLen + MaxInt64TextLen);
1251+
var span = writer.GetSpan(5 + Format.MaxInt32TextLen + Format.MaxInt64TextLen);
12671252

1268-
Span<byte> valueSpan = stackalloc byte[MaxInt64TextLen];
1269-
if (!Utf8Formatter.TryFormat(value, valueSpan, out var len))
1270-
throw new InvalidOperationException("TryFormat failed");
1253+
Span<byte> valueSpan = stackalloc byte[Format.MaxInt64TextLen];
1254+
var len = Format.FormatUInt64(value, valueSpan);
12711255
span[0] = (byte)'$';
12721256
int offset = WriteRaw(span, len, withLengthPrefix: false, offset: 1);
12731257
valueSpan.Slice(0, len).CopyTo(span.Slice(offset));
@@ -1280,7 +1264,7 @@ internal static void WriteInteger(PipeWriter writer, long value)
12801264
//note: client should never write integer; only server does this
12811265

12821266
// :{asc}\r\n = MaxInt64TextLen + 3
1283-
var span = writer.GetSpan(3 + MaxInt64TextLen);
1267+
var span = writer.GetSpan(3 + Format.MaxInt64TextLen);
12841268

12851269
span[0] = (byte)':';
12861270
var bytes = WriteRaw(span, value, withLengthPrefix: false, offset: 1);

src/StackExchange.Redis/RawResult.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ internal ref struct Tokenizer
7777
public Tokenizer GetEnumerator() => this;
7878
private BufferReader _value;
7979

80-
public Tokenizer(in ReadOnlySequence<byte> value)
80+
public Tokenizer(scoped in ReadOnlySequence<byte> value)
8181
{
8282
_value = new BufferReader(value);
8383
Current = default;
@@ -384,7 +384,7 @@ internal bool TryGetDouble(out double val)
384384

385385
internal bool TryGetInt64(out long value)
386386
{
387-
if (IsNull || Payload.IsEmpty || Payload.Length > PhysicalConnection.MaxInt64TextLen)
387+
if (IsNull || Payload.IsEmpty || Payload.Length > Format.MaxInt64TextLen)
388388
{
389389
value = 0;
390390
return false;

src/StackExchange.Redis/RedisValue.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ internal StorageType Type
333333
StorageType.Null => 0,
334334
StorageType.Raw => _memory.Length,
335335
StorageType.String => Encoding.UTF8.GetByteCount((string)_objectOrSentinel!),
336+
StorageType.Int64 => Format.MeasureInt64(OverlappedValueInt64),
337+
StorageType.UInt64 => Format.MeasureUInt64(OverlappedValueUInt64),
338+
StorageType.Double => Format.MeasureDouble(OverlappedValueDouble),
336339
_ => throw new InvalidOperationException("Unable to compute length of type: " + Type),
337340
};
338341

@@ -824,16 +827,15 @@ private static string ToHex(ReadOnlySpan<byte> src)
824827

825828
return value._memory.ToArray();
826829
case StorageType.Int64:
827-
Span<byte> span = stackalloc byte[PhysicalConnection.MaxInt64TextLen + 2];
830+
Span<byte> span = stackalloc byte[Format.MaxInt64TextLen + 2];
828831
int len = PhysicalConnection.WriteRaw(span, value.OverlappedValueInt64, false, 0);
829832
arr = new byte[len - 2]; // don't need the CRLF
830833
span.Slice(0, arr.Length).CopyTo(arr);
831834
return arr;
832835
case StorageType.UInt64:
833836
// we know it is a huge value - just jump straight to Utf8Formatter
834-
span = stackalloc byte[PhysicalConnection.MaxInt64TextLen];
835-
if (!Utf8Formatter.TryFormat(value.OverlappedValueUInt64, span, out len))
836-
throw new InvalidOperationException("TryFormat failed");
837+
span = stackalloc byte[Format.MaxInt64TextLen];
838+
len = Format.FormatUInt64(value.OverlappedValueUInt64, span);
837839
arr = new byte[len];
838840
span.Slice(0, len).CopyTo(arr);
839841
return arr;
@@ -1123,11 +1125,11 @@ private ReadOnlyMemory<byte> AsMemory(out byte[]? leased)
11231125
s = Format.ToString(OverlappedValueDouble);
11241126
goto HaveString;
11251127
case StorageType.Int64:
1126-
leased = ArrayPool<byte>.Shared.Rent(PhysicalConnection.MaxInt64TextLen + 2); // reused code has CRLF terminator
1128+
leased = ArrayPool<byte>.Shared.Rent(Format.MaxInt64TextLen + 2); // reused code has CRLF terminator
11271129
len = PhysicalConnection.WriteRaw(leased, OverlappedValueInt64) - 2; // drop the CRLF
11281130
return new ReadOnlyMemory<byte>(leased, 0, len);
11291131
case StorageType.UInt64:
1130-
leased = ArrayPool<byte>.Shared.Rent(PhysicalConnection.MaxInt64TextLen); // reused code has CRLF terminator
1132+
leased = ArrayPool<byte>.Shared.Rent(Format.MaxInt64TextLen); // reused code has CRLF terminator
11311133
// value is huge, jump direct to Utf8Formatter
11321134
if (!Utf8Formatter.TryFormat(OverlappedValueUInt64, leased, out len))
11331135
throw new InvalidOperationException("TryFormat failed");

0 commit comments

Comments
 (0)