Skip to content

Conversation

@mgravell
Copy link
Collaborator

@mgravell mgravell commented Jan 22, 2026

Keyspace notifications are poorly supported; here we extend this

  1. a new API is added to simplify working with keyspace and keyevent messages, which have non-trivial semantics2
  2. the existing channel-creation API is extended to support simple handling for keyspace/keyevent notifications
  3. the implementation is adjusted to properly support cluster-based keyspace notifications, which have different subscription mechanisms

1: new KeyNotification API

  • new readonly struct KeyNotification that wraps a channel and value, and exposes .Database, .Type, .GetKey(), etc - and the raw .Channel and .Value for unknown message types; overall usage is shown in KeyNotificationTests.cs.
  • new enum KeyNotificationType for the expected values (KeyNotification.Type, else KeyNotificationType.Unknown)
  • the existing ChannelMessage type gains a new TryParseKeyNotification API, "do we recognize this as a KeyNotification?"

The ChannelMessage and KeyNotification types are very similar; I considered just adding the new members to ChannelMessage, but IMO KeyNotification deserves calling out into a dedicated type.

Note that keyspace and keyevent messages have different semantics for where the type and key are located; KeyNotification hides that detail, providing a single API that works for both scenarios. The individual values are parsed lazily in the accessors - they are not pre-computed; so if nobody accesses .Database: it is never parsed from the channel, etc. Internally, the [FastHash] approach is used for parsing .Type, since there is not a direct map between the enum names and the raw expected values (the enums follow .NET conventions; the raw values have a mixture of underscores, hyphens, etc - plus different capitalization); this is handled in the internal KeyNotificationTypeFastHash, with extensive unit testing.

2: new API for subscribing to keyspace/keyevent channels

Proposed API:

  public class RedisChannel
  {
+     // single exact key, database is required, always uses SUBSCRIBE
+     // example "SUBSCRIBE __keyspace@42__:mykey`
+     // note that this scenario can use single-node key-like routing
+     public static RedisChannel KeySpace(RedisKey key, int database)
+ 
+     // key pattern, database optional, interpreted as "any" when omitted, always uses PSUBSCRIBE
+     // example: "PSUBSCRIBE __keyspace@42__:customer/*"
+     // example: "PSUBSCRIBE __keyspace@*__:customer/*"
+     public static RedisChannel KeySpacePattern(RedisKey pattern, int? database = null)
+ 
+     // type-based, database optional, interpreted as "any" when omitted; uses PSUBSCRIBE when database omitted
+     // example: "SUBSCRIBE __keyevent@42__:del"
+     // example: "PSUBSCRIBE __keyevent@*__:del"
+     public static RedisChannel KeyEvent(KeyNotificationType type, int? database = null);
  }

Additionally, note that the WithKeyRouting() method intentionally throws if used with any of the channels that would use multi-node semantics, as key-routing and multi-node are fundamentally incompatible.

No explicit fully-open API (i.e. all-keys keyspace or all-type keyevent) is provided; this is intentional; we should discourage this for performance/impact reasons, but this is covered indirectly via KeyPattern("*", null), which allows the usage without making it a too-obvious default.

When db is null, it is interpreted as "any database". We intentionally do not provide simplified access to the default database as a substituted value - this is because we know that active:active is on the horizon, and that gets into very ugly overlap with pub/sub if different config groups have different default databases. If we want to support this scenario (for example, so that negative values are interpreted as the default database), we should probably enforce, in active:active, that all config groups have the same effective default database.

3: internal changes to support cluster routing

(incomplete) The key challenge here is that the events are routed to all primaries; this contrasts with the current scenarios:

  • var channel = RedisChannel.Literal("abc"); - single-node, arbitrary routing, uses SUBSCRIBE and PUBLISH
  • var channel = RedisChannel.Pattern("abc*"); - single-node, arbitrary routing, uses PSUBSCRIBE and PUBLISH
  • as either of above, but with .WithKeyRouting(); - same, but routed according to channel as a key
  • var channel = RedisChannel.Sharded("abc"); - single node, routed according to channel as a key, uses SSUBSCRIBE and SPUBLISH

This will demand a new model for the initial subscription, and subscription maintenance over both reconnects and topology changes, where the semantics are SUBSCRIBE or PSUBSCRIBE (whichever is more appropriate), but multi-node routing. We should not support or allow any publish API, since these events are server-originated.

- KeyNotification wraps channel+value, exposes friendly parsed members (db, type, etc)
- KeyNotificationType is new enum for known values
- add TryParseKeyNotification help to ChannelMessage (and use explicit fields)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants