Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
<!-- SER001/SER002/SER003/SER006/SER007/SER008/SER010/SER013 retired (vector sets, Redis 8.4/8.6/8.8/8.10
features, geo-redundant failover, maintenance notifications, and the interpolated writer, no longer
experimental); IDs reserved, see Experiments.cs.
SER011 is deliberately absent: hand-written RespFragment is meant to be seen at every call site. -->
<NoWarn>$(NoWarn);NU5105;NU1507;SER004;SER005;SER009;SER012;SER014</NoWarn>
<PackageReleaseNotes>https://github.com/StackExchange/StackExchange.Redis/releases</PackageReleaseNotes>
SER101 is deliberately absent: hand-written RespFragment is meant to be seen at every call site. -->
<NoWarn>$(NoWarn);NU5105;NU1507;SER004;SER005;SER009;SER012;SER102;SER103</NoWarn>
<PackageReleaseNotes>https://github.com/seredis/StackExchange.Redis/releases</PackageReleaseNotes>
<PackageProjectUrl>https://seredis.dev/</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

<LangVersion>14</LangVersion>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/StackExchange/StackExchange.Redis/</RepositoryUrl>
<RepositoryUrl>https://github.com/seredis/StackExchange.Redis/</RepositoryUrl>

<DebugSymbols>true</DebugSymbols>
<DebugType>embedded</DebugType>
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ 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

| Package | NuGet Stable | NuGet Pre-release | Downloads |
| ------- | ------------ | ----------------- | --------- |
| [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))
2 changes: 1 addition & 1 deletion docs/404.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<li><a href="/Timeouts">Timeouts</a> &mdash; reading a timeout message</li>
<li><a href="/Configuration">Configuration</a></li>
<li><a href="/Basics">Basics</a></li>
<li><a href="https://github.com/StackExchange/StackExchange.Redis/releases">Release notes</a>
<li><a href="https://github.com/seredis/StackExchange.Redis/releases">Release notes</a>
(3.0 onwards; <a href="/ReleaseNotes">earlier releases</a>)</li>
</ul>

Expand Down
62 changes: 62 additions & 0 deletions docs/ClusterSlotStats.md
Original file line number Diff line number Diff line change
@@ -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 ...`.
3 changes: 1 addition & 2 deletions docs/CommandCoverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions docs/Groups.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ ValueTask<TResult> 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<T>`: 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<T>`: 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:

Expand All @@ -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
---
Expand Down
4 changes: 2 additions & 2 deletions docs/LegacyApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>($"...")` - see [Extending the client](Extending) |
| `ExecuteResp` | `db.Context.SendAsync<T>($"...")` - see [Extending the client](Extending) |
| `GeoRadius` | `db.Geospatial.SearchAsync` (`GEORADIUS` is deprecated by the server in favour of `GEOSEARCH`) |
Expand Down
Loading
Loading