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.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IsPackable>false</IsPackable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<PublicKey>00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912fff</PublicKey>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
Expand Down
48 changes: 22 additions & 26 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,43 +1,39 @@
<Project>
<ItemGroup>
<!-- Packages we depend on for StackExchange.Redis, upgrades can create binding redirect pain! -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.14" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageVersion Include="Pipelines.Sockets.Unofficial" Version="2.2.16" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="5.0.0" />
<PackageVersion Include="System.Threading.Channels" Version="5.0.0" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.5" />
<PackageVersion Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
<PackageVersion Include="System.IO.Compression" Version="4.3.0" />
<!-- note that this bumps System.Buffers, so is pinned in down-level in SE csproj -->
<PackageVersion Include="System.IO.Hashing" Version="10.0.2" />
<PackageVersion Include="System.IO.Pipelines" Version="10.0.5" />
<!-- note that this bumps System.Buffers, so is pinned in down-level in SE csproj -->
<PackageVersion Include="System.IO.Hashing" Version="10.0.5" />
<!-- for RESPite -->
<PackageVersion Include="System.Buffers" Version="4.6.1" />
<PackageVersion Include="System.Memory" Version="4.6.1" />

<PackageVersion Include="System.Memory" Version="4.6.3" />
<!-- For analyzers, tied to the consumer's build SDK; at the moment, that means "us" -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />

<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<!-- Packages only used in the solution, upgrade at will -->
<PackageVersion Include="BenchmarkDotNet" Version="0.15.2" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.Testing.Platform" Version="1.7.3" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.7.115" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.201" />
<PackageVersion Include="Microsoft.Testing.Platform" Version="2.1.0" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.9.50" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.6.96" />
<PackageVersion Include="StackExchange.Redis" Version="2.12.4" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.0" />
<PackageVersion Include="System.Reflection.Metadata" Version="9.0.0" />

