- 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
6.6 KiB
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.*matchesfoo.barbut notfoo.bar.baz(only one token afterfoo).foo.>matchesfoo.barandfoo.bar.baz(one or more tokens afterfoo).>must be the last token in a subject.foo.>.baris 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.
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.nullmeans this is a plain subscription.Sid— the subscription ID string assigned by the client. Used to identify the subscription inUNSUBcommands.MessageCount— updated withInterlocked.Incrementon each delivered message. Compared againstMaxMessagesto implementUNSUB maxMessages.MaxMessages— the auto-unsubscribe limit.0means unlimited.Client— back-reference to theNatsClientthat owns this subscription. Used during message delivery to writeMSGframes to the connection.
The SubListResult Class
SubListResult is the return type of SubList.Match(). It separates plain subscribers from queue group subscribers.
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:
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.
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.