diff --git a/Directory.Build.props b/Directory.Build.props index b5167d95f..404615e3d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,15 +13,15 @@ - $(NoWarn);NU5105;NU1507;SER004;SER005;SER009;SER012;SER014 - https://github.com/StackExchange/StackExchange.Redis/releases + SER101 is deliberately absent: hand-written RespFragment is meant to be seen at every call site. --> + $(NoWarn);NU5105;NU1507;SER004;SER005;SER009;SER012;SER102;SER103 + https://github.com/seredis/StackExchange.Redis/releases https://seredis.dev/ MIT 14 git - https://github.com/StackExchange/StackExchange.Redis/ + https://github.com/seredis/StackExchange.Redis/ true embedded diff --git a/README.md b/README.md index 5d2b6cc7e..dbc27b1c0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ For all documentation, [see here](https://seredis.dev/). #### Build Status -[![CI](https://github.com/StackExchange/StackExchange.Redis/actions/workflows/CI.yml/badge.svg)](https://github.com/StackExchange/StackExchange.Redis/actions/workflows/CI.yml) +[![CI](https://github.com/seredis/StackExchange.Redis/actions/workflows/CI.yml/badge.svg)](https://github.com/seredis/StackExchange.Redis/actions/workflows/CI.yml) #### Package Status @@ -15,4 +15,4 @@ For all documentation, [see here](https://seredis.dev/). | ------- | ------------ | ----------------- | --------- | | [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/absoluteLatest) | [![StackExchange.Redis](https://img.shields.io/nuget/dt/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | -Release notes at: [GitHub Releases](https://github.com/StackExchange/StackExchange.Redis/releases) (3.0 onwards; [earlier releases](https://seredis.dev/ReleaseNotes)) +Release notes at: [GitHub Releases](https://github.com/seredis/StackExchange.Redis/releases) (3.0 onwards; [earlier releases](https://seredis.dev/ReleaseNotes)) diff --git a/docs/404.html b/docs/404.html index cb4dd0ac4..2a0c5b66a 100644 --- a/docs/404.html +++ b/docs/404.html @@ -17,7 +17,7 @@
  • Timeouts — reading a timeout message
  • Configuration
  • Basics
  • -
  • Release notes +
  • Release notes (3.0 onwards; earlier releases)
  • 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! projection, out RESPite.Messages.RespAggregate value) -> bool -[SER012]RESPite.Messages.RespPairAggregate -[SER012]RESPite.Messages.RespPairAggregate.Count.get -> int -[SER012]static RESPite.Messages.RespPairAggregate.Empty.get -> RESPite.Messages.RespPairAggregate -[SER012]RESPite.Messages.RespPairAggregate.Enumerator -[SER012]RESPite.Messages.RespPairAggregate.Enumerator.Current.get -> T -[SER012]RESPite.Messages.RespPairAggregate.Enumerator.Enumerator() -> void -[SER012]RESPite.Messages.RespPairAggregate.Enumerator.MoveNext() -> bool -[SER012]RESPite.Messages.RespPairAggregate.GetEnumerator() -> RESPite.Messages.RespPairAggregate.Enumerator -[SER012]RESPite.Messages.RespPairAggregate.IsJagged.get -> bool -[SER012]RESPite.Messages.RespPairAggregate.Prefix.get -> RESPite.Messages.RespPrefix -[SER012]RESPite.Messages.RespPairAggregate.RespPairAggregate() -> void -[SER012]RESPite.Messages.RespPairAggregate.ToArray() -> T[]! -[SER012]override RESPite.Messages.RespPairAggregate.ToString() -> string! -[SER012]static RESPite.Messages.RespPairAggregate.TryCaptureNext(object? owner, scoped ref RESPite.Messages.RespReader reader, RESPite.Messages.RespReader.PairProjection! projection, bool allowJagged, out RESPite.Messages.RespPairAggregate value) -> bool +[SER102]RESPite.Messages.IRespBufferOwner +[SER102]RESPite.Messages.IRespBufferOwner.GetReadOnlySpan() -> System.ReadOnlySpan +[SER102]RESPite.Messages.RespAggregate +[SER102]RESPite.Messages.RespAggregate.Count.get -> int +[SER102]static RESPite.Messages.RespAggregate.Empty.get -> RESPite.Messages.RespAggregate +[SER102]RESPite.Messages.RespAggregate.Enumerator +[SER102]RESPite.Messages.RespAggregate.Enumerator.Current.get -> T +[SER102]RESPite.Messages.RespAggregate.Enumerator.Enumerator() -> void +[SER102]RESPite.Messages.RespAggregate.Enumerator.MoveNext() -> bool +[SER102]RESPite.Messages.RespAggregate.GetEnumerator() -> RESPite.Messages.RespAggregate.Enumerator +[SER102]RESPite.Messages.RespAggregate.Prefix.get -> RESPite.Messages.RespPrefix +[SER102]RESPite.Messages.RespAggregate.RespAggregate() -> void +[SER102]RESPite.Messages.RespAggregate.ToArray() -> T[]! +[SER102]override RESPite.Messages.RespAggregate.ToString() -> string! +[SER102]static RESPite.Messages.RespAggregate.TryCaptureNext(object? owner, scoped ref RESPite.Messages.RespReader reader, RESPite.Messages.RespReader.Projection! projection, out RESPite.Messages.RespAggregate value) -> bool +[SER102]RESPite.Messages.RespPairAggregate +[SER102]RESPite.Messages.RespPairAggregate.Count.get -> int +[SER102]static RESPite.Messages.RespPairAggregate.Empty.get -> RESPite.Messages.RespPairAggregate +[SER102]RESPite.Messages.RespPairAggregate.Enumerator +[SER102]RESPite.Messages.RespPairAggregate.Enumerator.Current.get -> T +[SER102]RESPite.Messages.RespPairAggregate.Enumerator.Enumerator() -> void +[SER102]RESPite.Messages.RespPairAggregate.Enumerator.MoveNext() -> bool +[SER102]RESPite.Messages.RespPairAggregate.GetEnumerator() -> RESPite.Messages.RespPairAggregate.Enumerator +[SER102]RESPite.Messages.RespPairAggregate.IsJagged.get -> bool +[SER102]RESPite.Messages.RespPairAggregate.Prefix.get -> RESPite.Messages.RespPrefix +[SER102]RESPite.Messages.RespPairAggregate.RespPairAggregate() -> void +[SER102]RESPite.Messages.RespPairAggregate.ToArray() -> T[]! +[SER102]override RESPite.Messages.RespPairAggregate.ToString() -> string! +[SER102]static RESPite.Messages.RespPairAggregate.TryCaptureNext(object? owner, scoped ref RESPite.Messages.RespReader reader, RESPite.Messages.RespReader.PairProjection! projection, bool allowJagged, out RESPite.Messages.RespPairAggregate value) -> bool RESPite.Messages.RespReader.CopyTo(scoped System.Span target, System.Text.Encoding? encoding = null) -> int RESPite.Messages.RespReader.IsAllJaggedPairs() -> bool RESPite.Messages.RespReader.PairProjection virtual RESPite.Messages.RespReader.PairProjection.Invoke(ref TState state, ref RESPite.Messages.RespReader first, ref RESPite.Messages.RespReader second) -> TResult -[SER012]RESPite.Messages.RespValue -[SER012]RESPite.Messages.RespValue.AsBoolean() -> bool -[SER012]RESPite.Messages.RespValue.AsDouble() -> double -[SER012]RESPite.Messages.RespValue.AsInt32() -> int -[SER012]RESPite.Messages.RespValue.AsInt64() -> long -[SER012]RESPite.Messages.RespValue.CopyTo(scoped System.Span target) -> int -[SER012]RESPite.Messages.RespValue.CopyTo(scoped System.Span target, System.Text.Encoding? encoding = null) -> int -[SER012]RESPite.Messages.RespValue.Equals(RESPite.Messages.RespValue other) -> bool -[SER012]override RESPite.Messages.RespValue.Equals(object? obj) -> bool -[SER012]RESPite.Messages.RespValue.Frame.get -> System.ReadOnlySpan -[SER012]override RESPite.Messages.RespValue.GetHashCode() -> int -[SER012]RESPite.Messages.RespValue.HasValue.get -> bool -[SER012]RESPite.Messages.RespValue.IsNull.get -> bool -[SER012]RESPite.Messages.RespValue.Length.get -> int -[SER012]static RESPite.Messages.RespValue.Null.get -> RESPite.Messages.RespValue -[SER012]RESPite.Messages.RespValue.Prefix.get -> RESPite.Messages.RespPrefix -[SER012]RESPite.Messages.RespValue.RespValue() -> void -[SER012]override RESPite.Messages.RespValue.ToString() -> string! -[SER012]static RESPite.Messages.RespValue.TryCaptureNext(object? owner, scoped ref RESPite.Messages.RespReader reader, out RESPite.Messages.RespValue value) -> bool -[SER012]RESPite.Messages.RespValue.TryGetSpan(out System.ReadOnlySpan payload) -> bool -[SER012]static RESPite.Messages.RespValue.explicit operator string?(RESPite.Messages.RespValue value) -> string? +[SER102]RESPite.Messages.RespValue +[SER102]RESPite.Messages.RespValue.AsBoolean() -> bool +[SER102]RESPite.Messages.RespValue.AsDouble() -> double +[SER102]RESPite.Messages.RespValue.AsInt32() -> int +[SER102]RESPite.Messages.RespValue.AsInt64() -> long +[SER102]RESPite.Messages.RespValue.CopyTo(scoped System.Span target) -> int +[SER102]RESPite.Messages.RespValue.CopyTo(scoped System.Span target, System.Text.Encoding? encoding = null) -> int +[SER102]RESPite.Messages.RespValue.Equals(RESPite.Messages.RespValue other) -> bool +[SER102]override RESPite.Messages.RespValue.Equals(object? obj) -> bool +[SER102]RESPite.Messages.RespValue.Frame.get -> System.ReadOnlySpan +[SER102]override RESPite.Messages.RespValue.GetHashCode() -> int +[SER102]RESPite.Messages.RespValue.HasValue.get -> bool +[SER102]RESPite.Messages.RespValue.IsNull.get -> bool +[SER102]RESPite.Messages.RespValue.Length.get -> int +[SER102]static RESPite.Messages.RespValue.Null.get -> RESPite.Messages.RespValue +[SER102]RESPite.Messages.RespValue.Prefix.get -> RESPite.Messages.RespPrefix +[SER102]RESPite.Messages.RespValue.RespValue() -> void +[SER102]override RESPite.Messages.RespValue.ToString() -> string! +[SER102]static RESPite.Messages.RespValue.TryCaptureNext(object? owner, scoped ref RESPite.Messages.RespReader reader, out RESPite.Messages.RespValue value) -> bool +[SER102]RESPite.Messages.RespValue.TryGetSpan(out System.ReadOnlySpan payload) -> bool +[SER102]static RESPite.Messages.RespValue.explicit operator string?(RESPite.Messages.RespValue value) -> string? diff --git a/src/RESPite/Shared/Experiments.cs b/src/RESPite/Shared/Experiments.cs index 46de8a116..aee70da7c 100644 --- a/src/RESPite/Shared/Experiments.cs +++ b/src/RESPite/Shared/Experiments.cs @@ -30,6 +30,13 @@ internal static class Experiments public const string Respite = "SER004"; public const string UnitTesting = "SER005"; public const string Transport = "SER009"; + public const string ClusterSlotStats = "SER012"; // SER011 is Server_8_12, on the open set-member-expiry PR + + // Numbering convention: the 3.x line allocates SER0xx as features release there, and those + // numbers ship - so this branch can never claim the next free SER0xx without racing it (the + // SER010 collision above, and SER011/SER012 after it, were exactly that race). Branch-only + // experiments therefore live in a SER1xx block that 3.x will never reach; released numbers + // always win, and a SER1xx experiment keeps its number even after a merge. /// /// Constructing a RespFragment by hand. Deliberately NOT in the global NoWarn: the whole point @@ -40,7 +47,7 @@ internal static class Experiments /// surface being supported does not blunt. /// /// - 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