Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d3a1f5b
WIP v3 (#3035)
mgravell Mar 26, 2026
2cd3cea
don't test WriteMode.Sync/Async yet
mgravell Mar 26, 2026
9df7380
failing test for #3048
mgravell Apr 2, 2026
9c81690
upstream #3049 into v3
mgravell Apr 2, 2026
5984b40
release notes
mgravell Apr 2, 2026
f24edd9
fix broken config test
mgravell May 14, 2026
f5b08ad
re-add channel handling (merge)
mgravell May 14, 2026
e015faf
optimize pub/sub check for server-defined channels
mgravell May 14, 2026
d8be7b6
prefer WriteBulkString("..."u8) => WriteRaw(RESPu8)
mgravell May 14, 2026
29272f0
optimize CommandMap by pre-generating all the RESP chunks
mgravell May 15, 2026
9b3067b
Move CI to 8.8 rc1 (#3079)
mgravell May 15, 2026
01746eb
Draft support for 8.8 arrays (#3076)
mgravell May 15, 2026
4249939
CI: graceful skip if DEBUG not available
mgravell May 18, 2026
0a6e80b
Alternative AwaitableMutex implementation for netfx (#3081)
mgravell May 19, 2026
bc83781
Merge branch 'main' into v3
mgravell May 19, 2026
4da23d9
Merge branch 'main' into v3
mgravell May 19, 2026
a3820dc
re-implement TCP keep-alive
mgravell May 19, 2026
c2fe0d2
default RESP3 everywhere (#3084)
mgravell May 20, 2026
05f0554
improve logging for ReadNotificationAsync
mgravell May 21, 2026
09797bb
stabilize CI for RetryPolicyFailureCases
mgravell May 21, 2026
f9a6d11
Merge branch 'main' into v3
mgravell May 21, 2026
4f3a22d
Merge branch 'main' into v3
mgravell May 21, 2026
046f2b7
merge
mgravell May 21, 2026
85b6c48
Merge branch 'main' into v3
mgravell May 21, 2026
70757ae
broken merge (unshipped.txt)
mgravell May 21, 2026
9229aac
Merge branch 'main' into v3
mgravell May 22, 2026
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

- Fix logic inversion with `ARGREP NOCASE`, add `IsReversed` to simplify ordering, and support `ARINFO FULL`. ([#3087 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3087))

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