Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a2be468
Iniial stab at vector-set API
mgravell Aug 11, 2025
e8b580e
Use `bool` as the return from `VADD`
mgravell Aug 11, 2025
a7a2f6a
working on impl
mgravell Aug 11, 2025
9a4b9c3
more tests
mgravell Aug 11, 2025
ebce7eb
ack experimental
mgravell Aug 12, 2025
579133b
ack experimental
mgravell Aug 12, 2025
f20db67
links
mgravell Aug 12, 2025
4b4225e
VLINKS impl
mgravell Aug 12, 2025
e0b959d
implement VSIM message
mgravell Aug 12, 2025
f755e7a
fixins
mgravell Aug 12, 2025
0ed581e
core for fast-hash
mgravell Aug 13, 2025
d69145d
VINFO complete
mgravell Aug 13, 2025
8933579
VSIM; watch out for RESP3+WITHSCORES+WITHATTRIBS, that's a doozy!
mgravell Aug 13, 2025
828b408
VSIM filter integration tests
mgravell Aug 13, 2025
697d312
allow VectorSet as a method prefix (CheckSignatures)
mgravell Aug 13, 2025
a148d99
Split VectorSet* code into partial files
mgravell Aug 14, 2025
337cbba
Split VectorSet code from ResultProcessor (also: *Lease*)
mgravell Aug 14, 2025
d82c9a4
key can be embstr or raw
mgravell Aug 14, 2025
6320185
Use code-generator for `[FastHash]`.
mgravell Aug 14, 2025
b14934d
Move the literals to be better scoped.
mgravell Aug 14, 2025
26a4a62
tyop
mgravell Aug 14, 2025
f9d3af9
lost a using directive somehow
mgravell Aug 14, 2025
51f93c6
disable spell-checker on literals
mgravell Aug 14, 2025
c421ccb
literals can be private
mgravell Aug 14, 2025
536efe4
- add FastHashTests
mgravell Aug 15, 2025
65eb889
Merge branch 'main' into marc/vectorsets
mgravell Aug 19, 2025
d53f1ab
fix PR nits
mgravell Sep 4, 2025
2157db1
change VSIM API to allow future extensivility:
mgravell Sep 9, 2025
a53365b
remove redundant property
mgravell Sep 9, 2025
65efb51
refactor VADD API:
mgravell Sep 10, 2025
8004eb2
unify attributesJson over jsonAttributes, and add [StringSyntax] appr…
mgravell Sep 10, 2025
167434b
add more context on ConfigGet test failure
mgravell Sep 10, 2025
3f98c56
move API to shipped
mgravell Sep 10, 2025
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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<CodeAnalysisRuleset>$(MSBuildThisFileDirectory)Shared.ruleset</CodeAnalysisRuleset>
<MSBuildWarningsAsMessages>NETSDK1069</MSBuildWarningsAsMessages>
<NoWarn>NU5105;NU1507</NoWarn>
<NoWarn>$(NoWarn);NU5105;NU1507;SER001</NoWarn>
<PackageReleaseNotes>https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes</PackageReleaseNotes>
<PackageProjectUrl>https://stackexchange.github.io/StackExchange.Redis/</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<PackageVersion Include="System.Threading.Channels" Version="5.0.0" />
<PackageVersion Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
<PackageVersion Include="System.IO.Compression" Version="4.3.0" />

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

<!-- Packages only used in the solution, upgrade at will -->
<PackageVersion Include="BenchmarkDotNet" Version="0.15.2" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
Expand All @@ -23,6 +27,7 @@
<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" />

