Skip to content

Commit fd8bbb1

Browse files
authored
Merge branch 'main' into marc/respite
2 parents cc3558f + 862a70e commit fd8bbb1

File tree

60 files changed

+3770
-91
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+3770
-91
lines changed

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
138138
csharp_preserve_single_line_statements = true
139139
csharp_preserve_single_line_blocks = true
140140

141+
# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852
142+
# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd
143+
dotnet_diagnostic.CA1852.severity = warning
141144

142145
# IDE preferences
143146
dotnet_diagnostic.IDE0090.severity = silent # IDE0090: Use 'new(...)'

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
1414
<CodeAnalysisRuleset>$(MSBuildThisFileDirectory)Shared.ruleset</CodeAnalysisRuleset>
1515
<MSBuildWarningsAsMessages>NETSDK1069</MSBuildWarningsAsMessages>
16-
<NoWarn>NU5105;NU1507</NoWarn>
16+
<NoWarn>$(NoWarn);NU5105;NU1507;SER001</NoWarn>
1717
<PackageReleaseNotes>https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes</PackageReleaseNotes>
1818
<PackageProjectUrl>https://stackexchange.github.io/StackExchange.Redis/</PackageProjectUrl>
1919
<PackageLicenseExpression>MIT</PackageLicenseExpression>

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<PackageVersion Include="StackExchange.Redis" Version="2.6.96" />
3232
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
3333
<PackageVersion Include="System.Reflection.Metadata" Version="9.0.0" />
34+
3435
<!-- For binding redirect testing, main package gets this transitively -->
3536
<PackageVersion Include="System.IO.Pipelines" Version="9.0.0" />
3637
<PackageVersion Include="System.Runtime.Caching" Version="9.0.0" />

StackExchange.Redis.sln.DotSettings

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SE/@EntryIndexedValue">SE</s:String>
66
<s:Boolean x:Key="/Default/UserDictionary/Words/=pite/@EntryIndexedValue">True</s:Boolean>
77
<s:Boolean x:Key="/Default/UserDictionary/Words/=RESPite/@EntryIndexedValue">True</s:Boolean>
8-
<s:Boolean x:Key="/Default/UserDictionary/Words/=pubsub/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
8+
<s:Boolean x:Key="/Default/UserDictionary/Words/=pubsub/@EntryIndexedValue">True</s:Boolean>
9+
<s:Boolean x:Key="/Default/UserDictionary/Words/=vectorset/@EntryIndexedValue">True</s:Boolean>
10+
</wpf:ResourceDictionary>

