Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protocol support: RESP3 #2396

Merged
merged 26 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ed1b107
first stab at RESP3; no actual parse code at the moment - this is API…
mgravell Aug 10, 2023
4c2cd0d
pass connection to test API to give warn rather than error if not eno…
mgravell Aug 10, 2023
843a84e
test setup: ensure we don't false-negative due to confusion between s…
mgravell Aug 10, 2023
a548b51
update RESP3 guidance
mgravell Aug 10, 2023
e0efe48
- remove double CLIENT ID (retain the "main" property, ConnectionId)
mgravell Aug 11, 2023
9f01470
- fix initial connect pause
mgravell Aug 16, 2023
af46e9e
- advertise RedisProtocol on IServer
mgravell Aug 16, 2023
7efe5f8
fix INFO (needs verbatim string support)
mgravell Aug 16, 2023
cf956c1
generalize such that verbatim strings always return just the value po…
mgravell Aug 16, 2023
0a5505f
- fix pubsub failover tests (resp3 changes which connections need to …
mgravell Aug 16, 2023
29c88d0
add RedisProtocol parsing on ClientInfo
mgravell Aug 16, 2023
00daa23
Merge branch 'main' into resp3
NickCraver Aug 22, 2023
a3feb31
Fix test logging
NickCraver Aug 22, 2023
e592e01
deal with problems with order of tests breaking connections; don't le…
mgravell Aug 23, 2023
951ee96
fix (or rather: re-fix) not connecting sub connection in RESP3
mgravell Aug 23, 2023
117b93f
Tests: Add support for [RunPerProtocol] and simplify multi-protocol t…
NickCraver Aug 23, 2023
0c74763
Merge remote-tracking branch 'origin/main' into resp3
NickCraver Sep 5, 2023
126e32a
Test fixups
NickCraver Sep 5, 2023
8c388a9
Fix tests on 3.1 server setup
NickCraver Sep 5, 2023
af96022
Stream tests: fix unique keys
NickCraver Sep 5, 2023
6d53672
Bump version to 2.7
NickCraver Sep 5, 2023
5c24f7a
Remove unused usings to tidy up
NickCraver Sep 5, 2023
eb36efb
Doc tweaks and minor fixes
NickCraver Sep 5, 2023
517eaea
Remove console write
NickCraver Sep 5, 2023
18e0ea9
Doc + ref fixes
NickCraver Sep 5, 2023
8ddd99a
Doc fixes
NickCraver Sep 7, 2023
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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<!-- For binding redirect testing, main package gets this transitively -->
<PackageVersion Include="System.IO.Pipelines" Version="5.0.1" />
<PackageVersion Include="System.Runtime.Caching" Version="5.0.0" />
<PackageVersion Include="xunit" Version="2.4.2-pre.12" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageVersion Include="xunit" Version="2.5.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.0" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions StackExchange.Redis.sln
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{153A10E4-E
docs\Profiling_v2.md = docs\Profiling_v2.md
docs\PubSubOrder.md = docs\PubSubOrder.md
docs\ReleaseNotes.md = docs\ReleaseNotes.md
docs\Resp3.md = docs\Resp3.md
docs\Scripting.md = docs\Scripting.md
docs\Server.md = docs\Server.md
docs\Testing.md = docs\Testing.md
Expand Down
42 changes: 31 additions & 11 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Configuration
# Configuration
===

When connecting to Redis version 6 or above with an ACL configured, your ACL user needs to at least have permissions to run the ECHO command. We run this command to verify that we have a valid connection to the Redis service.
Expand All @@ -15,7 +15,7 @@ The `configuration` here can be either:

The latter is *basically* a tokenized form of the former.

Basic Configuration Strings
## Basic Configuration Strings
-

The *simplest* configuration example is just the host name:
Expand Down Expand Up @@ -66,7 +66,7 @@ Microsoft Azure Redis example with password
var conn = ConnectionMultiplexer.Connect("contoso5.redis.cache.windows.net,ssl=true,password=...");
```

Configuration Options
## Configuration Options
---

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:
Expand Down Expand Up @@ -98,6 +98,7 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a
| version={string} | `DefaultVersion` | (`4.0` in Azure, else `2.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 |

Additional code-only options:
- LoggerFactory (`ILoggerFactory`) - Default: `null`
Expand All @@ -123,16 +124,17 @@ Additional code-only options:
Tokens in the configuration string are comma-separated; any without an `=` sign are assumed to be redis server endpoints. Endpoints without an explicit port will use 6379 if ssl is not enabled, and 6380 if ssl is enabled.
Tokens starting with `$` are taken to represent command maps, for example: `$config=cfg`.

Obsolete Configuration Options
## Obsolete Configuration Options
---

These options are parsed in connection strings for backwards compatibility (meaning they do not error as invalid), but no longer have any effect.

| Configuration string | `ConfigurationOptions` | Previous Default | Previous Meaning |
| ---------------------- | ---------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------- |
| responseTimeout={int} | `ResponseTimeout` | `SyncTimeout` | Time (ms) to decide whether the socket is unhealthy |
| writeBuffer={int} | `WriteBuffer` | `4096` | Size of the output buffer |

Automatic and Manual Configuration
## Automatic and Manual Configuration
---

In many common scenarios, StackExchange.Redis will automatically configure a lot of settings, including the server type and version, connection timeouts, and primary/replica relationships. Sometimes, though, the commands for this have been disabled on the redis server. In this case, it is useful to provide more information:
Expand Down Expand Up @@ -161,7 +163,8 @@ Which is equivalent to the command string:
```config
redis0:6379,redis1:6380,keepAlive=180,version=2.8.8,$CLIENT=,$CLUSTER=,$CONFIG=,$ECHO=,$INFO=,$PING=
```
Renaming Commands

## Renaming Commands
---

A slightly unusual feature of redis is that you can disable and/or rename individual commands. As per the previous example, this is done via the `CommandMap`, but instead of passing a `HashSet<string>` to `Create()` (to indicate the available or unavailable commands), you pass a `Dictionary<string,string>`. All commands not mentioned in the dictionary are assumed to be enabled and not renamed. A `null` or blank value records that the command is disabled. For example:
Expand All @@ -184,8 +187,9 @@ The above is equivalent to (in the connection string):
$INFO=,$SELECT=use
```

Redis Server Permissions
## Redis Server Permissions
---

If the user you're connecting to Redis with is limited, it still needs to have certain commands enabled for the StackExchange.Redis to succeed in connecting. The client uses:
- `AUTH` to authenticate
- `CLIENT` to set the client name
Expand All @@ -205,7 +209,7 @@ For example, a common _very_ minimal configuration ACL on the server (non-cluste

Note that if you choose to disable access to the above commands, it needs to be done via the `CommandMap` and not only the ACL on the server (otherwise we'll attempt the command and fail the handshake). Also, if any of the these commands are disabled, some functionality may be diminished or broken.

twemproxy
## twemproxy
---

[twemproxy](https://github.com/twitter/twemproxy) is a tool that allows multiple redis instances to be used as though it were a single server, with inbuilt sharding and fault tolerance (much like redis cluster, but implemented separately). The feature-set available to Twemproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used:
Expand All @@ -218,8 +222,9 @@ var options = new ConfigurationOptions
};
```

envoyproxy
##envoyproxy
---

[Envoyproxy](https://github.com/envoyproxy/envoy) is a tool that allows to front a redis cluster with a set of proxies, with inbuilt discovery and fault tolerance. The feature-set available to Envoyproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used:
```csharp
var options = new ConfigurationOptions+{
Expand All @@ -229,7 +234,7 @@ var options = new ConfigurationOptions+{
```


Tiebreakers and Configuration Change Announcements
## Tiebreakers and Configuration Change Announcements
---

Normally StackExchange.Redis will resolve primary/replica nodes automatically. However, if you are not using a management tool such as redis-sentinel or redis cluster, there is a chance that occasionally you will get multiple primary nodes (for example, while resetting a node for maintenance it may reappear on the network as a primary). To help with this, StackExchange.Redis can use the notion of a *tie-breaker* - which is only used when multiple primaries are detected (not including redis cluster, where multiple primaries are *expected*). For compatibility with BookSleeve, this defaults to the key named `"__Booksleeve_TieBreak"` (always in database 0). This is used as a crude voting mechanism to help determine the *preferred* primary, so that work is routed correctly.
Expand All @@ -240,8 +245,9 @@ Both options can be customized or disabled (set to `""`), via the `.Configuratio

These settings are also used by the `IServer.MakeMaster()` method, which can set the tie-breaker in the database and broadcast the configuration change message. The configuration message can also be used separately to primary/replica changes simply to request all nodes to refresh their configurations, via the `ConnectionMultiplexer.PublishReconfigure` method.

ReconnectRetryPolicy
## ReconnectRetryPolicy
---

StackExchange.Redis automatically tries to reconnect in the background when the connection is lost for any reason. It keeps retrying until the connection has been restored. It would use ReconnectRetryPolicy to decide how long it should wait between the retries.
ReconnectRetryPolicy can be exponential (default), linear or a custom retry policy.

Expand All @@ -266,3 +272,17 @@ config.ReconnectRetryPolicy = new LinearRetry(5000);
//5 5000
//6 5000
```

## Redis protocol

Without specific configuration, StackExchange.Redis will use the RESP2 protocol; this means that pub/sub requires a separatate connection to the server. RESP3 is a newer protocol
(usually, but not always, available on v6 servers and above) which allows (among other changes) pub/sub messages to be communicated on the *same* connection - which can be very
NickCraver marked this conversation as resolved.
Show resolved Hide resolved
desirable in servers with a large number of clients. The protocol handshake needs to happen very early in the connection, so *by default* the library does not attempt a RESP3 connection
unless it has reason to expect it to work.

The library determines whether to use RESP3 by:
- The `HELLO` command has been disabled: RESP2 is used
- A protocol *other than* `resp3` or `3` is specified: RESP2 is used
- A protocol of `resp3` or `3` is specified: RESP3 is attempted (with fallback if it fails)
- A version of at least 6 is specified: RESP3 is attempted (with fallback if it fails)
- In all other scenarios: RESP2 is used
NickCraver marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Current package versions:

## Unreleased

- Adds: RESP3 support ([#2396 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2396)) - see https://stackexchange.github.io/StackExchange.Redis/Resp3
- Fix [#2507](https://github.com/StackExchange/StackExchange.Redis/issues/2507): Pub/sub with multi-item payloads should be usable ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508))
- Add: connection-id tracking (internal only, no public API) ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508))
- Add: `ConfigurationOptions.LoggerFactory` for logging to an `ILoggerFactory` (e.g. `ILogger`) all connection and error events ([#2051 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2051))
Expand Down
45 changes: 45 additions & 0 deletions docs/Resp3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# RESP3 and StackExchange.Redis

RESP2 and RESP3 are evolutions of the Redis protocol, with RESP3 existing from Redis server version 6 onwards (v7.2+ for Redis Enterprise). The main differences are:

1. RESP3 can carry out-of-band / "push" messages on a single connection, where-as RESP2 requires a separate connection for these messages
2. RESP3 can (when appropriate) convey additional semantic meaning about returned payloads inside the same result structure
3. Some commands (see [this topic](https://github.com/redis/redis-doc/issues/2511)) return different result structures in RESP3 mode; for example a flat interleaved array might become a jagged array

For most people, #1 is the main reason to consider RESP3, as in high-usage servers - this can halve the number of connections required.
This is particularly useful in hosted environments where the number of inbound connections to the server is capped as part of a service plan.
Alternatively, where users are currently choosing to disable the out-of-band connection to achieve this, they may now be able to re-enable this
(for example, to receive server maintenance notifications) *without* incurring any additional connection overhead.

Because of the significance of #3 (and to avoid breaking your code), the library does not currently default to RESP3 mode. This must be enabled explicitly
via `ConfigurationOptions.Protocol` or by adding `,protocol=resp3` (or `,protocol=3`) to the configuration string.

---

#3 is a critical one - the library *should* already handle all documented commands that have revised results in RESP3, but if you're using
`Execute[Async]` to issue ad-hoc commands, you may need to update your processing code to compensate for this, ideally using detection to handle
*either* format so that the same code works in both REP2 and RESP3. Since the impacted commands are handled internally by the library, in reality
this should usually not usually present a difficulty.
NickCraver marked this conversation as resolved.
Show resolved Hide resolved

The minor (#2) and major (#3) differences to results are only visible to your code when using:

- Lua scripts invoked via the `ScriptEvaluate[Async](...)` or related APIs, that either:
- Uses the `redis.setresp(3)` API and returns a value from `redis.[p]call(...)`
- Returns a value that satisfies the [LUA to RESP3 type conversion rules](https://redis.io/docs/manual/programmability/lua-api/#lua-to-resp3-type-conversion)
- Ad-hoc commands (in particular: *modules*) that are invoked via the `Execute[Async](string command, ...)` API

...both which return `RedisResult`. **If you are not using these APIs, you should not need to do anything additional.**

Historically, you could use the `RedisResult.Type` property to query the type of data returned (integer, string, etc). In particular:

- Two new properties are added: `RedisResult.Resp2Type` and `RedisResult.Resp3Type`
- The `Resp3Type` property exposes the new semantic data (when using RESP3) - for example, it can indicate that a value is a double-precision number, a boolean, a map, etc (types that did not historically exist)
- The `Resp2Type` property exposes the same value that *would* have been returned if this data had been returned over RESP2
- The `Type` property is now marked obsolete, but functions identically to `Resp2Type`, so that pre-existing code (for example, that has a `switch` on the type) is not impacted by RESP3
- The `ResultType.MultiBulk` is superseded by `ResultType.Array` (this is a nomenclature change only; they are the same value and function identically)

Possible changes required due to RESP3:

1. To prevent build warnings, replace usage of `ResultType.MultiBulk` with `ResultType.Array`, and usage of `RedisResult.Type` with `RedisResult.Resp2Type`
2. If you wish to exploit the additional semantic data when enabling RESP3, use `RedisResult.Resp3Type` where appropriate
3. If you are enabling RESP3, you must verify whether the commands you are using can give different result shapes on RESP3 connections
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Documentation
- [Transactions](Transactions) - how atomic transactions work in redis
- [Events](Events) - the events available for logging / information purposes
- [Pub/Sub Message Order](PubSubOrder) - advice on sequential and concurrent processing
- [Using RESP3](Resp3) - information on using RESP3
- [ServerMaintenanceEvent](ServerMaintenanceEvent) - how to listen and prepare for hosted server maintenance (e.g. Azure Cache for Redis)
- [Streams](Streams) - how to use the Stream data type
- [Where are `KEYS` / `SCAN` / `FLUSH*`?](KeysScan) - how to use server-based commands
Expand Down
2 changes: 1 addition & 1 deletion src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ private sealed class Processor : ArrayResultProcessor<LatencyHistoryEntry>
{
protected override bool TryParse(in RawResult raw, out LatencyHistoryEntry parsed)
{
if (raw.Type == ResultType.MultiBulk)
if (raw.Resp2TypeArray == ResultType.Array)
{
var items = raw.GetItems();
if (items.Length >= 2
Expand Down
2 changes: 1 addition & 1 deletion src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ private sealed class Processor : ArrayResultProcessor<LatencyLatestEntry>
{
protected override bool TryParse(in RawResult raw, out LatencyLatestEntry parsed)
{
if (raw.Type == ResultType.MultiBulk)
if (raw.Resp2TypeArray == ResultType.Array)
{
var items = raw.GetItems();
if (items.Length >= 4
Expand Down
5 changes: 4 additions & 1 deletion src/StackExchange.Redis/ChannelMessageQueue.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
#if NETCOREAPP3_1
using System.Reflection;
#endif

namespace StackExchange.Redis
{
Expand Down Expand Up @@ -125,6 +127,7 @@ public ValueTask<ChannelMessage> ReadAsync(CancellationToken cancellationToken =
/// <param name="count">The (approximate) count of items in the Channel.</param>
public bool TryGetCount(out int count)
{
// This is specific to netcoreapp3.1, because full framework was out of band and the new prop is present
#if NETCOREAPP3_1
// get this using the reflection
try
Expand Down
13 changes: 9 additions & 4 deletions src/StackExchange.Redis/ClientInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,18 +181,23 @@ public ClientType ClientType
}

/// <summary>
/// Client RESP protocol version. Added in Redis 7.0
/// Client RESP protocol version. Added in Redis 7.0.
/// </summary>
public string? ProtocolVersion { get; private set; }

/// <summary>
/// Client library name. Added in Redis 7.2
/// Client RESP protocol version. Added in Redis 7.0.
/// </summary>
public RedisProtocol? Protocol => ConfigurationOptions.TryParseRedisProtocol(ProtocolVersion, out var value) ? value : null;

/// <summary>
/// Client library name. Added in Redis 7.2.
/// </summary>
/// <remarks><seealso href="https://redis.io/commands/client-setinfo"/></remarks>
public string? LibraryName { get; private set; }

/// <summary>
/// Client library version. Added in Redis 7.2
/// Client library version. Added in Redis 7.2.
/// </summary>
/// <remarks><seealso href="https://redis.io/commands/client-setinfo"/></remarks>
public string? LibraryVersion { get; private set; }
Expand Down Expand Up @@ -280,7 +285,7 @@ private class ClientInfoProcessor : ResultProcessor<ClientInfo[]>
{
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
{
switch(result.Type)
switch(result.Resp2TypeBulkString)
{
case ResultType.BulkString:
var raw = result.GetString();
Expand Down
4 changes: 2 additions & 2 deletions src/StackExchange.Redis/CommandTrace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ private class CommandTraceProcessor : ResultProcessor<CommandTrace[]>
{
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
{
switch(result.Type)
switch(result.Resp2TypeArray)
{
case ResultType.MultiBulk:
case ResultType.Array:
var parts = result.GetItems();
CommandTrace[] arr = new CommandTrace[parts.Length];
int i = 0;
Expand Down
Loading
Loading