Skip to content

Commit 54e01e1

Browse files
authored
8.8 - subkey (hash field) notifications (#3062)
* inital stab at subkey notifications * clean up API * docs, simplify usage * subkeys API * avoid a fixed stackalloc * CI file * tyop * integration tests for subkey notifications (something channel-routing related still failing) * fix channel-prefix scenarios * fix unit test (bad payload) * fix default-span "fixed" scenario; we could pre-check the length, but let's just fix the underlying problem * fix cluster routing of subkey notifications; assert newline logic; use deterministic tests (estimate was causing flakiness) * ignore codex files * experimental efficiency API * cleanup
1 parent d53a40c commit 54e01e1

28 files changed

Lines changed: 3956 additions & 952 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ launchSettings.json
2929
*.diagsession
3030
TestResults/
3131
BenchmarkDotNet.Artifacts/
32+
.codex

docs/KeyspaceNotifications.md

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ notify-keyspace-events AKE
2121
The two types of event (keyspace and keyevent) encode the same information, but in different formats.
2222
To simplify consumption, StackExchange.Redis provides a unified API for both types of event, via the `KeyNotification` type.
2323

24+
**From Redis 8.8**, you can also optionally enable sub-key (hash field) notifications, using additional tokens:
25+
26+
``` conf
27+
notify-keyspace-events AKESTIV
28+
```
29+
30+
- **S** - SubKeySpace notifications (`__subkeyspace@<db>__:<key>`)
31+
- **T** - SubKeyEvent notifications (`__subkeyevent@<db>__:<event>`)
32+
- **I** - SubKeySpaceItem notifications (`__subkeyspaceitem@<db>__:<key>\n<subkey>`)
33+
- **V** - SubKeySpaceEvent notifications (`__subkeyspaceevent@<db>__:<event>|<key>`)
34+
35+
These sub-key notification types allow you to monitor operations on hash fields (subkeys) in addition to key-level operations.
36+
The different formats provide the same information but organized differently, and StackExchange.Redis provides a unified API
37+
via the same `KeyNotification` type.
38+
2439
### Event Broadcasting in Redis Cluster
2540

2641
Importantly, in Redis Cluster, keyspace notifications are **not** broadcast to all nodes - they are only received by clients connecting to the
@@ -48,6 +63,17 @@ Note that there are a range of other `KeySpace...` and `KeyEvent...` methods for
4863

4964
The `KeySpace*` methods are similar, and are presented separately to make the intent clear. For example, `KeySpacePattern("foo*")` is equivalent to `KeySpacePrefix("foo")`, and will subscribe to all keys beginning with `"foo"`.
5065

66+
**From Redis 8.8**, there are corresponding `SubKeySpace...` and `SubKeyEvent...` methods for sub-key (hash field) notifications:
67+
68+
- `SubKeySpaceSingleKey` - subscribe to sub-key notifications for a single key in a specific database
69+
- `SubKeySpacePattern` - subscribe to sub-key notifications for a key pattern, optionally in a specific database
70+
- `SubKeySpacePrefix` - subscribe to sub-key notifications for all keys with a specific prefix, optionally in a specific database
71+
- `SubKeySpaceItem` - subscribe to sub-key notifications for a specific key and field combination in a specific database
72+
- `SubKeyEvent` - subscribe to sub-key notifications for a specific event type, optionally in a specific database
73+
- `SubKeySpaceEvent` - subscribe to sub-key notifications for a specific event type and key, optionally in a specific database
74+
75+
These work similarly to their key-level counterparts, but monitor hash field operations instead of key operations.
76+
5177
Next, we subscribe to the channel and process the notifications using the normal pub/sub subscription API; there are two
5278
main approaches: queue-based and callback-based.
5379

@@ -79,19 +105,34 @@ sub.Subscribe(channel, (recvChannel, recvValue) =>
79105
Console.WriteLine($"Key: {notification.GetKey()}");
80106
Console.WriteLine($"Type: {notification.Type}");
81107
Console.WriteLine($"Database: {notification.Database}");
108+
Console.WriteLine($"Kind: {notification.Kind}");
109+
110+
// For sub-key notifications (Redis 8.8+), you can access sub-keys in a uniform way,
111+
// regardless of the notification type
112+
if (notification.HasSubKey)
113+
{
114+
// Get the first sub-key
115+
Console.WriteLine($"First SubKey: {notification.GetSubKeys().First()}");
116+
117+
// Or iterate all sub-keys (for notifications with multiple fields)
118+
foreach (var subKey in notification.GetSubKeys())
119+
{
120+
Console.WriteLine($"SubKey: {subKey}");
121+
}
122+
}
82123
}
83124
});
84125
```
85126

86127
Note that the channels created by the `KeySpace...` and `KeyEvent...` methods cannot be used to manually *publish* events,
87128
only to subscribe to them. The events are published automatically by the Redis server when keys are modified. If you
88129
want to simulate keyspace notifications by publishing events manually, you should use regular pub/sub channels that avoid
89-
the `__keyspace@` and `__keyevent@` prefixes.
130+
the `__keyspace@` and `__keyevent@` prefixes (and similarly for sub-key events).
90131

91132
## Performance considerations for KeyNotification
92133

93134
The `KeyNotification` struct provides parsed notification data, including (as already shown) the key, event type,
94-
database, etc. Note that using `GetKey()` will allocate a copy of the key bytes; to avoid allocations,
135+
database, kind, etc. Note that using `GetKey()` will allocate a copy of the key bytes; to avoid allocations,
95136
you can use `TryCopyKey()` to copy the key bytes into a provided buffer (potentially with `GetKeyByteCount()`,
96137
`GetKeyMaxCharCount()`, etc in order to size the buffer appropriately). Similarly, `KeyStartsWith()` can be used to
97138
efficiently check the key prefix without allocating a string. This approach is designed to be efficient for high-volume
@@ -105,6 +146,66 @@ for the key entirely, and instead just copy the bytes into a buffer. If we consi
105146
contain the key for the majority of notifications (since they are for cache invalidation), this can be a significant
106147
performance win.
107148

149+
## Working with Sub-Key (Hash Field) Notifications
150+
151+
**From Redis 8.8**, Redis supports notifications for hash field (sub-key) operations. These notifications provide
152+
more granular monitoring of hash operations, allowing you to observe changes to individual hash fields rather than
153+
just key-level operations.
154+
155+
### Understanding Sub-Key Notification Types
156+
157+
There are four sub-key notification kinds, analogous to the two key-level notification kinds:
158+
159+
- **SubKeySpace** (`__subkeyspace@<db>__:<key>`) - Notifications for a specific hash key, with the event type and sub-key in the payload
160+
- **SubKeyEvent** (`__subkeyevent@<db>__:<event>`) - Notifications for a specific event type, with the key and sub-key in the payload
161+
- **SubKeySpaceItem** (`__subkeyspaceitem@<db>__:<key>\n<subkey>`) - Notifications for a specific hash key and field combination
162+
- **SubKeySpaceEvent** (`__subkeyspaceevent@<db>__:<event>|<key>`) - Notifications for a specific event and key, with the sub-key in the payload
163+
164+
In most cases, the application code already knows the kind of event being consumed, but if that logic is centralized,
165+
you can determine the notification family using the `notification.Kind` property (which returns a
166+
`KeyNotificationKind` enum value), and optionally extract sub-keys using `notification.GetSubKeys()`.
167+
168+
### Example: Monitoring Hash Field Changes
169+
170+
```csharp
171+
// Subscribe to all sub-key changes for hashes with prefix "user:"
172+
var channel = RedisChannel.SubKeySpacePrefix("user:", database: 0);
173+
174+
sub.Subscribe(channel, (recvChannel, recvValue) =>
175+
{
176+
if (KeyNotification.TryParse(recvChannel, recvValue, out var notification))
177+
{
178+
Console.WriteLine($"Hash Key: {notification.GetKey()}");
179+
Console.WriteLine($"Operation: {notification.Type}");
180+
Console.WriteLine($"Kind: {notification.Kind}");
181+
182+
// Process all affected fields
183+
foreach (var field in notification.GetSubKeys())
184+
{
185+
Console.WriteLine($"Field: {field}");
186+
}
187+
188+
// Or get just the first field for single-field operations
189+
var firstField = notification.GetSubKeys().FirstOrDefault();
190+
191+
// Utility methods available:
192+
// - Count() - get the number of fields
193+
// - First() / FirstOrDefault() - get the first field
194+
// - Single() / SingleOrDefault() - get the only field (throws if multiple)
195+
// - ToArray() / ToList() - convert to collection
196+
// - CopyTo(Span<RedisValue>) - copy to a span (allocation-free)
197+
}
198+
});
199+
200+
// Or subscribe to specific hash field events (e.g., HSET operations)
201+
var eventChannel = RedisChannel.SubKeyEvent(KeyNotificationType.HSet, database: 0);
202+
```
203+
204+
### Sub-Key and Key Prefix Filtering
205+
206+
When using key-prefix filtering with sub-key notifications, the prefix is applied to the **key** only, not to the
207+
sub-key (hash field). The sub-key is always returned as-is from the notification, without any prefix stripping.
208+
108209
## Considerations when using database isolation
109210

110211
Database isolation is controlled either via the `ConfigurationOptions.DefaultDatabase` option when connecting to Redis,
@@ -123,6 +224,14 @@ For example:
123224
- `RedisChannel.KeyEvent(KeyNotificationType.Set, 0)` maps to `SUBSCRIBE __keyevent@0__:set`
124225
- `RedisChannel.KeyEvent(KeyNotificationType.Set)` maps to `PSUBSCRIBE __keyevent@*__:set`
125226

227+
**From Redis 8.8**, the sub-key notification methods work similarly:
228+
229+
- `RedisChannel.SubKeySpaceSingleKey("myhash", 0)` maps to `SUBSCRIBE __subkeyspace@0__:myhash`
230+
- `RedisChannel.SubKeySpacePrefix("hash:", 0)` maps to `PSUBSCRIBE __subkeyspace@0__:hash:*`
231+
- `RedisChannel.SubKeySpaceItem("myhash", "field1", 0)` maps to `SUBSCRIBE __subkeyspaceitem@0__:myhash\nfield1`
232+
- `RedisChannel.SubKeyEvent(KeyNotificationType.HSet, 0)` maps to `SUBSCRIBE __subkeyevent@0__:hset`
233+
- `RedisChannel.SubKeySpaceEvent(KeyNotificationType.HSet, "myhash", 0)` maps to `SUBSCRIBE __subkeyspaceevent@0__:hset|myhash`
234+
126235
Additionally, note that while most of these examples require multi-node subscriptions on Redis Cluster, `KeySpaceSingleKey`
127236
is an exception, and will only subscribe to the single node that owns the key `foo`.
128237

docs/ReleaseNotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Current package versions:
1212
- Add Redis 8.8 stream negative acknowledgements (`XNACK`) ([#3058 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3058))
1313
- Update experimental `GCRA` APIs and wire protocol terminology from "requests" to "tokens", to match server change ([#3051 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3051))
1414
- 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))
15+
- Support sub-key (hash field) notifications ([#3062 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3062))
1516
- Add `ValueCondition` overloads for `SortedSetIncrement`/`SortedSetIncrementAsync`, supporting `ZADD INCR` with existence conditions ([#3071 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3071))
1617
- Recognize Azure Managed Redis (AMR) resources in new Azure clouds ([#3068 by @philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/3068))
1718

src/StackExchange.Redis/FrameworkShims.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#else
77
// To support { get; init; } properties
88
using System.ComponentModel;
9+
using System.Runtime.CompilerServices;
10+
using System.Runtime.InteropServices;
911
using System.Text;
1012

1113
namespace System.Runtime.CompilerServices
@@ -35,9 +37,9 @@ internal static class EncodingExtensions
3537
{
3638
public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan<char> source, Span<byte> destination)
3739
{
38-
fixed (byte* bPtr = destination)
40+
fixed (byte* bPtr = &MemoryMarshal.GetReference(destination))
3941
{
40-
fixed (char* cPtr = source)
42+
fixed (char* cPtr = &MemoryMarshal.GetReference(source))
4143
{
4244
return encoding.GetBytes(cPtr, source.Length, bPtr, destination.Length);
4345
}
@@ -46,9 +48,9 @@ public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan<char> sou
4648

4749
public static unsafe int GetChars(this Encoding encoding, ReadOnlySpan<byte> source, Span<char> destination)
4850
{
49-
fixed (byte* bPtr = source)
51+
fixed (byte* bPtr = &MemoryMarshal.GetReference(source))
5052
{
51-
fixed (char* cPtr = destination)
53+
fixed (char* cPtr = &MemoryMarshal.GetReference(destination))
5254
{
5355
return encoding.GetChars(bPtr, source.Length, cPtr, destination.Length);
5456
}
@@ -57,15 +59,15 @@ public static unsafe int GetChars(this Encoding encoding, ReadOnlySpan<byte> sou
5759

5860
public static unsafe int GetCharCount(this Encoding encoding, ReadOnlySpan<byte> source)
5961
{
60-
fixed (byte* bPtr = source)
62+
fixed (byte* bPtr = &MemoryMarshal.GetReference(source))
6163
{
6264
return encoding.GetCharCount(bPtr, source.Length);
6365
}
6466
}
6567

6668
public static unsafe string GetString(this Encoding encoding, ReadOnlySpan<byte> source)
6769
{
68-
fixed (byte* bPtr = source)
70+
fixed (byte* bPtr = &MemoryMarshal.GetReference(source))
6971
{
7072
return encoding.GetString(bPtr, source.Length);
7173
}

0 commit comments

Comments
 (0)