From 73f1b4823c28a57dbd7645abef819b4f775c5aa2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 18 Sep 2026 11:09:49 +0100 Subject: [PATCH 1/3] Reinstate auto-enlistment for the hosted providers 3.3.0 shipped maintenance notifications purely opt-in, with the provider overrides commented out rather than deleted so that what came back would be exactly what was reviewed. This is that, pending the thumbs up. The three overrides return in their original form: Redis Cloud, Azure Managed Redis and Redis Enterprise select Auto, so a connection string naming nothing but a recognized hostname gets the feature. Auto rather than Enabled throughout, because a deployment that cannot deliver has to keep working - the opt-in is refused and the feature stays off, rather than the connection being rejected. The library default stays Disabled: OSS Redis, Valkey and Garnet do not know the subcommand, and an unsolicited error on every connection would be a poor first impression. Docs follow the behaviour back: - ServerMaintenanceEvent's "do I need to configure anything?" returns to "usually not", with the two-column now/will-be table collapsed and the temporary notice dropped. Kept the `defaults=` table row that was added while the feature was opt-in, since it is useful either way. - Configuration.md and exp/SER010.md lose their "purely opt-in" paragraphs. - DefaultOptionsTests.MaintenanceNotificationDefaultPerProvider flips its first two rows to Auto, which is what it was written to do. The last two rows stay Disabled, and they are what stops the theory passing merely because every host returns the same answer. MaintenanceOptInClientTests.DefaultIsOff needed no change: it connects to the in-process server on loopback, which no provider recognizes, so Disabled is still the right answer there. The three API entries go back into Unshipped; eng/public-api.py --check agrees the file is sorted. Also preserved each source file's existing BOM state - RedisEnterpriseOptionsProvider.cs has none, and writing one in would have put an unrelated first-line change in the diff. --- docs/Configuration.md | 9 ++---- docs/ServerMaintenanceEvent.md | 31 ++++++++----------- docs/exp/SER010.md | 6 ---- .../AzureManagedRedisOptionsProvider.cs | 23 +++++++------- .../RedisCloudOptionsProvider.cs | 21 +++++++------ .../RedisEnterpriseOptionsProvider.cs | 25 +++++++-------- .../PublicAPI/PublicAPI.Unshipped.txt | 3 ++ .../DefaultOptionsTests.cs | 14 ++++----- 8 files changed, 59 insertions(+), 73 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 9297386a86..b86f2148de 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -368,13 +368,8 @@ broken connection. This requires RESP3, and the client asks for it per connectio | `enabled` | **Required**: ask, and reject the connection unless notifications are live. Only point this at a deployment you know supports them. | Note that `enabled` means *required*, not merely *on* - it is the cross-client name for that mode, and it will -refuse a connection that cannot deliver notifications, including any RESP2 connection. - -**For now this is purely opt-in: you must set `maintNotifications` yourself.** The `amr`, `rediscloud` and -`enterprise` providers are intended to select `auto` for you, so that on those deployments nothing needs -setting - but that auto-enlistment is held back until the feature has been through formal acceptance testing, -and is expected in a follow-up release. Naming a provider today sets its other defaults without turning -notifications on. +refuse a connection that cannot deliver notifications, including any RESP2 connection. The `amr`, `rediscloud` +and `enterprise` providers select `auto` for you, so on those deployments you need not set anything. While a disruption has been announced, command timeouts are relaxed - raised to `maintRelaxedTimeout`, never lowered, so a caller with a more generous timeout keeps it. The window ends when the server says the disruption diff --git a/docs/ServerMaintenanceEvent.md b/docs/ServerMaintenanceEvent.md index 91790bf46e..3a6ebf3807 100644 --- a/docs/ServerMaintenanceEvent.md +++ b/docs/ServerMaintenanceEvent.md @@ -29,34 +29,29 @@ Server-side you may also see it discussed as *maintenance mode*, *shard migratio ## Do I need to configure anything? -**Yes — for now.** In this release maintenance notifications are **purely opt-in**: nothing turns them on for you, whatever you connect to. You get them only by asking, with `maintNotifications=Auto` (or `Enabled`). +Usually not. If you connect using the hostname your provider gave you, the matching options provider recognizes it and turns the feature on for you. -> **This is temporary.** The intent is that the options providers enlist you automatically, so that connecting to a recognized Redis Cloud or Azure Managed Redis hostname turns the feature on without any configuration — exactly as the table below describes. That is held back only until the feature has been through formal acceptance testing, and is expected to land in a follow-up release. Until then, treat the "will be" column as a statement of direction, not of current behaviour. - -| You connect to | Recognized as | Notifications now | Will be | -|---|---|---|---| -| `something.cloud.redislabs.com`, `.cloud.redis.io`, `.redislabs.com` | Redis Cloud | **off** unless asked | on (`Auto`) | -| `something.redis.azure.net`, `.redisenterprise.cache.azure.net` | Azure Managed Redis | **off** unless asked | on (`Auto`) | -| `defaults=enterprise`, `defaults=rediscloud`, `defaults=amr` | named explicitly | **off** unless asked | on (`Auto`) | -| your own hostname, a CNAME, private DNS, or through a proxy | nothing | **off** | **off** | -| a self-managed Redis Enterprise cluster | nothing (there is no DNS pattern to recognize) | **off** | **off** | +| You connect to | Recognized as | Notifications | +|---|---|---| +| `something.cloud.redislabs.com`, `.cloud.redis.io`, `.redislabs.com` | Redis Cloud | on (`Auto`) | +| `something.redis.azure.net`, `.redisenterprise.cache.azure.net` | Azure Managed Redis | on (`Auto`) | +| `defaults=enterprise`, `defaults=rediscloud`, `defaults=amr` | named explicitly | on (`Auto`) | +| your own hostname, a CNAME, private DNS, or through a proxy | nothing | **off** | +| a self-managed Redis Enterprise cluster | nothing (there is no DNS pattern to recognize) | **off** | -Nothing fails when the feature is off: the connection works normally and you simply never receive a notification. So to use it today, ask for it explicitly. Either: +The last two rows are the ones to know about, because nothing fails: the connection works normally and you simply never receive a notification. If your endpoint does not look like your provider's, say so explicitly. Either: ```csharp -// change nothing except this feature -var options = ConfigurationOptions.Parse("my-redis.internal.example.com:6379,maintNotifications=Auto"); +// the whole deployment posture: prefer RESP3, skip the OSS config-broadcast channel, and ask for notifications +var options = ConfigurationOptions.Parse("my-redis.internal.example.com:6379,defaults=enterprise"); ``` -or, to take the whole deployment posture as well - prefer RESP3 and skip the OSS config-broadcast channel: +or, to change nothing except this feature: ```csharp -var options = ConfigurationOptions.Parse( - "my-redis.internal.example.com:6379,defaults=enterprise,maintNotifications=Auto"); +var options = ConfigurationOptions.Parse("my-redis.internal.example.com:6379,maintNotifications=Auto"); ``` -Note that `maintNotifications` is needed in *both* forms for now: while the feature is opt-in, naming a provider sets that provider's other defaults but does not turn notifications on. Once auto-enlistment lands, `defaults=enterprise` alone will be enough. - `defaults=` accepts `rediscloud`, `enterprise`, `amr` and `azure`; see [Configuration](Configuration.md) for what each provider sets. It is also the right answer for a *hosted* deployment reached somewhere its own provider cannot see it, such as behind a CNAME or a private endpoint. ### RESP3 is required, and is already the default diff --git a/docs/exp/SER010.md b/docs/exp/SER010.md index 89dcbff70f..a09ccd2796 100644 --- a/docs/exp/SER010.md +++ b/docs/exp/SER010.md @@ -17,12 +17,6 @@ The feature is experimental here for three separate reasons: timeout relaxation, then endpoint handoff. Behaviour may therefore change materially between versions while the diagnostic is in place, even where the API does not. -**Currently purely opt-in.** Nothing enables this for you: the options providers for Redis Cloud, Azure -Managed Redis and Redis Enterprise are intended to select `Auto` so that a recognized endpoint needs no -configuration, but that auto-enlistment is held back until the feature has been through formal acceptance -testing, and is expected in a follow-up release. Until then the only thing that turns notifications on is -setting `maintNotifications` yourself. - `MaintenanceNotificationMode`: - `Disabled` (the default) - never ask; the server sends nothing. diff --git a/src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs b/src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs index 1f3198eda8..51e2caacfc 100644 --- a/src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs @@ -71,16 +71,17 @@ public override Task AfterConnectAsync(ConnectionMultiplexer muxer, Action public override string ConfigurationChannel => ""; // disable on AMR - // Auto-enlistment, deliberately withheld for now; maintenance notifications ship purely opt-in, and - // this goes back in a follow-up once the feature has been through formal acceptance testing. Kept here - // commented rather than deleted so that what returns is exactly what was reviewed. - // - // Pre-emptive: AMR does not emit these yet, and support is being added concurrently with this - // client-side work. Auto is what makes that safe - until the server side ships, the opt-in is refused - // and the feature stays off, and it then starts working without anybody needing to change a connection - // string. AMR also already prefers RESP3 here, which the feature requires. - // - // [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] - // public override MaintenanceNotificationMode MaintenanceNotifications => MaintenanceNotificationMode.Auto; + /// + /// Ask for maintenance notifications, tolerating a server that doesn't offer them. + /// + /// + /// Pre-emptive: AMR does not emit these yet, and support is being added concurrently with this + /// client-side work. is what makes that safe - until + /// the server side ships, the opt-in is refused and the feature stays off, and it then starts working + /// without anybody needing to change a connection string. AMR also already prefers RESP3 here, which + /// the feature requires. + /// + [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] + public override MaintenanceNotificationMode MaintenanceNotifications => MaintenanceNotificationMode.Auto; } } diff --git a/src/StackExchange.Redis/Configuration/RedisCloudOptionsProvider.cs b/src/StackExchange.Redis/Configuration/RedisCloudOptionsProvider.cs index 8787a578aa..fc00c4f9bd 100644 --- a/src/StackExchange.Redis/Configuration/RedisCloudOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/RedisCloudOptionsProvider.cs @@ -59,16 +59,17 @@ private static bool IsHostInDomains(string hostName, string[] domains) /// public override string ConfigurationChannel => ""; - // Auto-enlistment, deliberately withheld for now; maintenance notifications ship purely opt-in, and - // this goes back in a follow-up once the feature has been through formal acceptance testing. Kept here - // commented rather than deleted so that what returns is exactly what was reviewed. - // - // This is the deployment family the feature exists for, and Auto rather than Enabled because a - // database that has not been updated yet must keep working: the opt-in is then refused and the feature - // stays off, rather than the connection being rejected. - // - // [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] - // public override MaintenanceNotificationMode MaintenanceNotifications => MaintenanceNotificationMode.Auto; + /// + /// Ask for maintenance notifications, tolerating a server that doesn't offer them. + /// + /// + /// This is the deployment family the feature exists for. + /// rather than because a database that has not been + /// updated yet must keep working: the opt-in is then refused and the feature stays off, rather than the + /// connection being rejected. + /// + [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] + public override MaintenanceNotificationMode MaintenanceNotifications => MaintenanceNotificationMode.Auto; // Two things AzureManagedRedisOptionsProvider does that are deliberately *not* repeated here: // diff --git a/src/StackExchange.Redis/Configuration/RedisEnterpriseOptionsProvider.cs b/src/StackExchange.Redis/Configuration/RedisEnterpriseOptionsProvider.cs index aafe4c54d1..87634fbf58 100644 --- a/src/StackExchange.Redis/Configuration/RedisEnterpriseOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/RedisEnterpriseOptionsProvider.cs @@ -31,20 +31,17 @@ public class RedisEnterpriseOptionsProvider : DefaultOptionsProvider /// public override string ConfigurationChannel => ""; - // Auto-enlistment, deliberately withheld for now; maintenance notifications ship purely opt-in, and - // this goes back in a follow-up once the feature has been through formal acceptance testing. Kept here - // commented rather than deleted so that what returns is exactly what was reviewed. - // - // Note this provider is reached only by naming it (defaults=enterprise), never by endpoint matching, - // so restoring it enlists nobody who did not already ask for this deployment posture. It is withheld - // anyway, so that in this release the *only* thing that turns the feature on is maintNotifications. - // - // Auto rather than Enabled: a cluster that has not been updated yet, or has the feature switched off, - // must keep working. Choose Enabled explicitly if you would rather a connection be refused than run - // without advance warning. - // - // [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] - // public override MaintenanceNotificationMode MaintenanceNotifications => MaintenanceNotificationMode.Auto; + /// + /// Ask for maintenance notifications, tolerating a server that doesn't offer them. + /// + /// + /// rather than + /// : a cluster that has not been updated yet, or has + /// the feature switched off, must keep working. Choose Enabled explicitly if you would rather a + /// connection be refused than run without advance warning. + /// + [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] + public override MaintenanceNotificationMode MaintenanceNotifications => MaintenanceNotificationMode.Auto; // Note: no GetDefaultSsl and no DefaultVersion override. Both are deployment choices here rather than // properties of the product - TLS is configured per database, and the version is whatever was diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index ab058de62d..43eab028e8 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +[SER010]override StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode +[SER010]override StackExchange.Redis.Configuration.RedisCloudOptionsProvider.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode +[SER010]override StackExchange.Redis.Configuration.RedisEnterpriseOptionsProvider.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode diff --git a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs index 445027bd48..1f508f2625 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs @@ -197,17 +197,17 @@ public void RedisCloudDoesNotInheritTheAzureManagedAssumptions() } [Theory] - [InlineData("contoso.redis.azure.net", MaintenanceNotificationMode.Disabled)] // AMR: will be Auto - [InlineData("contoso.cloud.redislabs.com", MaintenanceNotificationMode.Disabled)] // Redis Cloud: will be Auto + [InlineData("contoso.redis.azure.net", MaintenanceNotificationMode.Auto)] // AMR + [InlineData("contoso.cloud.redislabs.com", MaintenanceNotificationMode.Auto)] // Redis Cloud [InlineData("contoso.redis.cache.windows.net", MaintenanceNotificationMode.Disabled)] // classic Azure: stays off [InlineData("contoso.example.com", MaintenanceNotificationMode.Disabled)] // and so does anything unrecognized public void MaintenanceNotificationDefaultPerProvider(string hostName, MaintenanceNotificationMode expected) { - // Every endpoint is Disabled for now: maintenance notifications ship purely opt-in, so *nothing* - // enlists you and the only thing that turns them on is setting maintNotifications yourself. The - // providers for AMR and Redis Cloud are intended to select Auto - see the commented-out overrides on - // each - and this theory is what flips back when auto-enlistment lands: the first two rows become - // Auto, and the last two stay Disabled because nothing recognizes them either way. + // The recognized hosted families enlist automatically, which is the whole point of the providers: a + // connection string that names nothing but the hostname gets the feature. Auto rather than Enabled so + // that a deployment which cannot deliver keeps working - the opt-in is refused and the feature stays + // off. The last two rows stay Disabled because nothing recognizes them either way, and they are what + // stops this test passing merely because everything returns the same answer. var epc = new EndPointCollection(new List() { new DnsEndPoint(hostName, 0) }); var provider = DefaultOptionsProvider.GetProvider(epc); Output.WriteLine($"{hostName} -> {provider.GetType().Name}"); From d177b257a46a1658273a33653cd49a74110e3e8c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 6 Oct 2026 16:25:33 +0100 Subject: [PATCH 2/3] Retire SER010: maintenance notifications are not experimental any more The hosted providers now enlist by default, so callers on Redis Cloud, Redis Enterprise and Azure Managed Redis get this behaviour without touching a gated member; keeping the knobs behind [Experimental] no longer made sense. Removed the attribute everywhere, the [SER010] prefix from the PublicAPI entries, and the suppression from NoWarn and the maintenance toys. SER010 is added to the reserved-IDs list in Experiments.cs so it is never recycled. docs/exp/SER010.md stays, since the help URL points at it, rewritten in the retired form. The Redis Enterprise cluster-flag prerequisite that only lived there moves into ServerMaintenanceEvent.md, which also loses its experimental note and its stale "Disabled everywhere, until auto-enlistment lands" wording. --- Directory.Build.props | 6 +- docs/ServerMaintenanceEvent.md | 8 +- docs/exp/SER010.md | 89 +++++++------- src/RESPite/Shared/Experiments.cs | 2 +- .../Availability/FaultContext.cs | 1 - .../AzureManagedRedisOptionsProvider.cs | 1 - .../Configuration/DefaultOptionsProvider.cs | 5 - .../RedisCloudOptionsProvider.cs | 1 - .../RedisEnterpriseOptionsProvider.cs | 1 - .../ConfigurationOptions.cs | 5 - .../Maintenance/ClusterSlotMigration.cs | 1 - .../Maintenance/MaintenanceFaultSurface.cs | 2 - .../MaintenanceNotificationType.cs | 1 - .../Maintenance/PushMaintenanceEvent.cs | 1 - .../MaintenanceEndpointType.cs | 1 - .../MaintenanceNotificationMode.cs | 1 - .../PublicAPI/PublicAPI.Shipped.txt | 110 +++++++++--------- .../PublicAPI/PublicAPI.Unshipped.txt | 6 +- .../ServerEndPoint.Maintenance.cs | 1 - toys/MaintenanceSoak/MaintenanceSoak.csproj | 7 +- toys/MaintenanceWatch/MaintenanceWatch.csproj | 2 - 21 files changed, 109 insertions(+), 143 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index fbb5c39f5e..55c0088feb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,9 +10,9 @@ true $(MSBuildThisFileDirectory)Shared.ruleset NETSDK1069 - - $(NoWarn);NU5105;NU1507;SER004;SER005;SER009;SER010 + + $(NoWarn);NU5105;NU1507;SER004;SER005;SER009 https://github.com/StackExchange/StackExchange.Redis/releases https://seredis.dev/ MIT diff --git a/docs/ServerMaintenanceEvent.md b/docs/ServerMaintenanceEvent.md index 3a6ebf3807..be3c4dbb15 100644 --- a/docs/ServerMaintenanceEvent.md +++ b/docs/ServerMaintenanceEvent.md @@ -13,8 +13,6 @@ If you are a Redis vendor and want to integrate support for ServerMaintenanceEve # Server-native maintenance notifications (Redis Enterprise and Redis Cloud) -> These APIs are experimental, behind diagnostic id `SER010`; see [SER010](exp/SER010.md). - Redis Enterprise and Redis Cloud can tell a client *directly* that a disruption is coming: a shard is migrating, a node is failing over, or the endpoint you are connected to is being replaced. These arrive as RESP3 push frames on the connection itself, and the client does not merely report them: it relaxes timeouts for the duration, re-reads the cluster topology when slots have moved, recovers sharded subscriptions that were stranded, and moves off an endpoint that is going away rather than waiting to be disconnected. ### What this feature is called @@ -72,8 +70,8 @@ options.MaintenanceNotifications = MaintenanceNotificationMode.Auto; | Mode | Meaning | |---|---| -| `Disabled` | never ask. **The default everywhere, for now** - see above | -| `Auto` | ask, and carry on if the server says no. What the providers will select once auto-enlistment lands | +| `Disabled` | never ask. The default unless a provider recognizes the endpoint - see above | +| `Auto` | ask, and carry on if the server says no. What the Redis Cloud, Redis Enterprise and Azure Managed Redis providers select | | `Enabled` | **require** them: if the server will not deliver them, or the connection ends up on RESP2, the connection is **rejected** | `Auto` is the right choice almost always: asking costs one command during the handshake, and a server that accepts and then never sends anything costs nothing at all. `Enabled` exists for the case where running without advance warning is worse than not running: it turns a silent absence into a startup failure, which also makes it a useful way to prove the feature is live in a staging environment. @@ -226,7 +224,7 @@ Alternatively set `MaintenanceNotifications = Enabled` in a test or staging envi So for now the connection succeeds *without* the feature and logs a warning saying so. Note this is the one case where `Enabled` does not reject the connection - it would otherwise be impossible to configure an explicit opt-in for a group at all. Expect this restriction to be lifted; if you are relying on maintenance notifications today, use a single-group connection. -Redis Enterprise and Redis Cloud send them, subject to the feature being enabled on the cluster. Azure Managed Redis is configured to ask for them ahead of its own rollout, so the setting is harmless until their servers begin emitting. Redis Open Source, Valkey and other servers do not send them at all, and the setting is simply inert there: the opt-in is refused and the client carries on. +Redis Enterprise and Redis Cloud send them, subject to the feature being enabled on the cluster. On Redis Enterprise there are *two* switches, and they are easy to conflate: a **cluster-level flag** decides whether the subcommand exists at all (`client_maint_notifications` for proxy-routed databases, `oss_cluster_client_maint_notifications` for `oss_cluster` ones), and the per-connection opt-in that `maintNotifications` controls asks one connection to receive them. With the flag off, `CLIENT MAINT_NOTIFICATIONS ON` is refused, which `Auto` absorbs silently and `Enabled` turns into a refused connection - so if `Enabled` rejects connections to a deployment you believe supports the feature, check the cluster flag before suspecting the client. Azure Managed Redis is configured to ask for them ahead of its own rollout, so the setting is harmless until their servers begin emitting. Redis Open Source, Valkey and other servers do not send them at all, and the setting is simply inert there: the opt-in is refused and the client carries on. # Azure Cache for Redis maintenance events (pub/sub) diff --git a/docs/exp/SER010.md b/docs/exp/SER010.md index a09ccd2796..9f2f9dd7d2 100644 --- a/docs/exp/SER010.md +++ b/docs/exp/SER010.md @@ -1,51 +1,39 @@ -Maintenance notifications ("smart client handoffs") are a server feature by which a server warns a -connected client in advance of a disruptive event - a shard migration, a failover, or the endpoint -itself being replaced - so the client can act before the disruption rather than react to a broken -connection afterwards. The client opts in per connection with `CLIENT MAINT_NOTIFICATIONS ON`, and the -notifications then arrive as RESP3 push frames on the connection that carries commands. - -The feature is experimental here for three separate reasons: - -1. **The server side is not universal.** Only Redis Enterprise and Redis Cloud emit these - notifications today; OSS Redis, Valkey and Garnet do not recognize the opt-in at all. That is why - `MaintenanceNotificationMode.Auto` exists, and why the default is `Disabled`. -2. **The wire contract is still evolving.** Notification types have been proposed and withdrawn during - its development, and the payloads are described in prose rather than pinned down by a formal - specification. Our parsing is validated against frames captured from live deployments and is - deliberately liberal about what it accepts, but the shapes may still change. -3. **What the client *does* in response is the substantial part**, and it is being built in stages - - timeout relaxation, then endpoint handoff. Behaviour may therefore change materially between - versions while the diagnostic is in place, even where the API does not. - -`MaintenanceNotificationMode`: - -- `Disabled` (the default) - never ask; the server sends nothing. -- `Enabled` - **required**: ask, and *reject the connection* unless notifications end up live. That covers a - server that refuses, a server that answers `HELLO 3` as RESP2, and a configuration that could never ask in - the first place (`Protocol = Resp2`, or `HELLO` unavailable) - requiring a RESP3-only feature over RESP2 is - a contradiction, and it fails rather than being half-honoured. Only point this at a deployment you know - supports them. -- `Auto` - ask, and carry on if the server refuses or the connection ends up RESP2; the feature is then - simply off for that server and the connection is never rejected over it. Safe against a mixture of - servers, or one whose support you don't know, and what most callers want. - -The names are the cross-client ones (they match go-redis and redis-py, so a connection string ports -between clients), which makes `Enabled` easy to misread as "on" - it means "required". One deliberate -divergence: the cross-client spec only calls for interrupting the connection when the *server* errors, -whereas we extend that to the RESP2 cases above, on the grounds that a required feature which cannot -possibly be delivered is the same failure either way. - -## A deployment prerequisite worth knowing - -On Redis Enterprise there are *two* switches, and they are easy to conflate. A **cluster-level flag** decides -whether the subcommand exists at all - `client_maint_notifications` for proxy-routed databases, -`oss_cluster_client_maint_notifications` for `oss_cluster` ones - and the **per-connection opt-in** this option -controls asks one connection to receive them. A cluster running a supporting version with the flag off will -refuse `CLIENT MAINT_NOTIFICATIONS ON`, which `Auto` absorbs silently and `Enabled` turns into a refused -connection. So if `Enabled` is rejecting connections against a deployment you believe supports the feature, -check the cluster flag before suspecting the client. - -If you accept the above, you can suppress this warning by adding the following to your `csproj` file: +# Maintenance notifications - SER010 retired + +**This diagnostic is no longer produced.** Server-native maintenance notifications ("smart client +handoffs") are no longer marked `[Experimental]`, so nothing emits `SER010` and no suppression is needed. +The page is kept because the diagnostic's help link pointed here, and older builds still carry it. + +If you have a suppression for `SER010` - in a `csproj`, a `.editorconfig`, or a `#pragma` - it is now +inert, and can be removed whenever it suits you. + +For how the feature works and how to configure it, see [ServerMaintenanceEvent](../ServerMaintenanceEvent.md). + +## What it covered + +Maintenance notifications are a server feature by which a server warns a connected client in advance of +a disruptive event - a shard migration, a failover, or the endpoint itself being replaced - so the client +can act before the disruption rather than react to a broken connection afterwards. The gated surface was: + +- the `MaintenanceNotificationMode` and `MaintenanceEndpointType` enums, and the + `MaintenanceNotifications`, `MaintenanceMovingEndpointType`, `MaintenanceRelaxedTimeout`, + `MaintenanceRelaxedWindowMax` and `MaintenancePostEventRelaxedDuration` options on + `ConfigurationOptions` and `DefaultOptionsProvider` +- `PushMaintenanceEvent`, `ClusterSlotMigration` and `MaintenanceNotificationType` +- the `MaintenanceType` property on `FaultContext`, `RedisConnectionException` and `RedisTimeoutException` + +## What it used to mean + +While the feature was experimental, it was flagged as subject to change for three reasons: + +1. **The server side is not universal.** Only Redis Enterprise and Redis Cloud emitted these + notifications; OSS Redis, Valkey and Garnet do not recognize the opt-in at all. +2. **The wire contract was still evolving.** Notification types were proposed and withdrawn during its + development, and the payloads are described in prose rather than by a formal specification. +3. **What the client *does* in response was being built in stages** - timeout relaxation, then endpoint + handoff - so behaviour could change materially between versions even where the API did not. + +Acknowledging that was what the suppression meant: ```xml $(NoWarn);SER010 @@ -56,3 +44,8 @@ or more granularly / locally in C#: ``` c# #pragma warning disable SER010 ``` + +## Reserved, not recycled + +`SER010` is not reused for anything else, so an old suppression cannot silently hide an unrelated +warning. The same applies to the other retired identifiers - see `Experiments.cs`. diff --git a/src/RESPite/Shared/Experiments.cs b/src/RESPite/Shared/Experiments.cs index 40fc397e08..93fd067f8e 100644 --- a/src/RESPite/Shared/Experiments.cs +++ b/src/RESPite/Shared/Experiments.cs @@ -17,12 +17,12 @@ internal static class Experiments // SER006 = Server_8_8 (Redis 8.8 features) // SER007 = GeoRedundantFailover (multi-group connections, health checks, retry) // SER008 = Server_8_10 (Redis 8.10 features) + // SER010 = MaintenanceNotifications (server-native maintenance notifications / smart client handoffs) // ReSharper disable InconsistentNaming public const string Respite = "SER004"; public const string UnitTesting = "SER005"; public const string Transport = "SER009"; - public const string MaintenanceNotifications = "SER010"; // ReSharper restore InconsistentNaming diff --git a/src/StackExchange.Redis/Availability/FaultContext.cs b/src/StackExchange.Redis/Availability/FaultContext.cs index d139631471..3c43f10f8c 100644 --- a/src/StackExchange.Redis/Availability/FaultContext.cs +++ b/src/StackExchange.Redis/Availability/FaultContext.cs @@ -128,7 +128,6 @@ public FaultContext(Exception fault) /// was for. Left to policy rather than acted on here, since "ignore faults during maintenance" is a /// judgement about the deployment, not about the protocol. /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public Maintenance.MaintenanceNotificationType MaintenanceType { get; } private static bool IsKnownNotApplied(RedisErrorKind kind, CommandStatus status) diff --git a/src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs b/src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs index 51e2caacfc..1ab612bea7 100644 --- a/src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs @@ -81,7 +81,6 @@ public override Task AfterConnectAsync(ConnectionMultiplexer muxer, Action - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public override MaintenanceNotificationMode MaintenanceNotifications => MaintenanceNotificationMode.Auto; } } diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 055c3d23d0..c0cb836e08 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -362,7 +362,6 @@ protected virtual string GetDefaultClientName() => /// CLIENT subcommand is not guaranteed to be answered as politely as a server would. A provider /// that recognizes a deployment which supports the feature should override this. /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public virtual MaintenanceNotificationMode MaintenanceNotifications => MaintenanceNotificationMode.Disabled; /// @@ -381,14 +380,12 @@ protected virtual string GetDefaultClientName() => /// fall back to a bare opt-in; nothing observed so far needs it. /// /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public virtual MaintenanceEndpointType MaintenanceMovingEndpointType => MaintenanceEndpointType.Auto; /// /// Gets the value command timeouts are relaxed to during an announced disruption; 10 seconds, as the /// notification contract prescribes. /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public virtual TimeSpan MaintenanceRelaxedTimeout => TimeSpan.FromSeconds(10); /// @@ -402,7 +399,6 @@ protected virtual string GetDefaultClientName() => /// there is ample room for a legitimate window while a stuck one is bounded to half a minute. Return a /// value here to pin it regardless of the relaxed timeout. /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public virtual TimeSpan? MaintenanceRelaxedWindowMax => null; /// @@ -420,7 +416,6 @@ protected virtual string GetDefaultClientName() => /// to for that behaviour. /// /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public virtual TimeSpan? MaintenancePostEventRelaxedDuration => null; /// diff --git a/src/StackExchange.Redis/Configuration/RedisCloudOptionsProvider.cs b/src/StackExchange.Redis/Configuration/RedisCloudOptionsProvider.cs index fc00c4f9bd..59f24df0cb 100644 --- a/src/StackExchange.Redis/Configuration/RedisCloudOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/RedisCloudOptionsProvider.cs @@ -68,7 +68,6 @@ private static bool IsHostInDomains(string hostName, string[] domains) /// updated yet must keep working: the opt-in is then refused and the feature stays off, rather than the /// connection being rejected. /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public override MaintenanceNotificationMode MaintenanceNotifications => MaintenanceNotificationMode.Auto; // Two things AzureManagedRedisOptionsProvider does that are deliberately *not* repeated here: diff --git a/src/StackExchange.Redis/Configuration/RedisEnterpriseOptionsProvider.cs b/src/StackExchange.Redis/Configuration/RedisEnterpriseOptionsProvider.cs index 87634fbf58..fb1f47cafe 100644 --- a/src/StackExchange.Redis/Configuration/RedisEnterpriseOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/RedisEnterpriseOptionsProvider.cs @@ -40,7 +40,6 @@ public class RedisEnterpriseOptionsProvider : DefaultOptionsProvider /// the feature switched off, must keep working. Choose Enabled explicitly if you would rather a /// connection be refused than run without advance warning. /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public override MaintenanceNotificationMode MaintenanceNotifications => MaintenanceNotificationMode.Auto; // Note: no GetDefaultSsl and no DefaultVersion override. Both are deployment choices here rather than diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 5b75fd4ed6..1dae28c9ac 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -1599,7 +1599,6 @@ public RedisProtocol? Protocol /// configure for a group. Expect the restriction to be lifted. /// /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public MaintenanceNotificationMode MaintenanceNotifications { get => HasValue(OptionFlags.MaintenanceNotificationsHasValue) ? _maintenanceNotifications : Defaults.MaintenanceNotifications; @@ -1616,7 +1615,6 @@ public MaintenanceNotificationMode MaintenanceNotifications /// a client that wants a named replacement should ask for one. Prefer an FQDN form under TLS: an address /// cannot be validated against a certificate carrying only DNS names. /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public MaintenanceEndpointType MaintenanceMovingEndpointType { get => HasValue(OptionFlags.MaintenanceMovingEndpointTypeHasValue) @@ -1636,7 +1634,6 @@ public MaintenanceEndpointType MaintenanceMovingEndpointType /// relaxed; keep-alive, the heartbeat and connection-failure detection are deliberately untouched, so /// a server that dies mid-maintenance is still noticed on the usual schedule. /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public TimeSpan MaintenanceRelaxedTimeout { get => HasValue(OptionFlags.MaintenanceRelaxedTimeoutHasValue) ? _maintenanceRelaxedTimeout : Defaults.MaintenanceRelaxedTimeout; @@ -1652,7 +1649,6 @@ public TimeSpan MaintenanceRelaxedTimeout /// closes early, since relaxation delays the point at which a genuinely slow server surfaces as a /// timeout. Expressed in seconds in a configuration string. /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public TimeSpan MaintenanceRelaxedWindowMax { get => HasValue(OptionFlags.MaintenanceRelaxedWindowMaxHasValue) @@ -1672,7 +1668,6 @@ public TimeSpan MaintenanceRelaxedWindowMax /// : in that case nothing told us the event finished, and /// extending past the backstop would defeat it. Expressed in seconds in a configuration string. /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public TimeSpan MaintenancePostEventRelaxedDuration { get => HasValue(OptionFlags.MaintenancePostEventRelaxedDurationHasValue) diff --git a/src/StackExchange.Redis/Maintenance/ClusterSlotMigration.cs b/src/StackExchange.Redis/Maintenance/ClusterSlotMigration.cs index 7a9b7d950e..39f0c841fe 100644 --- a/src/StackExchange.Redis/Maintenance/ClusterSlotMigration.cs +++ b/src/StackExchange.Redis/Maintenance/ClusterSlotMigration.cs @@ -14,7 +14,6 @@ namespace StackExchange.Redis.Maintenance; /// that sent it - every node in the cluster reports the same movements, so the sender is not implicitly the /// source. /// -[Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public readonly struct ClusterSlotMigration { internal ClusterSlotMigration(EndPoint? source, EndPoint? target, IReadOnlyList slots, string? raw) diff --git a/src/StackExchange.Redis/Maintenance/MaintenanceFaultSurface.cs b/src/StackExchange.Redis/Maintenance/MaintenanceFaultSurface.cs index fbcae61498..e7bb2044e8 100644 --- a/src/StackExchange.Redis/Maintenance/MaintenanceFaultSurface.cs +++ b/src/StackExchange.Redis/Maintenance/MaintenanceFaultSurface.cs @@ -34,7 +34,6 @@ public sealed partial class RedisTimeoutException /// occasionally naming a window that a *different*, later command merely followed. /// /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public MaintenanceNotificationType MaintenanceType { get; internal init; } } @@ -50,6 +49,5 @@ public sealed partial class RedisConnectionException /// its deadline surfaces either way round: as a timeout when the command was already in flight, and as a /// connection fault when the endpoint went away underneath it. /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public MaintenanceNotificationType MaintenanceType { get; internal init; } } diff --git a/src/StackExchange.Redis/Maintenance/MaintenanceNotificationType.cs b/src/StackExchange.Redis/Maintenance/MaintenanceNotificationType.cs index ae66e4ce2d..7ae80b57f2 100644 --- a/src/StackExchange.Redis/Maintenance/MaintenanceNotificationType.cs +++ b/src/StackExchange.Redis/Maintenance/MaintenanceNotificationType.cs @@ -12,7 +12,6 @@ namespace StackExchange.Redis.Maintenance; /// ). Unrecognized types are dropped rather than surfaced, so no member here means /// "something we didn't understand". /// -[Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public enum MaintenanceNotificationType { /// diff --git a/src/StackExchange.Redis/Maintenance/PushMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/PushMaintenanceEvent.cs index 67d2ac955f..28bdf1a0b7 100644 --- a/src/StackExchange.Redis/Maintenance/PushMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/PushMaintenanceEvent.cs @@ -24,7 +24,6 @@ namespace StackExchange.Redis.Maintenance; /// what to do with it. /// /// -[Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public sealed class PushMaintenanceEvent : ServerMaintenanceEvent { internal PushMaintenanceEvent( diff --git a/src/StackExchange.Redis/MaintenanceEndpointType.cs b/src/StackExchange.Redis/MaintenanceEndpointType.cs index 8f0639e1c4..15e1bfa917 100644 --- a/src/StackExchange.Redis/MaintenanceEndpointType.cs +++ b/src/StackExchange.Redis/MaintenanceEndpointType.cs @@ -21,7 +21,6 @@ namespace StackExchange.Redis; /// connecting with TLS. /// /// -[Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public enum MaintenanceEndpointType { /// diff --git a/src/StackExchange.Redis/MaintenanceNotificationMode.cs b/src/StackExchange.Redis/MaintenanceNotificationMode.cs index fc7c156232..5d08de2754 100644 --- a/src/StackExchange.Redis/MaintenanceNotificationMode.cs +++ b/src/StackExchange.Redis/MaintenanceNotificationMode.cs @@ -19,7 +19,6 @@ namespace StackExchange.Redis; /// this is the one place they are easy to misread. /// /// -[Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] public enum MaintenanceNotificationMode { /// diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 8920089dea..dca665b06b 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -102,7 +102,7 @@ StackExchange.Redis.Availability.FaultContext.FaultContext() -> void StackExchange.Redis.Availability.FaultContext.FaultContext(System.Exception! fault) -> void StackExchange.Redis.Availability.FaultContext.Flags.get -> StackExchange.Redis.CommandFlags StackExchange.Redis.Availability.FaultContext.IsFault.get -> bool -[SER010]StackExchange.Redis.Availability.FaultContext.MaintenanceType.get -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.Availability.FaultContext.MaintenanceType.get -> StackExchange.Redis.Maintenance.MaintenanceNotificationType StackExchange.Redis.Availability.FaultContext.NotApplied.get -> bool StackExchange.Redis.Availability.GroupConnectionChangedEventArgs StackExchange.Redis.Availability.GroupConnectionChangedEventArgs.ChangeType @@ -584,11 +584,11 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.KeepAliveInterv virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.LibraryName.get -> string! static StackExchange.Redis.Configuration.DefaultOptionsProvider.LibraryVersion.get -> string! virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.LoggerFactory.get -> Microsoft.Extensions.Logging.ILoggerFactory? -[SER010]virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.MaintenanceMovingEndpointType.get -> StackExchange.Redis.MaintenanceEndpointType -[SER010]virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode -[SER010]virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.MaintenancePostEventRelaxedDuration.get -> System.TimeSpan? -[SER010]virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.MaintenanceRelaxedTimeout.get -> System.TimeSpan -[SER010]virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.MaintenanceRelaxedWindowMax.get -> System.TimeSpan? +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.MaintenanceMovingEndpointType.get -> StackExchange.Redis.MaintenanceEndpointType +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.MaintenancePostEventRelaxedDuration.get -> System.TimeSpan? +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.MaintenanceRelaxedTimeout.get -> System.TimeSpan +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.MaintenanceRelaxedWindowMax.get -> System.TimeSpan? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Name.get -> string? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Password.get -> string? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Protocol.get -> StackExchange.Redis.RedisProtocol? @@ -731,16 +731,16 @@ StackExchange.Redis.ConfigurationOptions.LibraryName.get -> string? StackExchange.Redis.ConfigurationOptions.LibraryName.set -> void StackExchange.Redis.ConfigurationOptions.LoggerFactory.get -> Microsoft.Extensions.Logging.ILoggerFactory? StackExchange.Redis.ConfigurationOptions.LoggerFactory.set -> void -[SER010]StackExchange.Redis.ConfigurationOptions.MaintenanceMovingEndpointType.get -> StackExchange.Redis.MaintenanceEndpointType -[SER010]StackExchange.Redis.ConfigurationOptions.MaintenanceMovingEndpointType.set -> void -[SER010]StackExchange.Redis.ConfigurationOptions.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode -[SER010]StackExchange.Redis.ConfigurationOptions.MaintenanceNotifications.set -> void -[SER010]StackExchange.Redis.ConfigurationOptions.MaintenancePostEventRelaxedDuration.get -> System.TimeSpan -[SER010]StackExchange.Redis.ConfigurationOptions.MaintenancePostEventRelaxedDuration.set -> void -[SER010]StackExchange.Redis.ConfigurationOptions.MaintenanceRelaxedTimeout.get -> System.TimeSpan -[SER010]StackExchange.Redis.ConfigurationOptions.MaintenanceRelaxedTimeout.set -> void -[SER010]StackExchange.Redis.ConfigurationOptions.MaintenanceRelaxedWindowMax.get -> System.TimeSpan -[SER010]StackExchange.Redis.ConfigurationOptions.MaintenanceRelaxedWindowMax.set -> void +StackExchange.Redis.ConfigurationOptions.MaintenanceMovingEndpointType.get -> StackExchange.Redis.MaintenanceEndpointType +StackExchange.Redis.ConfigurationOptions.MaintenanceMovingEndpointType.set -> void +StackExchange.Redis.ConfigurationOptions.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode +StackExchange.Redis.ConfigurationOptions.MaintenanceNotifications.set -> void +StackExchange.Redis.ConfigurationOptions.MaintenancePostEventRelaxedDuration.get -> System.TimeSpan +StackExchange.Redis.ConfigurationOptions.MaintenancePostEventRelaxedDuration.set -> void +StackExchange.Redis.ConfigurationOptions.MaintenanceRelaxedTimeout.get -> System.TimeSpan +StackExchange.Redis.ConfigurationOptions.MaintenanceRelaxedTimeout.set -> void +StackExchange.Redis.ConfigurationOptions.MaintenanceRelaxedWindowMax.get -> System.TimeSpan +StackExchange.Redis.ConfigurationOptions.MaintenanceRelaxedWindowMax.set -> void static StackExchange.Redis.ConfigurationOptions.Parse(string! configuration) -> StackExchange.Redis.ConfigurationOptions! static StackExchange.Redis.ConfigurationOptions.Parse(string! configuration, bool ignoreUnknown) -> StackExchange.Redis.ConfigurationOptions! StackExchange.Redis.ConfigurationOptions.Password.get -> string? @@ -2202,48 +2202,48 @@ StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceScheduled = StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceStart = 3 -> StackExchange.Redis.Maintenance.AzureNotificationType StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceStarting = 2 -> StackExchange.Redis.Maintenance.AzureNotificationType StackExchange.Redis.Maintenance.AzureNotificationType.Unknown = 0 -> StackExchange.Redis.Maintenance.AzureNotificationType -[SER010]StackExchange.Redis.Maintenance.ClusterSlotMigration -[SER010]StackExchange.Redis.Maintenance.ClusterSlotMigration.ClusterSlotMigration() -> void -[SER010]StackExchange.Redis.Maintenance.ClusterSlotMigration.RawSlots.get -> string? -[SER010]StackExchange.Redis.Maintenance.ClusterSlotMigration.Slots.get -> System.Collections.Generic.IReadOnlyList! -[SER010]StackExchange.Redis.Maintenance.ClusterSlotMigration.Source.get -> System.Net.EndPoint? -[SER010]StackExchange.Redis.Maintenance.ClusterSlotMigration.Target.get -> System.Net.EndPoint? -[SER010]override StackExchange.Redis.Maintenance.ClusterSlotMigration.ToString() -> string! -[SER010]StackExchange.Redis.Maintenance.MaintenanceNotificationType -[SER010]StackExchange.Redis.Maintenance.MaintenanceNotificationType.FailedOver = 5 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType -[SER010]StackExchange.Redis.Maintenance.MaintenanceNotificationType.FailingOver = 4 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType -[SER010]StackExchange.Redis.Maintenance.MaintenanceNotificationType.Migrated = 3 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType -[SER010]StackExchange.Redis.Maintenance.MaintenanceNotificationType.Migrating = 2 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType -[SER010]StackExchange.Redis.Maintenance.MaintenanceNotificationType.Moving = 1 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType -[SER010]StackExchange.Redis.Maintenance.MaintenanceNotificationType.None = 0 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType -[SER010]StackExchange.Redis.Maintenance.MaintenanceNotificationType.SlotMigrated = 7 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType -[SER010]StackExchange.Redis.Maintenance.MaintenanceNotificationType.SlotMigrating = 6 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType -[SER010]StackExchange.Redis.Maintenance.PushMaintenanceEvent -[SER010]StackExchange.Redis.Maintenance.PushMaintenanceEvent.EndPoint.get -> System.Net.EndPoint? -[SER010]StackExchange.Redis.Maintenance.PushMaintenanceEvent.NewEndPoint.get -> System.Net.EndPoint? -[SER010]StackExchange.Redis.Maintenance.PushMaintenanceEvent.NotificationType.get -> StackExchange.Redis.Maintenance.MaintenanceNotificationType -[SER010]StackExchange.Redis.Maintenance.PushMaintenanceEvent.Payload.get -> string? -[SER010]StackExchange.Redis.Maintenance.PushMaintenanceEvent.SequenceId.get -> long -[SER010]StackExchange.Redis.Maintenance.PushMaintenanceEvent.SlotMigrations.get -> System.Collections.Generic.IReadOnlyList! -[SER010]StackExchange.Redis.Maintenance.PushMaintenanceEvent.Time.get -> System.TimeSpan? -[SER010]override StackExchange.Redis.Maintenance.PushMaintenanceEvent.ToString() -> string? +StackExchange.Redis.Maintenance.ClusterSlotMigration +StackExchange.Redis.Maintenance.ClusterSlotMigration.ClusterSlotMigration() -> void +StackExchange.Redis.Maintenance.ClusterSlotMigration.RawSlots.get -> string? +StackExchange.Redis.Maintenance.ClusterSlotMigration.Slots.get -> System.Collections.Generic.IReadOnlyList! +StackExchange.Redis.Maintenance.ClusterSlotMigration.Source.get -> System.Net.EndPoint? +StackExchange.Redis.Maintenance.ClusterSlotMigration.Target.get -> System.Net.EndPoint? +override StackExchange.Redis.Maintenance.ClusterSlotMigration.ToString() -> string! +StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.Maintenance.MaintenanceNotificationType.FailedOver = 5 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.Maintenance.MaintenanceNotificationType.FailingOver = 4 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.Maintenance.MaintenanceNotificationType.Migrated = 3 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.Maintenance.MaintenanceNotificationType.Migrating = 2 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.Maintenance.MaintenanceNotificationType.Moving = 1 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.Maintenance.MaintenanceNotificationType.None = 0 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.Maintenance.MaintenanceNotificationType.SlotMigrated = 7 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.Maintenance.MaintenanceNotificationType.SlotMigrating = 6 -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.Maintenance.PushMaintenanceEvent +StackExchange.Redis.Maintenance.PushMaintenanceEvent.EndPoint.get -> System.Net.EndPoint? +StackExchange.Redis.Maintenance.PushMaintenanceEvent.NewEndPoint.get -> System.Net.EndPoint? +StackExchange.Redis.Maintenance.PushMaintenanceEvent.NotificationType.get -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.Maintenance.PushMaintenanceEvent.Payload.get -> string? +StackExchange.Redis.Maintenance.PushMaintenanceEvent.SequenceId.get -> long +StackExchange.Redis.Maintenance.PushMaintenanceEvent.SlotMigrations.get -> System.Collections.Generic.IReadOnlyList! +StackExchange.Redis.Maintenance.PushMaintenanceEvent.Time.get -> System.TimeSpan? +override StackExchange.Redis.Maintenance.PushMaintenanceEvent.ToString() -> string? StackExchange.Redis.Maintenance.ServerMaintenanceEvent StackExchange.Redis.Maintenance.ServerMaintenanceEvent.RawMessage.get -> string? StackExchange.Redis.Maintenance.ServerMaintenanceEvent.ReceivedTimeUtc.get -> System.DateTime StackExchange.Redis.Maintenance.ServerMaintenanceEvent.StartTimeUtc.get -> System.DateTime? override StackExchange.Redis.Maintenance.ServerMaintenanceEvent.ToString() -> string? -[SER010]StackExchange.Redis.MaintenanceEndpointType -[SER010]StackExchange.Redis.MaintenanceEndpointType.Auto = 1 -> StackExchange.Redis.MaintenanceEndpointType -[SER010]StackExchange.Redis.MaintenanceEndpointType.ExternalFqdn = 5 -> StackExchange.Redis.MaintenanceEndpointType -[SER010]StackExchange.Redis.MaintenanceEndpointType.ExternalIp = 4 -> StackExchange.Redis.MaintenanceEndpointType -[SER010]StackExchange.Redis.MaintenanceEndpointType.InternalFqdn = 3 -> StackExchange.Redis.MaintenanceEndpointType -[SER010]StackExchange.Redis.MaintenanceEndpointType.InternalIp = 2 -> StackExchange.Redis.MaintenanceEndpointType -[SER010]StackExchange.Redis.MaintenanceEndpointType.None = 6 -> StackExchange.Redis.MaintenanceEndpointType -[SER010]StackExchange.Redis.MaintenanceEndpointType.ServerDefault = 0 -> StackExchange.Redis.MaintenanceEndpointType -[SER010]StackExchange.Redis.MaintenanceNotificationMode -[SER010]StackExchange.Redis.MaintenanceNotificationMode.Auto = 2 -> StackExchange.Redis.MaintenanceNotificationMode -[SER010]StackExchange.Redis.MaintenanceNotificationMode.Disabled = 0 -> StackExchange.Redis.MaintenanceNotificationMode -[SER010]StackExchange.Redis.MaintenanceNotificationMode.Enabled = 1 -> StackExchange.Redis.MaintenanceNotificationMode +StackExchange.Redis.MaintenanceEndpointType +StackExchange.Redis.MaintenanceEndpointType.Auto = 1 -> StackExchange.Redis.MaintenanceEndpointType +StackExchange.Redis.MaintenanceEndpointType.ExternalFqdn = 5 -> StackExchange.Redis.MaintenanceEndpointType +StackExchange.Redis.MaintenanceEndpointType.ExternalIp = 4 -> StackExchange.Redis.MaintenanceEndpointType +StackExchange.Redis.MaintenanceEndpointType.InternalFqdn = 3 -> StackExchange.Redis.MaintenanceEndpointType +StackExchange.Redis.MaintenanceEndpointType.InternalIp = 2 -> StackExchange.Redis.MaintenanceEndpointType +StackExchange.Redis.MaintenanceEndpointType.None = 6 -> StackExchange.Redis.MaintenanceEndpointType +StackExchange.Redis.MaintenanceEndpointType.ServerDefault = 0 -> StackExchange.Redis.MaintenanceEndpointType +StackExchange.Redis.MaintenanceNotificationMode +StackExchange.Redis.MaintenanceNotificationMode.Auto = 2 -> StackExchange.Redis.MaintenanceNotificationMode +StackExchange.Redis.MaintenanceNotificationMode.Disabled = 0 -> StackExchange.Redis.MaintenanceNotificationMode +StackExchange.Redis.MaintenanceNotificationMode.Enabled = 1 -> StackExchange.Redis.MaintenanceNotificationMode StackExchange.Redis.MigrateOptions StackExchange.Redis.MigrateOptions.Copy = 1 -> StackExchange.Redis.MigrateOptions StackExchange.Redis.MigrateOptions.None = 0 -> StackExchange.Redis.MigrateOptions @@ -2415,7 +2415,7 @@ StackExchange.Redis.RedisConnectionException.CommandStatus.get -> StackExchange. StackExchange.Redis.RedisConnectionException.FailureType.get -> StackExchange.Redis.ConnectionFailureType StackExchange.Redis.RedisConnectionException.Flags.get -> StackExchange.Redis.CommandFlags override StackExchange.Redis.RedisConnectionException.GetObjectData(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void -[SER010]StackExchange.Redis.RedisConnectionException.MaintenanceType.get -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.RedisConnectionException.MaintenanceType.get -> StackExchange.Redis.Maintenance.MaintenanceNotificationType StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, StackExchange.Redis.CommandFlags flags, string! message, System.Exception? innerException = null, StackExchange.Redis.CommandStatus commandStatus = StackExchange.Redis.CommandStatus.Unknown) -> void StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, string! message) -> void StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, string! message, System.Exception? innerException) -> void @@ -2616,7 +2616,7 @@ StackExchange.Redis.RedisTimeoutException StackExchange.Redis.RedisTimeoutException.Commandstatus.get -> StackExchange.Redis.CommandStatus StackExchange.Redis.RedisTimeoutException.Flags.get -> StackExchange.Redis.CommandFlags override StackExchange.Redis.RedisTimeoutException.GetObjectData(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void -[SER010]StackExchange.Redis.RedisTimeoutException.MaintenanceType.get -> StackExchange.Redis.Maintenance.MaintenanceNotificationType +StackExchange.Redis.RedisTimeoutException.MaintenanceType.get -> StackExchange.Redis.Maintenance.MaintenanceNotificationType StackExchange.Redis.RedisTimeoutException.RedisTimeoutException(StackExchange.Redis.CommandFlags flags, string! message, StackExchange.Redis.CommandStatus commandStatus) -> void StackExchange.Redis.RedisTimeoutException.RedisTimeoutException(string! message, StackExchange.Redis.CommandStatus commandStatus) -> void StackExchange.Redis.RedisType diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 43eab028e8..444a5d1fb3 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ #nullable enable -[SER010]override StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode -[SER010]override StackExchange.Redis.Configuration.RedisCloudOptionsProvider.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode -[SER010]override StackExchange.Redis.Configuration.RedisEnterpriseOptionsProvider.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode +override StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode +override StackExchange.Redis.Configuration.RedisCloudOptionsProvider.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode +override StackExchange.Redis.Configuration.RedisEnterpriseOptionsProvider.MaintenanceNotifications.get -> StackExchange.Redis.MaintenanceNotificationMode diff --git a/src/StackExchange.Redis/ServerEndPoint.Maintenance.cs b/src/StackExchange.Redis/ServerEndPoint.Maintenance.cs index 315ab95656..6b0c4c46ea 100644 --- a/src/StackExchange.Redis/ServerEndPoint.Maintenance.cs +++ b/src/StackExchange.Redis/ServerEndPoint.Maintenance.cs @@ -41,7 +41,6 @@ internal sealed partial class ServerEndPoint /// cannot be made. /// /// - [Experimental(Experiments.MaintenanceNotifications, UrlFormat = Experiments.UrlFormat)] private MaintenanceNotificationMode MaintenanceMode => Multiplexer.IsGroupMember ? MaintenanceNotificationMode.Disabled diff --git a/toys/MaintenanceSoak/MaintenanceSoak.csproj b/toys/MaintenanceSoak/MaintenanceSoak.csproj index 0591dff3c4..323c4302f3 100644 --- a/toys/MaintenanceSoak/MaintenanceSoak.csproj +++ b/toys/MaintenanceSoak/MaintenanceSoak.csproj @@ -7,10 +7,9 @@ StackExchange.Redis.MaintenanceSoak true - - - $(NoWarn);SER010;StringToRedisValue + + $(NoWarn);StringToRedisValue diff --git a/toys/MaintenanceWatch/MaintenanceWatch.csproj b/toys/MaintenanceWatch/MaintenanceWatch.csproj index b400a6ced7..13da823780 100644 --- a/toys/MaintenanceWatch/MaintenanceWatch.csproj +++ b/toys/MaintenanceWatch/MaintenanceWatch.csproj @@ -4,8 +4,6 @@ Exe net10.0 enable - - $(NoWarn);SER010 From 1dbdddc8a266c7a3c680a7fd3d3b0c479c9c0097 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 6 Oct 2026 16:33:01 +0100 Subject: [PATCH 3/3] Update docs/ServerMaintenanceEvent.md Co-authored-by: Philo --- docs/ServerMaintenanceEvent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ServerMaintenanceEvent.md b/docs/ServerMaintenanceEvent.md index be3c4dbb15..15ef44c5a8 100644 --- a/docs/ServerMaintenanceEvent.md +++ b/docs/ServerMaintenanceEvent.md @@ -27,7 +27,7 @@ Server-side you may also see it discussed as *maintenance mode*, *shard migratio ## Do I need to configure anything? -Usually not. If you connect using the hostname your provider gave you, the matching options provider recognizes it and turns the feature on for you. +Usually not. If you connect using the hostname your provider gave you, the matching options provider recognizes it and turns the feature on for you by default. | You connect to | Recognized as | Notifications | |---|---|---|