- Add Microsoft.Extensions.Logging + Serilog to NatsServer and NatsClient - Convert all test assertions from xUnit Assert to Shouldly - Add NSubstitute package for future mocking needs - Introduce Central Package Management via Directory.Packages.props - Add documentation_rules.md with style guide, generation/update rules, component map - Generate 10 documentation files across 5 component folders (GettingStarted, Protocol, Subscriptions, Server, Configuration/Operations) - Update CLAUDE.md with logging, testing, porting, agent model, CPM, and documentation guidance
170 lines
6.6 KiB
Markdown
170 lines
6.6 KiB
Markdown
# Subscriptions Overview
|
|
|
|
The subscription system maps published subjects to interested subscribers. It consists of four classes: `Subscription` (the subscription model), `SubListResult` (a match result container), `SubjectMatch` (subject validation and wildcard matching), and `SubList` (the trie that routes messages to subscribers).
|
|
|
|
Go reference: `golang/nats-server/server/sublist.go`
|
|
|
|
---
|
|
|
|
## Subjects
|
|
|
|
A NATS subject is a dot-separated string of tokens: `foo.bar.baz`. Each token is a non-empty sequence of non-whitespace characters. The token separator is `.`.
|
|
|
|
### Wildcards
|
|
|
|
Two wildcard tokens are supported:
|
|
|
|
| Token | Name | Matches |
|
|
|-------|------|---------|
|
|
| `*` | Partial wildcard (Pwc) | Exactly one token at that position |
|
|
| `>` | Full wildcard (Fwc) | One or more remaining tokens |
|
|
|
|
Rules:
|
|
- `foo.*` matches `foo.bar` but not `foo.bar.baz` (only one token after `foo`).
|
|
- `foo.>` matches `foo.bar` and `foo.bar.baz` (one or more tokens after `foo`).
|
|
- `>` must be the last token in a subject. `foo.>.bar` is invalid.
|
|
- Publishers cannot use wildcards — only subscribers can.
|
|
|
|
### Queue Groups
|
|
|
|
Subscribers with the same queue name form a queue group. When a message matches a queue group, exactly one member of that group receives the message. The server selects a member per published message, which distributes load across the group.
|
|
|
|
Queue subscriptions are independent of plain subscriptions. If a subject has both plain subscribers and queue group subscribers, all plain subscribers receive the message and one member from each queue group receives it.
|
|
|
|
---
|
|
|
|
## The `Subscription` Class
|
|
|
|
A `Subscription` represents one subscriber's interest in a subject.
|
|
|
|
```csharp
|
|
public sealed class Subscription
|
|
{
|
|
public required string Subject { get; init; }
|
|
public string? Queue { get; init; }
|
|
public required string Sid { get; init; }
|
|
public long MessageCount; // Interlocked
|
|
public long MaxMessages; // 0 = unlimited
|
|
public NatsClient? Client { get; set; }
|
|
}
|
|
```
|
|
|
|
- `Subject` — the subject pattern, which may contain wildcards.
|
|
- `Queue` — the queue group name. `null` means this is a plain subscription.
|
|
- `Sid` — the subscription ID string assigned by the client. Used to identify the subscription in `UNSUB` commands.
|
|
- `MessageCount` — updated with `Interlocked.Increment` on each delivered message. Compared against `MaxMessages` to implement `UNSUB maxMessages`.
|
|
- `MaxMessages` — the auto-unsubscribe limit. `0` means unlimited.
|
|
- `Client` — back-reference to the `NatsClient` that owns this subscription. Used during message delivery to write `MSG` frames to the connection.
|
|
|
|
---
|
|
|
|
## The `SubListResult` Class
|
|
|
|
`SubListResult` is the return type of `SubList.Match()`. It separates plain subscribers from queue group subscribers.
|
|
|
|
```csharp
|
|
public sealed class SubListResult
|
|
{
|
|
public static readonly SubListResult Empty = new([], []);
|
|
|
|
public Subscription[] PlainSubs { get; }
|
|
public Subscription[][] QueueSubs { get; } // outer = groups, inner = members
|
|
|
|
public SubListResult(Subscription[] plainSubs, Subscription[][] queueSubs)
|
|
{
|
|
PlainSubs = plainSubs;
|
|
QueueSubs = queueSubs;
|
|
}
|
|
}
|
|
```
|
|
|
|
- `PlainSubs` — all plain subscribers whose subject pattern matches the published subject.
|
|
- `QueueSubs` — one entry per queue group. Each inner array contains all members of that group. The message router picks one member per group.
|
|
- `Empty` — a static singleton returned when no subscriptions match. Avoids allocation on the common case of no interest.
|
|
|
|
---
|
|
|
|
## The `SubjectMatch` Class
|
|
|
|
`SubjectMatch` is a static utility class for subject validation and wildcard pattern matching. It defines the three fundamental constants used throughout the subscription system:
|
|
|
|
```csharp
|
|
public static class SubjectMatch
|
|
{
|
|
public const char Pwc = '*'; // partial wildcard
|
|
public const char Fwc = '>'; // full wildcard
|
|
public const char Sep = '.'; // token separator
|
|
...
|
|
}
|
|
```
|
|
|
|
### Validation methods
|
|
|
|
**`IsValidSubject(string subject)`** — returns `true` if the subject is a well-formed NATS subject. Rejects:
|
|
- Empty strings.
|
|
- Empty tokens (consecutive separators, or leading/trailing separators).
|
|
- Tokens containing whitespace characters.
|
|
- Any token after `>` (full wildcard must be last).
|
|
|
|
**`IsLiteral(string subject)`** — returns `true` if the subject contains no wildcards. A character is treated as a wildcard only when it appears as a complete token (preceded and followed by `.` or the string boundary).
|
|
|
|
**`IsValidPublishSubject(string subject)`** — returns `true` if the subject is both valid and literal. Publishers must use literal subjects; this is the combined check applied before inserting a message into the routing path.
|
|
|
|
### Pattern matching
|
|
|
|
**`MatchLiteral(string literal, string pattern)`** — tests whether a literal subject matches a pattern that may contain wildcards. Used by the cache invalidation logic to determine which cached results are affected when a wildcard subscription is added or removed.
|
|
|
|
```csharp
|
|
public static bool MatchLiteral(string literal, string pattern)
|
|
{
|
|
int li = 0, pi = 0;
|
|
|
|
while (pi < pattern.Length)
|
|
{
|
|
// Get next pattern token
|
|
int pTokenStart = pi;
|
|
while (pi < pattern.Length && pattern[pi] != Sep)
|
|
pi++;
|
|
int pTokenLen = pi - pTokenStart;
|
|
if (pi < pattern.Length)
|
|
pi++; // skip separator
|
|
|
|
// Full wildcard -- matches everything remaining
|
|
if (pTokenLen == 1 && pattern[pTokenStart] == Fwc)
|
|
return li < literal.Length; // must have at least one token left
|
|
|
|
// Get next literal token
|
|
if (li >= literal.Length)
|
|
return false;
|
|
int lTokenStart = li;
|
|
while (li < literal.Length && literal[li] != Sep)
|
|
li++;
|
|
int lTokenLen = li - lTokenStart;
|
|
if (li < literal.Length)
|
|
li++; // skip separator
|
|
|
|
// Partial wildcard -- matches any single token
|
|
if (pTokenLen == 1 && pattern[pTokenStart] == Pwc)
|
|
continue;
|
|
|
|
// Literal comparison
|
|
if (pTokenLen != lTokenLen)
|
|
return false;
|
|
if (string.Compare(literal, lTokenStart, pattern, pTokenStart, pTokenLen, StringComparison.Ordinal) != 0)
|
|
return false;
|
|
}
|
|
|
|
return li >= literal.Length; // both exhausted
|
|
}
|
|
```
|
|
|
|
The algorithm walks both strings token by token without allocating. `>` short-circuits immediately, returning `true` as long as at least one literal token remains. `*` skips the corresponding literal token without comparison. Literal tokens are compared with `StringComparison.Ordinal`.
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [SubList Trie](../Subscriptions/SubList.md)
|
|
|
|
<!-- Last verified against codebase: 2026-02-22 -->
|