<PackageVersion Include="System.Collections.Immutable" Version="10.0.5" />
<PackageVersion Include="System.Reflection.Metadata" Version="10.0.5" />
<!-- For binding redirect testing, main package gets this transitively -->
<PackageVersion Include="System.IO.Pipelines" Version="9.0.0" />
<PackageVersion Include="System.Runtime.Caching" Version="9.0.0" />
<PackageVersion Include="xunit.v3" Version="3.0.0" />
<PackageVersion Include="xunit.v3.runner.console" Version="3.0.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
<PackageVersion Include="System.Runtime.Caching" Version="10.0.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="xunit.v3.runner.console" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
</Project>
3 changes: 2 additions & 1 deletion StackExchange.Redis.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=xreadgroup/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=xrevrange/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=zcard/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=zscan/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=zscan/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=zset/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
6 changes: 3 additions & 3 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ var conn = ConnectionMultiplexer.Connect("contoso5.redis.cache.windows.net,ssl=t
The `ConfigurationOptions` object has a wide range of properties, all of which are fully documented in intellisense. Some of the more common options to use include:

| Configuration string | `ConfigurationOptions` | Default | Meaning |
| ---------------------- | ---------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------- |
| ---------------------- | ---------------------- |------------------------------| --------------------------------------------------------------------------------------------------------- |
| abortConnect={bool} | `AbortOnConnectFail` | `true` (`false` on Azure) | If true, `Connect` will not create a connection while no servers are available |
| allowAdmin={bool} | `AllowAdmin` | `false` | Enables a range of commands that are considered risky |
| channelPrefix={string} | `ChannelPrefix` | `null` | Optional channel prefix for all pub/sub operations |
| checkCertificateRevocation={bool} | `CheckCertificateRevocation` | `true` | A Boolean value that specifies whether the certificate revocation list is checked during authentication. |
| checkCertificateRevocation={bool} | `CheckCertificateRevocation` | `true` | A Boolean value that specifies whether the certificate revocation list is checked during authentication. |
| connectRetry={int} | `ConnectRetry` | `3` | The number of times to repeat connect attempts during initial `Connect` |
| connectTimeout={int} | `ConnectTimeout` | `5000` | Timeout (ms) for connect operations |
| configChannel={string} | `ConfigurationChannel` | `__Booksleeve_MasterChanged` | Broadcast channel name for communicating configuration changes |
Expand All @@ -95,7 +95,7 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a
| syncTimeout={int} | `SyncTimeout` | `5000` | Time (ms) to allow for synchronous operations |
| asyncTimeout={int} | `AsyncTimeout` | `SyncTimeout` | Time (ms) to allow for asynchronous operations |
| tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous primary scenario |
| version={string} | `DefaultVersion` | (`4.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) |
| version={string} | `DefaultVersion` | (`7.4` in AMR, else `6.0`) | Redis version level (useful when the server does not make this available) |
| tunnel={string} | `Tunnel` | `null` | Tunnel for connections (use `http:{proxy url}` for "connect"-based proxy server) |
| setlib={bool} | `SetClientLibrary` | `true` | Whether to attempt to use `CLIENT SETINFO` to set the library name/version on the connection |
| protocol={string} | `Protocol` | `null` | Redis protocol to use; see section below |
Expand Down
9 changes: 8 additions & 1 deletion docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ Current package versions:
| ------------ | ----------------- | ----- |
| [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) |

## Unreleased
## 3.0

From 3.0, [release notes will be maintained in GitHub only](https://github.com/StackExchange/StackExchange.Redis/releases) to avoid duplication.

---


## 2.12.14

- Add experimental Redis 8.8 array support, including array APIs on `IDatabase`/`IDatabaseAsync`,
array helper types, `RedisType.Array`, and array delete keyspace notification event types. ([#3076 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3076))
Expand Down
15 changes: 15 additions & 0 deletions docs/exp/SER004.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# RESPite

RESPite is an experimental library that provides high-performance low-level RESP (Redis, etc) parsing and serialization.
It is used as the IO core for StackExchange.Redis v3+. You should not (yet) use it directly unless you have a very
good reason to do so.

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

or more granularly / locally in C#:

``` c#
#pragma warning disable SER004
```
21 changes: 21 additions & 0 deletions docs/exp/SER005.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Unit Testing

Unit testing is great! Yay, do more of that!

This type is provided for external unit testing, in particular by people using modules or server features
not directly implemented by SE.Redis - for example to verify messsage parsing or formatting without
talking to a RESP server.

These types are considered slightly more... *mercurial*. We encourage you to use them, but *occasionally*
(not just for fun) you might need to update your test code if we tweak something. This should not impact
"real" library usage.

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

or more granularly / locally in C#:

``` c#
#pragma warning disable SER005
```
47 changes: 46 additions & 1 deletion src/RESPite/Messages/RespReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,6 @@ private readonly unsafe bool TryParseSlow<T>(
/// <param name="value">The parsed value if successful.</param>
/// <returns><c>true</c> if parsing succeeded; otherwise, <c>false</c>.</returns>
#pragma warning disable RS0016, RS0027 // public API
[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#if DEBUG
[Obsolete("Please prefer the function-pointer API for library-internal use.")]
Expand All @@ -758,6 +757,52 @@ public readonly bool TryParseScalar<T>(ScalarParser<byte, T> parser, out T value
return TryGetSpan(out var span) ? parser(span, out value) : TryParseSlow(parser, out value);
}

private readonly ReadOnlySpan<char> BufferChars(Span<char> target, out char[]? lease)
{
byte[] byteLease = [];
var bytes = Buffer(ref byteLease, byteLease);

int len = RespConstants.UTF8.GetMaxCharCount(bytes.Length);
if (len <= target.Length)
{
lease = null;
}
else
{
target = lease = ArrayPool<char>.Shared.Rent(len);
}
len = RespConstants.UTF8.GetChars(bytes, target);
return target.Slice(0, len);
}

/// <summary>
/// Tries to read the current scalar element using a parser callback.
/// </summary>
/// <typeparam name="T">The type of data being parsed.</typeparam>
/// <param name="parser">The parser callback.</param>
/// <param name="value">The parsed value if successful.</param>
/// <returns><c>true</c> if parsing succeeded; otherwise, <c>false</c>.</returns>
public readonly bool TryParseScalar<T>(ScalarParser<char, T> parser, out T value)
{
// note: no benefit in a function-ptr overload, after we've dealt with decoding bytes etc
var buffer = BufferChars(stackalloc char[128], out var lease);
try
{
return parser(buffer, out value);
}
finally
{
if (lease is not null) ArrayPool<char>.Shared.Return(lease);
}
}

/// <summary>
/// Tries to read the current scalar element using a parser callback.
/// </summary>
/// <typeparam name="T">The type of data being parsed.</typeparam>
/// <param name="parser">The parser callback.</param>
/// <param name="value">The parsed value if successful.</param>
/// <returns><c>true</c> if parsing succeeded; otherwise, <c>false</c>.</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
private readonly bool TryParseSlow<T>(ScalarParser<byte, T> parser, out T value)
{
Expand Down
5 changes: 5 additions & 0 deletions src/RESPite/PublicAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
[SER004]RESPite.Messages.RespReader.TryParseScalar<T>(RESPite.Messages.RespReader.ScalarParser<char, T>! parser, out T value) -> bool
[SER004]static RESPite.AsciiHash.EqualsCI(System.ReadOnlySpan<byte> first, System.ReadOnlySpan<char> second) -> bool
[SER004]static RESPite.AsciiHash.EqualsCI(System.ReadOnlySpan<char> first, System.ReadOnlySpan<byte> second) -> bool
[SER004]static RESPite.AsciiHash.SequenceEqualsCI(System.ReadOnlySpan<byte> first, System.ReadOnlySpan<char> second) -> bool
[SER004]static RESPite.AsciiHash.SequenceEqualsCI(System.ReadOnlySpan<char> first, System.ReadOnlySpan<byte> second) -> bool
1 change: 1 addition & 0 deletions src/RESPite/PublicAPI/net6.0/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
1 change: 1 addition & 0 deletions src/RESPite/PublicAPI/net6.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
2 changes: 0 additions & 2 deletions src/RESPite/RESPite.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,4 @@
<InternalsVisibleTo Include="StackExchange.Redis.Server" />
<InternalsVisibleTo Include="StackExchange.Redis.Benchmarks" />
</ItemGroup>


</Project>
10 changes: 0 additions & 10 deletions src/RESPite/Shared/AsciiHash.Public.cs

This file was deleted.

56 changes: 51 additions & 5 deletions src/RESPite/Shared/AsciiHash.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
Expand All @@ -7,8 +6,6 @@

namespace RESPite;

#pragma warning disable SA1205 // deliberately omit accessibility - see AsciiHash.Public.cs

/// <summary>
/// This type is intended to provide fast hashing functions for small ASCII strings, for example well-known
/// RESP literals that are usually identifiable by their length and initial bytes; it is not intended
Expand All @@ -22,7 +19,7 @@ namespace RESPite;
Inherited = false)]
[Conditional("DEBUG")] // evaporate in release
[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)]
sealed partial class AsciiHashAttribute(string token = "") : Attribute
public sealed partial class AsciiHashAttribute(string token = "") : Attribute
{
/// <summary>
/// The token expected when parsing data, if different from the implied value. The implied
Expand All @@ -38,7 +35,7 @@ sealed partial class AsciiHashAttribute(string token = "") : Attribute

// note: instance members are in AsciiHash.Instance.cs.
[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)]
readonly partial struct AsciiHash
public readonly partial struct AsciiHash
{
/// <summary>
/// In-place ASCII upper-case conversion.
Expand Down Expand Up @@ -85,6 +82,9 @@ public static bool EqualsCI(ReadOnlySpan<byte> first, ReadOnlySpan<byte> second)
return len <= MaxBytesHashed ? HashUC(first) == HashUC(second) : SequenceEqualsCI(first, second);
}

public static bool EqualsCI(ReadOnlySpan<byte> first, ReadOnlySpan<char> second)
=> EqualsCI(second, first);

public static unsafe bool SequenceEqualsCI(ReadOnlySpan<byte> first, ReadOnlySpan<byte> second)
{
var len = first.Length;
Expand Down Expand Up @@ -120,6 +120,9 @@ public static unsafe bool SequenceEqualsCI(ReadOnlySpan<byte> first, ReadOnlySpa
}
}

public static bool SequenceEqualsCI(ReadOnlySpan<byte> first, ReadOnlySpan<char> second)
=> SequenceEqualsCI(second, first);

public static bool EqualsCS(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
{
var len = first.Length;
Expand All @@ -139,6 +142,14 @@ public static bool EqualsCI(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
return len <= MaxBytesHashed ? HashUC(first) == HashUC(second) : SequenceEqualsCI(first, second);
}

public static bool EqualsCI(ReadOnlySpan<char> first, ReadOnlySpan<byte> second)
{
var len = first.Length;
if (len != second.Length) return false;
// for very short values, the UC hash performs CI equality
return len <= MaxBytesHashed ? HashUC(first) == HashUC(second) : SequenceEqualsCI(first, second);
}

public static unsafe bool SequenceEqualsCI(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
{
var len = first.Length;
Expand Down Expand Up @@ -174,6 +185,41 @@ public static unsafe bool SequenceEqualsCI(ReadOnlySpan<char> first, ReadOnlySpa
}
}

public static unsafe bool SequenceEqualsCI(ReadOnlySpan<char> first, ReadOnlySpan<byte> second)
{
var len = first.Length;
if (len != second.Length) return false;

// OK, don't be clever (SIMD, etc); the purpose of FashHash is to compare RESP key tokens, which are
// typically relatively short, think 3-20 bytes. That wouldn't even touch a SIMD vector, so:
// just loop (the exact thing we'd need to do *anyway* in a SIMD implementation, to mop up the non-SIMD
// trailing bytes).
fixed (char* firstPtr = &MemoryMarshal.GetReference(first))
{
fixed (byte* secondPtr = &MemoryMarshal.GetReference(second))
{
const int CS_MASK = 0b0101_1111;
for (int i = 0; i < len; i++)
{
int x = (byte)firstPtr[i];
var xCI = x & CS_MASK;
if (xCI >= 'A' & xCI <= 'Z')
{
// alpha mismatch
if (xCI != (secondPtr[i] & CS_MASK)) return false;
}
else if (x != secondPtr[i])
{
// non-alpha mismatch
return false;
}
}

return true;
}
}
}

public static void Hash(scoped ReadOnlySpan<byte> value, out long cs, out long uc)
{
cs = HashCS(value);
Expand Down
Loading
Loading