diff --git a/docs/ClusterSlotStats.md b/docs/ClusterSlotStats.md
new file mode 100644
index 000000000..efff96110
--- /dev/null
+++ b/docs/ClusterSlotStats.md
@@ -0,0 +1,62 @@
+Cluster slot statistics
+===
+
+The `CLUSTER SLOT-STATS` command reports per-slot usage statistics - key counts, and optionally CPU,
+memory and network metrics - so you can see *where* in the keyspace a cluster's load lands, and which
+slots would be worth migrating. It is available in Redis 8.2 and later, on cluster-enabled servers
+only (a standalone server answers with an error).
+
+This command is available via the `IServer.ClusterSlotStats*` methods, which are
+[experimental (`SER012`)](exp/SER012) while the API shape settles:
+
+``` c#
+// Get a server instance; each node reports only the slots its own shard serves,
+// so ask each node to see the whole cluster.
+IConnectionMultiplexer muxer = ... // connect to a Redis 8.2+ cluster
+var server = muxer.GetServer(endpoint);
+
+// Form 1: the stats for a slot range (inclusive), in ascending slot order.
+var range = await server.ClusterSlotStatsBySlotRangeAsync(0, 16383);
+
+// Form 2: the node's slots sorted by a metric, busiest first; here, the ten
+// slots with the most keys.
+var top = await server.ClusterSlotStatsOrderByAsync(ClusterSlotStatsMetric.KeyCount, limit: 10);
+
+foreach (var slot in top.Slots)
+{
+ Console.WriteLine($"slot {slot.Slot}: {slot.KeyCount} keys, cpu: {slot.CpuTime}");
+}
+```
+
+Each `ClusterSlotStatsEntry` carries:
+
+- `Slot`: the hash slot the entry describes.
+- `KeyCount`: the number of keys in the slot; always collected.
+- `CpuTime`: CPU time spent serving the slot's keys (the server's `cpu-usec`, as a `TimeSpan?`).
+- `MemoryBytes`: memory used by the slot's keys.
+- `NetworkBytesIn` / `NetworkBytesOut`: network traffic for the slot's keys.
+- `Metadata`: any metric the server reports that this library does not recognize; the metric set is
+ configuration-dependent and has grown before, so unknown metrics are preserved rather than discarded.
+
+Metric collection
+---
+
+Only `key-count` is always collected. The other metrics are gated by the `cluster-slot-stats-enabled`
+server configuration, which takes the metric groups to enable (`cpu`, `net`, `mem`, or `yes` for all;
+`mem` additionally requires memory tracking from server startup):
+
+```
+CONFIG SET cluster-slot-stats-enabled "cpu net"
+```
+
+A metric that is not being collected comes back as `null` on `ClusterSlotStatsEntry` - deliberately
+distinct from a collected zero. Sorting (`ClusterSlotStatsOrderBy`) by a metric that is not being
+collected is refused by the server; `ClusterSlotStatsMetric.KeyCount` always works.
+
+Relationship to `HOTKEYS`
+---
+
+The two are complementary: [`HOTKEYS`](HotKeys) profiles *which keys* are hot on a server, while
+`CLUSTER SLOT-STATS` reports *which slots* carry the load - the granularity at which a cluster can
+actually rebalance. A typical investigation finds the heavy slots with `CLUSTER SLOT-STATS ORDERBY`,
+then profiles the keys within them with `HOTKEYS START ... SLOTS ...`.
diff --git a/docs/CommandCoverage.md b/docs/CommandCoverage.md
index ed40b7595..667c11535 100644
--- a/docs/CommandCoverage.md
+++ b/docs/CommandCoverage.md
@@ -4,7 +4,7 @@ Which server commands StackExchange.Redis does not expose as typed APIs, and why
Upstream: redis/redis@6a5fee67583480de1fec323649f43fd000001b49 (`src/commands`, unstable, 2026-11-01). Maintained by the `audit-command-coverage` skill under `.claude/skills/`, which re-diffs this file against upstream and reports only what changed.
-This supersedes the hand-maintained list in [#2055](https://github.com/StackExchange/StackExchange.Redis/issues/2055).
+This supersedes the hand-maintained list in [#2055](https://github.com/seredis/StackExchange.Redis/issues/2055).
## Rules
@@ -150,7 +150,6 @@ Operator tooling. All of it works through `Execute` on an `IServer`; none of it
| `CLIENT NO-TOUCH` | 7.2.0 | `deferred` | Per-connection flag, as `NO-EVICT`. |
| `CLUSTER SHARDS` | 7.0.0 | `deferred` | Not used anywhere today: `ClusterConfiguration` is built from `CLUSTER NODES`, and configuration export captures `NODES` and `SLOTS`. Moving topology discovery to `SHARDS`, or exposing it typed, is a design question. |
| `CLUSTER LINKS` | 7.0.0 | `deferred` | Topology introspection. |
-| `CLUSTER SLOT-STATS` | 8.2.0 | `deferred` | Per-slot statistics; pairs with hot-key tooling. |
## Todo: accepted gaps
diff --git a/docs/Groups.md b/docs/Groups.md
index 9e125de79..3f6133f3b 100644
--- a/docs/Groups.md
+++ b/docs/Groups.md
@@ -82,7 +82,7 @@ ValueTask SomethingAsync(/* the command's arguments */,
- **`ValueTask`, not `Task`.** It completes without allocating when the reply is already known (a client-side cache hit, say), and otherwise costs no more than a `Task` would. Await it once; don't store it to await twice. If you need a `Task` (to pass to `Task.WhenAll`, say), `.AsTask()` gives you one. A `ValueTask` has no `AsyncState`; if you rely on `Task.AsyncState`, use the original `IDatabase` methods.
- **`CommandFlags`** work as they always have: `FireAndForget`, replica preference, retry categories.
- **`CancellationToken`** cancels the *wait*: a command not yet written is never sent; one already written still runs on the server, and its reply is discarded.
-- **Leases.** A reply of variable length - several values, several counts, several members - comes back as a `ReadOnlyLease`: a pooled array rather than a fresh one per call. Dispose it when you are done with it (`using var members = await db.Sets.MembersAsync(key);`); `.Span` reads it and `.Span.ToArray()` copies it out if you need to keep it. A single large or binary value often has a `...LeaseAsync` form too (`Strings.GetLeaseAsync`, for example). Leases of `RespValue` borrow from the reply buffer itself; see [SER012](exp/SER012).
+- **Leases.** A reply of variable length - several values, several counts, several members - comes back as a `ReadOnlyLease`: a pooled array rather than a fresh one per call. Dispose it when you are done with it (`using var members = await db.Sets.MembersAsync(key);`); `.Span` reads it and `.Span.ToArray()` copies it out if you need to keep it. A single large or binary value often has a `...LeaseAsync` form too (`Strings.GetLeaseAsync`, for example). Leases of `RespValue` borrow from the reply buffer itself; see [SER102](exp/SER102).
There is no synchronous *twin* of each method. If you need to block, either use the original `IDatabase` methods, or make the context blocking - every group method then sends and waits on the calling thread, and returns a `ValueTask` that has already completed:
@@ -106,7 +106,7 @@ await batch.ExecuteAsync(); // sent as one contiguous run
RedisValue first = await a, second = await b;
```
-Nothing is sent until `ExecuteAsync`, so don't await a command's result before then - it has not been asked for yet. `BeginTransaction()` is the same, inside `MULTI`/`EXEC`. See [Transactions](Transactions) and [SER014](exp/SER014).
+Nothing is sent until `ExecuteAsync`, so don't await a command's result before then - it has not been asked for yet. `BeginTransaction()` is the same, inside `MULTI`/`EXEC`. See [Transactions](Transactions) and [SER103](exp/SER103).
Commands that aren't in a group
---
diff --git a/docs/LegacyApi.md b/docs/LegacyApi.md
index bfb21c924..6adba57c0 100644
--- a/docs/LegacyApi.md
+++ b/docs/LegacyApi.md
@@ -365,8 +365,8 @@ Generated from the v4 `IDatabase` implementation by `eng/docs/api-map.py`; each
| `IDatabase` | grouped |
|---|---|
-| `CreateBatch` | unchanged; or `db.BeginBatch()` ([SER014](exp/SER014), experimental) |
-| `CreateTransaction` | unchanged; or `db.BeginTransaction()` ([SER014](exp/SER014), experimental) |
+| `CreateBatch` | unchanged; or `db.BeginBatch()` ([SER103](exp/SER103), experimental) |
+| `CreateTransaction` | unchanged; or `db.BeginTransaction()` ([SER103](exp/SER103), experimental) |
| `Execute` | `db.Context.SendAsync($"...")` - see [Extending the client](Extending) |
| `ExecuteResp` | `db.Context.SendAsync($"...")` - see [Extending the client](Extending) |
| `GeoRadius` | `db.Geospatial.SearchAsync` (`GEORADIUS` is deprecated by the server in favour of `GEOSEARCH`) |
diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md
index 6c435d6cb..66945e686 100644
--- a/docs/ReleaseNotes.md
+++ b/docs/ReleaseNotes.md
@@ -8,26 +8,26 @@ Current package versions:
## 3.0
-From 3.0, [release notes will be maintained in GitHub only](https://github.com/StackExchange/StackExchange.Redis/releases) to avoid duplication.
+From 3.0, [release notes will be maintained in GitHub only](https://github.com/seredis/StackExchange.Redis/releases) to avoid duplication.
---
## 2.13.17
-- Fix TCP platform-dependent TCP keep-alive problems, and make an explicit option. ([#3090 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3090))
-- Tear down connection on write failure to prevent queue desync ([#3092 by @Pranish-Pantha](https://github.com/StackExchange/StackExchange.Redis/pull/3092))
-- Fix logic inversion with 8.8 `ARGREP NOCASE`, add `IsReversed` to simplify ordering, and support `ARINFO FULL`. ([#3087 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3087))
-- Avoid sentinel issues if `ROLE` unavailable; fix #3064 ([#3088 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3088))
-- Add 8.8 support for `INCREX` ([#3065 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3065))
+- Fix TCP platform-dependent TCP keep-alive problems, and make an explicit option. ([#3090 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3090))
+- Tear down connection on write failure to prevent queue desync ([#3092 by @Pranish-Pantha](https://github.com/seredis/StackExchange.Redis/pull/3092))
+- Fix logic inversion with 8.8 `ARGREP NOCASE`, add `IsReversed` to simplify ordering, and support `ARINFO FULL`. ([#3087 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3087))
+- Avoid sentinel issues if `ROLE` unavailable; fix #3064 ([#3088 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3088))
+- Add 8.8 support for `INCREX` ([#3065 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3065))
## 2.13.10
- Add experimental Redis 8.8 array support, including array APIs on `IDatabase`/`IDatabaseAsync`,
- array helper types, `RedisType.Array`, and array delete keyspace notification event types. ([#3076 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3076))
-- Enable TCP keep-alives ([#3078 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3078))
-- Fix incorrect routing of some sorted-set and stream commands ([#3080 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3080))
-- `ConfigurationOptions` : don't persist `Protocol` when it comes from the defaults-provider. ([#3082 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3082))
+ array helper types, `RedisType.Array`, and array delete keyspace notification event types. ([#3076 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3076))
+- Enable TCP keep-alives ([#3078 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3078))
+- Fix incorrect routing of some sorted-set and stream commands ([#3080 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3080))
+- `ConfigurationOptions` : don't persist `Protocol` when it comes from the defaults-provider. ([#3082 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3082))
## 2.13.1
@@ -43,446 +43,446 @@ which includes:
Changes:
-- Prefer RESP3 for Azure Managed Redis endpoints ([#3067 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3067))
+- Prefer RESP3 for Azure Managed Redis endpoints ([#3067 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3067))
## 2.12.27
-- Recognize Azure Managed Redis (AMR) resources in new Azure clouds ([#3068 by @philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/3068))
-- Remove `[Experimental]` 8.8 `GCRA` feature ([#3074 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3074))
-- Detect server-mode correctly on Valkey 8+ instances ([#3050 by @wipiano](https://github.com/StackExchange/StackExchange.Redis/pull/3050))
-- Add Redis 8.8 stream negative acknowledgements (`XNACK`) ([#3058 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3058))
-- 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))
-- Support Redis 8.8 sub-key (hash field) notifications ([#3062 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3062))
-- Add `ValueCondition` overloads for `SortedSetIncrement`/`SortedSetIncrementAsync`, supporting `ZADD INCR` with existence conditions ([#3071 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3071))
+- Recognize Azure Managed Redis (AMR) resources in new Azure clouds ([#3068 by @philon-msft](https://github.com/seredis/StackExchange.Redis/pull/3068))
+- Remove `[Experimental]` 8.8 `GCRA` feature ([#3074 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3074))
+- Detect server-mode correctly on Valkey 8+ instances ([#3050 by @wipiano](https://github.com/seredis/StackExchange.Redis/pull/3050))
+- Add Redis 8.8 stream negative acknowledgements (`XNACK`) ([#3058 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3058))
+- Add experimental `Aggregate.Count` support for sorted-set combination operations against Redis 8.8 ([#3059 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3059))
+- Support Redis 8.8 sub-key (hash field) notifications ([#3062 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3062))
+- Add `ValueCondition` overloads for `SortedSetIncrement`/`SortedSetIncrementAsync`, supporting `ZADD INCR` with existence conditions ([#3071 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3071))
## 2.12.14
-- Critical fix for high-integrity mode in cluster scenarios ([#3049 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/issues/3049))
+- Critical fix for high-integrity mode in cluster scenarios ([#3049 by @mgravell](https://github.com/seredis/StackExchange.Redis/issues/3049))
## 2.12.8
- (this release also includes experimental 8.8 `GCRA` support; this feature is not being shipped yet, and it is being removed from later releases)
-- Remove experimental flag on `VSIM` ([#3041 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3041))
+- Remove experimental flag on `VSIM` ([#3041 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3041))
- Add `IServer.GetProductVariant` to detect the product variant and version of the connected server, and use that internally
- to enable multi-DB operations on Valkey clusters ([#3040 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3040))
-- Ignore cluster nodes with the `handshake` flag ([#3043 by @TimLovellSmith](https://github.com/StackExchange/StackExchange.Redis/pull/3043))
+ to enable multi-DB operations on Valkey clusters ([#3040 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3040))
+- Ignore cluster nodes with the `handshake` flag ([#3043 by @TimLovellSmith](https://github.com/seredis/StackExchange.Redis/pull/3043))
## 2.12.4
-- Fix RESP3 client handshakes on non-RESP3 servers by ([#3037 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3037))
-- Improve detection of connect/handshake failures and how that impacts the retry-policy ([#3038 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3038))
+- Fix RESP3 client handshakes on non-RESP3 servers by ([#3037 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3037))
+- Improve detection of connect/handshake failures and how that impacts the retry-policy ([#3038 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3038))
## 2.12.1
-- Add missing `LCS` outputs and missing `RedisType.VectorSet` ([#3028 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3028))
-- Track and report multiplexer count ([#3030 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3030))
-- (docs) Add Entra ID authentication docs ([#3023 by @philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/3023))
-- (eng) Improve test infrastructure (toy-server) ([#3021 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3021), [#3022 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3022), [#3027 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3027), [#3028 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3028))
-- (eng) Pre-V2 work: bring RESPite down, toy-server, migrate to `AsciiHash` ([#3028 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3028))
+- Add missing `LCS` outputs and missing `RedisType.VectorSet` ([#3028 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3028))
+- Track and report multiplexer count ([#3030 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3030))
+- (docs) Add Entra ID authentication docs ([#3023 by @philon-msft](https://github.com/seredis/StackExchange.Redis/pull/3023))
+- (eng) Improve test infrastructure (toy-server) ([#3021 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3021), [#3022 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3022), [#3027 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3027), [#3028 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3028))
+- (eng) Pre-V2 work: bring RESPite down, toy-server, migrate to `AsciiHash` ([#3028 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3028))
## 2.11.8
-* Handle `-MOVED` error pointing to same endpoint. ([#3003 by @barshaul](https://github.com/StackExchange/StackExchange.Redis/pull/3003))
-* fix time conversion error in `HOTKEYS` ([#3017 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3017))
+* Handle `-MOVED` error pointing to same endpoint. ([#3003 by @barshaul](https://github.com/seredis/StackExchange.Redis/pull/3003))
+* fix time conversion error in `HOTKEYS` ([#3017 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/3017))
-- Add support for `VRANGE` ([#3011 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3011))
-- Add defensive code in azure-maintenance-events handling ([#3013 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3013))
+- Add support for `VRANGE` ([#3011 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/3011))
+- Add defensive code in azure-maintenance-events handling ([#3013 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/3013))
## 2.11.3
-- Add support for `VRANGE` ([#3011 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3011))
-- Add defensive code in azure-maintenance-events handling ([#3013 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3013))
+- Add support for `VRANGE` ([#3011 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/3011))
+- Add defensive code in azure-maintenance-events handling ([#3013 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/3013))
## 2.11.0
-- Add support for `HOTKEYS` ([#3008 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3008))
-- Add support for keyspace notifications ([#2995 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2995))
-- Add support for idempotent stream entry (`XADD IDMP[AUTO]`) support ([#3006 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3006))
-- (internals) split AMR out to a separate options provider ([#2986 by NickCraver and philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2986))
+- Add support for `HOTKEYS` ([#3008 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/3008))
+- Add support for keyspace notifications ([#2995 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2995))
+- Add support for idempotent stream entry (`XADD IDMP[AUTO]`) support ([#3006 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/3006))
+- (internals) split AMR out to a separate options provider ([#2986 by NickCraver and philon-msft](https://github.com/seredis/StackExchange.Redis/pull/2986))
## 2.10.14
-- Fix bug with connection startup failing in low-memory scenarios ([#3002 by nathan-miller23](https://github.com/StackExchange/StackExchange.Redis/pull/3002))
-- Fix under-count of `TotalOutstanding` in server-counters ([#2996 by nathan-miller23](https://github.com/StackExchange/StackExchange.Redis/pull/2996))
-- Fix incorrect debug assertion in `HGETEX` (no impact to release library) ([#2999 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2999))
+- Fix bug with connection startup failing in low-memory scenarios ([#3002 by nathan-miller23](https://github.com/seredis/StackExchange.Redis/pull/3002))
+- Fix under-count of `TotalOutstanding` in server-counters ([#2996 by nathan-miller23](https://github.com/seredis/StackExchange.Redis/pull/2996))
+- Fix incorrect debug assertion in `HGETEX` (no impact to release library) ([#2999 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2999))
## 2.10.1
- Support Redis 8.4 CAS/CAD operations (`DIGEST`, and the `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` modifiers on `SET` / `DEL`)
- via the new `ValueCondition` abstraction, and use CAS/CAD operations for `Lock*` APIs when possible ([#2978 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2978))
+ via the new `ValueCondition` abstraction, and use CAS/CAD operations for `Lock*` APIs when possible ([#2978 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2978))
- **note**: overload resolution for `StringSet[Async]` may be impacted in niche cases, requiring trivial build changes (there are no runtime-breaking changes such as missing methods)
-- Support `XREADGROUP CLAIM` ([#2972 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2972))
-- Support `MSETEX` (Redis 8.4.0) for multi-key operations with expiration ([#2977 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2977))
+- Support `XREADGROUP CLAIM` ([#2972 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2972))
+- Support `MSETEX` (Redis 8.4.0) for multi-key operations with expiration ([#2977 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2977))
## 2.9.32
-- Fix `SSUBSCRIBE` routing during slot migrations ([#2969 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2969))
+- Fix `SSUBSCRIBE` routing during slot migrations ([#2969 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2969))
## 2.9.25
-- (build) Fix SNK on non-Windows builds ([#2963 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2963))
+- (build) Fix SNK on non-Windows builds ([#2963 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2963))
## 2.9.24
-- Fix [#2951](https://github.com/StackExchange/StackExchange.Redis/issues/2951) - sentinel reconnection failure ([#2956 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2956))
-- Mitigate [#2955](https://github.com/StackExchange/StackExchange.Redis/issues/2955) (unbalanced pub/sub routing) / add `RedisValue.WithKeyRouting()` ([#2958 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2958))
-- Fix envoyproxy command exclusions ([#2957 by sshumakov](https://github.com/StackExchange/StackExchange.Redis/pull/2957))
-- Restrict `RedisValue` hex fallback (`string` conversion) to encoding failures ([2954 by jcaspes](https://github.com/StackExchange/StackExchange.Redis/pull/2954))
-- (internals) prefer `Volatile.Read` over `Thread.VolatileRead` ([2960 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2960))
+- Fix [#2951](https://github.com/seredis/StackExchange.Redis/issues/2951) - sentinel reconnection failure ([#2956 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2956))
+- Mitigate [#2955](https://github.com/seredis/StackExchange.Redis/issues/2955) (unbalanced pub/sub routing) / add `RedisValue.WithKeyRouting()` ([#2958 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2958))
+- Fix envoyproxy command exclusions ([#2957 by sshumakov](https://github.com/seredis/StackExchange.Redis/pull/2957))
+- Restrict `RedisValue` hex fallback (`string` conversion) to encoding failures ([2954 by jcaspes](https://github.com/seredis/StackExchange.Redis/pull/2954))
+- (internals) prefer `Volatile.Read` over `Thread.VolatileRead` ([2960 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2960))
## 2.9.17
-- Add vector-set support ([#2939 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2939))
-- Fix `RedisValue` special-value (NaN, Inf, etc) handling when casting from raw/string values to `double` ([#2950 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2950))
+- Add vector-set support ([#2939 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2939))
+- Fix `RedisValue` special-value (NaN, Inf, etc) handling when casting from raw/string values to `double` ([#2950 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2950))
- Internals:
- - Use `sealed` classes where possible ([#2942 by Henr1k80](https://github.com/StackExchange/StackExchange.Redis/pull/2942))
- - Add overlapped flushing in `LoggingTunnel` and avoid double-lookups ([#2943 by Henr1k80](https://github.com/StackExchange/StackExchange.Redis/pull/2943))
+ - Use `sealed` classes where possible ([#2942 by Henr1k80](https://github.com/seredis/StackExchange.Redis/pull/2942))
+ - Add overlapped flushing in `LoggingTunnel` and avoid double-lookups ([#2943 by Henr1k80](https://github.com/seredis/StackExchange.Redis/pull/2943))
## 2.9.11
-- Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2863))
-- Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863))
-- Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638))
-- Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822))
-- Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928))
-- Add `GetServer(RedisKey, ...)` API ([#2936 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2936))
-- Fix error constructing `StreamAdd` message ([#2941 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2941))
+- Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/seredis/StackExchange.Redis/pull/2863))
+- Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2863))
+- Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/seredis/StackExchange.Redis/pull/2638))
+- Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/seredis/StackExchange.Redis/pull/2822))
+- Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2928))
+- Add `GetServer(RedisKey, ...)` API ([#2936 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2936))
+- Fix error constructing `StreamAdd` message ([#2941 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2941))
## 2.8.58
-- Fix [#2679](https://github.com/StackExchange/StackExchange.Redis/issues/2679) - blocking call in long-running connects ([#2680 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2680))
-- Support async cancellation of `SCAN` enumeration ([#2911 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2911))
-- Add `XTRIM MINID` support ([#2842 by kijanawoodard](https://github.com/StackExchange/StackExchange.Redis/pull/2842))
-- Add new CE 8.2 stream support - `XDELEX`, `XACKDEL`, `{XADD|XTRIM} [KEEPREF|DELREF|ACKED]` ([#2912 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2912))
-- Fix `ZREVRANGEBYLEX` open-ended commands ([#2636 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2636))
-- Fix `StreamGroupInfo.Lag` when `null` ([#2902 by robhop](https://github.com/StackExchange/StackExchange.Redis/pull/2902))
+- Fix [#2679](https://github.com/seredis/StackExchange.Redis/issues/2679) - blocking call in long-running connects ([#2680 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2680))
+- Support async cancellation of `SCAN` enumeration ([#2911 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2911))
+- Add `XTRIM MINID` support ([#2842 by kijanawoodard](https://github.com/seredis/StackExchange.Redis/pull/2842))
+- Add new CE 8.2 stream support - `XDELEX`, `XACKDEL`, `{XADD|XTRIM} [KEEPREF|DELREF|ACKED]` ([#2912 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2912))
+- Fix `ZREVRANGEBYLEX` open-ended commands ([#2636 by ArnoKoll](https://github.com/seredis/StackExchange.Redis/pull/2636))
+- Fix `StreamGroupInfo.Lag` when `null` ([#2902 by robhop](https://github.com/seredis/StackExchange.Redis/pull/2902))
- Internals
- - Logging improvements ([#2903 by Meir017](https://github.com/StackExchange/StackExchange.Redis/pull/2903) and [#2917 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2917))
- - Update tests to xUnit v3 ([#2907 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2907))
- - Avoid `CLIENT PAUSE` in CI tests ([#2916 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2916))
+ - Logging improvements ([#2903 by Meir017](https://github.com/seredis/StackExchange.Redis/pull/2903) and [#2917 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2917))
+ - Update tests to xUnit v3 ([#2907 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2907))
+ - Avoid `CLIENT PAUSE` in CI tests ([#2916 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2916))
## 2.8.47
-- Add support for new `BITOP` operations in CE 8.2 ([#2900 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2900))
-- Package updates ([#2906 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2906))
-- Docs: added [guidance on async timeouts](https://seredis.dev/AsyncTimeouts) ([#2910 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2910))
-- Fix handshake error with `CLIENT ID` ([#2909 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2909))
+- Add support for new `BITOP` operations in CE 8.2 ([#2900 by atakavci](https://github.com/seredis/StackExchange.Redis/pull/2900))
+- Package updates ([#2906 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2906))
+- Docs: added [guidance on async timeouts](https://seredis.dev/AsyncTimeouts) ([#2910 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2910))
+- Fix handshake error with `CLIENT ID` ([#2909 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2909))
## 2.8.41
-- Add support for sharded pub/sub via `RedisChannel.Sharded` - ([#2887 by vandyvilla, atakavci and mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2887))
+- Add support for sharded pub/sub via `RedisChannel.Sharded` - ([#2887 by vandyvilla, atakavci and mgravell](https://github.com/seredis/StackExchange.Redis/pull/2887))
## 2.8.37
-- Add `ConfigurationOptions.SetUserPemCertificate(...)` and `ConfigurationOptions.SetUserPfxCertificate(...)` methods to simplify using client certificates ([#2873 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2873))
-- Add logging for when a Multiplexer reconfigures ([#2864 by st-dev-gh](https://github.com/StackExchange/StackExchange.Redis/pull/2864))
-- Fix: Move `AuthenticateAsClient` to fully async after dropping older framework support, to help client thread starvation in cases TLS negotiation stalls server-side ([#2878 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2878))
+- Add `ConfigurationOptions.SetUserPemCertificate(...)` and `ConfigurationOptions.SetUserPfxCertificate(...)` methods to simplify using client certificates ([#2873 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2873))
+- Add logging for when a Multiplexer reconfigures ([#2864 by st-dev-gh](https://github.com/seredis/StackExchange.Redis/pull/2864))
+- Fix: Move `AuthenticateAsClient` to fully async after dropping older framework support, to help client thread starvation in cases TLS negotiation stalls server-side ([#2878 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2878))
## 2.8.31
-- Fix: Respect `IReconnectRetryPolicy` timing in the case that a node that was present disconnects indefinitely ([#2853](https://github.com/StackExchange/StackExchange.Redis/pull/2853) & [#2856](https://github.com/StackExchange/StackExchange.Redis/pull/2856) by NickCraver)
+- Fix: Respect `IReconnectRetryPolicy` timing in the case that a node that was present disconnects indefinitely ([#2853](https://github.com/seredis/StackExchange.Redis/pull/2853) & [#2856](https://github.com/seredis/StackExchange.Redis/pull/2856) by NickCraver)
- Special thanks to [sampdei](https://github.com/sampdei) tracking this down and working a fix
-- Changes max default retry policy backoff to 60 seconds ([#2853 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2853))
-- Fix [#2652](https://github.com/StackExchange/StackExchange.Redis/issues/2652): Track client-initiated shutdown for any pipe type ([#2814 by bgrainger](https://github.com/StackExchange/StackExchange.Redis/pull/2814))
+- Changes max default retry policy backoff to 60 seconds ([#2853 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2853))
+- Fix [#2652](https://github.com/seredis/StackExchange.Redis/issues/2652): Track client-initiated shutdown for any pipe type ([#2814 by bgrainger](https://github.com/seredis/StackExchange.Redis/pull/2814))
## 2.8.24
-- Update Envoy command definitions to [allow `UNWATCH`](https://github.com/envoyproxy/envoy/pull/37620) ([#2824 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2824))
+- Update Envoy command definitions to [allow `UNWATCH`](https://github.com/envoyproxy/envoy/pull/37620) ([#2824 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2824))
## 2.8.22
-- Format IPv6 endpoints correctly when rewriting configration strings ([#2813 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2813))
-- Update default Redis version from `4.0.0` to `6.0.0` for Azure Redis resources ([#2810 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2810))
-- Detect Azure Managed Redis caches and tune default connection settings for them ([#2818 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2818))
-- Bump `Microsoft.Bcl.AsyncInterfaces` dependency from `5.0.0` to `6.0.0` ([#2820 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2820))
+- Format IPv6 endpoints correctly when rewriting configration strings ([#2813 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2813))
+- Update default Redis version from `4.0.0` to `6.0.0` for Azure Redis resources ([#2810 by philon-msft](https://github.com/seredis/StackExchange.Redis/pull/2810))
+- Detect Azure Managed Redis caches and tune default connection settings for them ([#2818 by philon-msft](https://github.com/seredis/StackExchange.Redis/pull/2818))
+- Bump `Microsoft.Bcl.AsyncInterfaces` dependency from `5.0.0` to `6.0.0` ([#2820 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2820))
## 2.8.16
-- Fix: PhysicalBridge: Always perform "last read" check in heartbeat when `HeartbeatConsistencyChecks` is enabled ([#2795 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2795))
+- Fix: PhysicalBridge: Always perform "last read" check in heartbeat when `HeartbeatConsistencyChecks` is enabled ([#2795 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2795))
## 2.8.14
-- Fix [#2793](https://github.com/StackExchange/StackExchange.Redis/issues/2793): Update Envoyproxy's command map according to latest Envoy documentation ([#2794 by dbarbosapn](https://github.com/StackExchange/StackExchange.Redis/pull/2794))
+- Fix [#2793](https://github.com/seredis/StackExchange.Redis/issues/2793): Update Envoyproxy's command map according to latest Envoy documentation ([#2794 by dbarbosapn](https://github.com/seredis/StackExchange.Redis/pull/2794))
## 2.8.12
-- Add support for hash field expiration (see [#2715](https://github.com/StackExchange/StackExchange.Redis/issues/2715)) ([#2716 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2716]))
-- Add support for `HSCAN NOVALUES` (see [#2721](https://github.com/StackExchange/StackExchange.Redis/issues/2721)) ([#2722 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2722))
-- Fix [#2763](https://github.com/StackExchange/StackExchange.Redis/issues/2763): Make ConnectionMultiplexer.Subscription thread-safe ([#2769 by Chuck-EP](https://github.com/StackExchange/StackExchange.Redis/pull/2769))
-- Fix [#2778](https://github.com/StackExchange/StackExchange.Redis/issues/2778): Run `CheckInfoReplication` even with `HeartbeatConsistencyChecks` ([#2784 by NickCraver and leachdaniel-clark](https://github.com/StackExchange/StackExchange.Redis/pull/2784))
+- Add support for hash field expiration (see [#2715](https://github.com/seredis/StackExchange.Redis/issues/2715)) ([#2716 by atakavci](https://github.com/seredis/StackExchange.Redis/pull/2716]))
+- Add support for `HSCAN NOVALUES` (see [#2721](https://github.com/seredis/StackExchange.Redis/issues/2721)) ([#2722 by atakavci](https://github.com/seredis/StackExchange.Redis/pull/2722))
+- Fix [#2763](https://github.com/seredis/StackExchange.Redis/issues/2763): Make ConnectionMultiplexer.Subscription thread-safe ([#2769 by Chuck-EP](https://github.com/seredis/StackExchange.Redis/pull/2769))
+- Fix [#2778](https://github.com/seredis/StackExchange.Redis/issues/2778): Run `CheckInfoReplication` even with `HeartbeatConsistencyChecks` ([#2784 by NickCraver and leachdaniel-clark](https://github.com/seredis/StackExchange.Redis/pull/2784))
## 2.8.0
-- Add high-integrity mode ([docs](https://seredis.dev/Configuration), [#2471 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2741))
-- TLS certificate/`TrustIssuer`: Check EKU in X509 chain checks when validating certificates ([#2670 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2670))
+- Add high-integrity mode ([docs](https://seredis.dev/Configuration), [#2471 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2741))
+- TLS certificate/`TrustIssuer`: Check EKU in X509 chain checks when validating certificates ([#2670 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2670))
## 2.7.33
-- **Potentially Breaking**: Fix `CheckTrustedIssuer` certificate validation for broken chain scenarios ([#2665 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2665))
+- **Potentially Breaking**: Fix `CheckTrustedIssuer` certificate validation for broken chain scenarios ([#2665 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2665))
- Users inadvertently trusting a remote cert with a broken chain could not be failing custom validation before this change. This is only in play if you are using `ConfigurationOptions.TrustIssuer` at all.
-- Add new `LoggingTunnel` API; see [https://seredis.dev/RespLogging](https://seredis.dev/RespLogging) ([#2660 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2660))
-- Fix [#2664](https://github.com/StackExchange/StackExchange.Redis/issues/2664): Move ProcessBacklog to fully sync to prevent thread pool hopping and blocking on awaits ([#2667 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2667))
+- Add new `LoggingTunnel` API; see [https://seredis.dev/RespLogging](https://seredis.dev/RespLogging) ([#2660 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2660))
+- Fix [#2664](https://github.com/seredis/StackExchange.Redis/issues/2664): Move ProcessBacklog to fully sync to prevent thread pool hopping and blocking on awaits ([#2667 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2667))
## 2.7.27
-- Support `HeartbeatConsistencyChecks` and `HeartbeatInterval` in `Clone()` ([#2658 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2658))
-- Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect* ([#2659 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2659))
+- Support `HeartbeatConsistencyChecks` and `HeartbeatInterval` in `Clone()` ([#2658 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2658))
+- Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect* ([#2659 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2659))
## 2.7.23
-- Fix [#2653](https://github.com/StackExchange/StackExchange.Redis/issues/2653): Client library metadata should validate contents ([#2654 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2654))
-- Add `HeartbeatConsistencyChecks` option (opt-in) to enabled per-heartbeat (defaults to once per second) checks to be sent to ensure no network stream corruption has occurred ([#2656 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2656))
+- Fix [#2653](https://github.com/seredis/StackExchange.Redis/issues/2653): Client library metadata should validate contents ([#2654 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2654))
+- Add `HeartbeatConsistencyChecks` option (opt-in) to enabled per-heartbeat (defaults to once per second) checks to be sent to ensure no network stream corruption has occurred ([#2656 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2656))
## 2.7.20
-- Fix [#2642](https://github.com/StackExchange/StackExchange.Redis/issues/2642): Detect and support multi-DB pseudo-cluster/proxy scenarios ([#2646](https://github.com/StackExchange/StackExchange.Redis/pull/2646) by mgravell)
+- Fix [#2642](https://github.com/seredis/StackExchange.Redis/issues/2642): Detect and support multi-DB pseudo-cluster/proxy scenarios ([#2646](https://github.com/seredis/StackExchange.Redis/pull/2646) by mgravell)
## 2.7.17
-- Fix [#2321](https://github.com/StackExchange/StackExchange.Redis/issues/2321): Honor disposition of select command in Command Map for transactions [(#2322 by slorello89)](https://github.com/StackExchange/StackExchange.Redis/pull/2322)
-- Fix [#2619](https://github.com/StackExchange/StackExchange.Redis/issues/2619): Type-forward `IsExternalInit` to support down-level TFMs ([#2621 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2621))
-- `InternalsVisibleTo` `PublicKey` enhancements([#2623 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/2623))
-- Fix [#2576](https://github.com/StackExchange/StackExchange.Redis/issues/2576): Prevent `NullReferenceException` during shutdown of connections ([#2629 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2629))
+- Fix [#2321](https://github.com/seredis/StackExchange.Redis/issues/2321): Honor disposition of select command in Command Map for transactions [(#2322 by slorello89)](https://github.com/seredis/StackExchange.Redis/pull/2322)
+- Fix [#2619](https://github.com/seredis/StackExchange.Redis/issues/2619): Type-forward `IsExternalInit` to support down-level TFMs ([#2621 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2621))
+- `InternalsVisibleTo` `PublicKey` enhancements([#2623 by WeihanLi](https://github.com/seredis/StackExchange.Redis/pull/2623))
+- Fix [#2576](https://github.com/seredis/StackExchange.Redis/issues/2576): Prevent `NullReferenceException` during shutdown of connections ([#2629 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2629))
## 2.7.10
-- Fix [#2593](https://github.com/StackExchange/StackExchange.Redis/issues/2593): `EXPIRETIME` and `PEXPIRETIME` miscategorized as `PrimaryOnly` commands causing them to fail when issued against a read-only replica ([#2593 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2593))
-- Fix [#2591](https://github.com/StackExchange/StackExchange.Redis/issues/2591): Add `HELLO` to Sentinel connections so they can support RESP3 ([#2601 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2601))
-- Fix [#2595](https://github.com/StackExchange/StackExchange.Redis/issues/2595): Add detection handling for dead sockets that the OS says are okay, seen especially in Linux environments ([#2610 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2610))
+- Fix [#2593](https://github.com/seredis/StackExchange.Redis/issues/2593): `EXPIRETIME` and `PEXPIRETIME` miscategorized as `PrimaryOnly` commands causing them to fail when issued against a read-only replica ([#2593 by slorello89](https://github.com/seredis/StackExchange.Redis/pull/2593))
+- Fix [#2591](https://github.com/seredis/StackExchange.Redis/issues/2591): Add `HELLO` to Sentinel connections so they can support RESP3 ([#2601 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2601))
+- Fix [#2595](https://github.com/seredis/StackExchange.Redis/issues/2595): Add detection handling for dead sockets that the OS says are okay, seen especially in Linux environments ([#2610 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2610))
## 2.7.4
-- Adds: RESP3 support ([#2396 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2396)) - see https://seredis.dev/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))
-- Fix [#2467](https://github.com/StackExchange/StackExchange.Redis/issues/2467): Add StreamGroupInfo EntriesRead and Lag ([#2510 by tvdias](https://github.com/StackExchange/StackExchange.Redis/pull/2510))
+- Adds: RESP3 support ([#2396 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2396)) - see https://seredis.dev/Resp3
+- Fix [#2507](https://github.com/seredis/StackExchange.Redis/issues/2507): Pub/sub with multi-item payloads should be usable ([#2508 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2508))
+- Add: connection-id tracking (internal only, no public API) ([#2508 by mgravell](https://github.com/seredis/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/seredis/StackExchange.Redis/pull/2051))
+- Fix [#2467](https://github.com/seredis/StackExchange.Redis/issues/2467): Add StreamGroupInfo EntriesRead and Lag ([#2510 by tvdias](https://github.com/seredis/StackExchange.Redis/pull/2510))
## 2.6.122
-- Change: Target net6.0 instead of net5.0, since net5.0 is end of life. ([#2497 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2497))
-- Fix: Fix nullability annotation of IConnectionMultiplexer.RegisterProfiler ([#2494 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2494))
-- Fix [#2520](https://github.com/StackExchange/StackExchange.Redis/issues/2520): Improve cluster connections in down scenarios by not re-pinging successful nodes ([#2525 by Matiszak](https://github.com/StackExchange/StackExchange.Redis/pull/2525))
-- Add: `Timer.ActiveCount` under `POOL` in timeout messages on .NET 6+ to help diagnose timer overload affecting timeout evaluations ([#2500 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2500))
-- Add: `LibraryName` configuration option; allows the library name to be controlled at the individual options level (in addition to the existing controls in `DefaultOptionsProvider`) ([#2502 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2502))
-- Add: `DefaultOptionsProvider.GetProvider` allows lookup of provider by endpoint ([#2502 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2502))
+- Change: Target net6.0 instead of net5.0, since net5.0 is end of life. ([#2497 by eerhardt](https://github.com/seredis/StackExchange.Redis/pull/2497))
+- Fix: Fix nullability annotation of IConnectionMultiplexer.RegisterProfiler ([#2494 by eerhardt](https://github.com/seredis/StackExchange.Redis/pull/2494))
+- Fix [#2520](https://github.com/seredis/StackExchange.Redis/issues/2520): Improve cluster connections in down scenarios by not re-pinging successful nodes ([#2525 by Matiszak](https://github.com/seredis/StackExchange.Redis/pull/2525))
+- Add: `Timer.ActiveCount` under `POOL` in timeout messages on .NET 6+ to help diagnose timer overload affecting timeout evaluations ([#2500 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2500))
+- Add: `LibraryName` configuration option; allows the library name to be controlled at the individual options level (in addition to the existing controls in `DefaultOptionsProvider`) ([#2502 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2502))
+- Add: `DefaultOptionsProvider.GetProvider` allows lookup of provider by endpoint ([#2502 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2502))
## 2.6.116
-- Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Add `RedisChannel.UseImplicitAutoPattern` (global) and `RedisChannel.IsPattern` ([#2480 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2480))
-- Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Mark `RedisChannel` conversion operators as obsolete; add `RedisChannel.Literal` and `RedisChannel.Pattern` helpers ([#2481 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2481))
-- Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Update `Pipelines.Sockets.Unofficial` to `v2.2.8` to support native AOT ([#2456 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2456))
+- Fix [#2479](https://github.com/seredis/StackExchange.Redis/issues/2479): Add `RedisChannel.UseImplicitAutoPattern` (global) and `RedisChannel.IsPattern` ([#2480 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2480))
+- Fix [#2479](https://github.com/seredis/StackExchange.Redis/issues/2479): Mark `RedisChannel` conversion operators as obsolete; add `RedisChannel.Literal` and `RedisChannel.Pattern` helpers ([#2481 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2481))
+- Fix [#2449](https://github.com/seredis/StackExchange.Redis/issues/2449): Update `Pipelines.Sockets.Unofficial` to `v2.2.8` to support native AOT ([#2456 by eerhardt](https://github.com/seredis/StackExchange.Redis/pull/2456))
## 2.6.111
-- Fix [#2426](https://github.com/StackExchange/StackExchange.Redis/issues/2426): Don't restrict multi-slot operations on Envoy proxy; let the proxy decide ([#2428 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2428))
-- Add: Support for `User`/`Password` in `DefaultOptionsProvider` to support token rotation scenarios ([#2445 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2445))
-- Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Resolve AOT trim warnings in `TryGetAzureRoleInstanceIdNoThrow` ([#2451 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2451))
-- Adds: Support for `HTTP/1.1 200 Connection established` in HTTP Tunnel ([#2448 by flobernd](https://github.com/StackExchange/StackExchange.Redis/pull/2448))
-- Adds: Timeout duration to backlog timeout error messages ([#2452 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2452))
-- Adds: `DefaultOptionsProvider.LibraryName` for specifying lib-name passed to `CLIENT SETINFO` in Redis 7.2+ ([#2453 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2453))
+- Fix [#2426](https://github.com/seredis/StackExchange.Redis/issues/2426): Don't restrict multi-slot operations on Envoy proxy; let the proxy decide ([#2428 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2428))
+- Add: Support for `User`/`Password` in `DefaultOptionsProvider` to support token rotation scenarios ([#2445 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2445))
+- Fix [#2449](https://github.com/seredis/StackExchange.Redis/issues/2449): Resolve AOT trim warnings in `TryGetAzureRoleInstanceIdNoThrow` ([#2451 by eerhardt](https://github.com/seredis/StackExchange.Redis/pull/2451))
+- Adds: Support for `HTTP/1.1 200 Connection established` in HTTP Tunnel ([#2448 by flobernd](https://github.com/seredis/StackExchange.Redis/pull/2448))
+- Adds: Timeout duration to backlog timeout error messages ([#2452 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2452))
+- Adds: `DefaultOptionsProvider.LibraryName` for specifying lib-name passed to `CLIENT SETINFO` in Redis 7.2+ ([#2453 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2453))
## 2.6.104
-- Fix [#2412](https://github.com/StackExchange/StackExchange.Redis/issues/2412): Critical (but rare) GC bug that can lead to async tasks never completing if the multiplexer is not held by the consumer ([#2408 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2408))
-- Add: Better error messages (over generic timeout) when commands are backlogged and unable to write to any connection ([#2408 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2408))
-- Fix [#2392](https://github.com/StackExchange/StackExchange.Redis/issues/2392): Dequeue *all* timed out messages from the backlog when not connected (including Fire+Forget) ([#2397 by kornelpal](https://github.com/StackExchange/StackExchange.Redis/pull/2397))
-- Fix [#2400](https://github.com/StackExchange/StackExchange.Redis/issues/2400): Expose `ChannelMessageQueue` as `IAsyncEnumerable` ([#2402 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2402))
-- Add: Support for `CLIENT SETINFO` (lib name/version) during handshake; opt-out is via `ConfigurationOptions`; also support read of `resp`, `lib-ver` and `lib-name` via `CLIENT LIST` ([#2414 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2414))
-- Documentation: clarify the meaning of `RedisValue.IsInteger` re [#2418](https://github.com/StackExchange/StackExchange.Redis/issues/2418) ([#2420 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2420))
+- Fix [#2412](https://github.com/seredis/StackExchange.Redis/issues/2412): Critical (but rare) GC bug that can lead to async tasks never completing if the multiplexer is not held by the consumer ([#2408 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2408))
+- Add: Better error messages (over generic timeout) when commands are backlogged and unable to write to any connection ([#2408 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2408))
+- Fix [#2392](https://github.com/seredis/StackExchange.Redis/issues/2392): Dequeue *all* timed out messages from the backlog when not connected (including Fire+Forget) ([#2397 by kornelpal](https://github.com/seredis/StackExchange.Redis/pull/2397))
+- Fix [#2400](https://github.com/seredis/StackExchange.Redis/issues/2400): Expose `ChannelMessageQueue` as `IAsyncEnumerable` ([#2402 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2402))
+- Add: Support for `CLIENT SETINFO` (lib name/version) during handshake; opt-out is via `ConfigurationOptions`; also support read of `resp`, `lib-ver` and `lib-name` via `CLIENT LIST` ([#2414 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2414))
+- Documentation: clarify the meaning of `RedisValue.IsInteger` re [#2418](https://github.com/seredis/StackExchange.Redis/issues/2418) ([#2420 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2420))
## 2.6.96
-- Fix [#2350](https://github.com/StackExchange/StackExchange.Redis/issues/2350): Properly parse lua script parameters in all cultures ([#2351 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2351))
-- Fix [#2362](https://github.com/StackExchange/StackExchange.Redis/issues/2362): Set `RedisConnectionException.FailureType` to `AuthenticationFailure` on all authentication scenarios for better handling ([#2367 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2367))
-- Fix [#2368](https://github.com/StackExchange/StackExchange.Redis/issues/2368): Support `RedisValue.Length()` for all storage types ([#2370 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2370))
-- Fix [#2376](https://github.com/StackExchange/StackExchange.Redis/issues/2376): Avoid a (rare) deadlock scenario ([#2378 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2378))
+- Fix [#2350](https://github.com/seredis/StackExchange.Redis/issues/2350): Properly parse lua script parameters in all cultures ([#2351 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2351))
+- Fix [#2362](https://github.com/seredis/StackExchange.Redis/issues/2362): Set `RedisConnectionException.FailureType` to `AuthenticationFailure` on all authentication scenarios for better handling ([#2367 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2367))
+- Fix [#2368](https://github.com/seredis/StackExchange.Redis/issues/2368): Support `RedisValue.Length()` for all storage types ([#2370 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2370))
+- Fix [#2376](https://github.com/seredis/StackExchange.Redis/issues/2376): Avoid a (rare) deadlock scenario ([#2378 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2378))
## 2.6.90
-- Adds: Support for `EVAL_RO` and `EVALSHA_RO` via `IDatabase.ScriptEvaluateReadOnly`/`IDatabase.ScriptEvaluateReadOnlyAsync` ([#2168 by shacharPash](https://github.com/StackExchange/StackExchange.Redis/pull/2168))
-- Fix [#1458](https://github.com/StackExchange/StackExchange.Redis/issues/1458): Fixes a leak condition when a connection completes on the TCP phase but not the Redis handshake ([#2238 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2238))
-- Internal: ServerSnapshot: Improve API and allow filtering with custom struct enumerator ([#2337 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2337))
+- Adds: Support for `EVAL_RO` and `EVALSHA_RO` via `IDatabase.ScriptEvaluateReadOnly`/`IDatabase.ScriptEvaluateReadOnlyAsync` ([#2168 by shacharPash](https://github.com/seredis/StackExchange.Redis/pull/2168))
+- Fix [#1458](https://github.com/seredis/StackExchange.Redis/issues/1458): Fixes a leak condition when a connection completes on the TCP phase but not the Redis handshake ([#2238 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2238))
+- Internal: ServerSnapshot: Improve API and allow filtering with custom struct enumerator ([#2337 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2337))
## 2.6.86
-- Fix [#1520](https://github.com/StackExchange/StackExchange.Redis/issues/1520) & [#1660](https://github.com/StackExchange/StackExchange.Redis/issues/1660): When `MOVED` is encountered from a cluster, a reconfigure will happen proactively to react to cluster changes ASAP ([#2286 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2286))
-- Fix [#2249](https://github.com/StackExchange/StackExchange.Redis/issues/2249): Properly handle a `fail` state (new `ClusterNode.IsFail` property) for `CLUSTER NODES` and expose `fail?` as a property (`IsPossiblyFail`) as well ([#2288 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2288))
-- Adds: `IConnectionMultiplexer.ServerMaintenanceEvent` (was on `ConnectionMultiplexer` but not the interface) ([#2306 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2306))
-- Adds: To timeout messages, additional debug information: `Sync-Ops` (synchronous operations), `Async-Ops` (asynchronous operations), and `Server-Connected-Seconds` (how long the connection in question has been connected, or `"n/a"`) ([#2300 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2300))
+- Fix [#1520](https://github.com/seredis/StackExchange.Redis/issues/1520) & [#1660](https://github.com/seredis/StackExchange.Redis/issues/1660): When `MOVED` is encountered from a cluster, a reconfigure will happen proactively to react to cluster changes ASAP ([#2286 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2286))
+- Fix [#2249](https://github.com/seredis/StackExchange.Redis/issues/2249): Properly handle a `fail` state (new `ClusterNode.IsFail` property) for `CLUSTER NODES` and expose `fail?` as a property (`IsPossiblyFail`) as well ([#2288 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2288))
+- Adds: `IConnectionMultiplexer.ServerMaintenanceEvent` (was on `ConnectionMultiplexer` but not the interface) ([#2306 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2306))
+- Adds: To timeout messages, additional debug information: `Sync-Ops` (synchronous operations), `Async-Ops` (asynchronous operations), and `Server-Connected-Seconds` (how long the connection in question has been connected, or `"n/a"`) ([#2300 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2300))
## 2.6.80
-- Adds: `last-in` and `cur-in` (bytes) to timeout exceptions to help identify timeouts that were just-behind another large payload off the wire ([#2276 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2276))
-- Adds: general-purpose tunnel support, with HTTP proxy "connect" support included ([#2274 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2274))
-- Removes: Package dependency (`System.Diagnostics.PerformanceCounter`) ([#2285 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2285))
+- Adds: `last-in` and `cur-in` (bytes) to timeout exceptions to help identify timeouts that were just-behind another large payload off the wire ([#2276 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2276))
+- Adds: general-purpose tunnel support, with HTTP proxy "connect" support included ([#2274 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2274))
+- Removes: Package dependency (`System.Diagnostics.PerformanceCounter`) ([#2285 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2285))
## 2.6.70
-- Fix: `MOVED` with `NoRedirect` (and other non-reachable errors) should respect the `IncludeDetailInExceptions` setting ([#2267 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2267))
-- Fix [#2251](https://github.com/StackExchange/StackExchange.Redis/issues/2251) & [#2265](https://github.com/StackExchange/StackExchange.Redis/issues/2265): Cluster endpoint connections weren't proactively connecting subscriptions in all cases and taking the full connection timeout to complete as a result ([#2268 by iteplov](https://github.com/StackExchange/StackExchange.Redis/pull/2268))
+- Fix: `MOVED` with `NoRedirect` (and other non-reachable errors) should respect the `IncludeDetailInExceptions` setting ([#2267 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2267))
+- Fix [#2251](https://github.com/seredis/StackExchange.Redis/issues/2251) & [#2265](https://github.com/seredis/StackExchange.Redis/issues/2265): Cluster endpoint connections weren't proactively connecting subscriptions in all cases and taking the full connection timeout to complete as a result ([#2268 by iteplov](https://github.com/seredis/StackExchange.Redis/pull/2268))
## 2.6.66
-- Fix [#2182](https://github.com/StackExchange/StackExchange.Redis/issues/2182): Be more flexible in which commands are "primary only" in order to support users with replicas that are explicitly configured to allow writes ([#2183 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2183))
-- Adds: `IConnectionMultiplexer` now implements `IAsyncDisposable` ([#2161 by kimsey0](https://github.com/StackExchange/StackExchange.Redis/pull/2161))
-- Adds: `IConnectionMultiplexer.GetServers()` to get all `IServer` instances for a multiplexer ([#2203 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2203))
-- Fix [#2016](https://github.com/StackExchange/StackExchange.Redis/issues/2016): Align server selection with supported commands (e.g. with writable servers) to reduce `Command cannot be issued to a replica` errors ([#2191 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2191))
-- Performance: Optimization around timeout processing to reduce lock contention in the case of many items that haven't yet timed out during a heartbeat ([#2217 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2217))
-- Fix [#2223](https://github.com/StackExchange/StackExchange.Redis/issues/2223): Resolve sync-context issues (missing `ConfigureAwait(false)`) ([#2229 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2229))
-- Fix [#1968](https://github.com/StackExchange/StackExchange.Redis/issues/1968): Improved handling of EVAL scripts during server restarts and failovers, detecting and re-sending the script for a retry when needed ([#2170 by martintmk](https://github.com/StackExchange/StackExchange.Redis/pull/2170))
-- Adds: `ConfigurationOptions.SslClientAuthenticationOptions` (`netcoreapp3.1`/`net5.0`+ only) to give more control over SSL/TLS authentication ([#2224 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2224))
-- Fix [#2240](https://github.com/StackExchange/StackExchange.Redis/pull/2241): Improve support for DNS-based IPv6 endpoints ([#2241 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2241))
-- Adds: `ConfigurationOptions.HeartbeatInterval` (**Advanced Setting** - [see docs](https://seredis.dev/Configuration#configuration-options)) To allow more finite control of the client heartbeat, which encompases how often command timeouts are actually evaluated - still defaults to 1,000 ms ([#2243 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2243))
-- Fix [#1879](https://github.com/StackExchange/StackExchange.Redis/issues/1879): Improve exception message when the wrong password is used ([#2246 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2246))
-- Fix [#2233](https://github.com/StackExchange/StackExchange.Redis/issues/2233): Repeated connection to Sentinel servers using the same ConfigurationOptions would fail ([#2242 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2242))
+- Fix [#2182](https://github.com/seredis/StackExchange.Redis/issues/2182): Be more flexible in which commands are "primary only" in order to support users with replicas that are explicitly configured to allow writes ([#2183 by slorello89](https://github.com/seredis/StackExchange.Redis/pull/2183))
+- Adds: `IConnectionMultiplexer` now implements `IAsyncDisposable` ([#2161 by kimsey0](https://github.com/seredis/StackExchange.Redis/pull/2161))
+- Adds: `IConnectionMultiplexer.GetServers()` to get all `IServer` instances for a multiplexer ([#2203 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2203))
+- Fix [#2016](https://github.com/seredis/StackExchange.Redis/issues/2016): Align server selection with supported commands (e.g. with writable servers) to reduce `Command cannot be issued to a replica` errors ([#2191 by slorello89](https://github.com/seredis/StackExchange.Redis/pull/2191))
+- Performance: Optimization around timeout processing to reduce lock contention in the case of many items that haven't yet timed out during a heartbeat ([#2217 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2217))
+- Fix [#2223](https://github.com/seredis/StackExchange.Redis/issues/2223): Resolve sync-context issues (missing `ConfigureAwait(false)`) ([#2229 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2229))
+- Fix [#1968](https://github.com/seredis/StackExchange.Redis/issues/1968): Improved handling of EVAL scripts during server restarts and failovers, detecting and re-sending the script for a retry when needed ([#2170 by martintmk](https://github.com/seredis/StackExchange.Redis/pull/2170))
+- Adds: `ConfigurationOptions.SslClientAuthenticationOptions` (`netcoreapp3.1`/`net5.0`+ only) to give more control over SSL/TLS authentication ([#2224 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2224))
+- Fix [#2240](https://github.com/seredis/StackExchange.Redis/pull/2241): Improve support for DNS-based IPv6 endpoints ([#2241 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2241))
+- Adds: `ConfigurationOptions.HeartbeatInterval` (**Advanced Setting** - [see docs](https://seredis.dev/Configuration#configuration-options)) To allow more finite control of the client heartbeat, which encompases how often command timeouts are actually evaluated - still defaults to 1,000 ms ([#2243 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2243))
+- Fix [#1879](https://github.com/seredis/StackExchange.Redis/issues/1879): Improve exception message when the wrong password is used ([#2246 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2246))
+- Fix [#2233](https://github.com/seredis/StackExchange.Redis/issues/2233): Repeated connection to Sentinel servers using the same ConfigurationOptions would fail ([#2242 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2242))
## 2.6.48
-- URGENT Fix: [#2167](https://github.com/StackExchange/StackExchange.Redis/issues/2167), [#2176](https://github.com/StackExchange/StackExchange.Redis/issues/2176): fix error in batch/transaction handling that can result in out-of-order instructions ([#2177 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2177))
-- Fix: [#2164](https://github.com/StackExchange/StackExchange.Redis/issues/2164): fix `LuaScript.Prepare` for scripts that don't have parameters ([#2166 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2166))
+- URGENT Fix: [#2167](https://github.com/seredis/StackExchange.Redis/issues/2167), [#2176](https://github.com/seredis/StackExchange.Redis/issues/2176): fix error in batch/transaction handling that can result in out-of-order instructions ([#2177 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2177))
+- Fix: [#2164](https://github.com/seredis/StackExchange.Redis/issues/2164): fix `LuaScript.Prepare` for scripts that don't have parameters ([#2166 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2166))
## 2.6.45
-- Adds: [Nullable reference type](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references) annotations ([#2041 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2041))
+- Adds: [Nullable reference type](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references) annotations ([#2041 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2041))
- Adds annotations themselves for nullability to everything in the library
- Fixes a few internal edge cases that will now throw proper errors (rather than a downstream null reference)
- Fixes inconsistencies with `null` vs. empty array returns (preferring an not-null empty array in those edge cases)
- Note: does *not* increment a major version (as these are warnings to consumers), because: they're warnings (errors are opt-in), removing obsolete types with a 3.0 rev _would_ be binary breaking (this isn't), and reving to 3.0 would cause binding redirect pain for consumers. Bumping from 2.5 to 2.6 only for this change.
-- Adds: Support for `COPY` with `.KeyCopy()`/`.KeyCopyAsync()` ([#2064 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2064))
-- Adds: Support for `LMOVE` with `.ListMove()`/`.ListMoveAsync()` ([#2065 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2065))
-- Adds: Support for `ZRANDMEMBER` with `.SortedSetRandomMember()`/`.SortedSetRandomMemberAsync()`, `.SortedSetRandomMembers()`/`.SortedSetRandomMembersAsync()`, and `.SortedSetRandomMembersWithScores()`/`.SortedSetRandomMembersWithScoresAsync()` ([#2076 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2076))
-- Adds: Support for `SMISMEMBER` with `.SetContains()`/`.SetContainsAsync()` ([#2077 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2077))
-- Adds: Support for `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZINTERCARD`, and `ZUNION` with `.SortedSetCombine()`/`.SortedSetCombineAsync()`, `.SortedSetCombineWithScores()`/`.SortedSetCombineWithScoresAsync()`, and `.SortedSetIntersectionLength()`/`.SortedSetIntersectionLengthAsync()` ([#2075 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2075))
-- Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078))
-- Adds: Support for `LPOS` with `.ListPosition()`/`.ListPositionAsync()` and `.ListPositions()`/`.ListPositionsAsync()` ([#2080 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2080))
-- Adds: Support for `ZMSCORE` with `.SortedSetScores()`/.`SortedSetScoresAsync()` ([#2082 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2082))
-- Adds: Support for `NX | XX | GT | LT` to `EXPIRE`, `EXPIREAT`, `PEXPIRE`, and `PEXPIREAT` with `.KeyExpire()`/`.KeyExpireAsync()` ([#2083 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2083))
-- Adds: Support for `EXPIRETIME`, and `PEXPIRETIME` with `.KeyExpireTime()`/`.KeyExpireTimeAsync()` ([#2083 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2083))
-- Fix: For streams, properly hash `XACK`, `XCLAIM`, and `XPENDING` in cluster scenarios to eliminate `MOVED` retries ([#2085 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2085))
-- Adds: Support for `OBJECT REFCOUNT` with `.KeyRefCount()`/`.KeyRefCountAsync()` ([#2087 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2087))
-- Adds: Support for `OBJECT ENCODING` with `.KeyEncoding()`/`.KeyEncodingAsync()` ([#2088 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2088))
-- Adds: Support for `GEOSEARCH` with `.GeoSearch()`/`.GeoSearchAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089))
-- Adds: Support for `GEOSEARCHSTORE` with `.GeoSearchAndStore()`/`.GeoSearchAndStoreAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089))
-- Adds: Support for `HRANDFIELD` with `.HashRandomField()`/`.HashRandomFieldAsync()`, `.HashRandomFields()`/`.HashRandomFieldsAsync()`, and `.HashRandomFieldsWithValues()`/`.HashRandomFieldsWithValuesAsync()` ([#2090 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2090))
-- Adds: Support for `LMPOP` with `.ListLeftPop()`/`.ListLeftPopAsync()` and `.ListRightPop()`/`.ListRightPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094))
-- Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094))
-- Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095))
-- Fix [#2071](https://github.com/StackExchange/StackExchange.Redis/issues/2071): Add `.StringSet()`/`.StringSetAsync()` overloads for source compat broken for 1 case in 2.5.61 ([#2098 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2098))
-- Fix [#2086](https://github.com/StackExchange/StackExchange.Redis/issues/2086): Correct HashSlot calculations for `XREAD` and `XREADGROUP` commands ([#2093 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2093))
-- Adds: Support for `LCS` with `.StringLongestCommonSubsequence()`/`.StringLongestCommonSubsequence()`, `.StringLongestCommonSubsequenceLength()`/`.StringLongestCommonSubsequenceLengthAsync()`, and `.StringLongestCommonSubsequenceWithMatches()`/`.StringLongestCommonSubsequenceWithMatchesAsync()` ([#2104 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2104))
-- Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105))
-- Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110))
-- Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111))
-- Adds: Support for `BIT | BYTE` to `BITCOUNT` and `BITPOS` with `.StringBitCount()`/`.StringBitCountAsync()` and `.StringBitPosition()`/`.StringBitPositionAsync()` ([#2116 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2116))
-- Adds: Support for pub/sub payloads that are unary arrays ([#2118 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2118))
-- Fix: Sentinel timer race during dispose ([#2133 by ewisuri](https://github.com/StackExchange/StackExchange.Redis/pull/2133))
-- Adds: Support for `GT`, `LT`, and `CH` on `ZADD` with `.SortedSetAdd()`/`.SortedSetAddAsync()` and `.SortedSetUpdate()`/`.SortedSetUpdateAsync()` ([#2136 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2136))
-- Adds: Support for `COMMAND COUNT`, `COMMAND GETKEYS`, and `COMMAND LIST`, with `.CommandCount()`/`.CommandCountAsync()`, `.CommandGetKeys()`/`.CommandGetKeysAsync()`, and `.CommandList()`/`.CommandListAsync()` ([#2143 by shacharPash](https://github.com/StackExchange/StackExchange.Redis/pull/2143))
+- Adds: Support for `COPY` with `.KeyCopy()`/`.KeyCopyAsync()` ([#2064 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2064))
+- Adds: Support for `LMOVE` with `.ListMove()`/`.ListMoveAsync()` ([#2065 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2065))
+- Adds: Support for `ZRANDMEMBER` with `.SortedSetRandomMember()`/`.SortedSetRandomMemberAsync()`, `.SortedSetRandomMembers()`/`.SortedSetRandomMembersAsync()`, and `.SortedSetRandomMembersWithScores()`/`.SortedSetRandomMembersWithScoresAsync()` ([#2076 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2076))
+- Adds: Support for `SMISMEMBER` with `.SetContains()`/`.SetContainsAsync()` ([#2077 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2077))
+- Adds: Support for `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZINTERCARD`, and `ZUNION` with `.SortedSetCombine()`/`.SortedSetCombineAsync()`, `.SortedSetCombineWithScores()`/`.SortedSetCombineWithScoresAsync()`, and `.SortedSetIntersectionLength()`/`.SortedSetIntersectionLengthAsync()` ([#2075 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2075))
+- Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2078))
+- Adds: Support for `LPOS` with `.ListPosition()`/`.ListPositionAsync()` and `.ListPositions()`/`.ListPositionsAsync()` ([#2080 by slorello89](https://github.com/seredis/StackExchange.Redis/pull/2080))
+- Adds: Support for `ZMSCORE` with `.SortedSetScores()`/.`SortedSetScoresAsync()` ([#2082 by ttingen](https://github.com/seredis/StackExchange.Redis/pull/2082))
+- Adds: Support for `NX | XX | GT | LT` to `EXPIRE`, `EXPIREAT`, `PEXPIRE`, and `PEXPIREAT` with `.KeyExpire()`/`.KeyExpireAsync()` ([#2083 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2083))
+- Adds: Support for `EXPIRETIME`, and `PEXPIRETIME` with `.KeyExpireTime()`/`.KeyExpireTimeAsync()` ([#2083 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2083))
+- Fix: For streams, properly hash `XACK`, `XCLAIM`, and `XPENDING` in cluster scenarios to eliminate `MOVED` retries ([#2085 by nielsderdaele](https://github.com/seredis/StackExchange.Redis/pull/2085))
+- Adds: Support for `OBJECT REFCOUNT` with `.KeyRefCount()`/`.KeyRefCountAsync()` ([#2087 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2087))
+- Adds: Support for `OBJECT ENCODING` with `.KeyEncoding()`/`.KeyEncodingAsync()` ([#2088 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2088))
+- Adds: Support for `GEOSEARCH` with `.GeoSearch()`/`.GeoSearchAsync()` ([#2089 by slorello89](https://github.com/seredis/StackExchange.Redis/pull/2089))
+- Adds: Support for `GEOSEARCHSTORE` with `.GeoSearchAndStore()`/`.GeoSearchAndStoreAsync()` ([#2089 by slorello89](https://github.com/seredis/StackExchange.Redis/pull/2089))
+- Adds: Support for `HRANDFIELD` with `.HashRandomField()`/`.HashRandomFieldAsync()`, `.HashRandomFields()`/`.HashRandomFieldsAsync()`, and `.HashRandomFieldsWithValues()`/`.HashRandomFieldsWithValuesAsync()` ([#2090 by slorello89](https://github.com/seredis/StackExchange.Redis/pull/2090))
+- Adds: Support for `LMPOP` with `.ListLeftPop()`/`.ListLeftPopAsync()` and `.ListRightPop()`/`.ListRightPopAsync()` ([#2094 by slorello89](https://github.com/seredis/StackExchange.Redis/pull/2094))
+- Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/seredis/StackExchange.Redis/pull/2094))
+- Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/seredis/StackExchange.Redis/pull/2095))
+- Fix [#2071](https://github.com/seredis/StackExchange.Redis/issues/2071): Add `.StringSet()`/`.StringSetAsync()` overloads for source compat broken for 1 case in 2.5.61 ([#2098 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2098))
+- Fix [#2086](https://github.com/seredis/StackExchange.Redis/issues/2086): Correct HashSlot calculations for `XREAD` and `XREADGROUP` commands ([#2093 by nielsderdaele](https://github.com/seredis/StackExchange.Redis/pull/2093))
+- Adds: Support for `LCS` with `.StringLongestCommonSubsequence()`/`.StringLongestCommonSubsequence()`, `.StringLongestCommonSubsequenceLength()`/`.StringLongestCommonSubsequenceLengthAsync()`, and `.StringLongestCommonSubsequenceWithMatches()`/`.StringLongestCommonSubsequenceWithMatchesAsync()` ([#2104 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2104))
+- Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2105))
+- Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2110))
+- Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/seredis/StackExchange.Redis/pull/2111))
+- Adds: Support for `BIT | BYTE` to `BITCOUNT` and `BITPOS` with `.StringBitCount()`/`.StringBitCountAsync()` and `.StringBitPosition()`/`.StringBitPositionAsync()` ([#2116 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2116))
+- Adds: Support for pub/sub payloads that are unary arrays ([#2118 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/2118))
+- Fix: Sentinel timer race during dispose ([#2133 by ewisuri](https://github.com/seredis/StackExchange.Redis/pull/2133))
+- Adds: Support for `GT`, `LT`, and `CH` on `ZADD` with `.SortedSetAdd()`/`.SortedSetAddAsync()` and `.SortedSetUpdate()`/`.SortedSetUpdateAsync()` ([#2136 by Avital-Fine](https://github.com/seredis/StackExchange.Redis/pull/2136))
+- Adds: Support for `COMMAND COUNT`, `COMMAND GETKEYS`, and `COMMAND LIST`, with `.CommandCount()`/`.CommandCountAsync()`, `.CommandGetKeys()`/`.CommandGetKeysAsync()`, and `.CommandList()`/`.CommandListAsync()` ([#2143 by shacharPash](https://github.com/seredis/StackExchange.Redis/pull/2143))
## 2.5.61
-- Adds: `GETEX` support with `.StringGetSetExpiry()`/`.StringGetSetExpiryAsync()` ([#1743 by benbryant0](https://github.com/StackExchange/StackExchange.Redis/pull/1743))
-- Fix [#1988](https://github.com/StackExchange/StackExchange.Redis/issues/1988): Don't issue `SELECT` commands if explicitly disabled ([#2023 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2023))
-- Adds: `KEEPTTL` support on `SET` operations ([#2029 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2029))
-- Fix: Allow `XTRIM` `MAXLEN` argument to be `0` ([#2030 by NicoAvanzDev](https://github.com/StackExchange/StackExchange.Redis/pull/2030))
-- Adds: `ConfigurationOptions.BeforeSocketConnect` for configuring sockets between creation and connection ([#2031 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2031))
-- Fix [#1813](https://github.com/StackExchange/StackExchange.Redis/issues/1813): Don't connect to endpoints we failed to parse ([#2042 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2042))
-- Fix: `ClientKill`/`ClientKillAsync` when using `ClientType` ([#2048 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2048))
-- Adds: Most `ConfigurationOptions` changes after `ConnectionMultiplexer` connections will now be respected, e.g. changing a timeout will work and changing a password for auth rotation would be used at the next reconnect ([#2050 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2050))
+- Adds: `GETEX` support with `.StringGetSetExpiry()`/`.StringGetSetExpiryAsync()` ([#1743 by benbryant0](https://github.com/seredis/StackExchange.Redis/pull/1743))
+- Fix [#1988](https://github.com/seredis/StackExchange.Redis/issues/1988): Don't issue `SELECT` commands if explicitly disabled ([#2023 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2023))
+- Adds: `KEEPTTL` support on `SET` operations ([#2029 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2029))
+- Fix: Allow `XTRIM` `MAXLEN` argument to be `0` ([#2030 by NicoAvanzDev](https://github.com/seredis/StackExchange.Redis/pull/2030))
+- Adds: `ConfigurationOptions.BeforeSocketConnect` for configuring sockets between creation and connection ([#2031 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2031))
+- Fix [#1813](https://github.com/seredis/StackExchange.Redis/issues/1813): Don't connect to endpoints we failed to parse ([#2042 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2042))
+- Fix: `ClientKill`/`ClientKillAsync` when using `ClientType` ([#2048 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2048))
+- Adds: Most `ConfigurationOptions` changes after `ConnectionMultiplexer` connections will now be respected, e.g. changing a timeout will work and changing a password for auth rotation would be used at the next reconnect ([#2050 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2050))
- **Obsolete**: This change also moves `ConnectionMultiplexer.IncludeDetailInExceptions` and `ConnectionMultiplexer.IncludePerformanceCountersInExceptions` to `ConfigurationOptions`. The old properties are `[Obsolete]` proxies that work until 3.0 for compatibility.
-- Adds: Support for `ZRANGESTORE` with `.SortedSetRangeAndStore()`/`.SortedSetRangeAndStoreAsync()` ([#2052 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2052))
+- Adds: Support for `ZRANGESTORE` with `.SortedSetRangeAndStore()`/`.SortedSetRangeAndStoreAsync()` ([#2052 by slorello89](https://github.com/seredis/StackExchange.Redis/pull/2052))
## 2.5.43
-- Adds: Bounds checking for `ExponentialRetry` backoff policy ([#1921 by gliljas](https://github.com/StackExchange/StackExchange.Redis/pull/1921))
-- Adds: `DefaultOptionsProvider` support for endpoint-based defaults configuration ([#1987 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1987))
-- Adds: Envoy proxy support ([#1989 by rkarthick](https://github.com/StackExchange/StackExchange.Redis/pull/1989))
-- Performance: When `SUBSCRIBE` is disabled, give proper errors and connect faster ([#2001 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2001))
-- Adds: `GET` on `SET` command support (present in Redis 6.2+ - [#2003 by martinekvili](https://github.com/StackExchange/StackExchange.Redis/pull/2003))
-- Performance: Improves concurrent load performance when backlogs are utilized ([#2008 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2008))
-- Stability: Improves cluster connections when `CLUSTER` command is disabled ([#2014 by tylerohlsen](https://github.com/StackExchange/StackExchange.Redis/pull/2014))
-- Logging: Improves connection logging and adds overall timing to it ([#2019 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2019))
+- Adds: Bounds checking for `ExponentialRetry` backoff policy ([#1921 by gliljas](https://github.com/seredis/StackExchange.Redis/pull/1921))
+- Adds: `DefaultOptionsProvider` support for endpoint-based defaults configuration ([#1987 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1987))
+- Adds: Envoy proxy support ([#1989 by rkarthick](https://github.com/seredis/StackExchange.Redis/pull/1989))
+- Performance: When `SUBSCRIBE` is disabled, give proper errors and connect faster ([#2001 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2001))
+- Adds: `GET` on `SET` command support (present in Redis 6.2+ - [#2003 by martinekvili](https://github.com/seredis/StackExchange.Redis/pull/2003))
+- Performance: Improves concurrent load performance when backlogs are utilized ([#2008 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2008))
+- Stability: Improves cluster connections when `CLUSTER` command is disabled ([#2014 by tylerohlsen](https://github.com/seredis/StackExchange.Redis/pull/2014))
+- Logging: Improves connection logging and adds overall timing to it ([#2019 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/2019))
## 2.5.27 (prerelease)
-- Adds: a backlog/retry mechanism for commands issued while a connection isn't available ([#1912 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1912))
+- Adds: a backlog/retry mechanism for commands issued while a connection isn't available ([#1912 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1912))
- Commands will be queued if a multiplexer isn't yet connected to a Redis server.
- Commands will be queued if a connection is lost and then sent to the server when the connection is restored.
- All commands queued will only remain in the backlog for the duration of the configured timeout.
- To revert to previous behavior, a new `ConfigurationOptions.BacklogPolicy` is available - old behavior is configured via `options.BacklogPolicy = BacklogPolicy.FailFast`. This backlogs nothing and fails commands immediately if no connection is available.
-- Adds: Makes `StreamEntry` constructor public for better unit test experience ([#1923 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/1923))
-- Fix: Integer overflow error (issue #1926) with 2GiB+ result payloads ([#1928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1928))
-- Change: Update assumed redis versions to v2.8 or v4.0 in the Azure case ([#1929 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1929))
-- Fix: Profiler showing `EVAL` instead `EVALSHA` ([#1930 by martinpotter](https://github.com/StackExchange/StackExchange.Redis/pull/1930))
-- Performance: Moved tiebreaker fetching in connections into the handshake phase (streamline + simplification) ([#1931 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1931))
+- Adds: Makes `StreamEntry` constructor public for better unit test experience ([#1923 by WeihanLi](https://github.com/seredis/StackExchange.Redis/pull/1923))
+- Fix: Integer overflow error (issue #1926) with 2GiB+ result payloads ([#1928 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/1928))
+- Change: Update assumed redis versions to v2.8 or v4.0 in the Azure case ([#1929 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1929))
+- Fix: Profiler showing `EVAL` instead `EVALSHA` ([#1930 by martinpotter](https://github.com/seredis/StackExchange.Redis/pull/1930))
+- Performance: Moved tiebreaker fetching in connections into the handshake phase (streamline + simplification) ([#1931 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1931))
- Stability: Fixed potential disposed object usage around Arenas (pulling in [Piplines.Sockets.Unofficial#63](https://github.com/mgravell/Pipelines.Sockets.Unofficial/pull/63) by MarcGravell)
-- Adds: Thread pool work item stats to exception messages to help diagnose contention ([#1964 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1964))
-- Fix/Performance: Overhauls pub/sub implementation for correctness ([#1947 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1947))
+- Adds: Thread pool work item stats to exception messages to help diagnose contention ([#1964 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1964))
+- Fix/Performance: Overhauls pub/sub implementation for correctness ([#1947 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1947))
- Fixes a race in subscribing right after connected
- Fixes a race in subscribing immediately before a publish
- Fixes subscription routing on clusters (spreading instead of choosing 1 node)
- More correctly reconnects subscriptions on connection failures, including to other endpoints
-- Adds "(vX.X.X)" version suffix to the default client ID so server-side `CLIENT LIST` can more easily see what's connected ([#1985 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1985))
-- Fix: Properly including or excluding key names on some message failures ([#1990 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1990))
-- Fix: Correct return of nil results in `LPOP`, `RPOP`, `SRANDMEMBER`, and `SPOP` ([#1993 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1993))
+- Adds "(vX.X.X)" version suffix to the default client ID so server-side `CLIENT LIST` can more easily see what's connected ([#1985 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1985))
+- Fix: Properly including or excluding key names on some message failures ([#1990 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1990))
+- Fix: Correct return of nil results in `LPOP`, `RPOP`, `SRANDMEMBER`, and `SPOP` ([#1993 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1993))
## 2.2.88
-- Change: Connection backoff default is now exponential instead of linear ([#1896 by lolodi](https://github.com/StackExchange/StackExchange.Redis/pull/1896))
-- Adds: Support for `NodeMaintenanceScaleComplete` event (handles Redis cluster scaling) ([#1902 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1902))
+- Change: Connection backoff default is now exponential instead of linear ([#1896 by lolodi](https://github.com/seredis/StackExchange.Redis/pull/1896))
+- Adds: Support for `NodeMaintenanceScaleComplete` event (handles Redis cluster scaling) ([#1902 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1902))
## 2.2.79
-- NRediSearch: Support on json index ([#1808 by AvitalFineRedis](https://github.com/StackExchange/StackExchange.Redis/pull/1808))
-- NRediSearch: Support sortable TagFields and unNormalizedForm for Tag & Text Fields ([#1862 by slorello89 & AvitalFineRedis](https://github.com/StackExchange/StackExchange.Redis/pull/1862))
-- Fix: Potential errors getting socket bytes ([#1836 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1836))
-- Logging: Adds (.NET Version and timestamps) for better debugging ([#1796 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/1796))
-- Adds: `Condition` APIs (transactions), now supports `StreamLengthEqual` and variants ([#1807 by AlphaGremlin](https://github.com/StackExchange/StackExchange.Redis/pull/1807))
-- Adds: Support for count argument to `ListLeftPop`, `ListLeftPopAsync`, `ListRightPop`, and `ListRightPopAsync` ([#1850 by jjfmarket](https://github.com/StackExchange/StackExchange.Redis/pull/1850))
-- Fix: Potential task/thread exhaustion from the backlog processor ([#1854 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1854))
-- Adds: Support for listening to Azure Maintenance Events ([#1876 by amsoedal](https://github.com/StackExchange/StackExchange.Redis/pull/1876))
-- Adds: `StringGetDelete`/`StringGetDeleteAsync` APIs for Redis `GETDEL` command([#1840 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/1840))
+- NRediSearch: Support on json index ([#1808 by AvitalFineRedis](https://github.com/seredis/StackExchange.Redis/pull/1808))
+- NRediSearch: Support sortable TagFields and unNormalizedForm for Tag & Text Fields ([#1862 by slorello89 & AvitalFineRedis](https://github.com/seredis/StackExchange.Redis/pull/1862))
+- Fix: Potential errors getting socket bytes ([#1836 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1836))
+- Logging: Adds (.NET Version and timestamps) for better debugging ([#1796 by philon-msft](https://github.com/seredis/StackExchange.Redis/pull/1796))
+- Adds: `Condition` APIs (transactions), now supports `StreamLengthEqual` and variants ([#1807 by AlphaGremlin](https://github.com/seredis/StackExchange.Redis/pull/1807))
+- Adds: Support for count argument to `ListLeftPop`, `ListLeftPopAsync`, `ListRightPop`, and `ListRightPopAsync` ([#1850 by jjfmarket](https://github.com/seredis/StackExchange.Redis/pull/1850))
+- Fix: Potential task/thread exhaustion from the backlog processor ([#1854 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/1854))
+- Adds: Support for listening to Azure Maintenance Events ([#1876 by amsoedal](https://github.com/seredis/StackExchange.Redis/pull/1876))
+- Adds: `StringGetDelete`/`StringGetDeleteAsync` APIs for Redis `GETDEL` command([#1840 by WeihanLi](https://github.com/seredis/StackExchange.Redis/pull/1840))
## 2.2.62
-- Stability: Sentinel potential memory leak fix in OnManagedConnectionFailed handler ([#1710 by alexSatov](https://github.com/StackExchange/StackExchange.Redis/pull/1710))
-- Fix: `GetOutstandingCount` could obscure underlying faults by faulting itself ([#1792 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1792))
-- Fix [#1719](https://github.com/StackExchange/StackExchange.Redis/issues/1791): With backlog messages becoming reordered ([#1779 by TimLovellSmith](https://github.com/StackExchange/StackExchange.Redis/pull/1779))
+- Stability: Sentinel potential memory leak fix in OnManagedConnectionFailed handler ([#1710 by alexSatov](https://github.com/seredis/StackExchange.Redis/pull/1710))
+- Fix: `GetOutstandingCount` could obscure underlying faults by faulting itself ([#1792 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/1792))
+- Fix [#1719](https://github.com/seredis/StackExchange.Redis/issues/1791): With backlog messages becoming reordered ([#1779 by TimLovellSmith](https://github.com/seredis/StackExchange.Redis/pull/1779))
## 2.2.50
-- Performance: Optimization for PING accuracy ([#1714 by eduardobr](https://github.com/StackExchange/StackExchange.Redis/pull/1714))
-- Fix: Improvement to reconnect logic (exponential backoff) ([#1735 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/1735))
-- Adds: Refresh replica endpoint list on failover ([#1684 by laurauzcategui](https://github.com/StackExchange/StackExchange.Redis/pull/1684))
-- Fix: `ReconfigureAsync` re-entrancy (caused connection issues) ([#1772 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1772))
-- Fix: `ReconfigureAsync` Sentinel race resulting in NoConnectionAvailable when using DemandMaster ([#1773 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1773))
-- Stability: Resolve race in AUTH and other connection reconfigurations ([#1759 by TimLovellSmith and NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1759))
+- Performance: Optimization for PING accuracy ([#1714 by eduardobr](https://github.com/seredis/StackExchange.Redis/pull/1714))
+- Fix: Improvement to reconnect logic (exponential backoff) ([#1735 by deepakverma](https://github.com/seredis/StackExchange.Redis/pull/1735))
+- Adds: Refresh replica endpoint list on failover ([#1684 by laurauzcategui](https://github.com/seredis/StackExchange.Redis/pull/1684))
+- Fix: `ReconfigureAsync` re-entrancy (caused connection issues) ([#1772 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1772))
+- Fix: `ReconfigureAsync` Sentinel race resulting in NoConnectionAvailable when using DemandMaster ([#1773 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1773))
+- Stability: Resolve race in AUTH and other connection reconfigurations ([#1759 by TimLovellSmith and NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1759))
## 2.2.4
-- Fix: Ambiguous signature of the new `RPUSHX`/`LPUSHX` methods ([#1620 by stefanloerwald](https://github.com/StackExchange/StackExchange.Redis/pull/1620))
+- Fix: Ambiguous signature of the new `RPUSHX`/`LPUSHX` methods ([#1620 by stefanloerwald](https://github.com/seredis/StackExchange.Redis/pull/1620))
## 2.2.3
- Adds: .NET 5 target
-- Fix: Mutex race condition ([#1585 by arsnyder16](https://github.com/StackExchange/StackExchange.Redis/pull/1585))
-- Adds: `CheckCertificateRevocation` can be controlled via the config string ([#1591 by lwlwalker](https://github.com/StackExchange/StackExchange.Redis/pull/1591))
-- Fix: Range end-value inversion ([#1573 by tombatron](https://github.com/StackExchange/StackExchange.Redis/pull/1573))
-- Adds: `ROLE` support ([#1551 by zmj](https://github.com/StackExchange/StackExchange.Redis/pull/1551))
-- Adds: varadic `RPUSHX`/`LPUSHX` support ([#1557 by dmytrohridin](https://github.com/StackExchange/StackExchange.Redis/pull/1557))
-- Fix: Server-selection strategy race condition ([#1532 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/1532))
-- Fix: Sentinel default port ([#1525 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1525))
-- Fix: `Int64` parse scenario ([#1568 by arsnyder16](https://github.com/StackExchange/StackExchange.Redis/pull/1568))
-- Add: Force replication check during failover ([#1563 by aravindyeduvaka & joroda](https://github.com/StackExchange/StackExchange.Redis/pull/1563))
+- Fix: Mutex race condition ([#1585 by arsnyder16](https://github.com/seredis/StackExchange.Redis/pull/1585))
+- Adds: `CheckCertificateRevocation` can be controlled via the config string ([#1591 by lwlwalker](https://github.com/seredis/StackExchange.Redis/pull/1591))
+- Fix: Range end-value inversion ([#1573 by tombatron](https://github.com/seredis/StackExchange.Redis/pull/1573))
+- Adds: `ROLE` support ([#1551 by zmj](https://github.com/seredis/StackExchange.Redis/pull/1551))
+- Adds: varadic `RPUSHX`/`LPUSHX` support ([#1557 by dmytrohridin](https://github.com/seredis/StackExchange.Redis/pull/1557))
+- Fix: Server-selection strategy race condition ([#1532 by deepakverma](https://github.com/seredis/StackExchange.Redis/pull/1532))
+- Fix: Sentinel default port ([#1525 by ejsmith](https://github.com/seredis/StackExchange.Redis/pull/1525))
+- Fix: `Int64` parse scenario ([#1568 by arsnyder16](https://github.com/seredis/StackExchange.Redis/pull/1568))
+- Add: Force replication check during failover ([#1563 by aravindyeduvaka & joroda](https://github.com/seredis/StackExchange.Redis/pull/1563))
- Documentation tweaks (multiple)
-- Fix: Backlog contention issue ([#1612 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1612/), see also [#1574 by devbv](https://github.com/StackExchange/StackExchange.Redis/pull/1574/))
+- Fix: Backlog contention issue ([#1612 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/1612/), see also [#1574 by devbv](https://github.com/seredis/StackExchange.Redis/pull/1574/))
## 2.1.58
@@ -496,28 +496,28 @@ Changes:
## 2.1.50
-- Adds: Bind directly to sentinel-managed instances from a configuration string/object ([#1431 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1431))
-- Adds: `last-delivered-id` to `StreamGroupInfo` ([#1477 by AndyPook](https://github.com/StackExchange/StackExchange.Redis/pull/1477))
-- Change: Update naming of replication-related commands to reflect Redis 5 naming ([#1488 by mgravell](https://github.com/StackExchange/StackExchange.Redis/issues/1488) & [#945 by mgravell](https://github.com/StackExchange/StackExchange.Redis/issues/945))
-- Fix [#1460](https://github.com/StackExchange/StackExchange.Redis/issues/1460): `IServer` commands that are database-specific (`DBSIZE`, `FLUSHDB`, `KEYS`, `SCAN`) now respect the default database on the config ([#1468 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1468))
+- Adds: Bind directly to sentinel-managed instances from a configuration string/object ([#1431 by ejsmith](https://github.com/seredis/StackExchange.Redis/pull/1431))
+- Adds: `last-delivered-id` to `StreamGroupInfo` ([#1477 by AndyPook](https://github.com/seredis/StackExchange.Redis/pull/1477))
+- Change: Update naming of replication-related commands to reflect Redis 5 naming ([#1488 by mgravell](https://github.com/seredis/StackExchange.Redis/issues/1488) & [#945 by mgravell](https://github.com/seredis/StackExchange.Redis/issues/945))
+- Fix [#1460](https://github.com/seredis/StackExchange.Redis/issues/1460): `IServer` commands that are database-specific (`DBSIZE`, `FLUSHDB`, `KEYS`, `SCAN`) now respect the default database on the config ([#1468 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/1468))
- Library updates
## 2.1.39
-- Fix: Mutex around connection was not "fair"; in specific scenario could lead to out-of-order commands ([#1440 by kennygea](https://github.com/StackExchange/StackExchange.Redis/pull/1440))
-- Fix [#1432](https://github.com/StackExchange/StackExchange.Redis/issues/1432): Update dependencies
-- Fix: Timing error on linux ([#1433 by pengweiqhca](https://github.com/StackExchange/StackExchange.Redis/pull/1433))
-- Fix: Add `auth` to command-map for Sentinel ([#1428 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1428))
+- Fix: Mutex around connection was not "fair"; in specific scenario could lead to out-of-order commands ([#1440 by kennygea](https://github.com/seredis/StackExchange.Redis/pull/1440))
+- Fix [#1432](https://github.com/seredis/StackExchange.Redis/issues/1432): Update dependencies
+- Fix: Timing error on linux ([#1433 by pengweiqhca](https://github.com/seredis/StackExchange.Redis/pull/1433))
+- Fix: Add `auth` to command-map for Sentinel ([#1428 by ejsmith](https://github.com/seredis/StackExchange.Redis/pull/1428))
## 2.1.30
-- Build: Fix deterministic builds ([#1420 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1420))
+- Build: Fix deterministic builds ([#1420 by @mgravell](https://github.com/seredis/StackExchange.Redis/pull/1420))
## 2.1.28
- Fix: Stability in new sentinel APIs
-- Fix [#1407](https://github.com/StackExchange/StackExchange.Redis/issues/1407): Include `SslProtocolos` in `ConfigurationOptions.ToString()` ([#1408 by vksampath and Sampath Vuyyuru](https://github.com/StackExchange/StackExchange.Redis/pull/1408))
-- Fix: Clarify messaging around disconnected multiplexers ([#1396 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1396))
+- Fix [#1407](https://github.com/seredis/StackExchange.Redis/issues/1407): Include `SslProtocolos` in `ConfigurationOptions.ToString()` ([#1408 by vksampath and Sampath Vuyyuru](https://github.com/seredis/StackExchange.Redis/pull/1408))
+- Fix: Clarify messaging around disconnected multiplexers ([#1396 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1396))
- Change: Tweak methods of new sentinel API (this is technically a breaking change, but since this is a new API that was pulled quickly, we consider this to be acceptable)
- Adds: New thread `SocketManager` mode (opt-in) to always use the regular thread-pool instead of the dedicated pool
- Adds: Improved counters in/around error messages
@@ -526,31 +526,31 @@ Changes:
## 2.1.0
-- Fix: Ensure active-message is cleared ([#1374 by hamish-omny](https://github.com/StackExchange/StackExchange.Redis/pull/1374))
-- Adds: Sentinel support ([#1067 by shadim](https://github.com/StackExchange/StackExchange.Redis/pull/1067), [#692 by lexxdark](https://github.com/StackExchange/StackExchange.Redis/pull/692))
-- Adds: `IAsyncEnumerable` scanning APIs now supported ([#1087 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1087))
+- Fix: Ensure active-message is cleared ([#1374 by hamish-omny](https://github.com/seredis/StackExchange.Redis/pull/1374))
+- Adds: Sentinel support ([#1067 by shadim](https://github.com/seredis/StackExchange.Redis/pull/1067), [#692 by lexxdark](https://github.com/seredis/StackExchange.Redis/pull/692))
+- Adds: `IAsyncEnumerable` scanning APIs now supported ([#1087 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/1087))
- Adds: New API for use with misbehaving sync-contexts ([more info](https://seredis.dev/ThreadTheft))
-- Adds: `TOUCH` support ([#1291 by gkorland](https://github.com/StackExchange/StackExchange.Redis/pull/1291))
-- Adds: `Condition` API (transactions) now supports `SortedSetLengthEqual` ([#1332 by phosphene47](https://github.com/StackExchange/StackExchange.Redis/pull/1332))
-- Adds: `SocketManager` is now more configurable ([#1115 by naile](https://github.com/StackExchange/StackExchange.Redis/pull/1115))
-- Adds: NRediSearch updated in line with JRediSearch ([#1267 by tombatron](https://github.com/StackExchange/StackExchange.Redis/pull/1267), [#1199 by oruchreis](https://github.com/StackExchange/StackExchange.Redis/pull/1199))
-- Adds: Support for `CheckCertificatRevocation` configuration ([#1234 by BLun78 and V912736](https://github.com/StackExchange/StackExchange.Redis/pull/1234))
-- Adds: More details about exceptions ([#1190 by marafiq](https://github.com/StackExchange/StackExchange.Redis/pull/1190))
-- Adds: Updated `StreamCreateConsumerGroup` methods to use the `MKSTREAM` option ([#1141 via ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/1141))
-- Adds: Support for NOACK in the StreamReadGroup methods ([#1154 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/1154))
-- Adds: Event-args now mockable ([#1326 by n1l](https://github.com/StackExchange/StackExchange.Redis/pull/1326))
-- Fix: No-op when adding 0 values to a set ([#1283 by omeaart](https://github.com/StackExchange/StackExchange.Redis/pull/1283))
-- Adds: Support for `LATENCY` and `MEMORY` ([#1204 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1204))
-- Adds: Support for `HSTRLEN` ([#1241 by eitanhs](https://github.com/StackExchange/StackExchange.Redis/pull/1241))
-- Adds: `GeoRadiusResult` is now mockable ([#1175 by firenero](https://github.com/StackExchange/StackExchange.Redis/pull/1175))
-- Fix: Various documentation fixes ([#1162 by SnakyBeaky](https://github.com/StackExchange/StackExchange.Redis/pull/1162), [#1135 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/1135), [#1203 by caveman-dick](https://github.com/StackExchange/StackExchange.Redis/pull/1203), [#1240 by Excelan](https://github.com/StackExchange/StackExchange.Redis/pull/1240), [#1245 by francoance](https://github.com/StackExchange/StackExchange.Redis/pull/1245), [#1159 by odyth](https://github.com/StackExchange/StackExchange.Redis/pull/1159), [#1311 by DillonAd](https://github.com/StackExchange/StackExchange.Redis/pull/1311), [#1339 by vp89](https://github.com/StackExchange/StackExchange.Redis/pull/1339), [#1336 by ERGeorgiev](https://github.com/StackExchange/StackExchange.Redis/issues/1336))
-- Fix: Rare race-condition around exception data ([#1342 by AdamOutcalt](https://github.com/StackExchange/StackExchange.Redis/pull/1342))
-- Fix: `ScriptEvaluateAsync` keyspace isolation ([#1377 by gliljas](https://github.com/StackExchange/StackExchange.Redis/pull/1377))
-- Fix: F# compatibility enhancements ([#1386 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1386))
-- Fix: Improved `ScriptResult` null support ([#1392 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1392))
-- Fix: Error with DNS resolution breaking endpoint iterator ([#1393 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1393))
-- Tests: Better docker support for tests ([#1389 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1389), [#1391 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1391))
-- Tests: General test improvements ([#1183 by mtreske](https://github.com/StackExchange/StackExchange.Redis/issues/1183), [#1385 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1385), [#1384 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1384))
+- Adds: `TOUCH` support ([#1291 by gkorland](https://github.com/seredis/StackExchange.Redis/pull/1291))
+- Adds: `Condition` API (transactions) now supports `SortedSetLengthEqual` ([#1332 by phosphene47](https://github.com/seredis/StackExchange.Redis/pull/1332))
+- Adds: `SocketManager` is now more configurable ([#1115 by naile](https://github.com/seredis/StackExchange.Redis/pull/1115))
+- Adds: NRediSearch updated in line with JRediSearch ([#1267 by tombatron](https://github.com/seredis/StackExchange.Redis/pull/1267), [#1199 by oruchreis](https://github.com/seredis/StackExchange.Redis/pull/1199))
+- Adds: Support for `CheckCertificatRevocation` configuration ([#1234 by BLun78 and V912736](https://github.com/seredis/StackExchange.Redis/pull/1234))
+- Adds: More details about exceptions ([#1190 by marafiq](https://github.com/seredis/StackExchange.Redis/pull/1190))
+- Adds: Updated `StreamCreateConsumerGroup` methods to use the `MKSTREAM` option ([#1141 via ttingen](https://github.com/seredis/StackExchange.Redis/pull/1141))
+- Adds: Support for NOACK in the StreamReadGroup methods ([#1154 by ttingen](https://github.com/seredis/StackExchange.Redis/pull/1154))
+- Adds: Event-args now mockable ([#1326 by n1l](https://github.com/seredis/StackExchange.Redis/pull/1326))
+- Fix: No-op when adding 0 values to a set ([#1283 by omeaart](https://github.com/seredis/StackExchange.Redis/pull/1283))
+- Adds: Support for `LATENCY` and `MEMORY` ([#1204 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/1204))
+- Adds: Support for `HSTRLEN` ([#1241 by eitanhs](https://github.com/seredis/StackExchange.Redis/pull/1241))
+- Adds: `GeoRadiusResult` is now mockable ([#1175 by firenero](https://github.com/seredis/StackExchange.Redis/pull/1175))
+- Fix: Various documentation fixes ([#1162 by SnakyBeaky](https://github.com/seredis/StackExchange.Redis/pull/1162), [#1135 by ttingen](https://github.com/seredis/StackExchange.Redis/pull/1135), [#1203 by caveman-dick](https://github.com/seredis/StackExchange.Redis/pull/1203), [#1240 by Excelan](https://github.com/seredis/StackExchange.Redis/pull/1240), [#1245 by francoance](https://github.com/seredis/StackExchange.Redis/pull/1245), [#1159 by odyth](https://github.com/seredis/StackExchange.Redis/pull/1159), [#1311 by DillonAd](https://github.com/seredis/StackExchange.Redis/pull/1311), [#1339 by vp89](https://github.com/seredis/StackExchange.Redis/pull/1339), [#1336 by ERGeorgiev](https://github.com/seredis/StackExchange.Redis/issues/1336))
+- Fix: Rare race-condition around exception data ([#1342 by AdamOutcalt](https://github.com/seredis/StackExchange.Redis/pull/1342))
+- Fix: `ScriptEvaluateAsync` keyspace isolation ([#1377 by gliljas](https://github.com/seredis/StackExchange.Redis/pull/1377))
+- Fix: F# compatibility enhancements ([#1386 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1386))
+- Fix: Improved `ScriptResult` null support ([#1392 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/1392))
+- Fix: Error with DNS resolution breaking endpoint iterator ([#1393 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/1393))
+- Tests: Better docker support for tests ([#1389 by ejsmith](https://github.com/seredis/StackExchange.Redis/pull/1389), [#1391 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1391))
+- Tests: General test improvements ([#1183 by mtreske](https://github.com/seredis/StackExchange.Redis/issues/1183), [#1385 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1385), [#1384 by NickCraver](https://github.com/seredis/StackExchange.Redis/pull/1384))
## 2.0.601
@@ -558,15 +558,15 @@ Changes:
## 2.0.600
-- Adds: `ulong` support to `RedisValue` and `RedisResult` ([#1104 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1104))
-- Fix: Remove odd equality: `"-" != 0` (we do, however, still allow `"-0"`, as that is at least semantically valid, and is logically `== 0`) (related to [#1103](https://github.com/StackExchange/StackExchange.Redis/issues/1103))
-- Performance: Rework how pub/sub queues are stored - reduces delegate overheads (related to [#1101](https://github.com/StackExchange/StackExchange.Redis/issues/1101))
-- Fix [#1108](https://github.com/StackExchange/StackExchange.Redis/issues/1108): Ensure that we don't try appending log data to the `TextWriter` once we've returned from a method that accepted one
+- Adds: `ulong` support to `RedisValue` and `RedisResult` ([#1104 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/1104))
+- Fix: Remove odd equality: `"-" != 0` (we do, however, still allow `"-0"`, as that is at least semantically valid, and is logically `== 0`) (related to [#1103](https://github.com/seredis/StackExchange.Redis/issues/1103))
+- Performance: Rework how pub/sub queues are stored - reduces delegate overheads (related to [#1101](https://github.com/seredis/StackExchange.Redis/issues/1101))
+- Fix [#1108](https://github.com/seredis/StackExchange.Redis/issues/1108): Ensure that we don't try appending log data to the `TextWriter` once we've returned from a method that accepted one
## 2.0.593
- Performance: Unify spin-wait usage on sync/async paths to one competitor
-- Fix [#1101](https://github.com/StackExchange/StackExchange.Redis/issues/1101): When a `ChannelMessageQueue` is involved, unsubscribing *via any route* should still unsubscribe and mark the queue-writer as complete
+- Fix [#1101](https://github.com/seredis/StackExchange.Redis/issues/1101): When a `ChannelMessageQueue` is involved, unsubscribing *via any route* should still unsubscribe and mark the queue-writer as complete
## 2.0.588
@@ -586,20 +586,20 @@ Changes:
## 2.0.519
-- Fix [#1007](https://github.com/StackExchange/StackExchange.Redis/issues/1007): Adapt to late changes in the RC streams API ([#983 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/983))
-- Documentation fixes ([#997 by MerelyRBLX](https://github.com/StackExchange/StackExchange.Redis/pull/997), [#1005 by zBrianW](https://github.com/StackExchange/StackExchange.Redis/pull/1005))
+- Fix [#1007](https://github.com/seredis/StackExchange.Redis/issues/1007): Adapt to late changes in the RC streams API ([#983 by mgravell](https://github.com/seredis/StackExchange.Redis/pull/983))
+- Documentation fixes ([#997 by MerelyRBLX](https://github.com/seredis/StackExchange.Redis/pull/997), [#1005 by zBrianW](https://github.com/seredis/StackExchange.Redis/pull/1005))
- Build: Switch to SDK 2.1.500
## 2.0.513
-- Fix [#961](https://github.com/StackExchange/StackExchange.Redis/issues/962): fix assembly binding redirect problems; IMPORTANT: this drops to an older `System.Buffers` version - if you have manually added redirects for `4.0.3.0`, you may need to manually update to `4.0.2.0` (or remove completely)
-- Fix [#962](https://github.com/StackExchange/StackExchange.Redis/issues/962): Avoid NRE in edge-case when fetching bridge
+- Fix [#961](https://github.com/seredis/StackExchange.Redis/issues/962): fix assembly binding redirect problems; IMPORTANT: this drops to an older `System.Buffers` version - if you have manually added redirects for `4.0.3.0`, you may need to manually update to `4.0.2.0` (or remove completely)
+- Fix [#962](https://github.com/seredis/StackExchange.Redis/issues/962): Avoid NRE in edge-case when fetching bridge
## 2.0.505
-- Fix [#943](https://github.com/StackExchange/StackExchange.Redis/issues/943): Ensure transaction inner tasks are completed prior to completing the outer transaction task
-- Fix [#946](https://github.com/StackExchange/StackExchange.Redis/issues/946): Reinstate missing `TryParse` methods on `RedisValue`
-- Fix [#940](https://github.com/StackExchange/StackExchange.Redis/issues/940): Off-by-one on pre-boxed integer cache (NRediSearch)
+- Fix [#943](https://github.com/seredis/StackExchange.Redis/issues/943): Ensure transaction inner tasks are completed prior to completing the outer transaction task
+- Fix [#946](https://github.com/seredis/StackExchange.Redis/issues/946): Reinstate missing `TryParse` methods on `RedisValue`
+- Fix [#940](https://github.com/seredis/StackExchange.Redis/issues/940): Off-by-one on pre-boxed integer cache (NRediSearch)
## 2.0.495
@@ -625,7 +625,7 @@ Changes:
- Fix: A *lot* of general bugs and issues have been resolved
- **Break**: `RedisValue.TryParse` was accidentally omitted in the overhaul; this has been rectified and will be available in the next build
-A more complete list of issues addressed can be seen in [this tracking issue](https://github.com/StackExchange/StackExchange.Redis/issues/871)
+A more complete list of issues addressed can be seen in [this tracking issue](https://github.com/seredis/StackExchange.Redis/issues/871)
Note: we currently have no plans to do an additional `1.*` release. In particular, even though there was a `1.2.7-alpha` build on nuget, we *do not* currently have plans to release `1.2.7`.
@@ -641,21 +641,21 @@ Note: we currently have no plans to do an additional `1.*` release. In particula
## 1.2.4
-- Fix: Incorrect build configuration ([#649 by jrlost](https://github.com/StackExchange/StackExchange.Redis/issues/649))
+- Fix: Incorrect build configuration ([#649 by jrlost](https://github.com/seredis/StackExchange.Redis/issues/649))
## 1.2.3
-- Fix: When using `redis-cluster` with multiple replicas, use round-robin when selecting replica ([#610 by mgravell](https://github.com/StackExchange/StackExchange.Redis/issues/610))
-- Adds: Can specify `NoScriptCache` flag when using `ScriptEvaluate` to bypass all cache features (always uses `EVAL` instead of `SCRIPT LOAD` and `EVALSHA`) ([#617 by Funbit](https://github.com/StackExchange/StackExchange.Redis/issues/617))
+- Fix: When using `redis-cluster` with multiple replicas, use round-robin when selecting replica ([#610 by mgravell](https://github.com/seredis/StackExchange.Redis/issues/610))
+- Adds: Can specify `NoScriptCache` flag when using `ScriptEvaluate` to bypass all cache features (always uses `EVAL` instead of `SCRIPT LOAD` and `EVALSHA`) ([#617 by Funbit](https://github.com/seredis/StackExchange.Redis/issues/617))
## 1.2.2 (preview)
- **Break**: .NET 4.0 support is not in this build, due to [a build issue](https://github.com/dotnet/cli/issues/5993) - looking into solutions
-- Adds: Make performance-counter tracking opt-in (`IncludePerformanceCountersInExceptions`) as it was causing problems ([#587 by AlexanderKot](https://github.com/StackExchange/StackExchange.Redis/issues/587))
-- Adds: Can now specifiy allowed SSL/TLS protocols ([#603 by JonCole](https://github.com/StackExchange/StackExchange.Redis/pull/603))
-- Adds: Track message status in exceptions ([#576 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/576))
+- Adds: Make performance-counter tracking opt-in (`IncludePerformanceCountersInExceptions`) as it was causing problems ([#587 by AlexanderKot](https://github.com/seredis/StackExchange.Redis/issues/587))
+- Adds: Can now specifiy allowed SSL/TLS protocols ([#603 by JonCole](https://github.com/seredis/StackExchange.Redis/pull/603))
+- Adds: Track message status in exceptions ([#576 by deepakverma](https://github.com/seredis/StackExchange.Redis/pull/576))
- Adds: `GetDatabase()` optimization for DB 0 and low numbered databases: `IDatabase` instance is retained and recycled (as long as no `asyncState` is provided)
-- Performance: Improved connection retry policy ([#510 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/510), [#572 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/572))
+- Performance: Improved connection retry policy ([#510 by deepakverma](https://github.com/seredis/StackExchange.Redis/pull/510), [#572 by deepakverma](https://github.com/seredis/StackExchange.Redis/pull/572))
- Adds: `Execute`/`ExecuteAsync` API to support "modules"; [more info](https://blog.marcgravell.com/2017/04/stackexchangeredis-and-redis-40-modules.html)
- Fix: Timeout link fixed re /docs change (below)
- [`NRediSearch`](https://www.nuget.org/packages/NRediSearch/) added as exploration into "modules"
@@ -670,8 +670,8 @@ Note: we currently have no plans to do an additional `1.*` release. In particula
## 1.2.0 (same as 1.2.0-alpha1)
-- Adds: GEO commands ([#489 by wjdavis5](https://github.com/StackExchange/StackExchange.Redis/pull/489))
-- Adds: ZADD support for new NX/XX switches ([#520 by seniorquico](https://github.com/StackExchange/StackExchange.Redis/pull/520))
+- Adds: GEO commands ([#489 by wjdavis5](https://github.com/seredis/StackExchange.Redis/pull/489))
+- Adds: ZADD support for new NX/XX switches ([#520 by seniorquico](https://github.com/seredis/StackExchange.Redis/pull/520))
- Adds: core-clr preview support improvements
## 1.1.608
diff --git a/docs/RespLogging.md b/docs/RespLogging.md
index cb0b84dc0..18ecab2f7 100644
--- a/docs/RespLogging.md
+++ b/docs/RespLogging.md
@@ -107,7 +107,7 @@ If the RESP streams are *not* valid, an exception will provide further details.
**An exception here is strong evidence that there is a fault either in the redis server, or an intermediate proxy**.
Conversely, if the library reported a protocol failure but the validation step here *does not* report an error, then
-that is strong evidence of a library error; [**please report this**](https://github.com/StackExchange/StackExchange.Redis/issues/new) (with details).
+that is strong evidence of a library error; [**please report this**](https://github.com/seredis/StackExchange.Redis/issues/new) (with details).
You can also *replay* the conversation locally, seeing the individual requests and responses:
diff --git a/docs/ServerMaintenanceEvent.md b/docs/ServerMaintenanceEvent.md
index 15ef44c5a..6a372b8d1 100644
--- a/docs/ServerMaintenanceEvent.md
+++ b/docs/ServerMaintenanceEvent.md
@@ -111,10 +111,12 @@ or `ServerDefault` to ask for nothing at all, which is what earlier versions did
| `MIGRATING`, `FAILING_OVER`, `SMIGRATING` | relaxes command timeouts for that server while the disruption lasts |
| `MIGRATED`, `FAILED_OVER` | ends the window, but keeps timeouts relaxed for a short tail while things settle |
| `SMIGRATED` | as above, and re-reads the cluster topology, and re-subscribes any sharded channels whose slots moved |
-| `MOVING` | works out the replacement address, lets in-flight work finish, then replaces the connections |
+| `MOVING` | works out the replacement address, holds new commands back while in-flight ones finish (for up to half the relaxed timeout, with timeouts relaxed meanwhile), then replaces the connections and sends the held commands on the replacement |
So an application that does nothing at all still benefits: commands that would have timed out during a migration are given more room, a moved slot is learned without waiting to be redirected, and a `MOVING` is acted on before the server closes the socket.
+During a `MOVING` handoff, commands the application issues are not written to the connection being retired: they wait in the backlog and are sent on the replacement once it is established, in the order they were issued. This happens whatever the `backlogPolicy`, because that connection is live and the command would otherwise have been written to it; and a `FailFast` policy does not abandon the held work when the old connection is closed, since a handoff is a replacement the client chose rather than a failure. The hold lasts until the commands already in flight have been answered, and at most half of `maintRelaxedTimeout` (five seconds by default), never more than half of what is left of the announced window, and never less than a second. Timeouts are relaxed for the hold and the reconnect, so with relaxation on nothing is timed out by the hold itself; an application that has turned relaxation off (`maintRelaxedTimeout=0`) and runs timeouts shorter than the hold can see held commands time out in the backlog, as they would in the backlog of any other reconnect. A command still in flight when the hold runs out - a long blocking read - is failed by the replacement, as the server's close would have failed it, so that it cannot stall everything else for the rest of the window.
+
### Notifications that arrive as you connect
Redis Enterprise **retains the most recent completion** - `MIGRATED` or `FAILED_OVER` - and replays it to each connection that opts in, so a client that connects after a disruption still learns that it happened. Measured behaviour, worth knowing if you handle these events yourself:
@@ -254,7 +256,7 @@ multiplexer.ServerMaintenanceEvent += (object sender, ServerMaintenanceEvent e)
}
};
```
-You can see the schema for the `AzureMaintenanceEvent` class [here](https://github.com/StackExchange/StackExchange.Redis/blob/main/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs). Note that the library automatically sets the `ReceivedTimeUtc` timestamp when the event is received, so if you see in your logs that `ReceivedTimeUtc` is after `StartTimeUtc`, this may indicate that your connections are under high load.
+You can see the schema for the `AzureMaintenanceEvent` class [here](https://github.com/seredis/StackExchange.Redis/blob/main/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs). Note that the library automatically sets the `ReceivedTimeUtc` timestamp when the event is received, so if you see in your logs that `ReceivedTimeUtc` is after `StartTimeUtc`, this may indicate that your connections are under high load.
## Walking through a sample maintenance event
diff --git a/docs/Testing.md b/docs/Testing.md
index 52776f3b6..a8427f870 100644
--- a/docs/Testing.md
+++ b/docs/Testing.md
@@ -16,7 +16,7 @@ This can be done either by installing Docker and running `docker compose up` in
Tests default to `127.0.0.1` as their server, however you can override any of the test IPs/hostnames and ports by placing a `TestConfig.json` in the `StackExchange.Redis.Tests\` folder. This file is intentionally in `.gitignore` already, as it's for *your* personal overrides. This is useful for testing local or remote servers, different versions, various ports, etc.
-You can find all the JSON properties at [TestConfig.cs](https://github.com/StackExchange/StackExchange.Redis/blob/main/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs). An example override (everything not specified being a default) would look like this:
+You can find all the JSON properties at [TestConfig.cs](https://github.com/seredis/StackExchange.Redis/blob/main/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs). An example override (everything not specified being a default) would look like this:
```json
{
"RunLongRunning": true,
diff --git a/docs/Transactions.md b/docs/Transactions.md
index bf3c5d0c2..846e83f2b 100644
--- a/docs/Transactions.md
+++ b/docs/Transactions.md
@@ -79,7 +79,7 @@ Note that the object returned from `CreateTransaction` only has access to the *a
each operation will not be known until after `Execute` (or `ExecuteAsync`) has completed. If the operations are not applied, all the `Task`s
will be marked as cancelled - otherwise, *after* the command has executed you can fetch the results of each as normal.
-The transaction carries the same command groups as the database (`tran.Strings`, `tran.Hashes`, ...), so commands are queued with the same spelling you would use outside it. There is also an experimental `db.BeginTransaction()` / `db.BeginBatch()` pair, where execution is explicit and leaving a `using` scope discards rather than sends; it is gated behind [SER014](exp/SER014) while its shape settles, so `CreateTransaction()` remains the one to reach for.
+The transaction carries the same command groups as the database (`tran.Strings`, `tran.Hashes`, ...), so commands are queued with the same spelling you would use outside it. There is also an experimental `db.BeginTransaction()` / `db.BeginBatch()` pair, where execution is explicit and leaving a `using` scope discards rather than sends; it is gated behind [SER103](exp/SER103) while its shape settles, so `CreateTransaction()` remains the one to reach for.
The set of available *conditions* is not extensive, but covers the most common scenarios; please contact me (or better: submit a pull-request) if
there are additional conditions that you would like to see.
diff --git a/docs/exp/SER012.md b/docs/exp/SER012.md
index f3311e08a..115a0eee8 100644
--- a/docs/exp/SER012.md
+++ b/docs/exp/SER012.md
@@ -1,41 +1,25 @@
-SER012
--
+The `IServer.ClusterSlotStats*` methods expose `CLUSTER SLOT-STATS` (Redis 8.2 and later); see
+[Cluster slot statistics](../ClusterSlotStats).
-Borrowed reply values: `RespValue`, `IRespBufferOwner`, and every command that hands back a
-`ReadOnlyLease`.
+The server command is stable, but this library binding is new and marked experimental while the API
+shape settles:
-A `RespValue` is a window onto a buffer somebody else owns - normally the lease it arrived in. That is
-what makes a multi-value reply nearly free: one pooled array of windows over one reply buffer, where a
-lease of `RedisValue` is one pooled array **plus an allocation per element** that is neither small nor a
-canonical number. Measured at 77x less garbage for a thousand 32-byte values.
+- The metric set is configuration-dependent (`cluster-slot-stats-enabled`) and documented by the
+ server as extensible; the typed surface (`ClusterSlotStatsMetric`, the properties on
+ `ClusterSlotStatsEntry`) may grow or change as metrics are added.
+- How the per-node results are best aggregated across a cluster (today: ask each node yourself) is an
+ open design question that may reshape the entry points.
-It also means the values are only valid until the owner is disposed. Reading one afterwards throws
-`ObjectDisposedException` rather than returning recycled bytes, but it still throws - so a value that has
-to outlive the lease must be copied out first, with `AsRedisValue()`, `(string?)` or `CopyTo`.
+Until the marker is removed, the API may change between any two versions without breaking-change
+ceremony. If you acknowledge this, you can suppress the warning by adding the following to your
+`csproj` file:
-**This ID is separate from SER010 on purpose.** The interpolated writer is one question ("should commands
-be written this way?"); this is another ("should replies be borrowed rather than owned?"), and they can
-be answered differently. Turning this one off tells you how much of your code depends on the borrowing.
+```xml
+$(NoWarn);SER012
+```
-Settled enough to build on:
+or more granularly / locally in C#:
-- the window-plus-owner shape, and that the owner rather than a `ReadOnlyMemory` is what preserves
- the disposal check
-- scalar-only, enforced at capture, so the accessors are total
-- delegation to `RespReader` for every accessor, so there is no second copy of the parse rules, the
- coercions, or the streamed-chunk walk
-- the lease owning both its array and the reply buffer, releasing both on dispose
-
-Not settled, and the reason this is gated:
-
-- **Equality is payload bytes.** `:1` and `$1\r\n1` are equal because their payloads are the same digit;
- `,1.0` and `:1` are not, where `RedisValue` would coerce. That is the honest rule for a wire-level type
- and a real behaviour difference from what it replaces.
-- **`GetHashCode` on a streamed value returns its length**, which is correct but coarse.
-- **Singular reads still return `RedisValue`.** `Get(key)` is allocation-free already for small values and
- needs no `using`; the large-single-value case is served by `GetLease`. Whether that split is right, or
- whether consistency should win, is open.
-- **Aggregates are not covered.** `RespValue` is scalar-only, so a nested reply - streams, `VLINKS` -
- still materialises. The type these become is the next question, not this one.
-
-To opt in, add `SER012` to `NoWarn`, or suppress it at the call site.
+``` c#
+#pragma warning disable SER012
+```
diff --git a/docs/exp/SER011.md b/docs/exp/SER101.md
similarity index 93%
rename from docs/exp/SER011.md
rename to docs/exp/SER101.md
index 71c2b41f1..74529b71e 100644
--- a/docs/exp/SER011.md
+++ b/docs/exp/SER101.md
@@ -1,4 +1,4 @@
-SER011
+SER101
-
You are constructing a `RespFragment` by hand, from raw bytes.
@@ -20,4 +20,4 @@ The generator produces the framing, the length prefixes and the argument count f
three are correct by construction. That is the only supported way to make one.
If you are the generator - or you genuinely know what you are doing and accept that this is undefined
-behaviour - suppress `SER011` at the point of construction, and nowhere wider.
+behaviour - suppress `SER101` at the point of construction, and nowhere wider.
diff --git a/docs/exp/SER102.md b/docs/exp/SER102.md
new file mode 100644
index 000000000..5ba504c97
--- /dev/null
+++ b/docs/exp/SER102.md
@@ -0,0 +1,41 @@
+SER102
+-
+
+Borrowed reply values: `RespValue`, `IRespBufferOwner`, and every command that hands back a
+`ReadOnlyLease`.
+
+A `RespValue` is a window onto a buffer somebody else owns - normally the lease it arrived in. That is
+what makes a multi-value reply nearly free: one pooled array of windows over one reply buffer, where a
+lease of `RedisValue` is one pooled array **plus an allocation per element** that is neither small nor a
+canonical number. Measured at 77x less garbage for a thousand 32-byte values.
+
+It also means the values are only valid until the owner is disposed. Reading one afterwards throws
+`ObjectDisposedException` rather than returning recycled bytes, but it still throws - so a value that has
+to outlive the lease must be copied out first, with `AsRedisValue()`, `(string?)` or `CopyTo`.
+
+**This ID is separate from SER010 on purpose.** The interpolated writer is one question ("should commands
+be written this way?"); this is another ("should replies be borrowed rather than owned?"), and they can
+be answered differently. Turning this one off tells you how much of your code depends on the borrowing.
+
+Settled enough to build on:
+
+- the window-plus-owner shape, and that the owner rather than a `ReadOnlyMemory` is what preserves
+ the disposal check
+- scalar-only, enforced at capture, so the accessors are total
+- delegation to `RespReader` for every accessor, so there is no second copy of the parse rules, the
+ coercions, or the streamed-chunk walk
+- the lease owning both its array and the reply buffer, releasing both on dispose
+
+Not settled, and the reason this is gated:
+
+- **Equality is payload bytes.** `:1` and `$1\r\n1` are equal because their payloads are the same digit;
+ `,1.0` and `:1` are not, where `RedisValue` would coerce. That is the honest rule for a wire-level type
+ and a real behaviour difference from what it replaces.
+- **`GetHashCode` on a streamed value returns its length**, which is correct but coarse.
+- **Singular reads still return `RedisValue`.** `Get(key)` is allocation-free already for small values and
+ needs no `using`; the large-single-value case is served by `GetLease`. Whether that split is right, or
+ whether consistency should win, is open.
+- **Aggregates are not covered.** `RespValue` is scalar-only, so a nested reply - streams, `VLINKS` -
+ still materialises. The type these become is the next question, not this one.
+
+To opt in, add `SER102` to `NoWarn`, or suppress it at the call site.
diff --git a/docs/exp/SER014.md b/docs/exp/SER103.md
similarity index 99%
rename from docs/exp/SER014.md
rename to docs/exp/SER103.md
index e9d076014..cb4820750 100644
--- a/docs/exp/SER014.md
+++ b/docs/exp/SER103.md
@@ -1,4 +1,4 @@
-SER014
+SER103
-
Batches and transactions on the context surface: `RespBatch`, `RespTransaction`, and the
diff --git a/docs/index.md b/docs/index.md
index bdc311f33..f80abdc5c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,7 +1,7 @@
StackExchange.Redis
===================
-- [Release Notes](https://github.com/StackExchange/StackExchange.Redis/releases) (3.0 onwards; [earlier releases](ReleaseNotes))
+- [Release Notes](https://github.com/seredis/StackExchange.Redis/releases) (3.0 onwards; [earlier releases](ReleaseNotes))
## Overview
@@ -54,6 +54,7 @@ Documentation
- [Vector Sets](VectorSets) - how to use Vector Sets for similarity search with embeddings
- [Hash Import](HImport) - bulk-importing many hashes that share a common set of field names
- [Hash Tags and Slots](HashTags) - co-locating keys in the same cluster slot for multi-key operations
+- [Cluster slot statistics](ClusterSlotStats) - per-slot usage statistics via `CLUSTER SLOT-STATS`
- [Where are `KEYS` / `SCAN` / `FLUSH*`?](KeysScan) - how to use server-based commands
- [Profiling](Profiling) - profiling interfaces, as well as how to profile in an `async` world
- [Scripting](Scripting) - running Lua scripts, including the low-allocation `ScriptEvalLease` API
@@ -74,4 +75,4 @@ Questions and Contributions
If you think you have found a bug or have a feature request, please [report an issue][2], or if appropriate: submit a pull request. If you have a question, feel free to [contact me](https://github.com/mgravell).
[1]: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl
- [2]: https://github.com/StackExchange/StackExchange.Redis/issues?state=open
+ [2]: https://github.com/seredis/StackExchange.Redis/issues?state=open
diff --git a/docs/rules/SER300.md b/docs/rules/SER300.md
index 98a64f588..b8d4eabb9 100644
--- a/docs/rules/SER300.md
+++ b/docs/rules/SER300.md
@@ -60,7 +60,7 @@ about the code - so it is deliberately conservative and stays quiet wherever it
still works, and will keep working: this is a suggestion, not a defect report.
That conservatism is meant to make a false positive rare, not impossible. **If you think the rule has flagged
-something it should not have, please [report it](https://github.com/StackExchange/StackExchange.Redis/issues/new)**,
+something it should not have, please [report it](https://github.com/seredis/StackExchange.Redis/issues/new)**,
including the transaction as written. A rule that fires on correct code is a bug in the rule - and one that
reaches every consumer of the package - so it is worth fixing rather than quietly suppressing.
diff --git a/docs/rules/SER301.md b/docs/rules/SER301.md
index 99ec41abc..4ac15f2ee 100644
--- a/docs/rules/SER301.md
+++ b/docs/rules/SER301.md
@@ -77,7 +77,7 @@ about the code - so it is deliberately conservative and stays quiet wherever it
still works, and will keep working: this is a suggestion, not a defect report.
That conservatism is meant to make a false positive rare, not impossible. **If you think the rule has flagged
-something it should not have, please [report it](https://github.com/StackExchange/StackExchange.Redis/issues/new)**,
+something it should not have, please [report it](https://github.com/seredis/StackExchange.Redis/issues/new)**,
including the transaction as written. A rule that fires on correct code is a bug in the rule - and one that
reaches every consumer of the package - so it is worth fixing rather than quietly suppressing.
diff --git a/docs/rules/SER302.md b/docs/rules/SER302.md
index 271cffc30..e41c0fc89 100644
--- a/docs/rules/SER302.md
+++ b/docs/rules/SER302.md
@@ -54,7 +54,7 @@ about the code - so it is deliberately conservative and stays quiet wherever it
still works, and will keep working: this is a suggestion, not a defect report.
That conservatism is meant to make a false positive rare, not impossible. **If you think the rule has flagged
-something it should not have, please [report it](https://github.com/StackExchange/StackExchange.Redis/issues/new)**,
+something it should not have, please [report it](https://github.com/seredis/StackExchange.Redis/issues/new)**,
including the transaction as written. A rule that fires on correct code is a bug in the rule - and one that
reaches every consumer of the package - so it is worth fixing rather than quietly suppressing.
diff --git a/docs/rules/SER303.md b/docs/rules/SER303.md
index e4152f8d6..646112dc9 100644
--- a/docs/rules/SER303.md
+++ b/docs/rules/SER303.md
@@ -81,7 +81,7 @@ about the code - so it is deliberately conservative and stays quiet wherever it
still works, and will keep working: this is a suggestion, not a defect report.
That conservatism is meant to make a false positive rare, not impossible. **If you think the rule has flagged
-something it should not have, please [report it](https://github.com/StackExchange/StackExchange.Redis/issues/new)**,
+something it should not have, please [report it](https://github.com/seredis/StackExchange.Redis/issues/new)**,
including the transaction as written. A rule that fires on correct code is a bug in the rule - and one that
reaches every consumer of the package - so it is worth fixing rather than quietly suppressing.
diff --git a/docs/rules/SER304.md b/docs/rules/SER304.md
index fc88f4a39..35e3f0d9b 100644
--- a/docs/rules/SER304.md
+++ b/docs/rules/SER304.md
@@ -81,7 +81,7 @@ about the code - so it is deliberately conservative and stays quiet wherever it
still works, and will keep working: this is a suggestion, not a defect report.
That conservatism is meant to make a false positive rare, not impossible. **If you think the rule has flagged
-something it should not have, please [report it](https://github.com/StackExchange/StackExchange.Redis/issues/new)**,
+something it should not have, please [report it](https://github.com/seredis/StackExchange.Redis/issues/new)**,
including the transaction as written. A rule that fires on correct code is a bug in the rule - and one that
reaches every consumer of the package - so it is worth fixing rather than quietly suppressing.
diff --git a/docs/rules/SER305.md b/docs/rules/SER305.md
index 1137971f5..311dd64e3 100644
--- a/docs/rules/SER305.md
+++ b/docs/rules/SER305.md
@@ -79,6 +79,6 @@ or locally:
If you believe it has flagged something that works, that is a bug in the rule and one that reaches every
consumer of the package - please
-[report it](https://github.com/StackExchange/StackExchange.Redis/issues/new) with the code as written.
+[report it](https://github.com/seredis/StackExchange.Redis/issues/new) with the code as written.
See also [Transactions](../Transactions) and [SER306](SER306).
diff --git a/docs/rules/SER309.md b/docs/rules/SER309.md
index dde7e7453..109c66388 100644
--- a/docs/rules/SER309.md
+++ b/docs/rules/SER309.md
@@ -29,7 +29,7 @@ costs nothing and is simply gone.
That is a considered trade rather than an oversight. The damage is **local** - the frame is still well-formed,
with one argument missing, so the server errors or does the wrong thing and the connection is unaffected.
Literals never contributed to the `*N` header, so the framing stays correct either way. Compare
-[SER011](../exp/SER011.md), where hand-written fragment bytes can desync the connection for every *subsequent*
+[SER101](../exp/SER101.md), where hand-written fragment bytes can desync the connection for every *subsequent*
command, and which is guarded far more aggressively for exactly that reason.
## A single space is allowed
diff --git a/docs/rules/SER351.md b/docs/rules/SER351.md
index 572d28e13..cf2c028d4 100644
--- a/docs/rules/SER351.md
+++ b/docs/rules/SER351.md
@@ -52,4 +52,4 @@ generated. If you need a fragment built at runtime rather than from declared tok
## Related
- [SER309](SER309) - literal text in a RESP command, which is what these declarations are usually for
-- [SER011](../exp/SER011.md) - constructing a fragment by hand, which this rule is steering you away from
+- [SER101](../exp/SER101.md) - constructing a fragment by hand, which this rule is steering you away from
diff --git a/docs/rules/index.md b/docs/rules/index.md
index bb44ab607..737e9aaac 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -81,7 +81,7 @@ whatever each rule's own page lists.
These are heuristics, and the list above is where the effort has gone - but it is meant to make a false positive
rare, not impossible. If one of these rules flags something it should not have, that is a bug in the rule rather
than something to work around: please
-[report it](https://github.com/StackExchange/StackExchange.Redis/issues/new) with the transaction as written.
+[report it](https://github.com/seredis/StackExchange.Redis/issues/new) with the transaction as written.
Four rules are not in this family, because they are not suggesting an improvement to working code: `SER350`
reports a build problem, and [SER305](SER305)/[SER306](SER306)/[SER307](../SyncOverAsync) report code that does
diff --git a/eng/StackExchange.Redis.Build/RespFragmentGenerator.cs b/eng/StackExchange.Redis.Build/RespFragmentGenerator.cs
index ac78d9d5f..5960634f0 100644
--- a/eng/StackExchange.Redis.Build/RespFragmentGenerator.cs
+++ b/eng/StackExchange.Redis.Build/RespFragmentGenerator.cs
@@ -17,7 +17,7 @@ namespace StackExchange.Redis.Build;
///
///
/// The point is that framing, length prefixes, casing and the argument count are correct by construction
-/// rather than by review. Hand-written fragments are gated behind SER011 precisely because none of that is
+/// rather than by review. Hand-written fragments are gated behind SER101 precisely because none of that is
/// checkable at the point of use, and malformed bytes desync the connection for every subsequent command.
///
///
@@ -192,9 +192,9 @@ private static void Emit(SourceProductionContext ctx, ImmutableArray").AppendLine()
.Append("// ").Append(nameof(RespFragmentGenerator)).AppendLine()
.AppendLine("#nullable enable")
- // SER011: hand-constructing a fragment. We ARE the sanctioned
+ // SER101: hand-constructing a fragment. We ARE the sanctioned
// construction site, so the suppression belongs here and nowhere wider.
- .AppendLine("#pragma warning disable SER011");
+ .AppendLine("#pragma warning disable SER101");
var writer = new CodeWriter(sb);
if (fragments.IsDefaultOrEmpty) return;
diff --git a/notes/cluster-slot-stats/plan.md b/notes/cluster-slot-stats/plan.md
new file mode 100644
index 000000000..a9c65ab3f
--- /dev/null
+++ b/notes/cluster-slot-stats/plan.md
@@ -0,0 +1,181 @@
+---
+feature: cluster-slot-stats
+decision_class: C
+estimated_size: small-medium
+tracks: redis/redis CLUSTER SLOT-STATS (src/commands/cluster-slot-stats.json, since 8.2.0; implementation src/cluster_slot_stats.c)
+target_version: "8.2"
+unit_targets:
+ - RoundTripUnitTests.ClusterSlotStatsRoundTrip
+ - ClusterSlotStatsTests (live, cluster topology)
+open_questions: 4
+---
+
+# CLUSTER SLOT-STATS: implementation plan
+
+No shared HLD exists for this command (it predates the parity pipeline); the facts below are read
+directly from the server: `src/commands/cluster-slot-stats.json` and `src/cluster_slot_stats.c`
+(`clusterSlotStatsCommand`, `addReplySlotStat`), verified against a live 8.10 cluster
+(`redislabs/client-libs-test`, the repo test topology). `docs/CommandCoverage.md` lists the command
+as `deferred` ("pairs with hot-key tooling"); the hot-key tooling (`HOTKEYS`, `IServer.HotKeys*`)
+has since shipped, so this promotes the row.
+
+## Server facts (all observed)
+
+- `CLUSTER SLOT-STATS `, arity -4,
+ flags STALE LOADING. Strictly a oneof: exactly one filter form per call.
+- **Cluster-only**: a non-cluster server answers `ERR This instance has cluster support disabled`
+ (observed on the topology's standalone node).
+- Only slots assigned to the answering node's **shard** are reported; `SLOTSRANGE` returns them in
+ ascending slot order (observed: a range owned by another shard returns an empty array), `ORDERBY`
+ sorted by the metric (ties: ascending slot), default `DESC`, `LIMIT` default = all (1..16384;
+ out of range → error).
+- Reply: array of `[slot, map]` pairs; the map always carries `key-count`, plus `memory-bytes`,
+ `cpu-usec`, `network-bytes-in`/`network-bytes-out` according to the `cluster-slot-stats-enabled`
+ config (multi-arg enum: `no`/`yes`/`cpu`/`net`/`mem`). Observed: `CONFIG SET
+ cluster-slot-stats-enabled "cpu net"` works at runtime; `yes` (which includes `mem`) is refused
+ at runtime when memory tracking wasn't enabled at boot. A map under RESP3, flat pairs under
+ RESP2 (`addReplyMapLen`).
+- `ORDERBY` by a metric whose collection is disabled → `ERR Unrecognized sort metric for ORDERBY.`
+- Metric wire names: `key-count`, `cpu-usec`, `memory-bytes`, `network-bytes-in`,
+ `network-bytes-out`.
+
+## Traced siblings
+
+- **`CLUSTER SLOTS`** (`ClusterSlots.cs`, `ClusterSlots.ResultProcessor.cs`,
+ `RedisServer.GetClusterSlotsMessage`, `ClusterSlotsTests.cs`) — the verified model for a parsed
+ `CLUSTER` subcommand on `IServer`: partial-interface file named for the feature, message built
+ with `Message.Create(-1, flags.WithCategory(NodeLocalRead), RedisCommand.CLUSTER, ...)`,
+ lenient aggregate parsing (drop a malformed entry, not the reply), `null` result for a nil reply.
+ No `RedisCommand` enum change (CLUSTER exists), no keyspace-isolation changes (server-scoped).
+- **`HOTKEYS`** (`HotKeys.cs`, `HotKeysField.cs`, `HotKeys.ResultProcessor.cs`,
+ `HotKeys.Server.cs`) — the model for the *reply-field* handling: an internal `[AsciiHash]` enum
+ (`HotKeysField`) with a generated `...Metadata.TryParse`, nominal not positional parsing, unknown
+ fields skipped/preserved, `cpu`-style counters exposed as `TimeSpan` via microseconds, and the
+ "profiling data on `IServer`" API shape this command pairs with operationally.
+
+## Requirements
+
+| # | Requirement | Where covered |
+|---|---|---|
+| R.1 | `SLOTSRANGE start end` form | `ClusterSlotStatsBySlotRange(Async)` |
+| R.2 | `ORDERBY metric [LIMIT n] [ASC\|DESC]` form | `ClusterSlotStatsOrderBy(Async)` |
+| R.3 | Reply: array of [slot, metric-map], RESP2 flat pairs / RESP3 map | `ClusterSlotStatsResult.Processor` (category tests, never `Prefix` comparisons) |
+| R.4 | `key-count` always; other metrics config-dependent | nullable properties on `ClusterSlotStatsEntry`; absent → `null` |
+| R.5 | Metrics the library doesn't know (future servers) | preserved in `ClusterSlotStatsEntry.Metadata`, as `ClusterSlotNode.Metadata` does |
+| R.6 | Cluster-only; shard-local answers | XML docs; live test asserts the standalone error and per-node ownership |
+| R.7 | Node-local read, safe to replay against that node | `flags.WithCategory(NodeLocalRead)`, as `GetClusterSlotsMessage` |
+| R.8 | Version gating | `RedisFeatures.ClusterSlotStats` => `v8_2_0_rc1` (8.2 RC1 = 8.1.240, the existing constant) |
+| NF.1 | New public surface is experimental | `[Experimental(Experiments.ClusterSlotStats)]`, new id `SER012` (SER011 is taken by the open set-member-expiry PR #3340) |
+
+## Public API (all `[Experimental("SER012")]`, on `IServer`)
+
+```csharp
+ClusterSlotStatsResult? ClusterSlotStatsBySlotRange(int startSlot, int endSlot, CommandFlags flags = CommandFlags.None);
+Task ClusterSlotStatsBySlotRangeAsync(int startSlot, int endSlot, CommandFlags flags = CommandFlags.None);
+ClusterSlotStatsResult? ClusterSlotStatsOrderBy(ClusterSlotStatsMetric metric, long limit = 0, Order order = Order.Descending, CommandFlags flags = CommandFlags.None);
+Task ClusterSlotStatsOrderByAsync(ClusterSlotStatsMetric metric, long limit = 0, Order order = Order.Descending, CommandFlags flags = CommandFlags.None);
+
+public enum ClusterSlotStatsMetric { KeyCount, CpuTime, MemoryBytes, NetworkBytesIn, NetworkBytesOut }
+
+public sealed class ClusterSlotStatsResult
+{
+ public IReadOnlyList Slots { get; }
+}
+
+public sealed class ClusterSlotStatsEntry
+{
+ public int Slot { get; }
+ public long KeyCount { get; }
+ public TimeSpan? CpuTime { get; } // cpu-usec; null when not collected
+ public long? MemoryBytes { get; } // null when not collected
+ public long? NetworkBytesIn { get; } // null when not collected
+ public long? NetworkBytesOut { get; } // null when not collected
+ public IReadOnlyList> Metadata { get; } // unrecognized metrics, preserved
+}
+```
+
+Grounding: two distinct method names (not overloads) because the grammar is a strict oneof and the
+optional-parameter shapes would otherwise trip RS0026; `Order` is the existing enum (`SORT`,
+sorted-set ranges); `limit = 0` means "don't send `LIMIT`" exactly as `HotKeysStart(count = 0)`
+means "server default"; `CpuTime` as `TimeSpan` from microseconds mirrors
+`HotKeysResult.TotalCpuTime`; the result wrapper + entry list mirrors
+`ClusterSlotsResult.Assignments`; `Metadata` preservation mirrors `ClusterSlotNode.Metadata`.
+Client-side validation: slots in `[SlotRange.MinSlot, SlotRange.MaxSlot]`, `startSlot <= endSlot`,
+`limit >= 0` (`ArgumentOutOfRangeException`), mirroring the server's own refusals.
+
+On the wire, `ASC` is sent only when `Order.Ascending` is requested and `LIMIT` only when
+`limit > 0`: the server defaults are descending/unlimited, and the repo convention (`Expiration`,
+`GetHashFieldExpireMessage`) is to send the minimal form.
+
+## Files
+
+| File | Change |
+|---|---|
+| `src/StackExchange.Redis/RedisLiterals.cs` | add `ORDERBY`, `SLOT_STATS` ("SLOT-STATS"), `SLOTSRANGE` (ASC/DESC/LIMIT exist) |
+| `src/StackExchange.Redis/ClusterSlotStats.cs` | add: `IServer` members, `ClusterSlotStatsMetric`, `ClusterSlotStatsResult`, `ClusterSlotStatsEntry` |
+| `src/StackExchange.Redis/ClusterSlotStatsField.cs` | add: internal `[AsciiHash]` field enum (as `HotKeysField`) |
+| `src/StackExchange.Redis/ClusterSlotStats.ResultProcessor.cs` | add: processor (as `ClusterSlots.ResultProcessor.cs`) |
+| `src/StackExchange.Redis/ClusterSlotStats.Server.cs` | add: `RedisServer` implementations + internal static message builders (as `HotKeys.Server.cs` / `GetClusterSlotsMessage`) |
+| `src/StackExchange.Redis/RedisFeatures.cs` | add `ClusterSlotStats` => `v8_2_0_rc1` |
+| `src/RESPite/Shared/Experiments.cs` | add `ClusterSlotStats = "SER012"` |
+| `Directory.Build.props` (root) | NoWarn += `SER012` |
+| `docs/exp/SER012.md` | add (experiment page) |
+| `docs/ClusterSlotStats.md` + `docs/index.md` | add usage page + index row |
+| `src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt` | `[SER012]`-prefixed entries + bare `RedisFeatures.ClusterSlotStats.get`; sorted by `eng/public-api.py` (RS0016 does not fire in this environment, so the entries are hand-written against the shipped `[SER009]`/branch `[SER011]` precedents) |
+| `docs/CommandCoverage.md` | remove the `CLUSTER SLOT-STATS` `deferred` row |
+| `tests/.../RoundTripUnitTests/ClusterSlotStatsRoundTrip.cs` | add: exact-bytes + parse tests, RESP2/RESP3 |
+| `tests/.../ClusterSlotStatsTests.cs` | add: live cluster tests |
+
+Not applicable: `Enums/RedisCommand.cs` (CLUSTER exists), keyspace isolation (server-scoped, as
+HOTKEYS), transaction analyzer (read-only reporting command; no pre-8.2 MULTI composition exists).
+
+## Ordered steps
+
+1. Literals; field enum; API types file; processor; server file. Build → collect RS0016 strings
+ into `PublicAPI.Unshipped.txt`.
+2. Features flag, experiment id, NoWarn, docs pages, coverage row.
+3. Round-trip tests (message bytes both forms incl. LIMIT/ASC minimality; replies: full map,
+ key-count-only map, unknown-metric preservation, empty array, nil → null) under RESP2 and RESP3.
+4. Live tests against the repo cluster topology (8.9.241 reports as 8.10; ≥ 8.2 requirement met):
+ ownership (a routed write's slot is reported by exactly the owning node with `key-count ≥ 1`),
+ ORDERBY limit/order, config-gated metrics appearing after `CONFIG SET cluster-slot-stats-enabled
+ "cpu net"` (restored in `finally`), disabled-metric ORDERBY error, standalone-server error.
+5. Full build `/p:CI=true`; run the new tests; update this plan's test section with results.
+
+## Older servers
+
+Pre-8.2 answers `ERR Unknown subcommand`; the feature flag lets callers probe. No behavioral
+change for any existing API.
+
+## Open questions
+
+1. **SER012 vs joining an existing experiment** — SER011 is "Server 8.12" on the open #3340 and
+ this is an 8.2 command; a feature-specific id (as SER007/SER010 were) avoids coupling the two
+ PRs. Default: `SER012 = ClusterSlotStats`.
+2. **`limit = 0` sentinel vs `long? limit = null`** — `HotKeysStart(count = 0)` is the precedent
+ next door, so 0-means-default wins; `null` would be the greenfield choice.
+3. **`CpuTime` naming** — the wire says `cpu-usec`; `HotKeysResult` exposes CPU microseconds as
+ `TimeSpan` properties named `*CpuTime*`, so the entry property and the metric enum member follow
+ it rather than the wire name.
+4. **8.12 cluster-topology note (out of scope here)** — 8.12-rc2 introduces
+ `cluster-bus-port-protected-mode` (default on), which refuses to start the repo's cluster nodes;
+ the fix (`cluster-bus-port-protected-mode no` in `tests/RedisConfigs/Cluster/*.conf`) can only
+ land together with an image bump, because servers ≤ 8.10 abort on the unknown directive. This
+ PR stays on the main topology image; the open 8.12 PRs that bump the image need the conf change.
+
+## Test plan
+
+- Round-trip: exact request bytes for `SLOTSRANGE 100 200`, `ORDERBY key-count`, `ORDERBY cpu-usec
+ LIMIT 5`, `ORDERBY network-bytes-out LIMIT 3 ASC`; replies parsed identically from RESP2 flat
+ pairs and RESP3 maps; unknown metric lands in `Metadata`; nil → null result; empty array → empty
+ `Slots`.
+- Live (cluster topology, `TestConfig.Current.ClusterServersAndPorts`): see step 4.
+- Results (2026-11-07, local): round-trip 10/10 pass (`ClusterSlotStatsRoundTrip`, includes the
+ argument-validation facts). Live 12/12 pass, zero skips (`ClusterSlotStatsTests`, 6 tests ×
+ RESP2/RESP3 against the repo cluster topology, server 8.9.241): the routed write was counted in
+ its slot, whole-range answers stayed shard-local and ascending, ORDERBY honoured LIMIT and both
+ orders, `CONFIG SET cluster-slot-stats-enabled "cpu net"` surfaced `CpuTime`/`NetworkBytes*`
+ (with `MemoryBytes` still null, and the config restored), ORDERBY on the disabled `mem` metric was
+ refused with the ORDERBY error, and the standalone node refused with "cluster support disabled".
+ Full solution build clean (0 warnings, 0 errors) across net461/net472/netstandard2.0/net8.0/net10.0;
+ `eng/public-api.py --check` passes.
diff --git a/notes/lag-aware/findings.md b/notes/lag-aware/findings.md
index 26ccdb84a..2822ce780 100644
--- a/notes/lag-aware/findings.md
+++ b/notes/lag-aware/findings.md
@@ -329,7 +329,7 @@ wrong twice over**: Redis ships a fault injection service for exactly this, *and
test tier built on it.*
**`tests/StackExchange.Redis.FaultInjector.Tests`** exists on `marc/maint-optin-server`, open as
-**[#3191](https://github.com/StackExchange/StackExchange.Redis/pull/3191)** (107 files, +13,160,
+**[#3191](https://github.com/seredis/StackExchange.Redis/pull/3191)** (107 files, +13,160,
unmerged at the time of writing). Per its README it drives *"a real Redis Enterprise deployment
through the fault injector, and watch[es] how SE.Redis reacts … the tier that can observe what no
in-process fake can: real DNS, real TLS identity, real timing."* Gating is
diff --git a/notes/lag-aware/plan.md b/notes/lag-aware/plan.md
index 9faef4b75..e91ec0b63 100644
--- a/notes/lag-aware/plan.md
+++ b/notes/lag-aware/plan.md
@@ -33,7 +33,7 @@ See [`api-sketch.md`](api-sketch.md).
`tests/StackExchange.Redis.FaultInjector.Tests` is present here now. The rest of this section records
why the wait was worth it.
-[#3191](https://github.com/StackExchange/StackExchange.Redis/pull/3191) ("Server-native maintenance
+[#3191](https://github.com/seredis/StackExchange.Redis/pull/3191) ("Server-native maintenance
notifications") is open, 107 files, +13,160. It carries
`tests/StackExchange.Redis.FaultInjector.Tests`, and with it the three things this work would
otherwise have to build from scratch:
diff --git a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt
index a68e8f01c..9434a7ed0 100644
--- a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt
@@ -1,55 +1,55 @@
#nullable enable
-[SER012]RESPite.Messages.IRespBufferOwner
-[SER012]RESPite.Messages.IRespBufferOwner.GetReadOnlySpan() -> System.ReadOnlySpan
-[SER012]RESPite.Messages.RespAggregate
-[SER012]RESPite.Messages.RespAggregate.Count.get -> int
-[SER012]static RESPite.Messages.RespAggregate.Empty.get -> RESPite.Messages.RespAggregate
-[SER012]RESPite.Messages.RespAggregate.Enumerator
-[SER012]RESPite.Messages.RespAggregate.Enumerator.Current.get -> T
-[SER012]RESPite.Messages.RespAggregate.Enumerator.Enumerator() -> void
-[SER012]RESPite.Messages.RespAggregate.Enumerator.MoveNext() -> bool
-[SER012]RESPite.Messages.RespAggregate.GetEnumerator() -> RESPite.Messages.RespAggregate.Enumerator
-[SER012]RESPite.Messages.RespAggregate.Prefix.get -> RESPite.Messages.RespPrefix
-[SER012]RESPite.Messages.RespAggregate.RespAggregate() -> void
-[SER012]RESPite.Messages.RespAggregate.ToArray() -> T[]!
-[SER012]override RESPite.Messages.RespAggregate.ToString() -> string!
-[SER012]static RESPite.Messages.RespAggregate.TryCaptureNext(object? owner, scoped ref RESPite.Messages.RespReader reader, RESPite.Messages.RespReader.Projection
///
- public const string HandWrittenRespFragment = "SER011";
+ public const string HandWrittenRespFragment = "SER101"; // renumbered from SER011 when 3.x reserved that for Server_8_12
///
/// Borrowed reply values: RespValue and the commands that hand back leases of them.
@@ -51,13 +58,13 @@ internal static class Experiments
/// can be answered differently. Turning this one off says how much of a codebase depends on the
/// borrowing.
///
- public const string BorrowedValues = "SER012";
+ public const string BorrowedValues = "SER102"; // renumbered from SER012 when 3.x released that as ClusterSlotStats
///
/// Batches and transactions on the context surface: RespBatch, RespTransaction, and
/// BeginBatch/BeginTransaction.
///
- public const string Batching = "SER014";
+ public const string Batching = "SER103"; // renumbered from SER014, clearing the whole contested range
// ReSharper restore InconsistentNaming
diff --git a/src/StackExchange.Redis/BitFieldOperation.Resp.cs b/src/StackExchange.Redis/BitFieldOperation.Resp.cs
index 788adb763..916f08ba6 100644
--- a/src/StackExchange.Redis/BitFieldOperation.Resp.cs
+++ b/src/StackExchange.Redis/BitFieldOperation.Resp.cs
@@ -54,9 +54,9 @@ internal void WriteTo(scoped ref RespRequestBuilder handler, ref BitFieldOverflo
if (_kind != OperationKind.Get && Overflow != overflow)
{
overflow = Overflow;
-#pragma warning disable SER011 // pre-framed constants owned by this type; see Expiration for the reasoning
+#pragma warning disable SER101 // pre-framed constants owned by this type; see Expiration for the reasoning
handler.AppendFormatted(new RespFragment(OverflowResp(overflow), argCount: 2));
-#pragma warning restore SER011
+#pragma warning restore SER101
}
var kind = KindResp;
@@ -68,9 +68,9 @@ internal void WriteTo(scoped ref RespRequestBuilder handler, ref BitFieldOverflo
throw new InvalidOperationException($"A default {nameof(BitFieldOperation)} is not a valid operation.");
}
-#pragma warning disable SER011 // as above
+#pragma warning disable SER101 // as above
handler.AppendFormatted(new RespFragment(kind));
-#pragma warning restore SER011
+#pragma warning restore SER101
Encoding.WriteTo(ref handler);
Offset.WriteTo(ref handler);
if (_kind != OperationKind.Get) handler.AppendFormatted(Value);
diff --git a/src/StackExchange.Redis/ClusterSlotStats.Parse.cs b/src/StackExchange.Redis/ClusterSlotStats.Parse.cs
new file mode 100644
index 000000000..b18c61148
--- /dev/null
+++ b/src/StackExchange.Redis/ClusterSlotStats.Parse.cs
@@ -0,0 +1,112 @@
+using System.Collections.Generic;
+using RESPite;
+using RESPite.Messages;
+using StackExchange.Redis.Protocol;
+
+namespace StackExchange.Redis;
+
+public sealed partial class ClusterSlotStatsResult
+{
+ /// Reads a CLUSTER SLOT-STATS reply on the RESP context.
+ internal sealed class Handler : IRespHandler
+ {
+ internal static readonly Handler Instance = new();
+
+ public ClusterSlotStatsResult? Parse(ref RespReader reader)
+ {
+ if (reader.IsNull) return null;
+ if (!reader.IsAggregate) throw new RespException("Unexpected CLUSTER SLOT-STATS reply.");
+
+ // the reply is an array of [slot, metric-map]; parse leniently, dropping anything
+ // malformed rather than failing the whole reply
+ List? entries = null;
+ var slots = reader.AggregateChildren();
+ while (slots.MoveNext())
+ {
+ if (TryParseEntry(ref slots.Value, out var entry))
+ {
+ (entries ??= new List()).Add(entry);
+ }
+ }
+
+ return new ClusterSlotStatsResult(AsReadOnly(entries));
+ }
+ }
+
+ private static bool TryParseEntry(ref RespReader reader, out ClusterSlotStatsEntry entry)
+ {
+ entry = null!;
+ if (!reader.IsAggregate || reader.IsNull) return false;
+
+ var children = reader.AggregateChildren();
+ if (!children.MoveNext() || !children.Value.TryReadInt64(out var slot)) return false;
+ if (slot < SlotRange.MinSlot || slot > SlotRange.MaxSlot) return false;
+ if (!children.MoveNext() || !children.Value.IsAggregate || children.Value.IsNull) return false;
+
+ long keyCount = 0, cpuMicroseconds = -1, memoryBytes = -1, networkBytesIn = -1, networkBytesOut = -1;
+ List>? metadata = null;
+
+ // the metric map: flat pairs under RESP2, a map under RESP3; read nominally, never
+ // positionally - the set is configuration-dependent and documented as extensible
+ var fields = children.Value.AggregateChildren();
+ while (fields.MoveNext() && fields.Value.IsScalar)
+ {
+ ClusterSlotStatsField field;
+ string? unknownName = null;
+ unsafe
+ {
+ if (!fields.Value.TryParseScalar(&ClusterSlotStatsFieldMetadata.TryParse, out field))
+ {
+ field = ClusterSlotStatsField.Unknown;
+ }
+ }
+ if (field == ClusterSlotStatsField.Unknown) unknownName = fields.Value.ReadString();
+ if (!fields.MoveNext()) break; // metric name with no value: out of data
+ if (!fields.Value.TryReadInt64(out var value)) continue; // a non-numeric metric: skipped
+
+ switch (field)
+ {
+ case ClusterSlotStatsField.KeyCount:
+ keyCount = value;
+ break;
+ case ClusterSlotStatsField.CpuMicroseconds:
+ cpuMicroseconds = value;
+ break;
+ case ClusterSlotStatsField.MemoryBytes:
+ memoryBytes = value;
+ break;
+ case ClusterSlotStatsField.NetworkBytesIn:
+ networkBytesIn = value;
+ break;
+ case ClusterSlotStatsField.NetworkBytesOut:
+ networkBytesOut = value;
+ break;
+ default:
+ // a metric added by a future server: preserved, not discarded
+ if (unknownName is not null)
+ {
+ (metadata ??= new List>()).Add(new(unknownName, value));
+ }
+ break;
+ }
+ }
+
+ entry = new ClusterSlotStatsEntry(
+ (int)slot,
+ keyCount,
+ cpuMicroseconds,
+ memoryBytes,
+ networkBytesIn,
+ networkBytesOut,
+ AsReadOnly(metadata));
+ return true;
+ }
+
+ // empty collection expressions to an interface target compile to Array.Empty(), so the common case
+ // costs nothing; only a genuinely populated list pays for the wrapper
+ private static IReadOnlyList AsReadOnly(List? values)
+ {
+ if (values is null) return [];
+ return values.AsReadOnly();
+ }
+}
diff --git a/src/StackExchange.Redis/ClusterSlotStats.Server.cs b/src/StackExchange.Redis/ClusterSlotStats.Server.cs
new file mode 100644
index 000000000..0aaba6a6c
--- /dev/null
+++ b/src/StackExchange.Redis/ClusterSlotStats.Server.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Threading.Tasks;
+using StackExchange.Redis.Protocol;
+
+namespace StackExchange.Redis;
+
+internal partial class RedisServer
+{
+ // CLUSTER SLOT-STATS: a node-local read, as CLUSTER SLOTS is - it reports what the answering node
+ // tracks for itself, so it is safe to replay against that node.
+ public ClusterSlotStatsResult? ClusterSlotStatsBySlotRange(int startSlot, int endSlot, CommandFlags flags = CommandFlags.None)
+ => Wait(SendClusterSlotStatsBySlotRange(startSlot, endSlot, flags));
+
+ public Task ClusterSlotStatsBySlotRangeAsync(int startSlot, int endSlot, CommandFlags flags = CommandFlags.None)
+ => SendClusterSlotStatsBySlotRange(startSlot, endSlot, flags).AsTask(asyncState, flags);
+
+ public ClusterSlotStatsResult? ClusterSlotStatsOrderBy(ClusterSlotStatsMetric metric, long limit = 0, Order order = Order.Descending, CommandFlags flags = CommandFlags.None)
+ => Wait(SendClusterSlotStatsOrderBy(metric, limit, order, flags));
+
+ public Task ClusterSlotStatsOrderByAsync(ClusterSlotStatsMetric metric, long limit = 0, Order order = Order.Descending, CommandFlags flags = CommandFlags.None)
+ => SendClusterSlotStatsOrderBy(metric, limit, order, flags).AsTask(asyncState, flags);
+
+ /// Sends CLUSTER SLOT-STATS SLOTSRANGE start end.
+ private ValueTask SendClusterSlotStatsBySlotRange(int startSlot, int endSlot, CommandFlags flags)
+ => Context.Raw.SendAsync(
+ $"{RedisCommand.CLUSTER}{ClusterSlotStatsSlotsRangeOperand.Create(startSlot, endSlot)}",
+ flags.WithRetryCategory(RespServerRetry.NodeLocalRead),
+ ClusterSlotStatsResult.Handler.Instance);
+
+ /// Sends CLUSTER SLOT-STATS ORDERBY metric [LIMIT n] [ASC].
+ private ValueTask SendClusterSlotStatsOrderBy(ClusterSlotStatsMetric metric, long limit, Order order, CommandFlags flags)
+ => Context.Raw.SendAsync(
+ $"{RedisCommand.CLUSTER}{ClusterSlotStatsOrderByOperand.Create(metric, limit, order)}",
+ flags.WithRetryCategory(RespServerRetry.NodeLocalRead),
+ ClusterSlotStatsResult.Handler.Instance);
+
+ /// The arguments of CLUSTER SLOT-STATS SLOTSRANGE: an inclusive slot range.
+ internal readonly struct ClusterSlotStatsSlotsRangeOperand(int startSlot, int endSlot) : IRespArgument
+ {
+ /// Validates and builds; the operand itself writes blindly, so the checking happens once, here.
+ internal static ClusterSlotStatsSlotsRangeOperand Create(int startSlot, int endSlot)
+ {
+ if (startSlot < SlotRange.MinSlot || startSlot > SlotRange.MaxSlot) throw new ArgumentOutOfRangeException(nameof(startSlot));
+ if (endSlot < startSlot || endSlot > SlotRange.MaxSlot) throw new ArgumentOutOfRangeException(nameof(endSlot));
+ return new(startSlot, endSlot);
+ }
+
+ public void WriteTo(scoped ref RespRequestBuilder handler)
+ {
+ handler.AppendFormatted(RespLiterals.SlotStats);
+ handler.AppendFormatted(RespLiterals.SlotsRange);
+ handler.AppendFormatted((RedisValue)startSlot);
+ handler.AppendFormatted((RedisValue)endSlot);
+ }
+ }
+
+ ///
+ /// The arguments of CLUSTER SLOT-STATS ORDERBY: the metric (as the server spells it, lower-case
+ /// and hyphenated), then LIMIT and ASC only when they say something the server default
+ /// (all slots, descending) does not.
+ ///
+ internal readonly struct ClusterSlotStatsOrderByOperand(ClusterSlotStatsMetric metric, long limit, Order order) : IRespArgument
+ {
+ ///
+ internal static ClusterSlotStatsOrderByOperand Create(ClusterSlotStatsMetric metric, long limit, Order order)
+ {
+ if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit));
+ if (metric is < ClusterSlotStatsMetric.KeyCount or > ClusterSlotStatsMetric.NetworkBytesOut) throw new ArgumentOutOfRangeException(nameof(metric));
+ return new(metric, limit, order);
+ }
+
+ public void WriteTo(scoped ref RespRequestBuilder handler)
+ {
+ handler.AppendFormatted(RespLiterals.SlotStats);
+ handler.AppendFormatted(RespLiterals.OrderBy);
+ handler.AppendFormatted(metric switch
+ {
+ ClusterSlotStatsMetric.CpuTime => RespLiterals.CpuUsecMetric,
+ ClusterSlotStatsMetric.MemoryBytes => RespLiterals.MemoryBytesMetric,
+ ClusterSlotStatsMetric.NetworkBytesIn => RespLiterals.NetworkBytesInMetric,
+ ClusterSlotStatsMetric.NetworkBytesOut => RespLiterals.NetworkBytesOutMetric,
+ _ => RespLiterals.KeyCountMetric,
+ });
+
+ if (limit > 0)
+ {
+ handler.AppendFormatted(RespLiterals.Limit);
+ handler.AppendFormatted((RedisValue)limit);
+ }
+
+ if (order != Order.Descending) handler.AppendFormatted(RespLiterals.Asc);
+ }
+ }
+}
diff --git a/src/StackExchange.Redis/ClusterSlotStats.cs b/src/StackExchange.Redis/ClusterSlotStats.cs
new file mode 100644
index 000000000..d7b3136ca
--- /dev/null
+++ b/src/StackExchange.Redis/ClusterSlotStats.cs
@@ -0,0 +1,163 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+public partial interface IServer
+{
+ ///
+ /// Obtains per-slot usage statistics from CLUSTER SLOT-STATS SLOTSRANGE, for the slots in an
+ /// inclusive range that are assigned to the answering node's shard, in ascending slot order.
+ ///
+ /// The first slot of the range (inclusive).
+ /// The last slot of the range (inclusive); must not be less than .
+ /// The command flags to use.
+ ///
+ /// Cluster-only: a non-cluster server answers with an error. Each node reports only the slots its own
+ /// shard serves, so slots in the range owned elsewhere are simply absent - ask each node to see the whole
+ /// cluster. key-count is always collected; the other metrics appear only when enabled via the
+ /// cluster-slot-stats-enabled server configuration - see .
+ ///
+ [Experimental(Experiments.ClusterSlotStats, UrlFormat = Experiments.UrlFormat)]
+ ClusterSlotStatsResult? ClusterSlotStatsBySlotRange(int startSlot, int endSlot, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.ClusterSlotStats, UrlFormat = Experiments.UrlFormat)]
+ Task ClusterSlotStatsBySlotRangeAsync(int startSlot, int endSlot, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Obtains per-slot usage statistics from CLUSTER SLOT-STATS ORDERBY: the answering node's shard's
+ /// slots, sorted by a metric (ties broken by ascending slot number).
+ ///
+ /// The metric to sort by. Sorting by a metric whose collection is not enabled on the
+ /// server (via cluster-slot-stats-enabled) is refused by the server;
+ /// is always available.
+ /// The maximum number of slots to report; zero (the default) reports them all.
+ /// The sort direction; the server (and this) defaults to ,
+ /// which puts the busiest slots first.
+ /// The command flags to use.
+ ///
+ [Experimental(Experiments.ClusterSlotStats, UrlFormat = Experiments.UrlFormat)]
+ ClusterSlotStatsResult? ClusterSlotStatsOrderBy(ClusterSlotStatsMetric metric, long limit = 0, Order order = Order.Descending, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.ClusterSlotStats, UrlFormat = Experiments.UrlFormat)]
+ Task ClusterSlotStatsOrderByAsync(ClusterSlotStatsMetric metric, long limit = 0, Order order = Order.Descending, CommandFlags flags = CommandFlags.None);
+}
+
+///
+/// A metric tracked per slot by CLUSTER SLOT-STATS.
+///
+[Experimental(Experiments.ClusterSlotStats, UrlFormat = Experiments.UrlFormat)]
+public enum ClusterSlotStatsMetric
+{
+ /// The number of keys in the slot (key-count); always collected.
+ KeyCount,
+
+ /// CPU time spent on the slot's keys (cpu-usec); collected when the cpu metric is enabled.
+ CpuTime,
+
+ /// Memory used by the slot's keys (memory-bytes); collected when the mem metric is
+ /// enabled, which additionally requires memory tracking from startup.
+ MemoryBytes,
+
+ /// Network bytes received for the slot's keys (network-bytes-in); collected when the net metric is enabled.
+ NetworkBytesIn,
+
+ /// Network bytes sent for the slot's keys (network-bytes-out); collected when the net metric is enabled.
+ NetworkBytesOut,
+}
+
+///
+/// The per-slot usage statistics reported by CLUSTER SLOT-STATS, as described by the node that answered.
+///
+[Experimental(Experiments.ClusterSlotStats, UrlFormat = Experiments.UrlFormat)]
+public sealed partial class ClusterSlotStatsResult
+{
+ internal ClusterSlotStatsResult(IReadOnlyList slots) => Slots = slots;
+
+ ///
+ /// Gets the per-slot entries, in the order the server reported them: ascending slot number for the
+ /// SLOTSRANGE form, metric order for the ORDERBY form.
+ ///
+ public IReadOnlyList Slots { get; }
+}
+
+///
+/// One slot's usage statistics from CLUSTER SLOT-STATS.
+///
+///
+/// Only is always collected; each other metric is null when the server is not
+/// collecting it (the cluster-slot-stats-enabled configuration), which is deliberately distinct from a
+/// collected zero.
+///
+[Experimental(Experiments.ClusterSlotStats, UrlFormat = Experiments.UrlFormat)]
+public sealed class ClusterSlotStatsEntry
+{
+ internal ClusterSlotStatsEntry(
+ int slot,
+ long keyCount,
+ long cpuMicroseconds,
+ long memoryBytes,
+ long networkBytesIn,
+ long networkBytesOut,
+ IReadOnlyList> metadata)
+ {
+ Slot = slot;
+ KeyCount = keyCount;
+ CpuMicroseconds = cpuMicroseconds;
+ MemoryBytes = memoryBytes < 0 ? null : memoryBytes;
+ NetworkBytesIn = networkBytesIn < 0 ? null : networkBytesIn;
+ NetworkBytesOut = networkBytesOut < 0 ? null : networkBytesOut;
+ Metadata = metadata;
+ }
+
+ ///
+ /// Gets the hash slot this entry describes.
+ ///
+ public int Slot { get; }
+
+ ///
+ /// Gets the number of keys in the slot.
+ ///
+ public long KeyCount { get; }
+
+ internal long CpuMicroseconds { get; } = -1;
+
+ ///
+ /// Gets the CPU time spent serving the slot's keys, or null when the cpu metric is not
+ /// being collected.
+ ///
+ public TimeSpan? CpuTime => CpuMicroseconds < 0 ? null : HotKeysResult.NonNegativeMicroseconds(CpuMicroseconds);
+
+ ///
+ /// Gets the memory used by the slot's keys, or null when the mem metric is not being
+ /// collected.
+ ///
+ public long? MemoryBytes { get; }
+
+ ///
+ /// Gets the network bytes received for the slot's keys, or null when the net metric is not
+ /// being collected.
+ ///
+ public long? NetworkBytesIn { get; }
+
+ ///
+ /// Gets the network bytes sent for the slot's keys, or null when the net metric is not
+ /// being collected.
+ ///
+ public long? NetworkBytesOut { get; }
+
+ ///
+ /// Gets the metric entries reported for this slot that this library does not recognize. The metric set is
+ /// configuration-dependent and has grown before, so these are preserved rather than discarded; the
+ /// recognized metrics are surfaced as the typed properties instead of appearing here.
+ ///
+ public IReadOnlyList> Metadata { get; }
+
+ ///
+ public override string ToString() => $"slot {Slot}: {KeyCount} keys";
+}
diff --git a/src/StackExchange.Redis/ClusterSlotStatsField.cs b/src/StackExchange.Redis/ClusterSlotStatsField.cs
new file mode 100644
index 000000000..ee9cc1bd0
--- /dev/null
+++ b/src/StackExchange.Redis/ClusterSlotStatsField.cs
@@ -0,0 +1,55 @@
+using System;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+///
+/// Metrics that can appear in a CLUSTER SLOT-STATS per-slot map.
+///
+internal enum ClusterSlotStatsField
+{
+ ///
+ /// Unknown or unrecognized metric.
+ ///
+ [AsciiHash("")]
+ Unknown = 0,
+
+ ///
+ /// The number of keys in the slot.
+ ///
+ [AsciiHash("key-count")]
+ KeyCount,
+
+ ///
+ /// CPU time spent on the slot's keys, in microseconds.
+ ///
+ [AsciiHash("cpu-usec")]
+ CpuMicroseconds,
+
+ ///
+ /// Memory used by the slot's keys, in bytes.
+ ///
+ [AsciiHash("memory-bytes")]
+ MemoryBytes,
+
+ ///
+ /// Network bytes received for the slot's keys.
+ ///
+ [AsciiHash("network-bytes-in")]
+ NetworkBytesIn,
+
+ ///
+ /// Network bytes sent for the slot's keys.
+ ///
+ [AsciiHash("network-bytes-out")]
+ NetworkBytesOut,
+}
+
+///
+/// Metadata and parsing methods for ClusterSlotStatsField.
+///
+internal static partial class ClusterSlotStatsFieldMetadata
+{
+ [AsciiHash]
+ internal static partial bool TryParse(ReadOnlySpan value, out ClusterSlotStatsField field);
+}
diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs
index ab65eb095..6080e8d69 100644
--- a/src/StackExchange.Redis/ConnectionMultiplexer.cs
+++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs
@@ -1924,7 +1924,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog
ValueStopwatch? watch = null;
int iterCount = first ? 2 : 1;
- // This is fix for https://github.com/StackExchange/StackExchange.Redis/issues/300
+ // This is fix for https://github.com/seredis/StackExchange.Redis/issues/300
// auto discoverability of cluster nodes is made synchronous.
// We try to connect to endpoints specified inside the user provided configuration
// and when we encounter an endpoint to which we are able to successfully connect,
diff --git a/src/StackExchange.Redis/Contexts/RespConnectionManager.cs b/src/StackExchange.Redis/Contexts/RespConnectionManager.cs
index 85d71bd48..93dd886d4 100644
--- a/src/StackExchange.Redis/Contexts/RespConnectionManager.cs
+++ b/src/StackExchange.Redis/Contexts/RespConnectionManager.cs
@@ -2034,6 +2034,39 @@ internal bool HasCallerWork(EndPoint endpoint)
|| (_subscriptions.TryGetValue(endpoint, out var subscription) && subscription.HasCallerWork());
}
+ /// Whether a caller is waiting for a reply on either of this core's connections to an endpoint; backlogs are not counted.
+ /// The endpoint.
+ ///
+ internal bool HasCallerMessagesInFlight(EndPoint endpoint)
+ {
+ if (endpoint is null) return false;
+
+ return (_endpoints.TryGetValue(endpoint, out var interactive) && interactive.HasCallerMessagesInFlight())
+ || (_subscriptions.TryGetValue(endpoint, out var subscription) && subscription.HasCallerMessagesInFlight());
+ }
+
+ /// Starts holding new caller work to an endpoint in the backlog, while a handoff drains its connections.
+ /// The endpoint being handed off.
+ ///
+ internal void BeginHandoffHold(EndPoint endpoint)
+ {
+ if (endpoint is null) return;
+
+ if (_endpoints.TryGetValue(endpoint, out var interactive)) interactive.BeginHandoffHold();
+ if (_subscriptions.TryGetValue(endpoint, out var subscription)) subscription.BeginHandoffHold();
+ }
+
+ /// Ends an endpoint's handoff hold; its connections have been replaced (or there were none).
+ /// The endpoint that was handed off.
+ ///
+ internal void EndHandoffHold(EndPoint endpoint)
+ {
+ if (endpoint is null) return;
+
+ if (_endpoints.TryGetValue(endpoint, out var interactive)) interactive.EndHandoffHold();
+ if (_subscriptions.TryGetValue(endpoint, out var subscription)) subscription.EndHandoffHold();
+ }
+
/// Replace this core's connections to an endpoint, because it is being moved.
/// The endpoint being handed off.
/// Whether there was anything to replace.
diff --git a/src/StackExchange.Redis/Contexts/RespEndpointExecutor.cs b/src/StackExchange.Redis/Contexts/RespEndpointExecutor.cs
index 014293c31..cc5efa2a2 100644
--- a/src/StackExchange.Redis/Contexts/RespEndpointExecutor.cs
+++ b/src/StackExchange.Redis/Contexts/RespEndpointExecutor.cs
@@ -127,6 +127,65 @@ internal bool IsDisposed
/// Whoever is waiting for the write slot, in the order they asked.
private Queue>? _writeWaiters;
+ ///
+ /// Whether a MOVING handoff is draining this executor's connection: new caller work goes to the
+ /// backlog instead of a connection about to be replaced, and the backlog is not flushed while it stands.
+ ///
+ ///
+ /// Holding new work back is what makes the drain finish: under steady traffic there is always a command
+ /// in flight, so a drain that waits for "no caller work" waits out the whole announced window and then
+ /// recycles with work in flight - the failure the handoff exists to remove. Internal traffic (probes,
+ /// keep-alives) still goes to the old connection, which is alive and serving; it must not be held behind
+ /// a hold whose end it cannot see.
+ ///
+ private bool _handoffHold;
+
+ /// Whether the hold diverts this command: a caller's work, while the hold stands.
+ private bool ShouldHoldForHandoff(CommandFlags flags)
+ => _handoffHold && (flags & CommandFlagsInternal.InternalCallFlag) == 0;
+
+ /// Starts holding new caller work in the backlog; see .
+ internal void BeginHandoffHold()
+ {
+ lock (_sync) { _handoffHold = true; }
+ }
+
+ /// Ends the hold; the connection it was draining has been replaced (or there was none).
+ ///
+ /// The usual case is that the connection has just been recycled and the reconnect's establishment
+ /// flushes the held work; this covers the one where nothing was recycled, so that nothing else would.
+ ///
+ internal void EndHandoffHold()
+ {
+ bool drain;
+ lock (_sync)
+ {
+ _handoffHold = false;
+ var live = _connection is { IsClosed: false };
+ var queued = _backlog is { Count: > 0 };
+ drain = live && queued && !_writeSlotHeld;
+ // held work enqueued against a live connection never asked for a dial; if that connection
+ // died without being recycled, somebody has to
+ if (!live && queued && !_disposed) EnsureConnecting();
+ }
+
+ if (drain) DrainBacklog();
+ }
+
+ /// Whether a caller is waiting for a reply on the current connection; the backlog is not counted.
+ ///
+ /// What the handoff's drain actually waits for. Distinct from , which
+ /// also counts the backlog - where the hold itself is putting work, so a drain waiting on it would be
+ /// waiting on its own shadow.
+ ///
+ internal bool HasCallerMessagesInFlight()
+ {
+ lock (_sync)
+ {
+ return _connection is { IsClosed: false } live && live.UnfinishedPendingCount > 0;
+ }
+ }
+
/// Create an executor that owns a connection to one endpoint.
/// Establishes and hands back a ready-to-use connection.
/// The database this executor sends to.
@@ -872,7 +931,7 @@ internal override RespPayload Send(RespRequest preamble, RespRequest request, IR
RespConnection? connection;
lock (_sync)
{
- connection = _disposed || _writeSlotHeld ? null : _connection;
+ connection = _disposed || _writeSlotHeld || _handoffHold ? null : _connection;
// No connection yet (or somebody holds the write slot), so there is nothing to pair ON.
// Do what an executor WITHOUT the capability does - send them in sequence - rather than
@@ -1339,9 +1398,9 @@ private bool Enqueue(RespPayloadOperation operation)
if (_disposed) return false;
connection = _connection;
- if (connection is null || connection.IsClosed || _writeSlotHeld)
+ if (connection is null || connection.IsClosed || _writeSlotHeld || ShouldHoldForHandoff(operation.Flags))
{
- if (!_queueWhileDisconnected && !_writeSlotHeld && !NeverConnected) return false;
+ if (!_queueWhileDisconnected && !_writeSlotHeld && !NeverConnected && !ShouldHoldForHandoff(operation.Flags)) return false;
operation.MarkQueued();
(_backlog ??= new()).Enqueue(operation);
@@ -1844,7 +1903,10 @@ private void KeepAlive()
{
if (Server is not { } server) return;
- var always = server.Multiplexer.RawConfig.HeartbeatConsistencyChecks;
+ // Interactive only, as the v3 bridge gated it: the check exists to notice a dropped stream on the
+ // connection carrying caller traffic, and the subscription connection keeps the idle schedule -
+ // which is also what keeps HeartbeatConsistencyCheckPingsAsync's operation count honest.
+ var always = server.Multiplexer.RawConfig.HeartbeatConsistencyChecks && !IsSubscriptionEndpoint;
if (!always)
{
var writeEverySeconds = server.WriteEverySeconds;
@@ -2124,11 +2186,13 @@ private RespPayloadOperation Dispatch(
}
connection = _connection;
- if (connection is null || connection.IsClosed || _writeSlotHeld)
+ if (connection is null || connection.IsClosed || _writeSlotHeld || ShouldHoldForHandoff(request.Flags))
{
// a claim is deliberately handled by the SAME branch as no-connection: both mean
- // "not writable right now", and both are answered by the backlog, in arrival order
- if (!_queueWhileDisconnected && !_writeSlotHeld && !NeverConnected)
+ // "not writable right now", and both are answered by the backlog, in arrival order.
+ // A handoff hold is the same shape again - and a FailFast policy does not abandon held
+ // work for the handoff's own disconnect: the hold IS the plan for delivering it.
+ if (!_queueWhileDisconnected && !_writeSlotHeld && !NeverConnected && !ShouldHoldForHandoff(request.Flags))
{
operation.EnsureFaulted(request.Flags, NoConnection(request.Command, operation.CommandAndKey));
return operation;
@@ -2361,8 +2425,9 @@ internal override void ReleaseWrites()
lock (_sync)
{
// re-checked under the lock rather than assumed: freeing the slot with anything still
- // queued would reopen the overtaking window the slot exists to close, one level down
- if (_connection is { IsClosed: false } && _backlog is { Count: > 0 }) continue;
+ // queued would reopen the overtaking window the slot exists to close, one level down.
+ // Not during a handoff hold, whose drain declines, or this would spin until it ends.
+ if (_connection is { IsClosed: false } && _backlog is { Count: > 0 } && !_handoffHold) continue;
if (_writeWaiters is { Count: > 0 })
{
@@ -2387,8 +2452,9 @@ private void DrainBacklog()
lock (_sync)
{
// only drain what a live connection can take; otherwise leave it for the connect,
- // which is the path that already knows how
- if (_connection is not { IsClosed: false } || _backlog is not { Count: > 0 }) return;
+ // which is the path that already knows how. A handoff hold declines outright: what is
+ // queued is being held AWAY from this connection, for its replacement.
+ if (_connection is not { IsClosed: false } || _backlog is not { Count: > 0 } || _handoffHold) return;
waiting = _backlog;
_backlog = null;
diff --git a/src/StackExchange.Redis/Contexts/RespOperationBatch.cs b/src/StackExchange.Redis/Contexts/RespOperationBatch.cs
index c7f61da05..613737adb 100644
--- a/src/StackExchange.Redis/Contexts/RespOperationBatch.cs
+++ b/src/StackExchange.Redis/Contexts/RespOperationBatch.cs
@@ -75,7 +75,7 @@ internal sealed class RespOperationBatchExecutor : RespExecutorBase
/// Whether executing leaves the batch open for more. The shipped always was:
/// Execute took what was pending and anything queued afterwards went into the next run, so
/// code that holds one batch and executes it repeatedly is legitimate and must keep working. The
- /// SER014 is execute-or-discard, and copies of it share this queue, so it
+ /// SER103 is execute-or-discard, and copies of it share this queue, so it
/// stays one-shot.
///
internal RespOperationBatchExecutor(RespExecutorBase inner, bool reusable = false)
diff --git a/src/StackExchange.Redis/Contexts/RespTransaction.cs b/src/StackExchange.Redis/Contexts/RespTransaction.cs
index 834f48487..486f37239 100644
--- a/src/StackExchange.Redis/Contexts/RespTransaction.cs
+++ b/src/StackExchange.Redis/Contexts/RespTransaction.cs
@@ -79,7 +79,7 @@ internal sealed class RespTransactionExecutor : RespExecutorBase, ITransactionEx
///
/// Whether executing leaves the transaction open for another. The shipped
/// always was - commands and conditions added after Execute went into a fresh queue - so the
- /// adapter over it asks for this; the SER014 does not.
+ /// adapter over it asks for this; the SER103 does not.
///
internal RespTransactionExecutor(RespExecutorBase inner, RespContext? context = null, bool reusable = false)
{
diff --git a/src/StackExchange.Redis/Expiration.cs b/src/StackExchange.Redis/Expiration.cs
index f0c702c5e..3ee298f17 100644
--- a/src/StackExchange.Redis/Expiration.cs
+++ b/src/StackExchange.Redis/Expiration.cs
@@ -325,11 +325,11 @@ void IRespArgument.WriteTo(scoped ref RespRequestBuilder handler)
var operand = OperandResp;
if (operand.IsEmpty) return; // Expiration.Default contributes no arguments
- // SER011 gates hand-written pre-framed fragments, because nothing validates the claim that the
+ // SER101 gates hand-written pre-framed fragments, because nothing validates the claim that the
// bytes are correctly framed. These are compile-time constants owned by this type and shared with
// the MessageWriter path (OperandResp), so the claim is as checked as it can be - and they are
// ALREADY framed, so AppendBulk, which frames what it is given, is not the right primitive.
-#pragma warning disable SER011
+#pragma warning disable SER101
handler.AppendFormatted(new RespFragment(operand));
if (HasExpirationValue)
{
@@ -337,6 +337,6 @@ void IRespArgument.WriteTo(scoped ref RespRequestBuilder handler)
var enx = ExpireIfNotExistsResp;
if (!enx.IsEmpty) handler.AppendFormatted(new RespFragment(enx));
}
-#pragma warning restore SER011
+#pragma warning restore SER101
}
}
diff --git a/src/StackExchange.Redis/FrameworkShims.IsExternalInit.cs b/src/StackExchange.Redis/FrameworkShims.IsExternalInit.cs
index 417975050..fd245abcc 100644
--- a/src/StackExchange.Redis/FrameworkShims.IsExternalInit.cs
+++ b/src/StackExchange.Redis/FrameworkShims.IsExternalInit.cs
@@ -1,7 +1,7 @@
using System.ComponentModel;
#if NET5_0_OR_GREATER
-// context: https://github.com/StackExchange/StackExchange.Redis/issues/2619
+// context: https://github.com/seredis/StackExchange.Redis/issues/2619
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
#else
diff --git a/src/StackExchange.Redis/Protocol/RespCommand.cs b/src/StackExchange.Redis/Protocol/RespCommand.cs
index 36481f73f..d24f648d1 100644
--- a/src/StackExchange.Redis/Protocol/RespCommand.cs
+++ b/src/StackExchange.Redis/Protocol/RespCommand.cs
@@ -109,7 +109,7 @@ public static class RespCommands
///
/// Validation happens here rather than per call, and rejects anything that would desynchronise the
/// connection if it reached the wire - which is the whole reason hand-built fragments are gated
- /// behind SER011. Here the library does the framing, so the caller cannot get it wrong; only
+ /// behind SER101. Here the library does the framing, so the caller cannot get it wrong; only
/// the name is theirs, and it is checked.
///
/// The command name, for example "GET" or "FT.SEARCH".
@@ -162,7 +162,7 @@ public static RespCommand Command(this string name, bool preform = false)
/// matches on
/// bytes directly, so this needs no transcoding even for known commands. Note this takes the command
/// name, not framed RESP: the framing is ours to get right, which is what separates this from
- /// a hand-built RespFragment and its SER011 gate.
+ /// a hand-built RespFragment and its SER101 gate.
///
public static RespCommand Command(this ReadOnlySpan name)
{
@@ -185,7 +185,7 @@ private static void Validate(char c, string? name)
if (c > ' ' && c <= '~') return;
// a CR, LF or space reaching the wire desynchronises the connection for every command that
- // follows - the SER011 hazard - so it is rejected here, once, rather than risked per call
+ // follows - the SER101 hazard - so it is rejected here, once, rather than risked per call
throw new ArgumentException(
$"Command names must be printable ASCII without whitespace; got '{name ?? ""}'.",
nameof(name));
diff --git a/src/StackExchange.Redis/Protocol/RespFragment.cs b/src/StackExchange.Redis/Protocol/RespFragment.cs
index e401c8cd4..6469c3cfc 100644
--- a/src/StackExchange.Redis/Protocol/RespFragment.cs
+++ b/src/StackExchange.Redis/Protocol/RespFragment.cs
@@ -33,7 +33,7 @@ public readonly ref struct RespFragment
/// wider.
///
// NOTE: no Message = here - ExperimentalAttribute.Message is .NET 9+, and this has to compile on
- // net461/netstandard2.0/net472/net8.0 too. SER011.md carries the explanation; UrlFormat links to it.
+ // net461/netstandard2.0/net472/net8.0 too. SER101.md carries the explanation; UrlFormat links to it.
[Experimental(Experiments.HandWrittenRespFragment, UrlFormat = Experiments.UrlFormat)]
public RespFragment(ReadOnlySpan bytes, int argCount = 1)
{
@@ -129,13 +129,13 @@ public static RespFragment CreateValidated(ReadOnlySpan bytes, int argCoun
if (found != argCount) throw Malformed($"contains {found} bulk string(s), but {argCount} was declared");
- // SER011 guards hand-constructing a fragment, because nothing else checks that the bytes really
+ // SER101 guards hand-constructing a fragment, because nothing else checks that the bytes really
// are well-formed RESP. THIS is the code that checks - everything above this line is that check -
// so this call site is the sanctioned one, and it is the reason the guard can be strict
// everywhere else. Scoped to the single statement rather than the file, deliberately.
-#pragma warning disable SER011
+#pragma warning disable SER101
return new RespFragment(bytes, argCount);
-#pragma warning restore SER011
+#pragma warning restore SER101
}
private static ArgumentException Malformed(string detail)
diff --git a/src/StackExchange.Redis/Protocol/RespLiterals.cs b/src/StackExchange.Redis/Protocol/RespLiterals.cs
index cce9cfcb1..d186c49e2 100644
--- a/src/StackExchange.Redis/Protocol/RespLiterals.cs
+++ b/src/StackExchange.Redis/Protocol/RespLiterals.cs
@@ -8,7 +8,7 @@ namespace StackExchange.Redis
///
/// The author writes the declaration; RespFragmentGenerator emits the bytes, the length
/// prefixes and the argument count, so all three are correct by construction rather than by review.
- /// Hand-written fragments are gated behind SER011 precisely because a mistake in any of them desyncs
+ /// Hand-written fragments are gated behind SER101 precisely because a mistake in any of them desyncs
/// the connection, with the first symptom appearing somewhere unrelated.
///
///
@@ -90,6 +90,38 @@ internal static partial class RespLiterals
[Resp]
internal static partial RespFragment Slots { get; }
+ /// The SLOT-STATS sub-command of CLUSTER.
+ [Resp("SLOT-STATS")]
+ internal static partial RespFragment SlotStats { get; }
+
+ /// The SLOTSRANGE operand of CLUSTER SLOT-STATS; an inclusive slot range follows it.
+ [Resp]
+ internal static partial RespFragment SlotsRange { get; }
+
+ /// The ORDERBY operand of CLUSTER SLOT-STATS; a metric name follows it.
+ [Resp]
+ internal static partial RespFragment OrderBy { get; }
+
+ /// The key-count metric of CLUSTER SLOT-STATS ORDERBY; the server spells the metrics lower-case.
+ [Resp("key-count")]
+ internal static partial RespFragment KeyCountMetric { get; }
+
+ /// The cpu-usec metric of CLUSTER SLOT-STATS ORDERBY.
+ [Resp("cpu-usec")]
+ internal static partial RespFragment CpuUsecMetric { get; }
+
+ /// The memory-bytes metric of CLUSTER SLOT-STATS ORDERBY.
+ [Resp("memory-bytes")]
+ internal static partial RespFragment MemoryBytesMetric { get; }
+
+ /// The network-bytes-in metric of CLUSTER SLOT-STATS ORDERBY.
+ [Resp("network-bytes-in")]
+ internal static partial RespFragment NetworkBytesInMetric { get; }
+
+ /// The network-bytes-out metric of CLUSTER SLOT-STATS ORDERBY.
+ [Resp("network-bytes-out")]
+ internal static partial RespFragment NetworkBytesOutMetric { get; }
+
/// The IDX operand of LCS.
[Resp]
internal static partial RespFragment Idx { get; }
diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
index b011ba575..560c29e96 100644
--- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
@@ -25,6 +25,9 @@ static StackExchange.Redis.Arrays.SetAsync(this StackExchange.Redis.RespArrays a
static StackExchange.Redis.Arrays.SetAsync(this StackExchange.Redis.RespArrays arrays, StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex index, System.ReadOnlySpan values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
static StackExchange.Redis.Arrays.SetAsync(this StackExchange.Redis.RespArrays arrays, StackExchange.Redis.RedisKey key, System.ReadOnlySpan entries, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
static StackExchange.Redis.Availability.DatabaseExtensions.WithRetry(this StackExchange.Redis.RespDatabaseContext context, StackExchange.Redis.Availability.RetryPolicy? retryPolicy = null) -> StackExchange.Redis.RespDatabaseContext
+StackExchange.Redis.Availability.MultiGroupOptions.Builder.ClientCache.get -> StackExchange.Redis.Caching.CacheOptions?
+StackExchange.Redis.Availability.MultiGroupOptions.Builder.ClientCache.set -> void
+StackExchange.Redis.Availability.MultiGroupOptions.ClientCache.get -> StackExchange.Redis.Caching.CacheOptions?
StackExchange.Redis.Bitmaps
static StackExchange.Redis.Bitmaps.CountAsync(this StackExchange.Redis.RespBitmaps bitmaps, StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.StringIndexType indexType = StackExchange.Redis.StringIndexType.Byte, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
static StackExchange.Redis.Bitmaps.FieldAsync(this StackExchange.Redis.RespBitmaps bitmaps, StackExchange.Redis.RedisKey key, StackExchange.Redis.BitFieldOperation operation, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
@@ -38,17 +41,17 @@ StackExchange.Redis.Caching.CacheAdmission.Default = 0 -> StackExchange.Redis.Ca
StackExchange.Redis.Caching.CacheAdmission.OnFirstMiss = 1 -> StackExchange.Redis.Caching.CacheAdmission
StackExchange.Redis.Caching.CacheAdmission.OnRepeatedMiss = 2 -> StackExchange.Redis.Caching.CacheAdmission
StackExchange.Redis.Caching.CacheKeyRemovalReason
+StackExchange.Redis.Caching.CacheKeyRemovalReason.Evicted = 4 -> StackExchange.Redis.Caching.CacheKeyRemovalReason
+StackExchange.Redis.Caching.CacheKeyRemovalReason.Expired = 3 -> StackExchange.Redis.Caching.CacheKeyRemovalReason
+StackExchange.Redis.Caching.CacheKeyRemovalReason.Flushed = 5 -> StackExchange.Redis.Caching.CacheKeyRemovalReason
StackExchange.Redis.Caching.CacheKeyRemovalReason.Invalidated = 0 -> StackExchange.Redis.Caching.CacheKeyRemovalReason
StackExchange.Redis.Caching.CacheKeyRemovalReason.LocalWrite = 1 -> StackExchange.Redis.Caching.CacheKeyRemovalReason
StackExchange.Redis.Caching.CacheKeyRemovalReason.Replaced = 2 -> StackExchange.Redis.Caching.CacheKeyRemovalReason
-StackExchange.Redis.Caching.CacheKeyRemovalReason.Expired = 3 -> StackExchange.Redis.Caching.CacheKeyRemovalReason
-StackExchange.Redis.Caching.CacheKeyRemovalReason.Evicted = 4 -> StackExchange.Redis.Caching.CacheKeyRemovalReason
-StackExchange.Redis.Caching.CacheKeyRemovalReason.Flushed = 5 -> StackExchange.Redis.Caching.CacheKeyRemovalReason
StackExchange.Redis.Caching.CacheKeyRemovedHandler
virtual StackExchange.Redis.Caching.CacheKeyRemovedHandler.Invoke(System.ReadOnlySpan key, StackExchange.Redis.Caching.CacheKeyRemovalReason reason) -> void
+StackExchange.Redis.Caching.CacheOptions
StackExchange.Redis.Caching.CacheOptions.Admission.get -> StackExchange.Redis.Caching.CacheAdmission
StackExchange.Redis.Caching.CacheOptions.Admission.init -> void
-StackExchange.Redis.Caching.CacheOptions
StackExchange.Redis.Caching.CacheOptions.CacheOptions() -> void
static StackExchange.Redis.Caching.CacheOptions.Default.get -> StackExchange.Redis.Caching.CacheOptions!
StackExchange.Redis.Caching.CacheOptions.DefaultPolicy.get -> StackExchange.Redis.Caching.CachePolicy!
@@ -70,15 +73,12 @@ StackExchange.Redis.Caching.CacheOptions.SweepInterval.init -> void
StackExchange.Redis.Caching.CacheOptions.TrackingMode.get -> StackExchange.Redis.Caching.CacheTrackingMode
StackExchange.Redis.Caching.CacheOptions.TrackingMode.init -> void
StackExchange.Redis.Caching.CachePolicy
-StackExchange.Redis.Caching.CachePolicy.CachePolicy() -> void
StackExchange.Redis.Caching.CachePolicy.$() -> StackExchange.Redis.Caching.CachePolicy!
+StackExchange.Redis.Caching.CachePolicy.CachePolicy() -> void
+static StackExchange.Redis.Caching.CachePolicy.Default.get -> StackExchange.Redis.Caching.CachePolicy!
StackExchange.Redis.Caching.CachePolicy.Equals(StackExchange.Redis.Caching.CachePolicy? other) -> bool
override StackExchange.Redis.Caching.CachePolicy.Equals(object? obj) -> bool
override StackExchange.Redis.Caching.CachePolicy.GetHashCode() -> int
-override StackExchange.Redis.Caching.CachePolicy.ToString() -> string!
-static StackExchange.Redis.Caching.CachePolicy.operator !=(StackExchange.Redis.Caching.CachePolicy? left, StackExchange.Redis.Caching.CachePolicy? right) -> bool
-static StackExchange.Redis.Caching.CachePolicy.operator ==(StackExchange.Redis.Caching.CachePolicy? left, StackExchange.Redis.Caching.CachePolicy? right) -> bool
-static StackExchange.Redis.Caching.CachePolicy.Default.get -> StackExchange.Redis.Caching.CachePolicy!
StackExchange.Redis.Caching.CachePolicy.InvalidationGracePeriod.get -> System.TimeSpan
StackExchange.Redis.Caching.CachePolicy.InvalidationGracePeriod.init -> void
StackExchange.Redis.Caching.CachePolicy.RefreshAfter.get -> System.TimeSpan
@@ -89,22 +89,25 @@ StackExchange.Redis.Caching.CachePolicy.Storage.get -> StackExchange.Redis.Cachi
StackExchange.Redis.Caching.CachePolicy.Storage.init -> void
StackExchange.Redis.Caching.CachePolicy.TimeToLive.get -> System.TimeSpan
StackExchange.Redis.Caching.CachePolicy.TimeToLive.init -> void
+override StackExchange.Redis.Caching.CachePolicy.ToString() -> string!
+static StackExchange.Redis.Caching.CachePolicy.operator !=(StackExchange.Redis.Caching.CachePolicy? left, StackExchange.Redis.Caching.CachePolicy? right) -> bool
+static StackExchange.Redis.Caching.CachePolicy.operator ==(StackExchange.Redis.Caching.CachePolicy? left, StackExchange.Redis.Caching.CachePolicy? right) -> bool
StackExchange.Redis.Caching.CacheStorage
-StackExchange.Redis.Caching.CacheStorage.Local = 0 -> StackExchange.Redis.Caching.CacheStorage
StackExchange.Redis.Caching.CacheStorage.External = 1 -> StackExchange.Redis.Caching.CacheStorage
+StackExchange.Redis.Caching.CacheStorage.Local = 0 -> StackExchange.Redis.Caching.CacheStorage
StackExchange.Redis.Caching.CacheTrackingMode
StackExchange.Redis.Caching.CacheTrackingMode.Broadcast = 2 -> StackExchange.Redis.Caching.CacheTrackingMode
StackExchange.Redis.Caching.CacheTrackingMode.Default = 1 -> StackExchange.Redis.Caching.CacheTrackingMode
StackExchange.Redis.Caching.CacheTrackingMode.KeyspaceNotifications = 6 -> StackExchange.Redis.Caching.CacheTrackingMode
StackExchange.Redis.Caching.CacheTrackingMode.None = 0 -> StackExchange.Redis.Caching.CacheTrackingMode
StackExchange.Redis.Caching.CacheTrackingMode.Tracking = 3 -> StackExchange.Redis.Caching.CacheTrackingMode
-StackExchange.Redis.Caching.CacheTrackingMode.TrackingOptIn = 4 -> StackExchange.Redis.Caching.CacheTrackingMode
StackExchange.Redis.Caching.CacheTrackingMode.TrackingByPrefix = 5 -> StackExchange.Redis.Caching.CacheTrackingMode
+StackExchange.Redis.Caching.CacheTrackingMode.TrackingOptIn = 4 -> StackExchange.Redis.Caching.CacheTrackingMode
StackExchange.Redis.Caching.ClientCache
StackExchange.Redis.Caching.ClientCache.Bytes.get -> long
StackExchange.Redis.Caching.ClientCache.Count.get -> int
-StackExchange.Redis.Caching.ClientCache.Evicted.get -> long
StackExchange.Redis.Caching.ClientCache.EffectiveTrackingMode.get -> StackExchange.Redis.Caching.CacheTrackingMode
+StackExchange.Redis.Caching.ClientCache.Evicted.get -> long
StackExchange.Redis.Caching.ClientCache.Expired.get -> long
StackExchange.Redis.Caching.ClientCache.Invalidate(StackExchange.Redis.RedisKey key) -> bool
StackExchange.Redis.Caching.ClientCache.Invalidate(System.ReadOnlySpan key) -> bool
@@ -115,6 +118,23 @@ StackExchange.Redis.Caching.ClientCache.Refreshes.get -> long
StackExchange.Redis.Caching.ClientCache.ServedStale.get -> long
StackExchange.Redis.Caching.ClientCache.Stored.get -> long
StackExchange.Redis.Caching.ClientCache.TrackedKeys.get -> int
+[SER012]StackExchange.Redis.ClusterSlotStatsEntry
+[SER012]StackExchange.Redis.ClusterSlotStatsEntry.CpuTime.get -> System.TimeSpan?
+[SER012]StackExchange.Redis.ClusterSlotStatsEntry.KeyCount.get -> long
+[SER012]StackExchange.Redis.ClusterSlotStatsEntry.MemoryBytes.get -> long?
+[SER012]StackExchange.Redis.ClusterSlotStatsEntry.Metadata.get -> System.Collections.Generic.IReadOnlyList>!
+[SER012]StackExchange.Redis.ClusterSlotStatsEntry.NetworkBytesIn.get -> long?
+[SER012]StackExchange.Redis.ClusterSlotStatsEntry.NetworkBytesOut.get -> long?
+[SER012]StackExchange.Redis.ClusterSlotStatsEntry.Slot.get -> int
+[SER012]override StackExchange.Redis.ClusterSlotStatsEntry.ToString() -> string!
+[SER012]StackExchange.Redis.ClusterSlotStatsMetric
+[SER012]StackExchange.Redis.ClusterSlotStatsMetric.CpuTime = 1 -> StackExchange.Redis.ClusterSlotStatsMetric
+[SER012]StackExchange.Redis.ClusterSlotStatsMetric.KeyCount = 0 -> StackExchange.Redis.ClusterSlotStatsMetric
+[SER012]StackExchange.Redis.ClusterSlotStatsMetric.MemoryBytes = 2 -> StackExchange.Redis.ClusterSlotStatsMetric
+[SER012]StackExchange.Redis.ClusterSlotStatsMetric.NetworkBytesIn = 3 -> StackExchange.Redis.ClusterSlotStatsMetric
+[SER012]StackExchange.Redis.ClusterSlotStatsMetric.NetworkBytesOut = 4 -> StackExchange.Redis.ClusterSlotStatsMetric
+[SER012]StackExchange.Redis.ClusterSlotStatsResult
+[SER012]StackExchange.Redis.ClusterSlotStatsResult.Slots.get -> System.Collections.Generic.IReadOnlyList!
StackExchange.Redis.CommandFlags.CacheNoLocalServe = 8388608 -> StackExchange.Redis.CommandFlags
StackExchange.Redis.CommandFlags.CacheNoLocalStore = 16777216 -> StackExchange.Redis.CommandFlags
StackExchange.Redis.CommandFlags.CacheOptIn = 2097152 -> StackExchange.Redis.CommandFlags
@@ -127,6 +147,11 @@ static StackExchange.Redis.Config.RewriteAsync(this StackExchange.Redis.RespConf
static StackExchange.Redis.Config.SetAsync(this StackExchange.Redis.RespConfig config, StackExchange.Redis.RedisValue setting, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
StackExchange.Redis.ConfigurationOptions.ClientCache.get -> StackExchange.Redis.Caching.CacheOptions?
StackExchange.Redis.ConfigurationOptions.ClientCache.set -> void
+StackExchange.Redis.DatabaseTaskExtensions
+static StackExchange.Redis.DatabaseTaskExtensions.AsTask(this StackExchange.Redis.IDatabaseAsync! database, System.Threading.Tasks.ValueTask pending) -> System.Threading.Tasks.Task!
+static StackExchange.Redis.DatabaseTaskExtensions.AsTask(this StackExchange.Redis.IDatabaseAsync! database, System.Threading.Tasks.ValueTask pending, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task!
+static StackExchange.Redis.DatabaseTaskExtensions.AsTask(this StackExchange.Redis.IDatabaseAsync! database, System.Threading.Tasks.ValueTask pending) -> System.Threading.Tasks.Task!
+static StackExchange.Redis.DatabaseTaskExtensions.AsTask(this StackExchange.Redis.IDatabaseAsync! database, System.Threading.Tasks.ValueTask pending, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task!
StackExchange.Redis.Diagnostics
static StackExchange.Redis.Diagnostics.CommandCountAsync(this StackExchange.Redis.RespDiagnostics diagnostics, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
static StackExchange.Redis.Diagnostics.EchoAsync(this StackExchange.Redis.RespDiagnostics diagnostics, StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
@@ -223,6 +248,10 @@ StackExchange.Redis.IRespServerTarget.Context.get -> StackExchange.Redis.RespSer
StackExchange.Redis.IRespTarget
StackExchange.Redis.IRespTarget.Context.get -> StackExchange.Redis.RespContext
StackExchange.Redis.IRespTarget.Context.get -> StackExchange.Redis.RespContext!
+[SER012]StackExchange.Redis.IServer.ClusterSlotStatsBySlotRange(int startSlot, int endSlot, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ClusterSlotStatsResult?
+[SER012]StackExchange.Redis.IServer.ClusterSlotStatsBySlotRangeAsync(int startSlot, int endSlot, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER012]StackExchange.Redis.IServer.ClusterSlotStatsOrderBy(StackExchange.Redis.ClusterSlotStatsMetric metric, long limit = 0, StackExchange.Redis.Order order = StackExchange.Redis.Order.Descending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ClusterSlotStatsResult?
+[SER012]StackExchange.Redis.IServer.ClusterSlotStatsOrderByAsync(StackExchange.Redis.ClusterSlotStatsMetric metric, long limit = 0, StackExchange.Redis.Order order = StackExchange.Redis.Order.Descending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
StackExchange.Redis.Keys
static StackExchange.Redis.Keys.CopyAsync(this StackExchange.Redis.RespKeys keys, StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, int? destinationDatabase = null, bool replace = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
static StackExchange.Redis.Keys.DebugObjectAsync(this StackExchange.Redis.RespKeys keys, StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
@@ -314,7 +343,7 @@ StackExchange.Redis.Protocol.RespFragment.ArgCount.get -> int
StackExchange.Redis.Protocol.RespFragment.Bytes.get -> System.ReadOnlySpan
static StackExchange.Redis.Protocol.RespFragment.CreateValidated(System.ReadOnlySpan bytes, int argCount = 1) -> StackExchange.Redis.Protocol.RespFragment
StackExchange.Redis.Protocol.RespFragment.RespFragment() -> void
-[SER011]StackExchange.Redis.Protocol.RespFragment.RespFragment(System.ReadOnlySpan bytes, int argCount = 1) -> void
+[SER101]StackExchange.Redis.Protocol.RespFragment.RespFragment(System.ReadOnlySpan bytes, int argCount = 1) -> void
StackExchange.Redis.Protocol.RespFragment.When(bool condition) -> StackExchange.Redis.Protocol.RespFragment
StackExchange.Redis.Protocol.RespFragment.When(T? value) -> StackExchange.Redis.Protocol.RespFragment
StackExchange.Redis.Protocol.RespFrameWriter
@@ -429,22 +458,23 @@ StackExchange.Redis.ReadOnlyLease.Length.get -> int
StackExchange.Redis.ReadOnlyLease.Memory.get -> System.ReadOnlyMemory
StackExchange.Redis.ReadOnlyLease.Span.get -> System.ReadOnlySpan
StackExchange.Redis.ReadOnlyLease.ToArray() -> T[]!
+StackExchange.Redis.RedisFeatures.ClusterSlotStats.get -> bool
StackExchange.Redis.RedisFeatures.GeoSearch.get -> bool
StackExchange.Redis.RedisFeatures.ListMove.get -> bool
StackExchange.Redis.RespArrays
StackExchange.Redis.RespArrays.RespArrays() -> void
StackExchange.Redis.RespArrays.RespArrays(StackExchange.Redis.RespContext! context) -> void
-[SER014]StackExchange.Redis.RespBatch
-[SER014]StackExchange.Redis.RespBatch.Context.get -> StackExchange.Redis.RespDatabaseContext
-[SER014]StackExchange.Redis.RespBatch.Count.get -> int
-[SER014]StackExchange.Redis.RespBatch.Dispose() -> void
-[SER014]StackExchange.Redis.RespBatch.ExecuteAsync() -> System.Threading.Tasks.Task!
-[SER014]StackExchange.Redis.RespBatch.RespBatch() -> void
+[SER103]StackExchange.Redis.RespBatch
+[SER103]StackExchange.Redis.RespBatch.Context.get -> StackExchange.Redis.RespDatabaseContext
+[SER103]StackExchange.Redis.RespBatch.Count.get -> int
+[SER103]StackExchange.Redis.RespBatch.Dispose() -> void
+[SER103]StackExchange.Redis.RespBatch.ExecuteAsync() -> System.Threading.Tasks.Task!
+[SER103]StackExchange.Redis.RespBatch.RespBatch() -> void
StackExchange.Redis.RespBatching
-[SER014]static StackExchange.Redis.RespBatching.BeginBatch(this StackExchange.Redis.RespDatabaseContext context) -> StackExchange.Redis.RespBatch
-[SER014]static StackExchange.Redis.RespBatching.BeginBatch(this TTarget target) -> StackExchange.Redis.RespBatch
-[SER014]static StackExchange.Redis.RespBatching.BeginTransaction(this StackExchange.Redis.RespDatabaseContext context) -> StackExchange.Redis.RespTransaction
-[SER014]static StackExchange.Redis.RespBatching.BeginTransaction(this TTarget target) -> StackExchange.Redis.RespTransaction
+[SER103]static StackExchange.Redis.RespBatching.BeginBatch(this StackExchange.Redis.RespDatabaseContext context) -> StackExchange.Redis.RespBatch
+[SER103]static StackExchange.Redis.RespBatching.BeginBatch(this TTarget target) -> StackExchange.Redis.RespBatch
+[SER103]static StackExchange.Redis.RespBatching.BeginTransaction(this StackExchange.Redis.RespDatabaseContext context) -> StackExchange.Redis.RespTransaction
+[SER103]static StackExchange.Redis.RespBatching.BeginTransaction(this TTarget target) -> StackExchange.Redis.RespTransaction
StackExchange.Redis.RespBitmaps
StackExchange.Redis.RespBitmaps.RespBitmaps() -> void
StackExchange.Redis.RespBitmaps.RespBitmaps(StackExchange.Redis.RespContext! context) -> void
@@ -458,9 +488,9 @@ static StackExchange.Redis.RespConnectionExtensions.GetServerContext(this StackE
static StackExchange.Redis.RespConnectionExtensions.GetServerContext(this StackExchange.Redis.IConnectionMultiplexer! multiplexer, string! host, int port, object? asyncState = null) -> StackExchange.Redis.RespServerContext
StackExchange.Redis.RespContext
StackExchange.Redis.RespContext.Blocking() -> StackExchange.Redis.RespContext!
-StackExchange.Redis.RespContext.Multiplexer.get -> StackExchange.Redis.IConnectionMultiplexer?
StackExchange.Redis.RespContext.Compose(ref StackExchange.Redis.Protocol.RespRequestBuilder handler) -> StackExchange.Redis.Protocol.RespRequestBuilder
StackExchange.Redis.RespContext.Compose(string! command, ref StackExchange.Redis.Protocol.RespRequestBuilder handler) -> StackExchange.Redis.Protocol.RespRequestBuilder
+StackExchange.Redis.RespContext.Multiplexer.get -> StackExchange.Redis.IConnectionMultiplexer?
StackExchange.Redis.RespContext.Render(ref StackExchange.Redis.Protocol.RespRequestBuilder handler) -> StackExchange.Redis.Protocol.RespRequestFrame
StackExchange.Redis.RespContext.Render(string! command, ref StackExchange.Redis.Protocol.RespRequestBuilder handler) -> StackExchange.Redis.Protocol.RespRequestFrame
StackExchange.Redis.RespContext.RespContext() -> void
@@ -469,12 +499,12 @@ StackExchange.Redis.RespDatabaseContext.AppendChannelPrefix(StackExchange.Redis.
StackExchange.Redis.RespDatabaseContext.AppendKeyPrefix(StackExchange.Redis.RedisKey prefix) -> StackExchange.Redis.RespDatabaseContext
StackExchange.Redis.RespDatabaseContext.AsDatabase(StackExchange.Redis.IConnectionMultiplexer! multiplexer, object? asyncState = null) -> StackExchange.Redis.IDatabase!
StackExchange.Redis.RespDatabaseContext.Blocking() -> StackExchange.Redis.RespDatabaseContext
-StackExchange.Redis.RespDatabaseContext.Multiplexer.get -> StackExchange.Redis.IConnectionMultiplexer?
StackExchange.Redis.RespDatabaseContext.Database.get -> int
+StackExchange.Redis.RespDatabaseContext.Multiplexer.get -> StackExchange.Redis.IConnectionMultiplexer?
StackExchange.Redis.RespDatabaseContext.RespDatabaseContext() -> void
StackExchange.Redis.RespDatabaseContext.RespDatabaseContext(StackExchange.Redis.RespContext! context) -> void
-StackExchange.Redis.RespDatabaseContext.WithDatabase(int database) -> StackExchange.Redis.RespDatabaseContext
StackExchange.Redis.RespDatabaseContext.WithCachePolicy(StackExchange.Redis.Caching.CachePolicy! policy) -> StackExchange.Redis.RespDatabaseContext
+StackExchange.Redis.RespDatabaseContext.WithDatabase(int database) -> StackExchange.Redis.RespDatabaseContext
StackExchange.Redis.RespDatabaseContext.WithServices(object? services) -> StackExchange.Redis.RespDatabaseContext
StackExchange.Redis.RespDatabaseContext.WithoutCache() -> StackExchange.Redis.RespDatabaseContext
static StackExchange.Redis.RespDatabaseContext.explicit operator StackExchange.Redis.RespContext!(StackExchange.Redis.RespDatabaseContext context) -> StackExchange.Redis.RespContext!
@@ -585,8 +615,8 @@ StackExchange.Redis.RespServerContext.Blocking() -> StackExchange.Redis.RespServ
StackExchange.Redis.RespServerContext.Multiplexer.get -> StackExchange.Redis.IConnectionMultiplexer?
StackExchange.Redis.RespServerContext.RespServerContext() -> void
StackExchange.Redis.RespServerContext.RespServerContext(StackExchange.Redis.RespContext! context) -> void
-StackExchange.Redis.RespServerContext.WithServices(object? services) -> StackExchange.Redis.RespServerContext
StackExchange.Redis.RespServerContext.WithCachePolicy(StackExchange.Redis.Caching.CachePolicy! policy) -> StackExchange.Redis.RespServerContext
+StackExchange.Redis.RespServerContext.WithServices(object? services) -> StackExchange.Redis.RespServerContext
StackExchange.Redis.RespServerContext.WithoutCache() -> StackExchange.Redis.RespServerContext
static StackExchange.Redis.RespServerContext.explicit operator StackExchange.Redis.RespContext!(StackExchange.Redis.RespServerContext context) -> StackExchange.Redis.RespContext!
static StackExchange.Redis.RespServerContext.explicit operator StackExchange.Redis.RespContext(StackExchange.Redis.RespServerContext context) -> StackExchange.Redis.RespContext
@@ -620,14 +650,14 @@ static StackExchange.Redis.RespSurface.PingMeasureAsync(this TTarget ta
StackExchange.Redis.RespSurface.extension(TTarget)
StackExchange.Redis.RespSurface.extension(TTarget).PingAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
StackExchange.Redis.RespSurface.extension(TTarget).PingMeasureAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
-[SER014]StackExchange.Redis.RespTransaction
-[SER014]StackExchange.Redis.RespTransaction.AddCondition(StackExchange.Redis.Condition! condition) -> StackExchange.Redis.ConditionResult!
-[SER014]StackExchange.Redis.RespTransaction.Context.get -> StackExchange.Redis.RespDatabaseContext
-[SER014]StackExchange.Redis.RespTransaction.Count.get -> int
-[SER014]StackExchange.Redis.RespTransaction.Dispose() -> void
-[SER014]StackExchange.Redis.RespTransaction.ExecuteAsync() -> System.Threading.Tasks.Task!
-[SER014]StackExchange.Redis.RespTransaction.RespTransaction() -> void
-[SER014]StackExchange.Redis.RespTransaction.WasWatchConflict.get -> bool
+[SER103]StackExchange.Redis.RespTransaction
+[SER103]StackExchange.Redis.RespTransaction.AddCondition(StackExchange.Redis.Condition! condition) -> StackExchange.Redis.ConditionResult!
+[SER103]StackExchange.Redis.RespTransaction.Context.get -> StackExchange.Redis.RespDatabaseContext
+[SER103]StackExchange.Redis.RespTransaction.Count.get -> int
+[SER103]StackExchange.Redis.RespTransaction.Dispose() -> void
+[SER103]StackExchange.Redis.RespTransaction.ExecuteAsync() -> System.Threading.Tasks.Task!
+[SER103]StackExchange.Redis.RespTransaction.RespTransaction() -> void
+[SER103]StackExchange.Redis.RespTransaction.WasWatchConflict.get -> bool
StackExchange.Redis.RespVectorSets
StackExchange.Redis.RespVectorSets.RespVectorSets() -> void
StackExchange.Redis.RespVectorSets.RespVectorSets(StackExchange.Redis.RespContext! context) -> void
@@ -833,11 +863,3 @@ static StackExchange.Redis.VectorSets.RangeEnumerateAsync(this StackExchange.Red
static StackExchange.Redis.VectorSets.RemoveAsync(this StackExchange.Redis.RespVectorSets sets, StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
static StackExchange.Redis.VectorSets.SetAttributesJsonAsync(this StackExchange.Redis.RespVectorSets sets, StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! attributesJson, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
static StackExchange.Redis.VectorSets.SimilaritySearchAsync(this StackExchange.Redis.RespVectorSets sets, StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask?>
-StackExchange.Redis.DatabaseTaskExtensions
-static StackExchange.Redis.DatabaseTaskExtensions.AsTask(this StackExchange.Redis.IDatabaseAsync! database, System.Threading.Tasks.ValueTask pending) -> System.Threading.Tasks.Task!
-static StackExchange.Redis.DatabaseTaskExtensions.AsTask(this StackExchange.Redis.IDatabaseAsync! database, System.Threading.Tasks.ValueTask pending, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task!
-static StackExchange.Redis.DatabaseTaskExtensions.AsTask(this StackExchange.Redis.IDatabaseAsync! database, System.Threading.Tasks.ValueTask pending) -> System.Threading.Tasks.Task!
-static StackExchange.Redis.DatabaseTaskExtensions.AsTask(this StackExchange.Redis.IDatabaseAsync! database, System.Threading.Tasks.ValueTask pending, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task!
-StackExchange.Redis.Availability.MultiGroupOptions.ClientCache.get -> StackExchange.Redis.Caching.CacheOptions?
-StackExchange.Redis.Availability.MultiGroupOptions.Builder.ClientCache.get -> StackExchange.Redis.Caching.CacheOptions?
-StackExchange.Redis.Availability.MultiGroupOptions.Builder.ClientCache.set -> void
diff --git a/src/StackExchange.Redis/README.md b/src/StackExchange.Redis/README.md
index 743cd1ef3..59caa67ff 100644
--- a/src/StackExchange.Redis/README.md
+++ b/src/StackExchange.Redis/README.md
@@ -1,6 +1,6 @@
StackExchange.Redis is a high-performance RESP (Redis, etc) client for .NET, available under the MIT license.
-- Release notes: [https://github.com/StackExchange/StackExchange.Redis/releases](https://github.com/StackExchange/StackExchange.Redis/releases) (3.0 onwards; [earlier](https://seredis.dev/ReleaseNotes))
+- Release notes: [https://github.com/seredis/StackExchange.Redis/releases](https://github.com/seredis/StackExchange.Redis/releases) (3.0 onwards; [earlier](https://seredis.dev/ReleaseNotes))
- NuGet package: [https://www.nuget.org/packages/StackExchange.Redis/](https://www.nuget.org/packages/StackExchange.Redis/)
- General docs: [https://seredis.dev/](https://seredis.dev/)
-- Code: [https://github.com/StackExchange/StackExchange.Redis/](https://github.com/StackExchange/StackExchange.Redis/)
\ No newline at end of file
+- Code: [https://github.com/seredis/StackExchange.Redis/](https://github.com/seredis/StackExchange.Redis/)
\ No newline at end of file
diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs
index 1669fb460..f958e0ed8 100644
--- a/src/StackExchange.Redis/RedisFeatures.cs
+++ b/src/StackExchange.Redis/RedisFeatures.cs
@@ -332,6 +332,11 @@ public RedisFeatures(Version version)
///
public bool HashImport => Version.IsAtLeast(v8_10_0);
+ ///
+ /// Are the CLUSTER SLOT-STATS per-slot usage statistics available?
+ ///
+ public bool ClusterSlotStats => Version.IsAtLeast(v8_2_0_rc1);
+
#pragma warning restore 1629 // Documentation text should end with a period.
///
diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs
index 2492a5ace..85dcbdf72 100644
--- a/src/StackExchange.Redis/RedisLiterals.cs
+++ b/src/StackExchange.Redis/RedisLiterals.cs
@@ -106,6 +106,7 @@ public static readonly RedisValue
OBO = RedisValue.FromRaw("OBO"u8),
ONE = RedisValue.FromRaw("ONE"u8),
OR = RedisValue.FromRaw("OR"u8),
+ ORDERBY = RedisValue.FromRaw("ORDERBY"u8),
PATTERN = RedisValue.FromRaw("PATTERN"u8),
PAUSE = RedisValue.FromRaw("PAUSE"u8),
PERSIST = RedisValue.FromRaw("PERSIST"u8),
@@ -128,6 +129,8 @@ public static readonly RedisValue
SETINFO = RedisValue.FromRaw("SETINFO"u8),
SETNAME = RedisValue.FromRaw("SETNAME"u8),
SKIPME = RedisValue.FromRaw("SKIPME"u8),
+ SLOT_STATS = RedisValue.FromRaw("SLOT-STATS"u8),
+ SLOTSRANGE = RedisValue.FromRaw("SLOTSRANGE"u8),
STATS = RedisValue.FromRaw("STATS"u8),
STOP = RedisValue.FromRaw("STOP"u8),
STORE = RedisValue.FromRaw("STORE"u8),
diff --git a/src/StackExchange.Redis/ServerEndPoint.Maintenance.cs b/src/StackExchange.Redis/ServerEndPoint.Maintenance.cs
index 0f062171d..c72ff8adc 100644
--- a/src/StackExchange.Redis/ServerEndPoint.Maintenance.cs
+++ b/src/StackExchange.Redis/ServerEndPoint.Maintenance.cs
@@ -356,15 +356,6 @@ internal void SetHandoffTarget(EndPoint target, TimeSpan window)
_handoffTarget = target;
}
- ///
- /// Forgets any handoff target, once a connection has been established.
- ///
- ///
- /// Called on full establishment rather than on the connect attempt: if the attempt fails we want the next
- /// one to try the target again, within its window. Once a connection is up, normal resolution resumes -
- /// by then DNS has usually caught up anyway.
- ///
- internal void ClearHandoffTarget() => _handoffTarget = null;
private volatile string? _lastHandoffOutcome;
///
@@ -403,18 +394,21 @@ internal void SetHandoffTarget(EndPoint target, TimeSpan window)
///
internal void OnMovingAnnounced(TimeSpan? window, EndPoint? successor, IPAddress? current)
{
- // One at a time per server. A rolling operation delivers one MOVING per connection, so a second one
- // arriving while a handoff is in flight is a repeat or a much later event; either way, starting a
- // second poll against the same endpoint achieves nothing.
+ var budget = window is { } value && value > TimeSpan.Zero
+ ? value
+ : Multiplexer.RawConfig.MaintenanceRelaxedTimeout; // no window given: use the relaxation floor
+
+ // One at a time per server: two drains on the same connections would end each other's holds. A second
+ // MOVING arriving while one is in flight (which can be most of a window: the jitter, the DNS poll, the
+ // drain) is not acted on separately - the handoff in flight replaces the connections - but if it names a
+ // successor, that is where the reconnect should go, so the pin is updated and the next dial takes it.
if (Interlocked.CompareExchange(ref _handoffInFlight, 1, 0) != 0)
{
- Multiplexer.Trace("MOVING: a handoff is already in flight", ToString());
+ if (successor is not null) SetHandoffTarget(successor, budget);
+ Multiplexer.Trace(successor is null ? "MOVING: a handoff is already in flight" : "MOVING: a handoff is already in flight; successor noted", ToString());
return;
}
- var budget = window is { } value && value > TimeSpan.Zero
- ? value
- : Multiplexer.RawConfig.MaintenanceRelaxedTimeout; // no window given: use the relaxation floor
_ = HandoffAsync(budget, successor, current);
}
@@ -439,6 +433,11 @@ private async Task HandoffAsync(TimeSpan window, EndPoint? successor, IPAddress?
resolve: Multiplexer.AddressResolver,
log: message => Multiplexer.Trace(message, ToString())).ForAwait();
+ // the decision may have spent a good part of the window polling DNS (4 to 19 seconds of a 15 second
+ // window has been measured); what the drain has to work with is what is actually left
+ remaining = window - TimeSpan.FromMilliseconds(watch.ElapsedMilliseconds);
+ if (remaining < TimeSpan.Zero) remaining = TimeSpan.Zero;
+
Multiplexer.Trace($"MOVING: {decision}", ToString());
_lastHandoffOutcome = decision.ToString();
@@ -488,35 +487,96 @@ private async Task HandoffAsync(TimeSpan window, EndPoint? successor, IPAddress?
}
///
- /// Lets in-flight caller work finish, then replaces the connections.
+ /// How long the handoff holds new work and waits for in-flight commands before replacing the connections
+ /// regardless: half the relaxed timeout (five seconds by default), never more than half of what is left of the
+ /// window, never less than , and never more than the window itself.
+ ///
+ ///
+ /// Two things pull in opposite directions. A blocking command in flight - BLPOP, XREAD BLOCK, WAIT - is failed
+ /// by the replacement if the wait is too short, when the server's close would have let it finish; but every
+ /// command the application issues during the wait is held, so a long wait is a latency spike for everyone.
+ /// Half the relaxed timeout lets a blocking command with a timeout up to that complete, keeps the hold well
+ /// inside the relaxed timeout that covers the held work, and follows the one setting an application already
+ /// uses to say how patient it is prepared to be during maintenance. Half of what is left of the window is a
+ /// ceiling whatever the relaxed timeout: a generous one must not turn the hold into a full-window stall that
+ /// recycles at the deadline with work in flight, which is the failure this exists to remove.
+ ///
+ private TimeSpan DrainCap(TimeSpan budget)
+ {
+ var cap = TimeSpan.FromTicks(Multiplexer.RawConfig.MaintenanceRelaxedTimeout.Ticks / 2);
+ var halfWindow = TimeSpan.FromTicks(budget.Ticks / 2);
+ if (cap > halfWindow) cap = halfWindow;
+ if (cap < DrainFloor) cap = DrainFloor; // a relaxed timeout of zero disables relaxation; it must not disable the drain
+ return cap > budget ? budget : cap;
+ }
+
+ /// The least the drain waits for in-flight commands, whatever the relaxed timeout: one round trip on any network, with room to spare.
+ private static readonly TimeSpan DrainFloor = TimeSpan.FromSeconds(1);
+
+ ///
+ /// Holds new caller work back, lets what has already been written finish, then replaces the connections.
///
///
/// Draining first because the socket is still working: anything already written may still be answered, and
- /// dropping it would fail commands that were about to succeed. Bounded by what is left of the window,
- /// because the server closes the socket at the end of it regardless - so anything not drained by then was
- /// going to fail either way, and draining strictly dominates.
+ /// dropping it would fail commands that were about to succeed.
+ ///
+ /// Holding new work back is what makes the drain finish. Measured live on a connection carrying about 450
+ /// operations a second: there was always something in flight, so a drain that waited for "no caller work"
+ /// waited out the entire 15s window and then recycled at the deadline - failing the eight commands in flight
+ /// at that instant, with no time left to establish the replacement. With new work held in the backlog, the
+ /// written commands complete within a round trip, the recycle follows, and the held work is flushed to the
+ /// replacement by the same path that flushes any backlog when a connection establishes.
+ ///
+ ///
+ /// The wait is bounded by , and timeouts are relaxed for the hold and the reconnect so
+ /// that the held work is not timed out by the hold itself. Past the cap the connections are replaced regardless,
+ /// and whatever was still in flight fails as the server's close would have failed it.
+ ///
///
/// Every connection to the node, not just the one that was told. The measured blast radius is the *node*:
/// four connections to one proxy, differing only in handshake, all closed simultaneously, and only the ones
/// that had opted in were warned.
///
///
+ /// What is left of the announced window.
+ /// Why; for the trace.
private async Task DrainThenRecycleAsync(TimeSpan budget, string reason)
{
var watch = ValueStopwatch.StartNew();
- while (HasCallerWork() && watch.ElapsedMilliseconds < budget.TotalMilliseconds)
+ var cap = DrainCap(budget);
+
+ // The held work waits out the hold, the replacement's handshake, then its own turn; relax timeouts for
+ // that span so the hold itself times nothing out. The reconnect can take until the end of the window plus
+ // a connect attempt, which the relaxed timeout comfortably covers at its default.
+ var relax = cap + Multiplexer.RawConfig.MaintenanceRelaxedTimeout;
+ var ceiling = Multiplexer.RawConfig.MaintenanceRelaxedWindowMax;
+ ExtendRelaxedWindow(relax > ceiling ? ceiling : relax, "MOVING handoff");
+
+ // a snapshot: a handoff in progress must not fault on a server going away mid-drain
+ var connections = Multiplexer.ConnectionsIfCreated;
+ connections?.BeginHandoffHold(EndPoint);
+ bool drained;
+ try
{
- await Task.Delay(TimeSpan.FromMilliseconds(20)).ForAwait();
- }
+ while (!(drained = connections?.HasCallerMessagesInFlight(EndPoint) != true)
+ && watch.ElapsedMilliseconds < cap.TotalMilliseconds)
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(20)).ForAwait();
+ }
- var drained = !HasCallerWork();
- // the handoff exists to stop using a connection before the server closes it
- var recycled = Multiplexer.ConnectionsIfCreated?.RecycleConnections(EndPoint) == true;
- if (recycled) Interlocked.Increment(ref _handoffRecycles);
- Multiplexer.Trace(
- $"MOVING: {(recycled ? "recycled" : "nothing to recycle")} after {watch.ElapsedMilliseconds}ms"
- + (drained ? " (drained)" : " (still busy; the window ran out)"),
- ToString());
+ // the handoff exists to stop using a connection before the server closes it
+ var recycled = connections?.RecycleConnections(EndPoint) == true;
+ if (recycled) Interlocked.Increment(ref _handoffRecycles);
+ Multiplexer.Trace(
+ $"MOVING: {(recycled ? "recycled" : "nothing to recycle")} after {watch.ElapsedMilliseconds}ms"
+ + (drained ? " (drained)" : " (still busy at the drain cap)"),
+ ToString());
+ }
+ finally
+ {
+ // after the recycle, so there is no instant at which the hold is down and the old connection live
+ connections?.EndHandoffHold(EndPoint);
+ }
}
///
diff --git a/src/StackExchange.Redis/SocketManager.cs b/src/StackExchange.Redis/SocketManager.cs
index 145c5ae06..c5c86adde 100644
--- a/src/StackExchange.Redis/SocketManager.cs
+++ b/src/StackExchange.Redis/SocketManager.cs
@@ -143,7 +143,7 @@ internal static bool TryEnableTcpKeepAlive(Socket socket, EndPoint endPoint)
{
#if !NET10_0_OR_GREATER
// Prior to .NET 10, enabling TCP keep-alive on host-based endpoints fails outside of Windows.
- // see https://github.com/StackExchange/StackExchange.Redis/issues/3086
+ // see https://github.com/seredis/StackExchange.Redis/issues/3086
case DnsEndPoint when !RuntimeInformation.IsOSPlatform(OSPlatform.Windows): return false;
#endif
case DnsEndPoint:
diff --git a/src/StackExchange.Redis/ValueCondition.cs b/src/StackExchange.Redis/ValueCondition.cs
index 24ac5f5db..9162f1352 100644
--- a/src/StackExchange.Redis/ValueCondition.cs
+++ b/src/StackExchange.Redis/ValueCondition.cs
@@ -309,9 +309,9 @@ void IRespArgument.WriteTo(scoped ref RespRequestBuilder handler)
var keyword = KeywordResp;
if (keyword.IsEmpty) return; // ValueCondition.Always contributes no arguments
-#pragma warning disable SER011 // pre-framed constants owned by this type; see Expiration for the reasoning
+#pragma warning disable SER101 // pre-framed constants owned by this type; see Expiration for the reasoning
handler.AppendFormatted(new RespFragment(keyword));
-#pragma warning restore SER011
+#pragma warning restore SER101
if (IsValueTest)
{
handler.AppendFormatted(_value);
diff --git a/tests/StackExchange.Redis.Build.Tests/SER309CodeFix.cs b/tests/StackExchange.Redis.Build.Tests/SER309CodeFix.cs
index 5e6fdf99e..5440a26c8 100644
--- a/tests/StackExchange.Redis.Build.Tests/SER309CodeFix.cs
+++ b/tests/StackExchange.Redis.Build.Tests/SER309CodeFix.cs
@@ -27,7 +27,7 @@ public class SER309CodeFix : CodeFixVerifier
+/// The MOVING handoff under steady traffic, against a real deployment: how long after the notification the
+/// connections are replaced, how long until the client is serving again, and how many commands failed meanwhile.
+///
+///
+/// A drain that waits for "no caller work" never finishes under load - there is always a command in flight - so it
+/// recycles at the end of the announced window and fails whatever is in flight at that instant. Holding new work
+/// back while the written commands complete is what lets the recycle happen within a round trip. This records the
+/// timeline rather than asserting a bound, so that a run against any branch yields a comparable row.
+///
+[Trait("tier", "fault-injector")]
+[Trait("scenario", "moving-handoff")]
+public class MovingUnderLoadScenarioTests(ExistingDatabaseFixture fixture, ITestOutputHelper log)
+ : IClassFixture
+{
+ [Theory]
+ [InlineData(MaintenanceEndpointType.Auto)]
+ public async Task HandoffUnderSteadyLoad(MaintenanceEndpointType endpointType)
+ {
+ fixture.RequireAvailable();
+ var cancellationToken = TestContext.Current.CancellationToken;
+
+ await using var scenario = await ScenarioRun.SetupAsync(
+ fixture.Injector, "topology-change-standalone", "conn_drop", "endpoint_rebind", log.WriteLine,
+ cancellationToken: cancellationToken);
+
+ var database = scenario.Database;
+ Assert.NotNull(database);
+
+ var clock = Stopwatch.StartNew();
+ var timeline = new List();
+ void Note(string what)
+ {
+ var entry = $" +{clock.Elapsed.TotalSeconds,6:0.0}s {what}";
+ lock (timeline) timeline.Add(entry);
+ log.WriteLine(entry);
+ }
+
+ var logs = new CapturingLogger();
+ var config = database.GetClientConfig(fixture.Environment);
+ config.MaintenanceMovingEndpointType = endpointType;
+ config.LoggerFactory = logs;
+ log.WriteLine($"moving-endpoint-type: {endpointType}");
+
+ await using var conn = await ConnectionMultiplexer.ConnectAsync(config);
+ var muxer = (IInternalConnectionMultiplexer)conn;
+ var endpoint = muxer.GetServerEndPoint(conn.GetEndPoints()[0]);
+
+ TimeSpan? movingAt = null, recycledAt = null, restoredAt = null;
+ var failures = new List<(TimeSpan At, string What)>();
+ conn.ConnectionFailed += (_, e) => Note($"disconnect: {e.FailureType} ({e.ConnectionType})");
+ conn.ConnectionRestored += (_, e) =>
+ {
+ restoredAt ??= clock.Elapsed;
+ Note($"reconnected ({e.ConnectionType})");
+ };
+ conn.ServerMaintenanceEvent += (_, e) =>
+ {
+ if (e is not PushMaintenanceEvent push) return;
+ Note($"{push.NotificationType} seq={push.SequenceId} time={push.Time?.TotalSeconds.ToString() ?? "-"}");
+ if (push.NotificationType == MaintenanceNotificationType.Moving) movingAt ??= clock.Elapsed;
+ };
+
+ // steady load: sixteen workers, each issuing its next PING the moment the previous one is answered, so there
+ // is never an instant with nothing in flight and a drain that waits for an idle connection cannot finish (a
+ // first cut paused 10ms between commands, and the old drain slipped through the gaps)
+ long completed = 0;
+ using var stop = new CancellationTokenSource();
+ var db = conn.GetDatabase();
+ var workload = new MultiThreadedWorkload(
+ async (_, _) =>
+ {
+ try
+ {
+ await db.PingAsync().ForAwait();
+ Interlocked.Increment(ref completed);
+ }
+ catch (Exception ex) when (ex is RedisException or TimeoutException)
+ {
+ lock (failures) failures.Add((clock.Elapsed, ex.GetType().Name + ": " + FirstLine(ex.Message)));
+ }
+ },
+ workerCount: 16);
+ workload.Start(stop.Token);
+ await Task.Delay(3000, cancellationToken);
+ var warmup = Interlocked.Read(ref completed);
+ log.WriteLine($"load: {warmup / 3.0:0} ops/s before the scenario fires");
+
+ clock.Restart();
+ // the injector call blocks until the scenario has run its course (about 20s), so watch for the recycle
+ // alongside it rather than after it; then cover the announced window plus the measured overshoot
+ var fire = scenario.FireAsync(cancellationToken);
+ var deadline = TimeSpan.FromSeconds(100);
+ while (clock.Elapsed < deadline)
+ {
+ if (recycledAt is null && endpoint.HandoffRecycles > 0)
+ {
+ recycledAt = clock.Elapsed;
+ Note($"connections replaced (recycles={endpoint.HandoffRecycles}); handoff: {endpoint.LastHandoffOutcome}");
+ }
+ if (fire is { IsCompleted: true })
+ {
+ await fire;
+ fire = null;
+ Note("injector reports the scenario finished");
+ }
+ await Task.Delay(100, cancellationToken);
+ }
+ if (fire is not null) await fire;
+
+ stop.Cancel();
+ try
+ {
+ await workload.Completion;
+ }
+ catch (OperationCanceledException)
+ {
+ }
+
+ var total = Interlocked.Read(ref completed);
+ int failed;
+ lock (failures)
+ {
+ failed = failures.Count;
+ foreach (var (at, what) in failures.Take(20)) log.WriteLine($" failed at +{at.TotalSeconds,6:0.0}s: {what}");
+ if (failures.Count > 20) log.WriteLine($" ... and {failures.Count - 20} more");
+ }
+ var missed = logs.Lines.Where(l => l.Contains("did not establish a replacement", StringComparison.Ordinal)).ToList();
+ foreach (var line in missed) log.WriteLine(" " + line);
+
+ log.WriteLine(string.Empty);
+ log.WriteLine("| event | at |");
+ log.WriteLine("|---|---|");
+ log.WriteLine($"| MOVING received | {Format(movingAt)} |");
+ log.WriteLine($"| connections replaced | {Format(recycledAt)} |");
+ log.WriteLine($"| ConnectionRestored | {Format(restoredAt)} |");
+ log.WriteLine($"| completed commands | {total} |");
+ log.WriteLine($"| failed commands | {failed} |");
+ log.WriteLine($"| missed-deadline warning | {(missed.Count > 0 ? "yes" : "no")} |");
+ log.WriteLine($"| handoff outcome | {endpoint.LastHandoffOutcome ?? "(none)"} |");
+
+ Assert.NotNull(movingAt);
+ Assert.True(endpoint.HandoffRecycles > 0, "the handoff should have replaced the connections");
+
+ static string Format(TimeSpan? at) => at is { } t ? $"+{t.TotalSeconds:0.0}s" : "(never)";
+ static string FirstLine(string s)
+ {
+ var i = s.IndexOf('\n');
+ return i < 0 ? s : s.Substring(0, i);
+ }
+ }
+
+ private sealed class CapturingLogger : ILoggerFactory, ILogger
+ {
+ private readonly List _lines = new();
+ public IReadOnlyList Lines
+ {
+ get
+ {
+ lock (_lines) return _lines.ToArray();
+ }
+ }
+
+ public void AddProvider(ILoggerProvider provider) { }
+ public ILogger CreateLogger(string categoryName) => this;
+ public void Dispose() { }
+ public IDisposable? BeginScope(TState state) where TState : notnull => null;
+ public bool IsEnabled(LogLevel logLevel) => true;
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ lock (_lines) _lines.Add($"{logLevel}: {formatter(state, exception)}");
+ }
+ }
+}
diff --git a/tests/StackExchange.Redis.Tests/ClusterHandshakeNodesUnitTests.cs b/tests/StackExchange.Redis.Tests/ClusterHandshakeNodesUnitTests.cs
index d52295903..581e1d61b 100644
--- a/tests/StackExchange.Redis.Tests/ClusterHandshakeNodesUnitTests.cs
+++ b/tests/StackExchange.Redis.Tests/ClusterHandshakeNodesUnitTests.cs
@@ -4,7 +4,7 @@
namespace StackExchange.Redis.Tests;
-// context: https://github.com/StackExchange/StackExchange.Redis/pull/3043
+// context: https://github.com/seredis/StackExchange.Redis/pull/3043
public class ClusterHandshakeNodesUnitTests(ITestOutputHelper log)
{
[Fact]
diff --git a/tests/StackExchange.Redis.Tests/ClusterSlotStatsTests.cs b/tests/StackExchange.Redis.Tests/ClusterSlotStatsTests.cs
new file mode 100644
index 000000000..c37bf0827
--- /dev/null
+++ b/tests/StackExchange.Redis.Tests/ClusterSlotStatsTests.cs
@@ -0,0 +1,163 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace StackExchange.Redis.Tests;
+
+///
+/// CLUSTER SLOT-STATS against a real cluster; the exact wire bytes and reply-shape matrix live in
+/// . Runs per protocol, since the per-slot metrics
+/// are a map under RESP3 and flat pairs under RESP2.
+///
+[RunPerProtocol]
+public class ClusterSlotStatsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture)
+{
+ protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000";
+
+ private static void RequireSlotStats(IServer server)
+ => Assert.SkipUnless(server.Features.ClusterSlotStats, $"CLUSTER SLOT-STATS needs 8.2, server is {server.Version}");
+
+ [Fact]
+ public async Task RoutedWriteIsCountedInItsSlot()
+ {
+ await using var conn = Create(allowAdmin: true);
+ RedisKey key = Me();
+ var server = conn.GetServer(key); // the primary that owns the key
+ RequireSlotStats(server);
+
+ await conn.GetDatabase().StringSetAsync(key, "x");
+ var slot = conn.HashSlot(key);
+
+ var result = await server.ClusterSlotStatsBySlotRangeAsync(slot, slot);
+ Assert.NotNull(result);
+ var entry = Assert.Single(result.Slots);
+ Assert.Equal(slot, entry.Slot);
+ Log($"slot {slot}: {entry.KeyCount} keys");
+ Assert.True(entry.KeyCount >= 1, "the slot we just wrote to should hold at least our key");
+ }
+
+ [Fact]
+ public async Task WholeRangeReportsOnlyTheNodesOwnSlots()
+ {
+ await using var conn = Create(allowAdmin: true);
+ var server = conn.GetServer(conn.GetEndPoints()[0]);
+ RequireSlotStats(server);
+
+ var result = await server.ClusterSlotStatsBySlotRangeAsync(SlotRange.MinSlot, SlotRange.MaxSlot);
+ Assert.NotNull(result);
+ Assert.NotEmpty(result.Slots);
+ Log($"{result.Slots.Count} slots reported by {server.EndPoint}");
+
+ // ascending slot order, each slot at most once, all in range
+ for (int i = 1; i < result.Slots.Count; i++)
+ {
+ Assert.True(result.Slots[i - 1].Slot < result.Slots[i].Slot, "SLOTSRANGE results should be in ascending slot order");
+ }
+ Assert.All(result.Slots, s => Assert.InRange(s.Slot, SlotRange.MinSlot, SlotRange.MaxSlot));
+ }
+
+ [Fact]
+ public async Task OrderByHonoursLimitAndOrder()
+ {
+ await using var conn = Create(allowAdmin: true);
+ var server = conn.GetServer(conn.GetEndPoints()[0]);
+ RequireSlotStats(server);
+
+ // make sure at least two slots on this node have different key counts
+ var db = conn.GetDatabase();
+ for (int i = 0; i < 10; i++)
+ {
+ await db.StringSetAsync($"{Me()}:{i}", i);
+ }
+
+ var top = await server.ClusterSlotStatsOrderByAsync(ClusterSlotStatsMetric.KeyCount, limit: 5);
+ Assert.NotNull(top);
+ Assert.InRange(top.Slots.Count, 1, 5);
+ for (int i = 1; i < top.Slots.Count; i++)
+ {
+ Assert.True(top.Slots[i - 1].KeyCount >= top.Slots[i].KeyCount, "descending is the default");
+ }
+
+ var bottom = await server.ClusterSlotStatsOrderByAsync(ClusterSlotStatsMetric.KeyCount, limit: 5, order: Order.Ascending);
+ Assert.NotNull(bottom);
+ Assert.InRange(bottom.Slots.Count, 1, 5);
+ for (int i = 1; i < bottom.Slots.Count; i++)
+ {
+ Assert.True(bottom.Slots[i - 1].KeyCount <= bottom.Slots[i].KeyCount, "ascending when asked");
+ }
+
+ Log($"top: {string.Join(", ", top.Slots.Select(s => s.ToString()))}");
+ Log($"bottom: {string.Join(", ", bottom.Slots.Select(s => s.ToString()))}");
+ }
+
+ [Fact]
+ public async Task EnablingMetricsSurfacesThem()
+ {
+ await using var conn = Create(allowAdmin: true, shared: false);
+ RedisKey key = Me();
+ var server = conn.GetServer(key);
+ RequireSlotStats(server);
+
+ var original = (await server.ConfigGetAsync("cluster-slot-stats-enabled")).Single().Value;
+ Log($"cluster-slot-stats-enabled was: '{original}'");
+ try
+ {
+ // cpu and net can be enabled at runtime; mem needs memory tracking from server startup
+ await server.ConfigSetAsync("cluster-slot-stats-enabled", "cpu net");
+
+ // generate some traffic on a slot this node owns, after enabling
+ var db = conn.GetDatabase();
+ var slot = conn.HashSlot(key);
+ for (int i = 0; i < 20; i++)
+ {
+ await db.StringSetAsync(key, i);
+ await db.StringGetAsync(key);
+ }
+
+ var result = await server.ClusterSlotStatsBySlotRangeAsync(slot, slot);
+ Assert.NotNull(result);
+ var entry = Assert.Single(result.Slots);
+ Log(entry.ToString());
+ Log($"cpu: {entry.CpuTime}, in: {entry.NetworkBytesIn}, out: {entry.NetworkBytesOut}, mem: {entry.MemoryBytes}");
+
+ Assert.NotNull(entry.CpuTime);
+ Assert.NotNull(entry.NetworkBytesIn);
+ Assert.NotNull(entry.NetworkBytesOut);
+ Assert.True(entry.NetworkBytesIn > 0, "we sent this slot twenty SETs; bytes-in should be non-zero");
+ Assert.Null(entry.MemoryBytes); // not enabled - null, not zero
+ }
+ finally
+ {
+ await server.ConfigSetAsync("cluster-slot-stats-enabled", original);
+ }
+ }
+
+ [Fact]
+ public async Task OrderByDisabledMetricIsRefused()
+ {
+ await using var conn = Create(allowAdmin: true);
+ var server = conn.GetServer(conn.GetEndPoints()[0]);
+ RequireSlotStats(server);
+
+ var enabled = (await server.ConfigGetAsync("cluster-slot-stats-enabled")).Single().Value.ToString();
+ Assert.SkipWhen(enabled.Contains("mem") || enabled.Contains("yes"), $"memory collection is enabled ('{enabled}'), so the server would not refuse it");
+
+ var ex = await Assert.ThrowsAsync(
+ () => server.ClusterSlotStatsOrderByAsync(ClusterSlotStatsMetric.MemoryBytes));
+ Log(ex.Message);
+ Assert.Contains("ORDERBY", ex.Message);
+ }
+
+ [Fact]
+ public async Task StandaloneServerRefuses()
+ {
+ await using var conn = Create(configuration: TestConfig.Current.PrimaryServerAndPort, allowAdmin: true, shared: false);
+ var server = conn.GetServer(conn.GetEndPoints()[0]);
+ RequireSlotStats(server);
+
+ var ex = await Assert.ThrowsAsync(
+ () => server.ClusterSlotStatsBySlotRangeAsync(0, 100));
+ Log(ex.Message);
+ Assert.Contains("cluster support disabled", ex.Message);
+ }
+}
diff --git a/tests/StackExchange.Redis.Tests/CommandRetryCategoryUnitTests.cs b/tests/StackExchange.Redis.Tests/CommandRetryCategoryUnitTests.cs
index 0bb82ed0b..de9094650 100644
--- a/tests/StackExchange.Redis.Tests/CommandRetryCategoryUnitTests.cs
+++ b/tests/StackExchange.Redis.Tests/CommandRetryCategoryUnitTests.cs
@@ -7,7 +7,7 @@ namespace StackExchange.Redis.Tests;
///
/// Covers the commands whose retry category depends on their *arguments* rather than just the command name
-/// (see https://github.com/StackExchange/StackExchange.Redis/issues/3148). Each command is issued through the
+/// (see https://github.com/seredis/StackExchange.Redis/issues/3148). Each command is issued through the
/// new surface against a fake executor, and the category is read off the flags the request reached the
/// executor with - so nothing here needs a real Redis.
///
diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs
index f01541b61..c700d0edb 100644
--- a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs
+++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs
@@ -123,6 +123,6 @@ public async Task HeartbeatConsistencyCheckPingsAsync(bool enableConsistencyChec
var opCount = postCount - preCount;
Log("OperationCount (diff): " + opCount);
- Assert.True(minExpected <= opCount && opCount >= minExpected, $"Expected opcount ({opCount}) between {minExpected}-{maxExpected}");
+ Assert.True(minExpected <= opCount && opCount <= maxExpected, $"Expected opcount ({opCount}) between {minExpected}-{maxExpected}");
}
}
diff --git a/tests/StackExchange.Redis.Tests/Helpers/FakeExecutor.cs b/tests/StackExchange.Redis.Tests/Helpers/FakeExecutor.cs
index 9e723592b..7fe90e476 100644
--- a/tests/StackExchange.Redis.Tests/Helpers/FakeExecutor.cs
+++ b/tests/StackExchange.Redis.Tests/Helpers/FakeExecutor.cs
@@ -41,6 +41,9 @@ internal class FakeExecutor(params string[] replies) : RespExecutorBase
/// The flags each request carried, for the tests that assert what reached the wire.
public List Flags { get; } = [];
+ /// The cluster slot each request carried, for the tests that assert how a command would route.
+ public List Slots { get; } = [];
+
/// What this fake server claims to support, or null for "no idea".
///
/// Null is the honest default and the interesting one: several command choices turn on a feature,
@@ -98,6 +101,7 @@ public override RespPayload Send(in RespRequest request)
{
Sent.Add(Text(request.Span));
Flags.Add(request.Flags);
+ Slots.Add(request.Slot);
if (CaptureKeys) RecordKeys(in request);
reply = replies[Math.Min(_next++, replies.Length - 1)];
}
diff --git a/tests/StackExchange.Redis.Tests/InterpolatedAppendTests.cs b/tests/StackExchange.Redis.Tests/InterpolatedAppendTests.cs
index 70ada30e9..d94d2aabd 100644
--- a/tests/StackExchange.Redis.Tests/InterpolatedAppendTests.cs
+++ b/tests/StackExchange.Redis.Tests/InterpolatedAppendTests.cs
@@ -19,9 +19,9 @@ private static string Text(in RespRequestFrame frame) =>
internal static partial class RespLiterals
{
-#pragma warning disable SER011 // stands in for the generator
+#pragma warning disable SER101 // stands in for the generator
internal static RespFragment EX => new("$2\r\nEX\r\n"u8);
-#pragma warning restore SER011
+#pragma warning restore SER101
}
[Fact]
diff --git a/tests/StackExchange.Redis.Tests/InterpolatedLiteralCommandTests.cs b/tests/StackExchange.Redis.Tests/InterpolatedLiteralCommandTests.cs
index a1322db45..e7f1e9452 100644
--- a/tests/StackExchange.Redis.Tests/InterpolatedLiteralCommandTests.cs
+++ b/tests/StackExchange.Redis.Tests/InterpolatedLiteralCommandTests.cs
@@ -13,7 +13,7 @@ namespace StackExchange.Redis.Tests;
public class InterpolatedLiteralCommandTests
{
// these tests exist to exercise the form SER309 warns about, so the warning is suppressed here and
- // nowhere wider - the same discipline the generator uses for SER011
+ // nowhere wider - the same discipline the generator uses for SER101
#pragma warning disable SER309
private static string Text(in RespRequestFrame frame) =>
diff --git a/tests/StackExchange.Redis.Tests/KeyedMessageSlotTests.cs b/tests/StackExchange.Redis.Tests/KeyedMessageSlotTests.cs
new file mode 100644
index 000000000..4d1c4dce5
--- /dev/null
+++ b/tests/StackExchange.Redis.Tests/KeyedMessageSlotTests.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace StackExchange.Redis.Tests;
+
+///
+/// A command that carries a key must report that key's hash slot, or a cluster client sends it to an arbitrary node
+/// and leans on -MOVED: an extra round trip in the usual case, and a failure under
+/// . The v3 core's INCREX and ARGREP messages both shipped
+/// without one (#3352); in this core the key append itself folds the slot, and these pin that.
+///
+public class KeyedMessageSlotTests(ITestOutputHelper output) : TestBase(output)
+{
+ private static (RespDatabaseContext Context, FakeExecutor Executor) CreateCluster(params string[] replies)
+ {
+ var executor = new FakeExecutor(replies);
+ var context = new RespDatabaseContext(new RespContext(serverType: ServerType.Cluster).WithExecutor(executor));
+ return (context, executor);
+ }
+
+ private static void AssertRoutedByKey(FakeExecutor executor, RedisKey key)
+ {
+ var expected = ServerSelectionStrategy.GetHashSlot(key);
+ Assert.NotEqual(ServerSelectionStrategy.NoSlot, expected);
+ Assert.Equal(expected, Assert.Single(executor.Slots));
+ }
+
+ [Fact]
+ public async Task Increx_IsRoutedByKey()
+ {
+ var (db, executor) = CreateCluster("*2\r\n:5\r\n:5\r\n");
+ var expiry = Expiration.CreateOrKeepTtl(TimeSpan.FromMinutes(1), keepTtl: false);
+
+ var result = await db.Strings.IncrementAsync("some-counter", 5, expiry);
+
+ Assert.Equal(5, result.Value);
+ AssertRoutedByKey(executor, "some-counter");
+ }
+
+ [Fact]
+ public async Task ArrayGrep_IsRoutedByKey()
+ {
+ var (db, executor) = CreateCluster("*1\r\n:0\r\n");
+
+ using var found = await db.Arrays.GrepAsync("some-array", CreateGrep());
+
+ Assert.Equal(1, found.Span.Length);
+ AssertRoutedByKey(executor, "some-array");
+ }
+
+ private static ArrayGrepRequest CreateGrep()
+ {
+ var request = new ArrayGrepRequest();
+ request.AddPredicate(ArrayGrepRequest.Predicate.Match("alpha"));
+ return request;
+ }
+
+ // enough keys that, sent to an arbitrary node, some would certainly miss their owner
+ private const int KeyCount = 32;
+
+ [Fact]
+ public async Task Increx_OnCluster_NeedsNoRedirect()
+ {
+ await using var conn = Create(configuration: GetClusterConfiguration(), require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ var expiry = Expiration.CreateOrKeepTtl(TimeSpan.FromMinutes(1), keepTtl: false);
+
+ for (int i = 0; i < KeyCount; i++)
+ {
+ RedisKey key = Me() + ":" + i;
+ await db.KeyDeleteAsync(key);
+
+ // NoRedirect: lands on the slot owner first time, or throws
+ var result = await db.StringIncrementAsync(key, 5, expiry, flags: CommandFlags.NoRedirect);
+ Assert.Equal(5, result.Value);
+
+ await db.KeyDeleteAsync(key, CommandFlags.FireAndForget);
+ }
+ }
+
+ [Fact]
+ public async Task ArrayGrep_OnCluster_NeedsNoRedirect()
+ {
+ await using var conn = Create(configuration: GetClusterConfiguration(), require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ var grep = CreateGrep();
+
+ for (int i = 0; i < KeyCount; i++)
+ {
+ RedisKey key = Me() + ":" + i;
+ await db.KeyDeleteAsync(key);
+ await db.ArraySetAsync(key, 0, "alpha");
+
+ var found = await db.ArrayGrepAsync(key, grep, CommandFlags.NoRedirect);
+ Assert.Equal(0, Assert.Single(found).Index);
+
+ await db.KeyDeleteAsync(key, CommandFlags.FireAndForget);
+ }
+ }
+}
diff --git a/tests/StackExchange.Redis.Tests/MaintenanceNotificationTests.cs b/tests/StackExchange.Redis.Tests/MaintenanceNotificationTests.cs
index bfdb3a7a5..79578a00c 100644
--- a/tests/StackExchange.Redis.Tests/MaintenanceNotificationTests.cs
+++ b/tests/StackExchange.Redis.Tests/MaintenanceNotificationTests.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StackExchange.Redis.Maintenance;
@@ -26,13 +27,15 @@ public class MaintenanceNotificationTests(ITestOutputHelper log)
///
private static async Task<(InProcessTestServer Server, ConnectionMultiplexer Connection, EventCollector Events)> ConnectAsync(
ITestOutputHelper log,
- ILoggerFactory? loggerFactory = null)
+ ILoggerFactory? loggerFactory = null,
+ Action? configure = null)
{
var server = new InProcessTestServer(log);
var config = server.GetClientConfig(defaultOnly: true);
config.Protocol = RedisProtocol.Resp3;
config.MaintenanceNotifications = MaintenanceNotificationMode.Enabled; // must be live, or the test is vacuous
config.LoggerFactory = loggerFactory;
+ configure?.Invoke(config);
var conn = await ConnectionMultiplexer.ConnectAsync(config);
return (server, conn, new EventCollector(conn));
@@ -525,11 +528,13 @@ await Poll.UntilAsync(() =>
await Poll.UntilAsync(() => conn.GetEndPoints().All(ep => muxer.GetServerEndPoint(ep).IsMaintenanceRelaxed)),
"every node that was told should have relaxed its own timeouts");
- // ...and the consumer sees it once
- Assert.Equal(1, events.Count);
+ // ...and the consumer sees it once: the callback is dispatched after the relaxation, not before, so wait
+ // for the one event rather than counting at the instant the second node relaxed, then prove the other
+ // delivery did not become a second event
var evt = await events.NextAsync();
Assert.Equal(5, evt.SequenceId);
- log.WriteLine($"{events.Count} event(s) from {delivered} deliveries: {evt.RawMessage}");
+ await events.AssertNoneAsync();
+ log.WriteLine($"1 event from {delivered} deliveries: {evt.RawMessage}");
Assert.Contains(evt.EndPoint, conn.GetEndPoints()); // whichever told us first
}
}
@@ -842,6 +847,192 @@ await Poll.UntilAsync(
}
}
+ [Fact]
+ public Task MovingUnderSteadyLoadRecyclesPromptlyAndFailsNothing() => SteadyLoadHandoffAsync(configure: null);
+
+ [Fact]
+ public Task MovingUnderSteadyLoadWithRelaxationDisabledStillDrains()
+ // a relaxed timeout of zero is the documented way to turn relaxation off; the drain cap is derived from it,
+ // and must keep a floor rather than recycle at once with commands still in flight
+ => SteadyLoadHandoffAsync(configure: c => c.MaintenanceRelaxedTimeout = TimeSpan.Zero);
+
+ private async Task SteadyLoadHandoffAsync(Action? configure)
+ {
+ // The drain used to wait for "no caller work", which under steady traffic never comes: measured live at
+ // about 450 ops/s, the recycle arrived at the end of the 15s window and failed the eight commands in
+ // flight at that instant, with no time left to establish the replacement. Holding new work back lets the
+ // written commands finish, so the recycle follows within a round trip and nothing fails: the held work is
+ // written to the replacement.
+ var logs = new CapturingLoggerFactory();
+ var (server, conn, events) = await ConnectAsync(log, logs, configure);
+ using (server)
+ await using (conn)
+ {
+ // every reply is held briefly, so with several workers something is always in flight
+ server.SetLatency(TimeSpan.FromMilliseconds(40));
+ var db = conn.GetDatabase();
+ using var stop = new CancellationTokenSource();
+ long completed = 0;
+ var failures = new List();
+ var workerTasks = Enumerable.Range(0, 4).Select(_ => Task.Run(async () =>
+ {
+ while (!stop.IsCancellationRequested)
+ {
+ try
+ {
+ await db.PingAsync();
+ Interlocked.Increment(ref completed);
+ }
+ catch (Exception ex)
+ {
+ lock (failures) failures.Add(ex);
+ }
+ }
+ })).ToArray();
+ try
+ {
+ Assert.True(await Poll.UntilAsync(() => Volatile.Read(ref completed) > 20, timeoutMilliseconds: 30_000), "the load should be flowing before the handoff");
+
+ var endpoint = ((IInternalConnectionMultiplexer)conn).GetServerEndPoint(server.DefaultEndPoint);
+ var successor = new IPEndPoint(IPAddress.Parse("127.0.0.9"), 6380);
+ var watch = System.Diagnostics.Stopwatch.StartNew();
+ server.SendMoving(null, timeSeconds: 10, newEndpoint: successor, sequenceId: 0);
+ var moving = await events.NextAsync();
+ Assert.Equal(MaintenanceNotificationType.Moving, moving.NotificationType);
+
+ Assert.True(
+ await Poll.UntilAsync(() => endpoint.HandoffRecycles > 0, timeoutMilliseconds: 8_000),
+ "the handoff should have replaced the connection");
+ var recycledAfter = watch.Elapsed;
+ log.WriteLine($"recycled after {recycledAfter.TotalMilliseconds:0}ms of a 10s window");
+
+ // jitter is at most a tenth of the window and the drain is one round trip; recycling at the deadline is the bug
+ Assert.True(recycledAfter < TimeSpan.FromSeconds(5), $"recycled after {recycledAfter}: that is waiting for the window, not draining");
+
+ // the replacement carries the held work and whatever follows. Note the in-process transport never dials the
+ // successor: its tunnel derives the socket target from the configured endpoint, so the reconnect goes
+ // straight back to this server; the timing here is the hold and the handshake, not a refused address.
+ var resumeWatch = System.Diagnostics.Stopwatch.StartNew();
+ var before = Volatile.Read(ref completed);
+ Assert.True(
+ await Poll.UntilAsync(() => Volatile.Read(ref completed) > before + 20, timeoutMilliseconds: 20_000),
+ "traffic should resume on the replacement connection");
+ log.WriteLine($"traffic resumed {resumeWatch.ElapsedMilliseconds}ms after the recycle was observed");
+
+ stop.Cancel();
+ await Task.WhenAll(workerTasks);
+ lock (failures)
+ {
+ log.WriteLine($"completed: {Volatile.Read(ref completed)}; failed: {failures.Count}");
+ Assert.Empty(failures.Select(f => f.Message));
+ }
+ Assert.DoesNotContain(logs.Lines, l => l.Contains("did not establish a replacement", StringComparison.Ordinal));
+ }
+ finally
+ {
+ stop.Cancel(); // on any exit: a failed assertion must not leave four workers hot-looping on a disposed multiplexer
+ }
+ }
+ }
+
+ [Fact]
+ public async Task MovingWaitsForSlowCommandsThatFitInsideTheDrainCap()
+ {
+ // A slow or blocking command that will finish inside the drain cap (half the relaxed timeout, 5s by default)
+ // is allowed to: the hold waits for it, timeouts are relaxed meanwhile, and the recycle follows its reply.
+ // The server's close would have let it finish too, so failing it would be a regression.
+ var logs = new CapturingLoggerFactory();
+ var (server, conn, events) = await ConnectAsync(log, logs);
+ using (server)
+ await using (conn)
+ {
+ server.SetLatency(TimeSpan.FromSeconds(3)); // the in-process server answers one request at a time, so one slow command, not several
+ var db = conn.GetDatabase();
+ var slow = db.PingAsync();
+ Assert.True(await Poll.UntilAsync(() => conn.GetCounters().Interactive.SentItemsAwaitingResponse > 0), "the slow command should be in flight before MOVING arrives");
+
+ var endpoint = ((IInternalConnectionMultiplexer)conn).GetServerEndPoint(server.DefaultEndPoint);
+ var successor = new IPEndPoint(IPAddress.Parse("127.0.0.9"), 6380);
+ var watch = System.Diagnostics.Stopwatch.StartNew();
+ server.SendMoving(null, timeSeconds: 10, newEndpoint: successor, sequenceId: 0);
+ var moving = await events.NextAsync();
+ Assert.Equal(MaintenanceNotificationType.Moving, moving.NotificationType);
+
+ Assert.True(
+ await Poll.UntilAsync(() => endpoint.HandoffRecycles > 0, timeoutMilliseconds: 10_000),
+ "the handoff should have replaced the connection");
+ var recycledAfter = watch.Elapsed;
+ server.SetLatency(TimeSpan.Zero);
+ log.WriteLine($"recycled after {recycledAfter.TotalMilliseconds:0}ms with a 3s command in flight");
+
+ // the slow command was answered, not failed: the recycle waited for it (had it not, this await would throw)
+ Assert.True(await slow >= TimeSpan.Zero);
+ // 3s for the command, up to 1s of jitter, poll granularity, and slack; the cap would be 5s + jitter
+ Assert.True(recycledAfter < TimeSpan.FromSeconds(7), $"recycled after {recycledAfter}: later than the command plus jitter");
+ }
+ }
+
+ [Fact]
+ public async Task MovingWithBlockingCommandsInFlightRecyclesAtTheDrainCap()
+ {
+ // A command that will not finish inside the cap - a long blocking read - must not hold everyone else's work
+ // in the backlog for the whole window: at the cap the connection is replaced regardless. The blocking command
+ // is failed by the recycle, as the server's close would have failed it; everything issued afterwards flows.
+ var logs = new CapturingLoggerFactory();
+ var (server, conn, events) = await ConnectAsync(log, logs);
+ using (server)
+ await using (conn)
+ {
+ server.SetLatency(TimeSpan.FromSeconds(12)); // well past the 5s cap and the slack below; the relaxed window covers it until the recycle
+ var db = conn.GetDatabase();
+ // one, not several: the test server answers one request at a time, so each extra 12s command would hold the
+ // replacement's handshake behind it as well
+ var blocking = db.PingAsync();
+ Assert.True(await Poll.UntilAsync(() => conn.GetCounters().Interactive.SentItemsAwaitingResponse > 0), "the blocking command should be in flight before MOVING arrives");
+
+ var endpoint = ((IInternalConnectionMultiplexer)conn).GetServerEndPoint(server.DefaultEndPoint);
+ var successor = new IPEndPoint(IPAddress.Parse("127.0.0.9"), 6380);
+ var watch = System.Diagnostics.Stopwatch.StartNew();
+ server.SendMoving(null, timeSeconds: 15, newEndpoint: successor, sequenceId: 0);
+ var moving = await events.NextAsync();
+ Assert.Equal(MaintenanceNotificationType.Moving, moving.NotificationType);
+
+ Assert.True(
+ await Poll.UntilAsync(() => endpoint.HandoffRecycles > 0, timeoutMilliseconds: 14_000),
+ "the handoff should have replaced the connection");
+ var recycledAfter = watch.Elapsed;
+ // Only now: the server reads its latency when it *executes* a request, and zeroing it any earlier races the
+ // one command this test needs to stay slow. From here the replacement's handshake waits behind at most
+ // that one 12s reply.
+ server.SetLatency(TimeSpan.Zero);
+ log.WriteLine($"recycled after {recycledAfter.TotalMilliseconds:0}ms with a 12s command in flight");
+
+ // jitter (at most a tenth of the window, capped at 1s) + the 5s cap + poll granularity is the mechanism's
+ // own worst case, about 6.5s; the bug being guarded against is a recycle at the 15s deadline, so 12s
+ // discriminates with room on both sides
+ Assert.True(recycledAfter < TimeSpan.FromSeconds(12), $"recycled after {recycledAfter}: the drain waited for the blocking command instead of capping");
+
+ // the in-flight command was failed by the recycle, which is what the server's close would have done
+ await Assert.ThrowsAnyAsync(async () => await blocking);
+
+ Assert.True(
+ await Poll.UntilAsync(
+ () =>
+ {
+ try
+ {
+ return conn.IsConnected && conn.GetDatabase().Ping() >= TimeSpan.Zero;
+ }
+ catch (Exception ex) when (ex is RedisException or TimeoutException)
+ {
+ return false;
+ }
+ },
+ timeoutMilliseconds: 40_000),
+ "the client should be serving commands on the replacement connection");
+ }
+ }
+
[Fact]
public async Task MovingRecyclesTheConnectionBeforeTheServerCloses()
{
@@ -884,7 +1075,7 @@ public async Task MovingRecyclesTheConnectionBeforeTheServerCloses()
Assert.True(
await Poll.UntilAsync(() => endpoint.HandoffTarget is not null || endpoint.HandoffRecycles > 0, timeoutMilliseconds: 10_000),
"the handoff should have taken the named successor");
- log.WriteLine($"handoff target: {endpoint.HandoffTarget?.ToString() ?? "(cleared after reconnect)"}; recycles={endpoint.HandoffRecycles}");
+ log.WriteLine($"handoff target: {endpoint.HandoffTarget?.ToString() ?? "(expired with the window)"}; recycles={endpoint.HandoffRecycles}");
// a fresh connection re-sends the opt-in, which is how a recycle is visible from the server's side
Assert.True(
diff --git a/tests/StackExchange.Redis.Tests/MultiGroupTests/GroupUnavailableTests.cs b/tests/StackExchange.Redis.Tests/MultiGroupTests/GroupUnavailableTests.cs
index 0f4c47b1b..12234125d 100644
--- a/tests/StackExchange.Redis.Tests/MultiGroupTests/GroupUnavailableTests.cs
+++ b/tests/StackExchange.Redis.Tests/MultiGroupTests/GroupUnavailableTests.cs
@@ -8,7 +8,7 @@ namespace StackExchange.Redis.Tests.MultiGroupTests;
///
/// What a group does when no member can serve: see
-/// .
+/// .
///
///
/// A group is an , so it has to fail the way one does. Reporting a
diff --git a/tests/StackExchange.Redis.Tests/RedisValueStorageKindUnitTests.cs b/tests/StackExchange.Redis.Tests/RedisValueStorageKindUnitTests.cs
index f23efe5fe..3a0b9973d 100644
--- a/tests/StackExchange.Redis.Tests/RedisValueStorageKindUnitTests.cs
+++ b/tests/StackExchange.Redis.Tests/RedisValueStorageKindUnitTests.cs
@@ -107,6 +107,6 @@ public void LiteralsResolveToExpectedStorageKinds()
// only two storage flavors should ever appear; a third means something new crept in (e.g. a numeric
// literal => Int64), which we want to notice
Assert.Equal(2, byKind.Count);
- Assert.Equal(28, Count(RedisValue.StorageType.ByteArray)); // literals > 8 bytes
+ Assert.Equal(30, Count(RedisValue.StorageType.ByteArray)); // literals > 8 bytes (+SLOT-STATS, +SLOTSRANGE)
}
}
diff --git a/tests/StackExchange.Redis.Tests/RespCommandTests.cs b/tests/StackExchange.Redis.Tests/RespCommandTests.cs
index f9755b137..524fb2ad3 100644
--- a/tests/StackExchange.Redis.Tests/RespCommandTests.cs
+++ b/tests/StackExchange.Redis.Tests/RespCommandTests.cs
@@ -194,8 +194,8 @@ public void ADefaultCommandIsRefusedRatherThanFramedAsNONE()
internal static partial class RespLiterals
{
-#pragma warning disable SER011 // stands in for the generator
+#pragma warning disable SER101 // stands in for the generator
internal static RespFragment Info => new("$4\r\nINFO\r\n"u8);
-#pragma warning restore SER011
+#pragma warning restore SER101
}
}
diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ClusterSlotStats.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ClusterSlotStats.cs
new file mode 100644
index 000000000..53cf8407c
--- /dev/null
+++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ClusterSlotStats.cs
@@ -0,0 +1,116 @@
+using System;
+using Xunit;
+
+namespace StackExchange.Redis.Tests.ResultProcessorUnitTests;
+
+public class ClusterSlotStats(ITestOutputHelper log) : ResultProcessorUnitTest(log)
+{
+ // a RESP2 reply with every documented metric present
+ private const string FullEntryResp2 =
+ "*1\r\n*2\r\n:100\r\n*10\r\n"
+ + "$9\r\nkey-count\r\n:42\r\n"
+ + "$8\r\ncpu-usec\r\n:1500\r\n"
+ + "$12\r\nmemory-bytes\r\n:2048\r\n"
+ + "$16\r\nnetwork-bytes-in\r\n:300\r\n"
+ + "$17\r\nnetwork-bytes-out\r\n:400\r\n";
+
+ // the same payload as a RESP3 map
+ private const string FullEntryResp3 =
+ "*1\r\n*2\r\n:100\r\n%5\r\n"
+ + "$9\r\nkey-count\r\n:42\r\n"
+ + "$8\r\ncpu-usec\r\n:1500\r\n"
+ + "$12\r\nmemory-bytes\r\n:2048\r\n"
+ + "$16\r\nnetwork-bytes-in\r\n:300\r\n"
+ + "$17\r\nnetwork-bytes-out\r\n:400\r\n";
+
+ private static void AssertFullEntry(ClusterSlotStatsResult? result)
+ {
+ Assert.NotNull(result);
+ var entry = Assert.Single(result.Slots);
+ Assert.Equal(100, entry.Slot);
+ Assert.Equal(42, entry.KeyCount);
+ Assert.Equal(TimeSpan.FromTicks(1500 * 10), entry.CpuTime); // 1500µs; 1 tick = 0.1µs
+ Assert.Equal(2048, entry.MemoryBytes);
+ Assert.Equal(300, entry.NetworkBytesIn);
+ Assert.Equal(400, entry.NetworkBytesOut);
+ Assert.Empty(entry.Metadata);
+ }
+
+ [Fact]
+ public void FullMap_Resp2()
+ => AssertFullEntry(Execute(FullEntryResp2, ClusterSlotStatsResult.Handler.Instance));
+
+ [Fact]
+ public void FullMap_Resp3()
+ => AssertFullEntry(Execute(FullEntryResp3, ClusterSlotStatsResult.Handler.Instance));
+
+ [Fact]
+ public void KeyCountOnly_OtherMetricsAreNull()
+ {
+ // key-count only: the other metrics are not being collected, and must come back null
+ const string resp = "*2\r\n"
+ + "*2\r\n:8000\r\n*2\r\n$9\r\nkey-count\r\n:7\r\n"
+ + "*2\r\n:12\r\n*2\r\n$9\r\nkey-count\r\n:3\r\n";
+
+ var result = Execute(resp, ClusterSlotStatsResult.Handler.Instance);
+ Assert.NotNull(result);
+ Assert.Equal(2, result.Slots.Count);
+ Assert.Equal(8000, result.Slots[0].Slot); // server order is preserved, not re-sorted
+ Assert.Equal(7, result.Slots[0].KeyCount);
+ Assert.Null(result.Slots[0].CpuTime);
+ Assert.Null(result.Slots[0].MemoryBytes);
+ Assert.Null(result.Slots[0].NetworkBytesIn);
+ Assert.Null(result.Slots[0].NetworkBytesOut);
+ Assert.Equal(12, result.Slots[1].Slot);
+ Assert.Equal(3, result.Slots[1].KeyCount);
+ }
+
+ [Fact]
+ public void UnknownMetric_IsPreservedAsMetadata()
+ {
+ // the metric set is documented as extensible; a metric this library has never
+ // heard of must survive as metadata, not be dropped
+ const string resp = "*1\r\n*2\r\n:0\r\n*4\r\n"
+ + "$9\r\nkey-count\r\n:1\r\n"
+ + "$11\r\nwidget-usec\r\n:99\r\n";
+
+ var result = Execute(resp, ClusterSlotStatsResult.Handler.Instance);
+ Assert.NotNull(result);
+ var entry = Assert.Single(result.Slots);
+ Assert.Equal(1, entry.KeyCount);
+ var extra = Assert.Single(entry.Metadata);
+ Assert.Equal("widget-usec", extra.Key);
+ Assert.Equal(99, extra.Value);
+ }
+
+ [Fact]
+ public void EmptyArray_IsEmpty()
+ {
+ var result = Execute("*0\r\n", ClusterSlotStatsResult.Handler.Instance);
+ Assert.NotNull(result);
+ Assert.Empty(result.Slots);
+ }
+
+ [Fact]
+ public void NilReply_IsNull()
+ => Assert.Null(Execute("*-1\r\n", ClusterSlotStatsResult.Handler.Instance));
+
+ [Fact]
+ public void MalformedEntry_IsDroppedNotFatal()
+ {
+ // an entry whose slot is not numeric is dropped; its well-formed sibling survives
+ const string resp = "*2\r\n"
+ + "*2\r\n$3\r\nwat\r\n*2\r\n$9\r\nkey-count\r\n:1\r\n"
+ + "*2\r\n:5\r\n*2\r\n$9\r\nkey-count\r\n:2\r\n";
+
+ var result = Execute(resp, ClusterSlotStatsResult.Handler.Instance);
+ Assert.NotNull(result);
+ var entry = Assert.Single(result.Slots);
+ Assert.Equal(5, entry.Slot);
+ Assert.Equal(2, entry.KeyCount);
+ }
+
+ [Fact]
+ public void ScalarReply_IsUnexpected()
+ => ExecuteUnexpected("+OK\r\n", ClusterSlotStatsResult.Handler.Instance);
+}
diff --git a/tests/StackExchange.Redis.Tests/RoundTripUnitTests/ClusterSlotStatsRoundTrip.cs b/tests/StackExchange.Redis.Tests/RoundTripUnitTests/ClusterSlotStatsRoundTrip.cs
new file mode 100644
index 000000000..9737fb2d6
--- /dev/null
+++ b/tests/StackExchange.Redis.Tests/RoundTripUnitTests/ClusterSlotStatsRoundTrip.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Text;
+using StackExchange.Redis.Protocol;
+using Xunit;
+
+namespace StackExchange.Redis.Tests.RoundTripUnitTests;
+
+///
+/// What CLUSTER SLOT-STATS puts on the wire, byte for byte - the operands carry the conditional
+/// clauses, so the shape is pinned here rather than discovered against a live server. The reply-shape
+/// matrix lives in .
+///
+public class ClusterSlotStatsRoundTrip
+{
+ [Fact]
+ public void SlotsRange_WritesTheRange()
+ => Assert.Equal(
+ "*5|$7|CLUSTER|$10|SLOT-STATS|$10|SLOTSRANGE|$3|100|$3|200|",
+ Render(RedisServer.ClusterSlotStatsSlotsRangeOperand.Create(100, 200)));
+
+ [Fact]
+ public void OrderBy_Defaults_OmitsLimitAndOrder()
+ => Assert.Equal(
+ "*4|$7|CLUSTER|$10|SLOT-STATS|$7|ORDERBY|$9|key-count|",
+ Render(RedisServer.ClusterSlotStatsOrderByOperand.Create(ClusterSlotStatsMetric.KeyCount, 0, Order.Descending)));
+
+ [Fact]
+ public void OrderBy_WithLimit_WritesLimit()
+ => Assert.Equal(
+ "*6|$7|CLUSTER|$10|SLOT-STATS|$7|ORDERBY|$8|cpu-usec|$5|LIMIT|$1|5|",
+ Render(RedisServer.ClusterSlotStatsOrderByOperand.Create(ClusterSlotStatsMetric.CpuTime, 5, Order.Descending)));
+
+ [Fact]
+ public void OrderBy_AscendingWithLimit_WritesBoth()
+ => Assert.Equal(
+ "*7|$7|CLUSTER|$10|SLOT-STATS|$7|ORDERBY|$17|network-bytes-out|$5|LIMIT|$1|3|$3|ASC|",
+ Render(RedisServer.ClusterSlotStatsOrderByOperand.Create(ClusterSlotStatsMetric.NetworkBytesOut, 3, Order.Ascending)));
+
+ [Fact]
+ public void OrderBy_AscendingNoLimit_WritesAscOnly()
+ => Assert.Equal(
+ "*5|$7|CLUSTER|$10|SLOT-STATS|$7|ORDERBY|$12|memory-bytes|$3|ASC|",
+ Render(RedisServer.ClusterSlotStatsOrderByOperand.Create(ClusterSlotStatsMetric.MemoryBytes, 0, Order.Ascending)));
+
+ [Fact]
+ public void SlotsRange_InvalidSlots_AreRejected()
+ {
+ Assert.Throws(() => RedisServer.ClusterSlotStatsSlotsRangeOperand.Create(-1, 100));
+ Assert.Throws(() => RedisServer.ClusterSlotStatsSlotsRangeOperand.Create(0, 16384));
+ Assert.Throws(() => RedisServer.ClusterSlotStatsSlotsRangeOperand.Create(200, 100));
+ }
+
+ [Fact]
+ public void OrderBy_InvalidArguments_AreRejected()
+ {
+ Assert.Throws(() => RedisServer.ClusterSlotStatsOrderByOperand.Create(ClusterSlotStatsMetric.KeyCount, -1, Order.Descending));
+ Assert.Throws(() => RedisServer.ClusterSlotStatsOrderByOperand.Create((ClusterSlotStatsMetric)42, 0, Order.Descending));
+ }
+
+ private static string Render(T operand)
+ where T : IRespArgument
+ {
+ var ctx = new RespContext();
+ using var frame = ctx.Render($"{RedisCommand.CLUSTER}{operand}");
+ return Encoding.UTF8.GetString(frame.Span.ToArray()).Replace("\r\n", "|");
+ }
+}
diff --git a/tests/StackExchange.Redis.Tests/ScanTests.cs b/tests/StackExchange.Redis.Tests/ScanTests.cs
index 84025ffc3..dff1e9f02 100644
--- a/tests/StackExchange.Redis.Tests/ScanTests.cs
+++ b/tests/StackExchange.Redis.Tests/ScanTests.cs
@@ -398,7 +398,7 @@ public async Task HashScanNoValuesLarge(int pageSize)
}
///
- /// See .
+ /// See .
///
[Fact]
public async Task HashScanThresholds()
diff --git a/tests/StackExchange.Redis.Tests/ValkeyUnitTests.cs b/tests/StackExchange.Redis.Tests/ValkeyUnitTests.cs
index 0ec3fc0c4..be7a1731f 100644
--- a/tests/StackExchange.Redis.Tests/ValkeyUnitTests.cs
+++ b/tests/StackExchange.Redis.Tests/ValkeyUnitTests.cs
@@ -23,7 +23,7 @@ public async Task IdentifyValkeyCluster(ServerType type)
private sealed class InProcessValkeyLikeServer(ITestOutputHelper log) : InProcessTestServer(log)
{
- // see https://github.com/StackExchange/StackExchange.Redis/pull/3050
+ // see https://github.com/seredis/StackExchange.Redis/pull/3050
protected override string ServerModeKey => "server_mode";
// Valkey reports its own version as HELLO "version" (see #3278); the name match is case-insensitive