<!-- 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" />
Expand Down
9 changes: 9 additions & 0 deletions StackExchange.Redis.sln
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs", "docs\docs.csproj",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Benchmarks", "tests\StackExchange.Redis.Benchmarks\StackExchange.Redis.Benchmarks.csproj", "{59889284-FFEE-82E7-94CB-3B43E87DA6CF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{5FA0958E-6EBD-45F4-808E-3447A293F96F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Build", "eng\StackExchange.Redis.Build\StackExchange.Redis.Build.csproj", "{190742E1-FA50-4E36-A8C4-88AE87654340}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -180,6 +184,10 @@ Global
{59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.Build.0 = Release|Any CPU
{190742E1-FA50-4E36-A8C4-88AE87654340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{190742E1-FA50-4E36-A8C4-88AE87654340}.Debug|Any CPU.Build.0 = Debug|Any CPU
{190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.ActiveCfg = Release|Any CPU
{190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -202,6 +210,7 @@ Global
{A0F89B8B-32A3-4C28-8F1B-ADE343F16137} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
{69A0ACF2-DF1F-4F49-B554-F732DCA938A3} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
{59889284-FFEE-82E7-94CB-3B43E87DA6CF} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
{190742E1-FA50-4E36-A8C4-88AE87654340} = {5FA0958E-6EBD-45F4-808E-3447A293F96F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B}
Expand Down
3 changes: 2 additions & 1 deletion StackExchange.Redis.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OK/@EntryIndexedValue">OK</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PONG/@EntryIndexedValue">PONG</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pubsub/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pubsub/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=vectorset/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
1 change: 1 addition & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Current package versions:
- Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638))
- Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822))
- Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928))
- Add vector-set support ([#2939 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2939))
- Add `GetServer(RedisKey, ...)` API ([#2936 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2936))
- Fix error constructing `StreamAdd` message ([#2941 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2941))

Expand Down
22 changes: 22 additions & 0 deletions docs/exp/SER001.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
At the current time, [Redis documents that](https://redis.io/docs/latest/commands/vadd/):

> Vector set is a new data type that is currently in preview and may be subject to change.

As such, the corresponding library feature must also be considered subject to change:

1. Existing bindings may cease working correctly if the underlying server API changes.
2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time
or run-time breaks.

While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress
this warning by adding the following to your `csproj` file:

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

or more granularly / locally in C#:

``` c#
#pragma warning disable SER001
```
215 changes: 215 additions & 0 deletions eng/StackExchange.Redis.Build/FastHashGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
using System.Buffers;
using System.Collections.Immutable;
using System.Reflection;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace StackExchange.Redis.Build;

[Generator(LanguageNames.CSharp)]
public class FastHashGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var literals = context.SyntaxProvider
.CreateSyntaxProvider(Predicate, Transform)
.Where(pair => pair.Name is { Length: > 0 })
.Collect();

context.RegisterSourceOutput(literals, Generate);
}

private bool Predicate(SyntaxNode node, CancellationToken cancellationToken)
{
// looking for [FastHash] partial static class Foo { }
if (node is ClassDeclarationSyntax decl
&& decl.Modifiers.Any(SyntaxKind.StaticKeyword)
&& decl.Modifiers.Any(SyntaxKind.PartialKeyword))
{
foreach (var attribList in decl.AttributeLists)
{
foreach (var attrib in attribList.Attributes)
{
if (attrib.Name.ToString() is "FastHashAttribute" or "FastHash") return true;
}
}
}

return false;
}

private static string GetName(INamedTypeSymbol type)
{
if (type.ContainingType is null) return type.Name;
var stack = new Stack<string>();
while (true)
{
stack.Push(type.Name);
if (type.ContainingType is null) break;
type = type.ContainingType;
}
var sb = new StringBuilder(stack.Pop());
while (stack.Count != 0)
{
sb.Append('.').Append(stack.Pop());
}
return sb.ToString();
}

private (string Namespace, string ParentType, string Name, string Value) Transform(
GeneratorSyntaxContext ctx,
CancellationToken cancellationToken)
{
// extract the name and value (defaults to name, but can be overridden via attribute) and the location
if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not INamedTypeSymbol named) return default;
string ns = "", parentType = "";
if (named.ContainingType is { } containingType)
{
parentType = GetName(containingType);
ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
}
else if (named.ContainingNamespace is { } containingNamespace)
{
ns = containingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
}

string name = named.Name, value = "";
foreach (var attrib in named.GetAttributes())
{
if (attrib.AttributeClass?.Name == "FastHashAttribute")
{
if (attrib.ConstructorArguments.Length == 1)
{
if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val)
{
value = val;
break;
}
}
}
}

if (string.IsNullOrWhiteSpace(value))
{
value = name.Replace("_", "-"); // if nothing explicit: infer from name
}

return (ns, parentType, name, value);
}

private string GetVersion()
{
var asm = GetType().Assembly;
if (asm.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault() is
AssemblyFileVersionAttribute { Version: { Length: > 0 } } version)
{
return version.Version;
}

return asm.GetName().Version?.ToString() ?? "??";
}

private void Generate(
SourceProductionContext ctx,
ImmutableArray<(string Namespace, string ParentType, string Name, string Value)> literals)
{
if (literals.IsDefaultOrEmpty) return;

var sb = new StringBuilder("// <auto-generated />")
.AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine();

// lease a buffer that is big enough for the longest string
var buffer = ArrayPool<byte>.Shared.Rent(
Encoding.UTF8.GetMaxByteCount(literals.Max(l => l.Value.Length)));
int indent = 0;

StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4);
NewLine().Append("using System;");
NewLine().Append("using StackExchange.Redis;");
NewLine().Append("#pragma warning disable CS8981");
foreach (var grp in literals.GroupBy(l => (l.Namespace, l.ParentType)))
{
NewLine();
int braces = 0;
if (!string.IsNullOrWhiteSpace(grp.Key.Namespace))
{
NewLine().Append("namespace ").Append(grp.Key.Namespace);
NewLine().Append("{");
indent++;
braces++;
}
if (!string.IsNullOrWhiteSpace(grp.Key.ParentType))
{
if (grp.Key.ParentType.Contains('.')) // nested types
{
foreach (var part in grp.Key.ParentType.Split('.'))
{
NewLine().Append("partial class ").Append(part);
NewLine().Append("{");
indent++;
braces++;
}
}
else
{
NewLine().Append("partial class ").Append(grp.Key.ParentType);
NewLine().Append("{");
indent++;
braces++;
}
}

foreach (var literal in grp)
{
int len;
unsafe
{
fixed (byte* bPtr = buffer) // netstandard2.0 forces fallback API
{
fixed (char* cPtr = literal.Value)
{
len = Encoding.UTF8.GetBytes(cPtr, literal.Value.Length, bPtr, buffer.Length);
}
}
}

// perform string escaping on the generated value (this includes the quotes, note)
var csValue = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)).ToFullString();

var hash = FastHash.Hash64(buffer.AsSpan(0, len));
NewLine().Append("static partial class ").Append(literal.Name);
NewLine().Append("{");
indent++;
NewLine().Append("public const int Length = ").Append(len).Append(';');
NewLine().Append("public const long Hash = ").Append(hash).Append(';');
NewLine().Append("public static ReadOnlySpan<byte> U8 => ").Append(csValue).Append("u8;");
NewLine().Append("public const string Text = ").Append(csValue).Append(';');
if (len <= 8)
{
// the hash enforces all the values
NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.Payload.Length == Length;");
NewLine().Append("public static bool Is(long hash, ReadOnlySpan<byte> value) => hash == Hash & value.Length == Length;");
}
else
{
NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);");
NewLine().Append("public static bool Is(long hash, ReadOnlySpan<byte> value) => hash == Hash && value.SequenceEqual(U8);");
}
indent--;
NewLine().Append("}");
}

// handle any closing braces
while (braces-- > 0)
{
indent--;
NewLine().Append("}");
}
}

ArrayPool<byte>.Shared.Return(buffer);
ctx.AddSource("FastHash.generated.cs", sb.ToString());
}
}
64 changes: 64 additions & 0 deletions eng/StackExchange.Redis.Build/FastHashGenerator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# FastHashGenerator

