Skip to content

Read HELLO "server" before trusting "version" as the Redis API version - #3279

Merged
mgravell merged 7 commits into
mainfrom
marc/hello-server-vendor
Oct 16, 2026
Merged

mgravell merged 7 commits into
mainfrom
marc/hello-server-vendor

Conversation

@mgravell

Copy link
Copy Markdown
Collaborator

Fixes #3278.

Problem

Since 3.4.0 the HELLO reply has been a source of the server version, and both the interactive and subscription connections issue HELLO. The handler assigned version straight to the Redis API version, but the two conventions in the wild differ (measured against real servers):

Server server version extra
Garnet 2.2.2 redis 7.4.3 (Redis API version) garnet_version => 2.2.2
Valkey 9.1.2 valkey 9.1.2 (Valkey's own version) INFO: redis_version:7.2.4

On Valkey the detected version therefore depended on whether the last reply processed was HELLO (9.1.2) or INFO (7.2.4), so version-gated commands (e.g. DELEX in LockRelease) were sometimes sent to a server that does not have them.

Rule

Decided once the whole HELLO map has been read:

  • server => redis (or absent): version is the Redis API version; a <vendor>_version key, if present, sets the product variant (Garnet shape).
  • server is a recognised non-Redis name (valkey, redict, ...): version is the vendor version and only feeds the product variant; the API version is left to INFO's redis_version.
  • server is unrecognised: version is ignored (logged). Conservative by design: if INFO is unavailable too, the version falls back to DefaultVersion rather than guessing.
  • A plain Redis HELLO never touches the product variant, so it cannot clobber INFO's KeyDB detection (which comes from executable, not a *_version key).

Verification

  • Patched library against real Valkey 9.1.2 and Garnet 2.2.2, 20 connections each: stable at API 7.2.4 / vendor 9.1.2 and API 7.4.3 / vendor 2.2.2 respectively.
  • The in-process test server can now emulate both shapes (HelloServerName, HelloVersionString, HelloExtraFields). ProductVariantUnitTests and ValkeyUnitTests run per protocol and assert the API version as well as the variant.
  • New DetectProductVariantFromHelloAlone disables INFO so HELLO is the only source; it fails deterministically on RESP2 and RESP3 with the fix reverted. The test vendor version is now 9.1.2 rather than 1.2.3-preview4, because an unparseable version was hiding the bug.
  • Full Build.csproj Release build with CI=true is green.

#3278)

Valkey reports its own version as HELLO "version" ("server" => "valkey",
"version" => "9.1.2") while INFO still says redis_version:7.2.4; Garnet
keeps "version" as the Redis API version ("server" => "redis") and adds
"garnet_version". Both the interactive and subscription connections issue
HELLO, so the detected version depended on whether the last reply processed
was HELLO or INFO, and version-gated commands (DELEX for LockRelease) were
sent to servers that don't have them.

Now "version" only sets the Redis API version when "server" is redis; for a
recognised non-Redis server it feeds the product variant instead, and for an
unrecognised server it is ignored. A "<vendor>_version" HELLO field sets the
product variant in the Garnet shape. The in-process test server can emulate
both measured shapes, and a HELLO-only test (INFO disabled) pins the rule
down on RESP2 and RESP3.
Parse it as a Version (no string) once we know "server" is redis; only the
vendor path, which stores the string anyway, materializes it.
Adds Format.TryParseVersion(ReadOnlySpan<byte>): net10.0 has a built-in
UTF-8 Version.TryParse; elsewhere a Utf8Parser loop over the dotted
components, so no char transcoding and, downlevel, no string. The HELLO
handler uses it via the function-pointer TryParseScalar overload; a parity
test keeps it in step with the char/string overloads.
…d the gate normalisation

System.Version treats "7.4" as (7, 4, -1, -1) and "7.4.0" as (7, 4, 0, -1); the
UTF-8 overload must produce the same raw parts as the char/string overloads,
and IsAtLeast/IsEqual must then treat the unspecified parts as zero.
The goal is identical results to the pre-existing char path for identical
text, whatever System.Version does with it (-1 for unspecified parts,
whitespace and sign tolerance per part), not a tidier parser. net10.0
fast-paths the built-in UTF-8 Version.TryParse; anything it rejects, and
every other TFM, transcodes and calls the char overload. On .NET Framework
that still allocates a string, which is acceptable. The parity test now
covers the whitespace/sign cases.

@mgravell mgravell left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@mgravell
mgravell merged commit 1f13e2d into main Oct 16, 2026
5 checks passed
@mgravell
mgravell deleted the marc/hello-server-vendor branch October 16, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detected server version is non-deterministic on Valkey since 3.4.0

1 participant