feat: add structured logging, Shouldly assertions, CPM, and project documentation
- 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
This commit is contained in:
169
Documentation/Subscriptions/Overview.md
Normal file
169
Documentation/Subscriptions/Overview.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# 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 -->
|
||||
Reference in New Issue
Block a user