Efficient matching of well-known short string tokens is a high-volume scenario, for example when matching RESP literals.

The purpose of this generator is to interpret inputs like:

``` c#
[FastHash] public static partial class bin { }
[FastHash] public static partial class f32 { }
```

Usually the token is inferred from the name; `[FastHash("real value")]` can be used if the token is not a valid identifier.
Underscore is replaced with hyphen, so a field called `my_token` has the default value `"my-token"`.
The generator demands *all* of `[FastHash] public static partial class`, and note that any *containing* types must
*also* be declared `partial`.

The output is of the form:

``` c#
static partial class bin
{
public const int Length = 3;
public const long Hash = 7235938;
public static ReadOnlySpan<byte> U8 => @"bin"u8;
public static string Text => @"bin";
public static bool Is(long hash, in RawResult value) => ...
public static bool Is(long hash, in ReadOnlySpan<byte> value) => ...
}
static partial class f32
{
public const int Length = 3;
public const long Hash = 3289958;
public static ReadOnlySpan<byte> U8 => @"f32"u8;
public const string Text = @"f32";
public static bool Is(long hash, in RawResult value) => ...
public static bool Is(long hash, in ReadOnlySpan<byte> value) => ...
}
```

(this API is strictly an internal implementation detail, and can change at any time)

This generated code allows for fast, efficient, and safe matching of well-known tokens, for example:

``` c#
var key = ...
var hash = key.Hash64();
switch (key.Length)
{
case bin.Length when bin.Is(hash, key):
// handle bin
break;
case f32.Length when f32.Is(hash, key):
// handle f32
break;
}
```

The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler)
as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches
must also perform a sequence equality check - the `Is(hash, value)` convenience method validates both hash and equality.

Note that `switch` requires `const` values, hence why we use generated *types* rather than partial-properties
that emit an instance with the known values. Also, the `"..."u8` syntax emits a span which is awkward to store, but
easy to return via a property.
Loading
Loading