docs/ReleaseNotes.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,19 @@ Current package versions:
66
| ------------ | ----------------- | ----- |
77
| [![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) |
88

9-
## Unreleased (2.9.xxx)
9+
## Unreleased
10+
11+
- Fix [#2951](https://github.com/StackExchange/StackExchange.Redis/issues/2951) - sentinel reconnection failure ([#2956 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2956))
12+
13+
## 2.9.17
14+
15+
- Add vector-set support ([#2939 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2939))
16+
- Fix `RedisValue` special-value (NaN, Inf, etc) handling when casting from raw/string values to `double` ([#2950 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2950))
17+
- Internals:
18+
- Use `sealed` classes where possible ([#2942 by Henr1k80](https://github.com/StackExchange/StackExchange.Redis/pull/2942))
19+
- Add overlapped flushing in `LoggingTunnel` and avoid double-lookups ([#2943 by Henr1k80](https://github.com/StackExchange/StackExchange.Redis/pull/2943))
20+
21+
## 2.9.11
1022

1123
- Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2863))
1224
- Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863))

docs/exp/SER001.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
At the current time, [Redis documents that](https://redis.io/docs/latest/commands/vadd/):
2+
3+
> Vector set is a new data type that is currently in preview and may be subject to change.
4+
5+
As such, the corresponding library feature must also be considered subject to change:
6+
7+
1. Existing bindings may cease working correctly if the underlying server API changes.
8+
2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time
9+
or run-time breaks.
10+
11+
While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress
12+
this warning by adding the following to your `csproj` file:
13+
14+
```xml
15+
<NoWarn>$(NoWarn);SER001</NoWarn>
16+
```
17+
18+
or more granularly / locally in C#:
19+
20+
``` c#
21+
#pragma warning disable SER001
22+
```
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
using System.Buffers;
2+
using System.Collections.Immutable;
3+
using System.Reflection;
4+
using System.Text;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
9+
namespace StackExchange.Redis.Build;
10+
11+
[Generator(LanguageNames.CSharp)]
12+
public class FastHashGenerator : IIncrementalGenerator
13+
{
14+
public void Initialize(IncrementalGeneratorInitializationContext context)
15+
{
16+
var literals = context.SyntaxProvider
17+
.CreateSyntaxProvider(Predicate, Transform)
18+
.Where(pair => pair.Name is { Length: > 0 })
19+
.Collect();
20+
21+
context.RegisterSourceOutput(literals, Generate);
22+
}
23+
24+
private bool Predicate(SyntaxNode node, CancellationToken cancellationToken)
25+
{
26+
// looking for [FastHash] partial static class Foo { }
27+
if (node is ClassDeclarationSyntax decl
28+
&& decl.Modifiers.Any(SyntaxKind.StaticKeyword)
29+
&& decl.Modifiers.Any(SyntaxKind.PartialKeyword))
30+
{
31+
foreach (var attribList in decl.AttributeLists)
32+
{
33+
foreach (var attrib in attribList.Attributes)
34+
{
35+
if (attrib.Name.ToString() is "FastHashAttribute" or "FastHash") return true;
36+
}
37+
}
38+
}
39+
40+
return false;
41+
}
42+
43+
private static string GetName(INamedTypeSymbol type)
44+
{
45+
if (type.ContainingType is null) return type.Name;
46+
var stack = new Stack<string>();
47+
while (true)
48+
{
49+
stack.Push(type.Name);
50+
if (type.ContainingType is null) break;
51+
type = type.ContainingType;
52+
}
53+
var sb = new StringBuilder(stack.Pop());
54+
while (stack.Count != 0)
55+
{
56+
sb.Append('.').Append(stack.Pop());
57+
}
58+
return sb.ToString();
59+
}
60+
61+
private (string Namespace, string ParentType, string Name, string Value) Transform(
62+
GeneratorSyntaxContext ctx,
63+
CancellationToken cancellationToken)
64+
{
65+
// extract the name and value (defaults to name, but can be overridden via attribute) and the location
66+
if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not INamedTypeSymbol named) return default;
67+
string ns = "", parentType = "";
68+
if (named.ContainingType is { } containingType)
69+
{
70+
parentType = GetName(containingType);
71+
ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
72+
}
73+
else if (named.ContainingNamespace is { } containingNamespace)
74+
{
75+
ns = containingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
76+
}
77+
78+
string name = named.Name, value = "";
79+
foreach (var attrib in named.GetAttributes())
80+
{
81+
if (attrib.AttributeClass?.Name == "FastHashAttribute")
82+
{
83+
if (attrib.ConstructorArguments.Length == 1)
84+
{
85+
if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val)
86+
{
87+
value = val;
88+
break;
89+
}
90+
}
91+
}
92+
}
93+
94+
if (string.IsNullOrWhiteSpace(value))
95+
{
96+
value = name.Replace("_", "-"); // if nothing explicit: infer from name
97+
}
98+
99+
return (ns, parentType, name, value);
100+
}
101+
102+
private string GetVersion()
103+
{
104+
var asm = GetType().Assembly;
105+
if (asm.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault() is
106+
AssemblyFileVersionAttribute { Version: { Length: > 0 } } version)
107+
{
108+
return version.Version;
109+
}
110+
111+
return asm.GetName().Version?.ToString() ?? "??";
112+
}
113+
114+
private void Generate(
115+
SourceProductionContext ctx,
116+
ImmutableArray<(string Namespace, string ParentType, string Name, string Value)> literals)
117+
{
118+
if (literals.IsDefaultOrEmpty) return;
119+
120+
var sb = new StringBuilder("// <auto-generated />")
121+
.AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine();
122+
123+
// lease a buffer that is big enough for the longest string
124+
var buffer = ArrayPool<byte>.Shared.Rent(
125+
Encoding.UTF8.GetMaxByteCount(literals.Max(l => l.Value.Length)));
126+
int indent = 0;
127+
128+
StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4);
129+
NewLine().Append("using System;");
130+
NewLine().Append("using StackExchange.Redis;");
131+
NewLine().Append("#pragma warning disable CS8981");
132+
foreach (var grp in literals.GroupBy(l => (l.Namespace, l.ParentType)))
133+
{
134+
NewLine();
135+
int braces = 0;
136+
if (!string.IsNullOrWhiteSpace(grp.Key.Namespace))
137+
{
138+
NewLine().Append("namespace ").Append(grp.Key.Namespace);
139+
NewLine().Append("{");
140+
indent++;
141+
braces++;
142+
}
143+
if (!string.IsNullOrWhiteSpace(grp.Key.ParentType))
144+
{
145+
if (grp.Key.ParentType.Contains('.')) // nested types
146+
{
147+
foreach (var part in grp.Key.ParentType.Split('.'))
148+
{
149+
NewLine().Append("partial class ").Append(part);
150+
NewLine().Append("{");
151+
indent++;
152+
braces++;
153+
}
154+
}
155+
else
156+
{
157+
NewLine().Append("partial class ").Append(grp.Key.ParentType);
158+
NewLine().Append("{");
159+
indent++;
160+
braces++;
161+
}
162+
}
163+
164+
foreach (var literal in grp)
165+
{
166+
int len;
167+
unsafe
168+
{
169+
fixed (byte* bPtr = buffer) // netstandard2.0 forces fallback API
170+
{
171+
fixed (char* cPtr = literal.Value)
172+
{
173+
len = Encoding.UTF8.GetBytes(cPtr, literal.Value.Length, bPtr, buffer.Length);
174+
}
175+
}
176+
}
177+
178+
// perform string escaping on the generated value (this includes the quotes, note)
179+
var csValue = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)).ToFullString();
180+
181+
var hash = FastHash.Hash64(buffer.AsSpan(0, len));
182+
NewLine().Append("static partial class ").Append(literal.Name);
183+
NewLine().Append("{");
184+
indent++;
185+
NewLine().Append("public const int Length = ").Append(len).Append(';');
186+
NewLine().Append("public const long Hash = ").Append(hash).Append(';');
187+
NewLine().Append("public static ReadOnlySpan<byte> U8 => ").Append(csValue).Append("u8;");
188+
NewLine().Append("public const string Text = ").Append(csValue).Append(';');
189+
if (len <= 8)
190+
{
191+
// the hash enforces all the values
192+
NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.Payload.Length == Length;");
193+
NewLine().Append("public static bool Is(long hash, ReadOnlySpan<byte> value) => hash == Hash & value.Length == Length;");
194+
}
195+
else
196+
{
197+
NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);");
198+
NewLine().Append("public static bool Is(long hash, ReadOnlySpan<byte> value) => hash == Hash && value.SequenceEqual(U8);");
199+
}
200+
indent--;
201+
NewLine().Append("}");
202+
}
203+
204+
// handle any closing braces
205+
while (braces-- > 0)
206+
{
207+
indent--;
208+
NewLine().Append("}");
209+
}
210+
}
211+
212+
ArrayPool<byte>.Shared.Return(buffer);
213+
ctx.AddSource("FastHash.generated.cs", sb.ToString());
214+
}
215+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# FastHashGenerator
2+
3+
Efficient matching of well-known short string tokens is a high-volume scenario, for example when matching RESP literals.
4+
5+
The purpose of this generator is to interpret inputs like:
6+
7+
``` c#
8+
[FastHash] public static partial class bin { }
9+
[FastHash] public static partial class f32 { }
10+
```
11+
12+
Usually the token is inferred from the name; `[FastHash("real value")]` can be used if the token is not a valid identifier.
13+
Underscore is replaced with hyphen, so a field called `my_token` has the default value `"my-token"`.
14+
The generator demands *all* of `[FastHash] public static partial class`, and note that any *containing* types must
15+
*also* be declared `partial`.
16+
17+
The output is of the form:
18+
19+
``` c#
20+
static partial class bin
21+
{
22+
public const int Length = 3;
23+
public const long Hash = 7235938;
24+
public static ReadOnlySpan<byte> U8 => @"bin"u8;
25+
public static string Text => @"bin";
26+
public static bool Is(long hash, in RawResult value) => ...
27+
public static bool Is(long hash, in ReadOnlySpan<byte> value) => ...
28+
}
29+
static partial class f32
30+
{
31+
public const int Length = 3;
32+
public const long Hash = 3289958;
33+
public static ReadOnlySpan<byte> U8 => @"f32"u8;
34+
public const string Text = @"f32";
35+
public static bool Is(long hash, in RawResult value) => ...
36+
public static bool Is(long hash, in ReadOnlySpan<byte> value) => ...
37+
}
38+
```
39+
40+
(this API is strictly an internal implementation detail, and can change at any time)
41+
42+
This generated code allows for fast, efficient, and safe matching of well-known tokens, for example:
43+
44+
``` c#
45+
var key = ...
46+
var hash = key.Hash64();
47+
switch (key.Length)
48+
{
49+
case bin.Length when bin.Is(hash, key):
50+
// handle bin
51+
break;
52+
case f32.Length when f32.Is(hash, key):
53+
// handle f32
54+
break;
55+
}
56+
```
57+
58+
The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler)
59+
as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches
60+
must also perform a sequence equality check - the `Is(hash, value)` convenience method validates both hash and equality.
61+
62+
Note that `switch` requires `const` values, hence why we use generated *types* rather than partial-properties
63+
that emit an instance with the known values. Also, the `"..."u8` syntax emits a span which is awkward to store, but
64+
easy to return via a property.

eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@
1212
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all"/>
1313
</ItemGroup>
1414

15+
<ItemGroup>
16+
<Compile Include="..\..\src\StackExchange.Redis\FastHash.cs">
17+
<Link>FastHash.cs</Link>
18+
</Compile>
19+
</ItemGroup>
1520
</Project>

src/StackExchange.Redis/ClusterConfiguration.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, s
180180
if (node.IsMyself)
181181
Origin = node.EndPoint;
182182

183-
if (nodeLookup.ContainsKey(node.EndPoint))
183+
if (nodeLookup.TryGetValue(node.EndPoint, out var lookedUpNode))
184184
{
185185
// Deal with conflicting node entries for the same endpoint
186186
// This can happen in dynamic environments when a node goes down and a new one is created
@@ -190,7 +190,7 @@ internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, s
190190
// The node we're trying to add is probably about to become stale. Ignore it.
191191
continue;
192192
}
193-
else if (!nodeLookup[node.EndPoint].IsConnected)
193+
else if (!lookedUpNode.IsConnected)
194194
{
195195
// The node we registered previously is probably stale. Replace it with a known good node.
196196
nodeLookup[node.EndPoint] = node;

0 commit comments

Comments
 (0)