Skip to content

Commit 981ba52

Browse files
authored
AMR: use RESP3 and disable the default config channel by default (#3067)
* AMR: use RESP3 and disable the default config channel by default * release notes * support TLS (self-signed) in the test server, use for the AMR test * don't do the AMR post-connection callback * fix test break in Issue883 due to lazy provider lookup * improved release notes * verify config override works as expected * spin up pub/sub on demand (or if pub/sub is active) * build warning cleanup * logging * breakfix * format * AMR test for RESP2
1 parent 1dbba36 commit 981ba52

16 files changed

Lines changed: 377 additions & 39 deletions

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
4444
</ItemGroup>
4545

46-
<ItemGroup Condition="'$(MSBuildProjectName)' != 'Build' and '$(MSBuildProjectName)' != 'StackExchange.Redis.Build'">
46+
<ItemGroup Condition="'$(MSBuildProjectName)' != 'Build' and '$(MSBuildProjectName)' != 'StackExchange.Redis.Build' and '$(MSBuildProjectName)' != 'docker' and '$(MSBuildProjectName)' != 'docs' and '$(MSBuildProjectName)' != '.github'">
4747
<!-- for everything except the build-tools project itself and the Build.csproj wrapper: use the build project as an analyzer -->
4848
<ProjectReference Include="..\..\eng\StackExchange.Redis.Build\StackExchange.Redis.Build.csproj"
4949
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

docs/ReleaseNotes.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ Current package versions:
88

99
## Unreleased
1010

11-
- (none)
11+
- Prefer RESP3 and avoid opening a separate subscription connection for Azure Managed Redis endpoints ([#3067 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3067))
1212

1313
## 2.12.27
1414

15+
- Recognize Azure Managed Redis (AMR) resources in new Azure clouds ([#3068 by @philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/3068))
1516
- Remove `[Experimental]` 8.8 `GCRA` feature ([#3074 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3074))
1617
- Detect server-mode correctly on Valkey 8+ instances ([#3050 by @wipiano](https://github.com/StackExchange/StackExchange.Redis/pull/3050))
1718
- Add Redis 8.8 stream negative acknowledgements (`XNACK`) ([#3058 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3058))
1819
- Add experimental `Aggregate.Count` support for sorted-set combination operations against Redis 8.8 ([#3059 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3059))
19-
- Support sub-key (hash field) notifications ([#3062 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3062))
20+
- Support Redis 8.8 sub-key (hash field) notifications ([#3062 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3062))
2021
- Add `ValueCondition` overloads for `SortedSetIncrement`/`SortedSetIncrementAsync`, supporting `ZADD INCR` with existence conditions ([#3071 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3071))
21-
- Recognize Azure Managed Redis (AMR) resources in new Azure clouds ([#3068 by @philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/3068))
2222

2323
## 2.12.14
2424

docs/Resp3.md

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,80 @@
22

33
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:
44

5-
1. RESP3 can carry out-of-band / "push" messages on a single connection, where-as RESP2 requires a separate connection for these messages
6-
2. RESP3 can (when appropriate) convey additional semantic meaning about returned payloads inside the same result structure
7-
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
5+
1. RESP3 can carry out-of-band / "push" messages on a single connection, where-as RESP2 requires a separate connection for out-of-band (pub/sub) messages
6+
- this single connection can be of huge benefit in high-usage servers, as it halves the number of connections required
7+
2. RESP3 supports *additional* out-of-band messages that cannot be expressed in RESP2, which allows advanced features such as "smart client handoffs" (a family of
8+
server maintenance notifications)
9+
- these features (not yet implemented in SE.Redis) allow for greater stability in complex deployments
10+
3. RESP3 can (when appropriate) convey additional semantic meaning about returned payloads inside the same result structure
11+
- this is *mostly* relevant to client libraries that do not explicitly interpret the results before exposing to the user, so this does not directly impact SE.Redis itself,
12+
but it is relevant to consumers of SE.Redis that use Lua scripts or ad-hoc commands
813

9-
For most people, #1 is the main reason to consider RESP3, as in high-usage servers - this can halve the number of connections required.
10-
This is particularly useful in hosted environments where the number of inbound connections to the server is capped as part of a service plan.
11-
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
12-
(for example, to receive server maintenance notifications) *without* incurring any additional connection overhead.
14+
For many users, using RESP3 is a "no-brainer" - it offers significant benefits with no real downsides. However, there are some important things to be aware of, and some
15+
migration work that may be required. In particular, some commands *return different result structures* in RESP3 mode; for example a jagged (nested) array might become a "map"
16+
(essentially an interleaved flat array). SE.Redis has been updated to handle these cases transparently, but if you are using `Execute[Async]` or `ScriptEvaluate[Async]` (or if
17+
you are using an additional library that issues ad-hoc commands or scripts on your behalf) you may need to update your processing code to compensate for this. This is discussed more below.
1318

14-
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
15-
via `ConfigurationOptions.Protocol` or by adding `,protocol=resp3` (or `,protocol=3`) to the configuration string.
19+
# Enabling RESP3
1620

17-
---
21+
RESP2 and RESP3 are both supported options (if the server does not support RESP3, RESP2 will always be used). To make full use of the benefits of RESP3,
22+
the library is moving in the direction of *preferring* RESP3. The default behaviour is:
1823

19-
#3 is a critical one - the library *should* already handle all documented commands that have revised results in RESP3, but if you're using
20-
`Execute[Async]` to issue ad-hoc commands, you may need to update your processing code to compensate for this, ideally using detection to handle
21-
*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
22-
this should not usually present a difficulty.
24+
| Library version | Endpoint | Default protocol
25+
|-------------------------|-----------------------------------------------------------------|-
26+
| &lt; 2.13 | (any) | RESP2
27+
| &gt;= 2.13 and &lt; 3.0 | (non-AMR) | RESP2
28+
| &gt;= 2.13 and &lt; 3.0 | [AMR](https://azure.microsoft.com/products/managed-redis) | RESP3
29+
| &gt; 3.0<sup>†</sup> | (any) | RESP3
2330

24-
The minor (#2) and major (#3) differences to results are only visible to your code when using:
31+
<sup>†</sup> = planned
32+
33+
You can override this behaviour by setting the `protocol` option in the connection string, or by setting the `ConfigurationOptions.Protocol` property:
34+
35+
```csharp
36+
var options = ConfigurationOptions.Parse("someserver");
37+
options.Protocol = RedisProtocol.Resp3; // or .Resp2
38+
var muxer = await ConnectionMultiplexer.ConnectAsync(options);
39+
```
40+
41+
or
42+
43+
```csharp
44+
var options = ConfigurationOptions.Parse("someserver,protocol=resp3"); // or =resp2
45+
var muxer = await ConnectionMultiplexer.ConnectAsync(options);
46+
```
47+
48+
You can use this configuration to *explicitly enable* RESP3 on earlier library versions, or to *explicitly disable* RESP3 on later versions, if you encounter issues.
49+
50+
# Handling RESP3
51+
52+
For most users, *no additional work will be required*, or the additional work may be limited to updating libraries; for example, For example, [NRedisStack](https://www.nuget.org/packages/NRedisStack/)
53+
now fully supports RESP3 for the commands it exposes (search, json, time-series, etc).
54+
55+
Scenarios impacted by RESP3 include:
2556

2657
- Lua scripts invoked via the `ScriptEvaluate[Async](...)` or related APIs, that either:
2758
- Uses the `redis.setresp(3)` API and returns a value from `redis.[p]call(...)`
2859
- 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)
29-
- Ad-hoc commands (in particular: *modules*) that are invoked via the `Execute[Async](string command, ...)` API
60+
- Ad-hoc commands that are invoked via the `Execute[Async](string command, ...)` API
61+
62+
This delta is *especially* pronounced for some of the "modules" in Redis, even those that now ship by default in OSS Redis, including:
63+
- "search" (`FT.SEARCH`, `FT.AGGREGATE`, etc.)
64+
- "time-series" (`TS.RANGE`, etc.)
65+
- "json" (`JSON.NUMINCRBY`, etc.)
66+
67+
Note that NRedisStack wraps most of these common modules, and has been updated to understand RESP3; if you are using these modules via NRedisStack, you should update to the latest version; if
68+
you are using these modules via ad-hoc commands, you may need to update your processing code to compensate for this, or consider using NRedisStack instead, which will handle the RESP3 conversion for you.
3069

31-
...both which return `RedisResult`. **If you are not using these APIs, you should not need to do anything additional.**
70+
This leaves a small category of users who are currently using the `RedisResult` type directly (via `Execute[Async](...)` or `ScriptEvaluate[Async](...)`).
3271

33-
Historically, you could use the `RedisResult.Type` property to query the type of data returned (integer, string, etc). In particular:
72+
## Impact on RedisResult
73+
74+
Firstly, note that it is possible that the *structure* of the data changes between RESP2 and RESP3; for example, a jagged array might become a map, or a single string value might become an array. You will
75+
need to identify these changes (typically via integration tests) and update your code accordingly, ideally with detection code to handle *either* structure so that the same code works in both REP2 and RESP3.
76+
77+
This is usually combined by using the `RedisResult.Resp3Type` property to query the type of data returned (integer, string, etc). Historically, you could use the `RedisResult.Type` property to query the type of data returned (integer, string, etc).
78+
With RESP3, this is extended:
3479

3580
- Two new properties are added: `RedisResult.Resp2Type` and `RedisResult.Resp3Type`
3681
- 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)
@@ -42,4 +87,7 @@ Possible changes required due to RESP3:
4287

4388
1. To prevent build warnings, replace usage of `ResultType.MultiBulk` with `ResultType.Array`, and usage of `RedisResult.Type` with `RedisResult.Resp2Type`
4489
2. If you wish to exploit the additional semantic data when enabling RESP3, use `RedisResult.Resp3Type` where appropriate
45-
3. If you are enabling RESP3, you must verify whether the commands you are using can give different result shapes on RESP3 connections
90+
3. If you are enabling RESP3, you must verify whether the commands you are using can give different result shapes on RESP3 connections
91+
92+
An example of the types of changes required may be seen in the [NRedisStack #471](https://github.com/redis/NRedisStack/pull/471) pull-request, which updates result processing for multiple modules
93+
(and changes the integration tests to run on RESP2 and RESP3 separately).

src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Net;
33
using System.Threading.Tasks;
4-
using StackExchange.Redis.Maintenance;
54

65
namespace StackExchange.Redis.Configuration
76
{
@@ -56,9 +55,15 @@ private bool IsHostInDomains(string hostName, string[] domains)
5655

5756
/// <inheritdoc/>
5857
public override Task AfterConnectAsync(ConnectionMultiplexer muxer, Action<string> log)
59-
=> AzureMaintenanceEvent.AddListenerAsync(muxer, log);
58+
=> Task.CompletedTask;
6059

6160
/// <inheritdoc/>
6261
public override bool GetDefaultSsl(EndPointCollection endPoints) => true;
62+
63+
/// <inheritdoc/>
64+
public override RedisProtocol? Protocol => RedisProtocol.Resp3; // prefer RESP3 on AMR
65+
66+
/// <inheritdoc/>
67+
public override string ConfigurationChannel => ""; // disable on AMR
6368
}
6469
}

src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,11 @@ protected virtual string GetDefaultClientName() =>
259259
/// </summary>
260260
public virtual bool SetClientLibrary => true;
261261

262+
/// <summary>
263+
/// Gets the preferred protocol to use for the connection.
264+
/// </summary>
265+
public virtual RedisProtocol? Protocol => null;
266+
262267
/// <summary>
263268
/// Tries to get the RoleInstance Id if Microsoft.WindowsAzure.ServiceRuntime is loaded.
264269
/// In case of any failure, swallows the exception and returns null.

src/StackExchange.Redis/ConfigurationOptions.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,13 +1169,18 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown)
11691169
/// <summary>
11701170
/// Specify the redis protocol type.
11711171
/// </summary>
1172-
public RedisProtocol? Protocol { get; set; }
1172+
public RedisProtocol? Protocol
1173+
{
1174+
get => field ?? Defaults.Protocol;
1175+
set;
1176+
}
11731177

11741178
internal bool TryResp3()
11751179
{
1180+
var protocol = Protocol;
11761181
// note: deliberately leaving the IsAvailable duplicated to use short-circuit
11771182

1178-
// if (Protocol is null)
1183+
// if (protocol is null)
11791184
// {
11801185
// // if not specified, lean on the server version and whether HELLO is available
11811186
// return new RedisFeatures(DefaultVersion).Resp3 && CommandMap.IsAvailable(RedisCommand.HELLO);
@@ -1187,7 +1192,7 @@ internal bool TryResp3()
11871192
// edge case in the library itself, the break is still visible to external callers via Execute[Async]; with an
11881193
// abundance of caution, we are therefore making RESP3 explicit opt-in only for now; we may revisit this in a major
11891194
{
1190-
return Protocol.GetValueOrDefault() >= RedisProtocol.Resp3 && CommandMap.IsAvailable(RedisCommand.HELLO);
1195+
return protocol.GetValueOrDefault() >= RedisProtocol.Resp3 && CommandMap.IsAvailable(RedisCommand.HELLO);
11911196
}
11921197
}
11931198

src/StackExchange.Redis/ConnectionMultiplexer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,9 +1370,11 @@ internal void GetStatus(ILogger? log)
13701370

13711371
private void ActivateAllServers(ILogger? log)
13721372
{
1373+
// bool hasSubscriptions = GetSubscriptionsCount() != 0;
13731374
foreach (var server in GetServerSnapshot())
13741375
{
13751376
server.Activate(ConnectionType.Interactive, log);
1377+
// if (hasSubscriptions && server.SupportsSubscriptions && !server.KnowOrAssumeResp3())
13761378
if (server.SupportsSubscriptions && !server.KnowOrAssumeResp3())
13771379
{
13781380
// Intentionally not logging the sub connection
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
#nullable enable
2+
override StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider.ConfigurationChannel.get -> string!
3+
override StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider.Protocol.get -> StackExchange.Redis.RedisProtocol?
4+
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Protocol.get -> StackExchange.Redis.RedisProtocol?

src/StackExchange.Redis/ServerEndPoint.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ public int Databases
107107
public bool IsConnecting => interactive?.IsConnecting == true;
108108
public bool IsConnected => interactive?.IsConnected == true;
109109
public bool IsSubscriberConnected => KnowOrAssumeResp3() ? IsConnected : subscription?.IsConnected == true;
110-
111110
public bool KnowOrAssumeResp3()
112111
{
113112
var protocol = interactive?.Protocol;
@@ -627,7 +626,7 @@ internal bool IsSelectable(RedisCommand command, bool allowDisconnected = false)
627626
{
628627
// Until we've connected at least once, we're going to have a DidNotRespond unselectable reason present
629628
var bridge = unselectableReasons == 0 || (allowDisconnected && unselectableReasons == UnselectableFlags.DidNotRespond)
630-
? GetBridge(command, false)
629+
? GetBridge(command, true)
631630
: null;
632631

633632
return bridge != null && (allowDisconnected || bridge.IsConnected);

tests/StackExchange.Redis.Tests/ConfigTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,26 @@ public void ConfigurationOptionsDefaultForAzureManagedRedis(string hostAndPort,
166166
Assert.Equal(sslShouldBeEnabled, options.Ssl);
167167
}
168168

169+
[Theory]
170+
// azure managed redis, no overrides
171+
[InlineData("contoso.redis.azure.net:10000", RedisProtocol.Resp3, true)] // default
172+
[InlineData("contoso.redis.azure.net:10000,protocol=resp2", RedisProtocol.Resp2, false)] // opt-out
173+
[InlineData("contoso.redis.azure.net:10000,protocol=resp3", RedisProtocol.Resp3, true)] // opt-in
174+
// azure redis cache, no overrides (we expect this to change in v3)
175+
[InlineData("contoso.redis.cache.windows.net:6380", null, false)] // default
176+
[InlineData("contoso.redis.cache.windows.net:6380,protocol=resp2", RedisProtocol.Resp2, false)] // opt-out
177+
[InlineData("contoso.redis.cache.windows.net:6380,protocol=resp3", RedisProtocol.Resp3, true)] // opt-in
178+
// arbitrary endpoint (we expect this to change in v3)
179+
[InlineData("myserver:6379", null, false)] // default
180+
[InlineData("myserver:6379,protocol=resp2", RedisProtocol.Resp2, false)] // opt-out
181+
[InlineData("myserver:6379,protocol=resp3", RedisProtocol.Resp3, true)] // opt-in
182+
public void CorrectRespProtocol(string config, RedisProtocol? expected, bool useResp3)
183+
{
184+
var options = ConfigurationOptions.Parse(config);
185+
Assert.Equal(expected, options.Protocol);
186+
Assert.Equal(useResp3, options.TryResp3());
187+
}
188+
169189
[Fact]
170190
public void ConfigurationOptionsForAzureWhenSpecified()
171191
{

0 commit comments

Comments
 (0)