snapshot of https://github.com/nats-io/nats-server.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
This commit is contained in:
566
server/gsl/gsl.go
Normal file
566
server/gsl/gsl.go
Normal file
@@ -0,0 +1,566 @@
|
||||
// Copyright 2025-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gsl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Sublist is a routing mechanism to handle subject distribution and
|
||||
// provides a facility to match subjects from published messages to
|
||||
// interested subscribers. Subscribers can have wildcard subjects to
|
||||
// match multiple published subjects.
|
||||
|
||||
// Common byte variables for wildcards and token separator.
|
||||
const (
|
||||
pwc = '*'
|
||||
pwcs = "*"
|
||||
fwc = '>'
|
||||
fwcs = ">"
|
||||
tsep = "."
|
||||
btsep = '.'
|
||||
_EMPTY_ = ""
|
||||
)
|
||||
|
||||
// Sublist related errors
|
||||
var (
|
||||
ErrInvalidSubject = errors.New("gsl: invalid subject")
|
||||
ErrNotFound = errors.New("gsl: no matches found")
|
||||
ErrNilChan = errors.New("gsl: nil channel")
|
||||
ErrAlreadyRegistered = errors.New("gsl: notification already registered")
|
||||
)
|
||||
|
||||
// SimpleSublist is an alias type for GenericSublist that takes
|
||||
// empty values, useful for tracking interest only without any
|
||||
// unnecessary allocations.
|
||||
type SimpleSublist = GenericSublist[struct{}]
|
||||
|
||||
// NewSimpleSublist will create a simple sublist.
|
||||
func NewSimpleSublist() *SimpleSublist {
|
||||
return &GenericSublist[struct{}]{root: newLevel[struct{}]()}
|
||||
}
|
||||
|
||||
// A GenericSublist stores and efficiently retrieves subscriptions.
|
||||
type GenericSublist[T comparable] struct {
|
||||
sync.RWMutex
|
||||
root *level[T]
|
||||
count uint32
|
||||
}
|
||||
|
||||
// A node contains subscriptions and a pointer to the next level.
|
||||
type node[T comparable] struct {
|
||||
next *level[T]
|
||||
subs map[T]string // value -> subject
|
||||
}
|
||||
|
||||
// A level represents a group of nodes and special pointers to
|
||||
// wildcard nodes.
|
||||
type level[T comparable] struct {
|
||||
nodes map[string]*node[T]
|
||||
pwc, fwc *node[T]
|
||||
}
|
||||
|
||||
// Create a new default node.
|
||||
func newNode[T comparable]() *node[T] {
|
||||
return &node[T]{subs: make(map[T]string)}
|
||||
}
|
||||
|
||||
// Create a new default level.
|
||||
func newLevel[T comparable]() *level[T] {
|
||||
return &level[T]{nodes: make(map[string]*node[T])}
|
||||
}
|
||||
|
||||
// NewSublist will create a default sublist with caching enabled per the flag.
|
||||
func NewSublist[T comparable]() *GenericSublist[T] {
|
||||
return &GenericSublist[T]{root: newLevel[T]()}
|
||||
}
|
||||
|
||||
// Insert adds a subscription into the sublist
|
||||
func (s *GenericSublist[T]) Insert(subject string, value T) error {
|
||||
s.Lock()
|
||||
|
||||
var sfwc bool
|
||||
var n *node[T]
|
||||
l := s.root
|
||||
|
||||
for t := range strings.SplitSeq(subject, tsep) {
|
||||
lt := len(t)
|
||||
if lt == 0 || sfwc {
|
||||
s.Unlock()
|
||||
return ErrInvalidSubject
|
||||
}
|
||||
|
||||
if lt > 1 {
|
||||
n = l.nodes[t]
|
||||
} else {
|
||||
switch t[0] {
|
||||
case pwc:
|
||||
n = l.pwc
|
||||
case fwc:
|
||||
n = l.fwc
|
||||
sfwc = true
|
||||
default:
|
||||
n = l.nodes[t]
|
||||
}
|
||||
}
|
||||
if n == nil {
|
||||
n = newNode[T]()
|
||||
if lt > 1 {
|
||||
l.nodes[t] = n
|
||||
} else {
|
||||
switch t[0] {
|
||||
case pwc:
|
||||
l.pwc = n
|
||||
case fwc:
|
||||
l.fwc = n
|
||||
default:
|
||||
l.nodes[t] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.next == nil {
|
||||
n.next = newLevel[T]()
|
||||
}
|
||||
l = n.next
|
||||
}
|
||||
|
||||
n.subs[value] = subject
|
||||
|
||||
s.count++
|
||||
s.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match will match all entries to the literal subject.
|
||||
// It will return a set of results for both normal and queue subscribers.
|
||||
func (s *GenericSublist[T]) Match(subject string, cb func(T)) {
|
||||
s.match(subject, cb, true)
|
||||
}
|
||||
|
||||
// MatchBytes will match all entries to the literal subject.
|
||||
// It will return a set of results for both normal and queue subscribers.
|
||||
func (s *GenericSublist[T]) MatchBytes(subject []byte, cb func(T)) {
|
||||
s.match(string(subject), cb, true)
|
||||
}
|
||||
|
||||
// HasInterest will return whether or not there is any interest in the subject.
|
||||
// In cases where more detail is not required, this may be faster than Match.
|
||||
func (s *GenericSublist[T]) HasInterest(subject string) bool {
|
||||
return s.hasInterest(subject, true, nil)
|
||||
}
|
||||
|
||||
// NumInterest will return the number of subs interested in the subject.
|
||||
// In cases where more detail is not required, this may be faster than Match.
|
||||
func (s *GenericSublist[T]) NumInterest(subject string) (np int) {
|
||||
s.hasInterest(subject, true, &np)
|
||||
return
|
||||
}
|
||||
|
||||
// MatchesFullWildcard returns true if there is top-level ">" interest.
|
||||
func (s *GenericSublist[T]) MatchesFullWildcard() bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
return s.root.fwc != nil
|
||||
}
|
||||
|
||||
// MatchesSingleFilter returns the filter when the sublist contains exactly one unique subject.
|
||||
func (s *GenericSublist[T]) MatchesSingleFilter() (string, bool) {
|
||||
if s == nil {
|
||||
return _EMPTY_, false
|
||||
}
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
return singleFilter(s.root, _EMPTY_)
|
||||
}
|
||||
|
||||
func singleFilter[T comparable](l *level[T], filter string) (string, bool) {
|
||||
if l == nil {
|
||||
return filter, filter != _EMPTY_
|
||||
}
|
||||
if len(l.nodes) > 1 {
|
||||
return _EMPTY_, false
|
||||
}
|
||||
var next *node[T]
|
||||
branches := 0
|
||||
if l.pwc != nil {
|
||||
next = l.pwc
|
||||
branches++
|
||||
}
|
||||
if l.fwc != nil {
|
||||
next = l.fwc
|
||||
branches++
|
||||
}
|
||||
for _, n := range l.nodes {
|
||||
next = n
|
||||
branches++
|
||||
}
|
||||
if branches != 1 {
|
||||
return _EMPTY_, false
|
||||
}
|
||||
for _, subj := range next.subs {
|
||||
filter = subj
|
||||
break
|
||||
}
|
||||
if next.next == nil {
|
||||
return filter, filter != _EMPTY_
|
||||
}
|
||||
if filter != _EMPTY_ {
|
||||
if next.next.numNodes() > 0 {
|
||||
return _EMPTY_, false
|
||||
}
|
||||
return filter, true
|
||||
}
|
||||
return singleFilter(next.next, filter)
|
||||
}
|
||||
|
||||
func (s *GenericSublist[T]) match(subject string, cb func(T), doLock bool) {
|
||||
tsa := [32]string{}
|
||||
tokens := tsa[:0]
|
||||
start := 0
|
||||
for i := 0; i < len(subject); i++ {
|
||||
if subject[i] == btsep {
|
||||
if i-start == 0 {
|
||||
return
|
||||
}
|
||||
tokens = append(tokens, subject[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start >= len(subject) {
|
||||
return
|
||||
}
|
||||
tokens = append(tokens, subject[start:])
|
||||
|
||||
if doLock {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
}
|
||||
matchLevel(s.root, tokens, cb)
|
||||
}
|
||||
|
||||
func (s *GenericSublist[T]) hasInterest(subject string, doLock bool, np *int) bool {
|
||||
tsa := [32]string{}
|
||||
tokens := tsa[:0]
|
||||
start := 0
|
||||
for i := 0; i < len(subject); i++ {
|
||||
if subject[i] == btsep {
|
||||
if i-start == 0 {
|
||||
return false
|
||||
}
|
||||
tokens = append(tokens, subject[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start >= len(subject) {
|
||||
return false
|
||||
}
|
||||
tokens = append(tokens, subject[start:])
|
||||
|
||||
if doLock {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
}
|
||||
return matchLevelForAny(s.root, tokens, np)
|
||||
}
|
||||
|
||||
func matchLevelForAny[T comparable](l *level[T], toks []string, np *int) bool {
|
||||
var pwc, n *node[T]
|
||||
for i, t := range toks {
|
||||
if l == nil {
|
||||
return false
|
||||
}
|
||||
if l.fwc != nil {
|
||||
if np != nil {
|
||||
*np += len(l.fwc.subs)
|
||||
}
|
||||
return true
|
||||
}
|
||||
if pwc = l.pwc; pwc != nil {
|
||||
if match := matchLevelForAny(pwc.next, toks[i+1:], np); match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
n = l.nodes[t]
|
||||
if n != nil {
|
||||
l = n.next
|
||||
} else {
|
||||
l = nil
|
||||
}
|
||||
}
|
||||
if n != nil {
|
||||
if np != nil {
|
||||
*np += len(n.subs)
|
||||
}
|
||||
if len(n.subs) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if pwc != nil {
|
||||
if np != nil {
|
||||
*np += len(pwc.subs)
|
||||
}
|
||||
return len(pwc.subs) > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// callbacksForResults will make the necessary callbacks for each
|
||||
// result in this node.
|
||||
func callbacksForResults[T comparable](n *node[T], cb func(T)) {
|
||||
for sub := range n.subs {
|
||||
cb(sub)
|
||||
}
|
||||
}
|
||||
|
||||
// matchLevel is used to recursively descend into the trie.
|
||||
func matchLevel[T comparable](l *level[T], toks []string, cb func(T)) {
|
||||
var pwc, n *node[T]
|
||||
for i, t := range toks {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
if l.fwc != nil {
|
||||
callbacksForResults(l.fwc, cb)
|
||||
}
|
||||
if pwc = l.pwc; pwc != nil {
|
||||
matchLevel(pwc.next, toks[i+1:], cb)
|
||||
}
|
||||
n = l.nodes[t]
|
||||
if n != nil {
|
||||
l = n.next
|
||||
} else {
|
||||
l = nil
|
||||
}
|
||||
}
|
||||
if n != nil {
|
||||
callbacksForResults(n, cb)
|
||||
}
|
||||
if pwc != nil {
|
||||
callbacksForResults(pwc, cb)
|
||||
}
|
||||
}
|
||||
|
||||
// lnt is used to track descent into levels for a removal for pruning.
|
||||
type lnt[T comparable] struct {
|
||||
l *level[T]
|
||||
n *node[T]
|
||||
t string
|
||||
}
|
||||
|
||||
// Raw low level remove, can do batches with lock held outside.
|
||||
func (s *GenericSublist[T]) remove(subject string, value T, shouldLock bool) error {
|
||||
if shouldLock {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
}
|
||||
|
||||
var sfwc bool
|
||||
var n *node[T]
|
||||
l := s.root
|
||||
|
||||
// Track levels for pruning
|
||||
var lnts [32]lnt[T]
|
||||
levels := lnts[:0]
|
||||
|
||||
for t := range strings.SplitSeq(subject, tsep) {
|
||||
lt := len(t)
|
||||
if lt == 0 || sfwc {
|
||||
return ErrInvalidSubject
|
||||
}
|
||||
if l == nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
if lt > 1 {
|
||||
n = l.nodes[t]
|
||||
} else {
|
||||
switch t[0] {
|
||||
case pwc:
|
||||
n = l.pwc
|
||||
case fwc:
|
||||
n = l.fwc
|
||||
sfwc = true
|
||||
default:
|
||||
n = l.nodes[t]
|
||||
}
|
||||
}
|
||||
if n != nil {
|
||||
levels = append(levels, lnt[T]{l, n, t})
|
||||
l = n.next
|
||||
} else {
|
||||
l = nil
|
||||
}
|
||||
}
|
||||
|
||||
if !s.removeFromNode(n, value) {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
s.count--
|
||||
|
||||
for i := len(levels) - 1; i >= 0; i-- {
|
||||
l, n, t := levels[i].l, levels[i].n, levels[i].t
|
||||
if n.isEmpty() {
|
||||
l.pruneNode(n, t)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove will remove a subscription.
|
||||
func (s *GenericSublist[T]) Remove(subject string, value T) error {
|
||||
return s.remove(subject, value, true)
|
||||
}
|
||||
|
||||
// HasInterestStartingIn is a helper for subject tree intersection.
|
||||
func (s *GenericSublist[T]) HasInterestStartingIn(subj string) bool {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
var _tokens [64]string
|
||||
tokens := tokenizeSubjectIntoSlice(_tokens[:0], subj)
|
||||
return hasInterestStartingIn(s.root, tokens)
|
||||
}
|
||||
|
||||
func hasInterestStartingIn[T comparable](l *level[T], tokens []string) bool {
|
||||
if l == nil {
|
||||
return false
|
||||
}
|
||||
if len(tokens) == 0 {
|
||||
return true
|
||||
}
|
||||
token := tokens[0]
|
||||
if l.fwc != nil {
|
||||
return true
|
||||
}
|
||||
found := false
|
||||
if pwc := l.pwc; pwc != nil {
|
||||
found = found || hasInterestStartingIn(pwc.next, tokens[1:])
|
||||
}
|
||||
if n := l.nodes[token]; n != nil {
|
||||
found = found || hasInterestStartingIn(n.next, tokens[1:])
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// pruneNode is used to prune an empty node from the tree.
|
||||
func (l *level[T]) pruneNode(n *node[T], t string) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
if n == l.fwc {
|
||||
l.fwc = nil
|
||||
} else if n == l.pwc {
|
||||
l.pwc = nil
|
||||
} else {
|
||||
delete(l.nodes, t)
|
||||
}
|
||||
}
|
||||
|
||||
// isEmpty will test if the node has any entries. Used
|
||||
// in pruning.
|
||||
func (n *node[T]) isEmpty() bool {
|
||||
return len(n.subs) == 0 && (n.next == nil || n.next.numNodes() == 0)
|
||||
}
|
||||
|
||||
// Return the number of nodes for the given level.
|
||||
func (l *level[T]) numNodes() int {
|
||||
if l == nil {
|
||||
return 0
|
||||
}
|
||||
num := len(l.nodes)
|
||||
if l.pwc != nil {
|
||||
num++
|
||||
}
|
||||
if l.fwc != nil {
|
||||
num++
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
// Remove the sub for the given node.
|
||||
func (s *GenericSublist[T]) removeFromNode(n *node[T], value T) (found bool) {
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
if _, found = n.subs[value]; found {
|
||||
delete(n.subs, value)
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// Count returns the number of subscriptions.
|
||||
func (s *GenericSublist[T]) Count() uint32 {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
return s.count
|
||||
}
|
||||
|
||||
// numLevels will return the maximum number of levels
|
||||
// contained in the Sublist tree.
|
||||
func (s *GenericSublist[T]) numLevels() int {
|
||||
return visitLevel(s.root, 0)
|
||||
}
|
||||
|
||||
// visitLevel is used to descend the Sublist tree structure
|
||||
// recursively.
|
||||
func visitLevel[T comparable](l *level[T], depth int) int {
|
||||
if l == nil || l.numNodes() == 0 {
|
||||
return depth
|
||||
}
|
||||
|
||||
depth++
|
||||
maxDepth := depth
|
||||
|
||||
for _, n := range l.nodes {
|
||||
if n == nil {
|
||||
continue
|
||||
}
|
||||
newDepth := visitLevel(n.next, depth)
|
||||
if newDepth > maxDepth {
|
||||
maxDepth = newDepth
|
||||
}
|
||||
}
|
||||
if l.pwc != nil {
|
||||
pwcDepth := visitLevel(l.pwc.next, depth)
|
||||
if pwcDepth > maxDepth {
|
||||
maxDepth = pwcDepth
|
||||
}
|
||||
}
|
||||
if l.fwc != nil {
|
||||
fwcDepth := visitLevel(l.fwc.next, depth)
|
||||
if fwcDepth > maxDepth {
|
||||
maxDepth = fwcDepth
|
||||
}
|
||||
}
|
||||
return maxDepth
|
||||
}
|
||||
|
||||
// use similar to append. meaning, the updated slice will be returned
|
||||
func tokenizeSubjectIntoSlice(tts []string, subject string) []string {
|
||||
start := 0
|
||||
for i := 0; i < len(subject); i++ {
|
||||
if subject[i] == btsep {
|
||||
tts = append(tts, subject[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
tts = append(tts, subject[start:])
|
||||
return tts
|
||||
}
|
||||
489
server/gsl/gsl_test.go
Normal file
489
server/gsl/gsl_test.go
Normal file
@@ -0,0 +1,489 @@
|
||||
// Copyright 2016-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gsl
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/internal/antithesis"
|
||||
)
|
||||
|
||||
func TestGenericSublistInit(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_Equal(t, s.count, 0)
|
||||
require_Equal(t, s.Count(), s.count)
|
||||
}
|
||||
|
||||
func TestGenericSublistInsertCount(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_NoError(t, s.Insert("foo", struct{}{}))
|
||||
require_NoError(t, s.Insert("bar", struct{}{}))
|
||||
require_NoError(t, s.Insert("foo.bar", struct{}{}))
|
||||
require_Equal(t, s.Count(), 3)
|
||||
}
|
||||
|
||||
func TestGenericSublistSimple(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_NoError(t, s.Insert("foo", struct{}{}))
|
||||
require_Matches(t, s, "foo", 1)
|
||||
}
|
||||
|
||||
func TestGenericSublistSimpleMultiTokens(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_NoError(t, s.Insert("foo.bar.baz", struct{}{}))
|
||||
require_Matches(t, s, "foo.bar.baz", 1)
|
||||
}
|
||||
|
||||
func TestGenericSublistPartialWildcard(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_NoError(t, s.Insert("a.b.c", struct{}{}))
|
||||
require_NoError(t, s.Insert("a.*.c", struct{}{}))
|
||||
require_Matches(t, s, "a.b.c", 2)
|
||||
}
|
||||
|
||||
func TestGenericSublistPartialWildcardAtEnd(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_NoError(t, s.Insert("a.b.c", struct{}{}))
|
||||
require_NoError(t, s.Insert("a.b.*", struct{}{}))
|
||||
require_Matches(t, s, "a.b.c", 2)
|
||||
}
|
||||
|
||||
func TestGenericSublistFullWildcard(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_NoError(t, s.Insert("a.b.c", struct{}{}))
|
||||
require_NoError(t, s.Insert("a.>", struct{}{}))
|
||||
require_Matches(t, s, "a.b.c", 2)
|
||||
require_Matches(t, s, "a.>", 1)
|
||||
}
|
||||
|
||||
func TestGenericSublistRemove(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
|
||||
require_NoError(t, s.Insert("a.b.c.d", struct{}{}))
|
||||
require_Equal(t, s.Count(), 1)
|
||||
require_Matches(t, s, "a.b.c.d", 1)
|
||||
|
||||
require_NoError(t, s.Remove("a.b.c.d", struct{}{}))
|
||||
require_Equal(t, s.Count(), 0)
|
||||
require_Matches(t, s, "a.b.c.d", 0)
|
||||
}
|
||||
|
||||
func TestGenericSublistRemoveWildcard(t *testing.T) {
|
||||
s := NewSublist[int]()
|
||||
|
||||
require_NoError(t, s.Insert("a.b.c.d", 11))
|
||||
require_NoError(t, s.Insert("a.b.*.d", 22))
|
||||
require_NoError(t, s.Insert("a.b.>", 33))
|
||||
require_Equal(t, s.Count(), 3)
|
||||
require_Matches(t, s, "a.b.c.d", 3)
|
||||
|
||||
require_NoError(t, s.Remove("a.b.*.d", 22))
|
||||
require_Equal(t, s.Count(), 2)
|
||||
require_Matches(t, s, "a.b.c.d", 2)
|
||||
|
||||
require_NoError(t, s.Remove("a.b.>", 33))
|
||||
require_Equal(t, s.Count(), 1)
|
||||
require_Matches(t, s, "a.b.c.d", 1)
|
||||
|
||||
require_NoError(t, s.Remove("a.b.c.d", 11))
|
||||
require_Equal(t, s.Count(), 0)
|
||||
require_Matches(t, s, "a.b.c.d", 0)
|
||||
}
|
||||
|
||||
func TestGenericSublistRemoveCleanup(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_Equal(t, s.numLevels(), 0)
|
||||
require_NoError(t, s.Insert("a.b.c.d.e.f", struct{}{}))
|
||||
require_Equal(t, s.numLevels(), 6)
|
||||
require_NoError(t, s.Remove("a.b.c.d.e.f", struct{}{}))
|
||||
require_Equal(t, s.numLevels(), 0)
|
||||
}
|
||||
|
||||
func TestGenericSublistRemoveCleanupWildcards(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_Equal(t, s.numLevels(), 0)
|
||||
require_NoError(t, s.Insert("a.b.*.d.e.>", struct{}{}))
|
||||
require_Equal(t, s.numLevels(), 6)
|
||||
require_NoError(t, s.Remove("a.b.*.d.e.>", struct{}{}))
|
||||
require_Equal(t, s.numLevels(), 0)
|
||||
}
|
||||
|
||||
func TestGenericSublistInvalidSubjectsInsert(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
// Insert, or subscriptions, can have wildcards, but not empty tokens,
|
||||
// and can not have a FWC that is not the terminal token.
|
||||
require_Error(t, s.Insert(".foo", struct{}{}), ErrInvalidSubject)
|
||||
require_Error(t, s.Insert("foo.", struct{}{}), ErrInvalidSubject)
|
||||
require_Error(t, s.Insert("foo..bar", struct{}{}), ErrInvalidSubject)
|
||||
require_Error(t, s.Insert("foo.bar..baz", struct{}{}), ErrInvalidSubject)
|
||||
require_Error(t, s.Insert("foo.>.baz", struct{}{}), ErrInvalidSubject)
|
||||
}
|
||||
|
||||
func TestGenericSublistBadSubjectOnRemove(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_Error(t, s.Insert("a.b..d", struct{}{}), ErrInvalidSubject)
|
||||
require_Error(t, s.Remove("a.b..d", struct{}{}), ErrInvalidSubject)
|
||||
require_Error(t, s.Remove("a.>.b", struct{}{}), ErrInvalidSubject)
|
||||
}
|
||||
|
||||
func TestGenericSublistTwoTokenPubMatchSingleTokenSub(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_NoError(t, s.Insert("foo", struct{}{}))
|
||||
require_Matches(t, s, "foo", 1)
|
||||
require_Matches(t, s, "foo.bar", 0)
|
||||
}
|
||||
|
||||
func TestGenericSublistInsertWithWildcardsAsLiterals(t *testing.T) {
|
||||
s := NewSublist[int]()
|
||||
for i, subject := range []string{"foo.*-", "foo.>-"} {
|
||||
require_NoError(t, s.Insert(subject, i))
|
||||
require_Matches(t, s, "foo.bar", 0)
|
||||
require_Matches(t, s, subject, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericSublistRemoveWithWildcardsAsLiterals(t *testing.T) {
|
||||
s := NewSublist[int]()
|
||||
for i, subject := range []string{"foo.*-", "foo.>-"} {
|
||||
require_NoError(t, s.Insert(subject, i))
|
||||
require_Matches(t, s, "foo.bar", 0)
|
||||
require_Matches(t, s, subject, 1)
|
||||
require_Error(t, s.Remove("foo.bar", i), ErrNotFound)
|
||||
require_Equal(t, s.Count(), 1)
|
||||
require_NoError(t, s.Remove(subject, i))
|
||||
require_Equal(t, s.Count(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericSublistMatchWithEmptyTokens(t *testing.T) {
|
||||
s := NewSublist[struct{}]()
|
||||
require_NoError(t, s.Insert(">", struct{}{}))
|
||||
for _, subject := range []string{".foo", "..foo", "foo..", "foo.", "foo..bar", "foo...bar"} {
|
||||
t.Run(subject, func(t *testing.T) {
|
||||
require_Matches(t, s, subject, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericSublistHasInterest(t *testing.T) {
|
||||
s := NewSublist[int]()
|
||||
require_NoError(t, s.Insert("foo", 11))
|
||||
|
||||
// Expect to find that "foo" matches but "bar" doesn't.
|
||||
// At this point nothing should be in the cache.
|
||||
require_True(t, s.HasInterest("foo"))
|
||||
require_False(t, s.HasInterest("bar"))
|
||||
|
||||
// Call Match on a subject we know there is no match.
|
||||
require_Matches(t, s, "bar", 0)
|
||||
require_False(t, s.HasInterest("bar"))
|
||||
|
||||
// Remove fooSub and check interest again
|
||||
require_NoError(t, s.Remove("foo", 11))
|
||||
require_False(t, s.HasInterest("foo"))
|
||||
|
||||
// Try with some wildcards
|
||||
require_NoError(t, s.Insert("foo.*", 22))
|
||||
require_False(t, s.HasInterest("foo"))
|
||||
require_True(t, s.HasInterest("foo.bar"))
|
||||
require_False(t, s.HasInterest("foo.bar.baz"))
|
||||
|
||||
// Remove sub, there should be no interest
|
||||
require_NoError(t, s.Remove("foo.*", 22))
|
||||
require_False(t, s.HasInterest("foo"))
|
||||
require_False(t, s.HasInterest("foo.bar"))
|
||||
require_False(t, s.HasInterest("foo.bar.baz"))
|
||||
|
||||
require_NoError(t, s.Insert("foo.>", 33))
|
||||
require_False(t, s.HasInterest("foo"))
|
||||
require_True(t, s.HasInterest("foo.bar"))
|
||||
require_True(t, s.HasInterest("foo.bar.baz"))
|
||||
|
||||
require_NoError(t, s.Remove("foo.>", 33))
|
||||
require_False(t, s.HasInterest("foo"))
|
||||
require_False(t, s.HasInterest("foo.bar"))
|
||||
require_False(t, s.HasInterest("foo.bar.baz"))
|
||||
|
||||
require_NoError(t, s.Insert("*.>", 44))
|
||||
require_False(t, s.HasInterest("foo"))
|
||||
require_True(t, s.HasInterest("foo.bar"))
|
||||
require_True(t, s.HasInterest("foo.baz"))
|
||||
require_NoError(t, s.Remove("*.>", 44))
|
||||
|
||||
require_NoError(t, s.Insert("*.bar", 55))
|
||||
require_False(t, s.HasInterest("foo"))
|
||||
require_True(t, s.HasInterest("foo.bar"))
|
||||
require_False(t, s.HasInterest("foo.baz"))
|
||||
require_NoError(t, s.Remove("*.bar", 55))
|
||||
|
||||
require_NoError(t, s.Insert("*", 66))
|
||||
require_True(t, s.HasInterest("foo"))
|
||||
require_False(t, s.HasInterest("foo.bar"))
|
||||
require_NoError(t, s.Remove("*", 66))
|
||||
}
|
||||
|
||||
func TestGenericSublistHasInterestOverlapping(t *testing.T) {
|
||||
s := NewSublist[int]()
|
||||
require_NoError(t, s.Insert("stream.A.child", 11))
|
||||
require_NoError(t, s.Insert("stream.*", 11))
|
||||
require_True(t, s.HasInterest("stream.A.child"))
|
||||
require_True(t, s.HasInterest("stream.A"))
|
||||
}
|
||||
|
||||
func TestGenericSublistMatchesFullWildcard(t *testing.T) {
|
||||
s := NewSublist[int]()
|
||||
require_NoError(t, s.Insert("stream.A.child", 11))
|
||||
require_NoError(t, s.Insert("stream.*", 11))
|
||||
require_True(t, s.HasInterest("stream.A.child"))
|
||||
require_True(t, s.HasInterest("stream.A"))
|
||||
require_False(t, s.MatchesFullWildcard())
|
||||
|
||||
require_NoError(t, s.Insert(">", 11))
|
||||
require_True(t, s.MatchesFullWildcard())
|
||||
}
|
||||
|
||||
func TestGenericSublistSingleFilter(t *testing.T) {
|
||||
var sn *GenericSublist[int]
|
||||
filter, ok := sn.MatchesSingleFilter()
|
||||
require_False(t, ok)
|
||||
require_Equal(t, filter, _EMPTY_)
|
||||
|
||||
s := NewSublist[int]()
|
||||
filter, ok = s.MatchesSingleFilter()
|
||||
require_False(t, ok)
|
||||
require_Equal(t, filter, _EMPTY_)
|
||||
|
||||
require_NoError(t, s.Insert("stream.A.child", 11))
|
||||
filter, ok = s.MatchesSingleFilter()
|
||||
require_True(t, ok)
|
||||
require_Equal(t, filter, "stream.A.child")
|
||||
|
||||
require_NoError(t, s.Insert("stream.A.child", 22))
|
||||
filter, ok = s.MatchesSingleFilter()
|
||||
require_True(t, ok)
|
||||
require_Equal(t, filter, "stream.A.child")
|
||||
|
||||
require_NoError(t, s.Insert("stream.A.other", 22))
|
||||
filter, ok = s.MatchesSingleFilter()
|
||||
require_False(t, ok)
|
||||
require_Equal(t, filter, _EMPTY_)
|
||||
|
||||
require_NoError(t, s.Insert("stream.*", 33))
|
||||
filter, ok = s.MatchesSingleFilter()
|
||||
require_False(t, ok)
|
||||
require_Equal(t, filter, _EMPTY_)
|
||||
|
||||
// Test that an ancestor with descendants correctly returns false.
|
||||
s2 := NewSublist[int]()
|
||||
require_NoError(t, s2.Insert("foo", 1))
|
||||
require_NoError(t, s2.Insert("foo.bar", 2))
|
||||
require_NoError(t, s2.Insert("foo.baz", 3))
|
||||
filter, ok = s2.MatchesSingleFilter()
|
||||
require_False(t, ok)
|
||||
require_Equal(t, filter, _EMPTY_)
|
||||
|
||||
// Test that an ancestor with only descendants correctly returns false.
|
||||
s3 := NewSublist[int]()
|
||||
require_NoError(t, s3.Insert("foo.bar", 2))
|
||||
require_NoError(t, s3.Insert("foo.baz", 3))
|
||||
filter, ok = s3.MatchesSingleFilter()
|
||||
require_False(t, ok)
|
||||
require_Equal(t, filter, _EMPTY_)
|
||||
}
|
||||
|
||||
// TestGenericSublistHasInterestStartingInRace tests that HasInterestStartingIn
|
||||
// is safe to call concurrently with modifications to the sublist.
|
||||
func TestGenericSublistHasInterestStartingInRace(t *testing.T) {
|
||||
s := NewSublist[int]()
|
||||
|
||||
// Pre-populate with some patterns
|
||||
for i := 0; i < 10; i++ {
|
||||
s.Insert("foo.bar.baz", i)
|
||||
s.Insert("foo.*.baz", i+10)
|
||||
s.Insert("foo.>", i+20)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
const iterations = 1000
|
||||
|
||||
// Goroutine 1: repeatedly call HasInterestStartingIn
|
||||
go func() {
|
||||
for i := 0; i < iterations; i++ {
|
||||
s.HasInterestStartingIn("foo")
|
||||
s.HasInterestStartingIn("foo.bar")
|
||||
s.HasInterestStartingIn("foo.bar.baz")
|
||||
s.HasInterestStartingIn("other.subject")
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
// Goroutine 2: repeatedly modify the sublist
|
||||
go func() {
|
||||
for i := 0; i < iterations; i++ {
|
||||
val := 1000 + i
|
||||
s.Insert("test.subject."+string(rune('a'+i%26)), val)
|
||||
s.Insert("foo.*.test", val)
|
||||
s.Remove("test.subject."+string(rune('a'+i%26)), val)
|
||||
s.Remove("foo.*.test", val)
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
// Goroutine 3: also call HasInterest (which does lock)
|
||||
go func() {
|
||||
for i := 0; i < iterations; i++ {
|
||||
s.HasInterest("foo.bar.baz")
|
||||
s.HasInterest("foo.something.baz")
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
// Wait for all goroutines
|
||||
<-done
|
||||
<-done
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestGenericSublistNumInterest(t *testing.T) {
|
||||
s := NewSublist[int]()
|
||||
require_NoError(t, s.Insert("foo", 11))
|
||||
|
||||
require_NumInterest := func(t *testing.T, subj string, wnp int) {
|
||||
t.Helper()
|
||||
require_Matches(t, s, subj, wnp)
|
||||
require_Equal(t, s.NumInterest(subj), wnp)
|
||||
}
|
||||
|
||||
// Expect to find that "foo" matches but "bar" doesn't.
|
||||
// At this point nothing should be in the cache.
|
||||
require_NumInterest(t, "foo", 1)
|
||||
require_NumInterest(t, "bar", 0)
|
||||
|
||||
// Remove fooSub and check interest again
|
||||
require_NoError(t, s.Remove("foo", 11))
|
||||
require_NumInterest(t, "foo", 0)
|
||||
|
||||
// Try with some wildcards
|
||||
require_NoError(t, s.Insert("foo.*", 22))
|
||||
require_NumInterest(t, "foo", 0)
|
||||
require_NumInterest(t, "foo.bar", 1)
|
||||
require_NumInterest(t, "foo.bar.baz", 0)
|
||||
|
||||
// Remove sub, there should be no interest
|
||||
require_NoError(t, s.Remove("foo.*", 22))
|
||||
require_NumInterest(t, "foo", 0)
|
||||
require_NumInterest(t, "foo.bar", 0)
|
||||
require_NumInterest(t, "foo.bar.baz", 0)
|
||||
|
||||
require_NoError(t, s.Insert("foo.>", 33))
|
||||
require_NumInterest(t, "foo", 0)
|
||||
require_NumInterest(t, "foo.bar", 1)
|
||||
require_NumInterest(t, "foo.bar.baz", 1)
|
||||
|
||||
require_NoError(t, s.Remove("foo.>", 33))
|
||||
require_NumInterest(t, "foo", 0)
|
||||
require_NumInterest(t, "foo.bar", 0)
|
||||
require_NumInterest(t, "foo.bar.baz", 0)
|
||||
|
||||
require_NoError(t, s.Insert("*.>", 44))
|
||||
require_NumInterest(t, "foo", 0)
|
||||
require_NumInterest(t, "foo.bar", 1)
|
||||
require_NumInterest(t, "foo.bar.baz", 1)
|
||||
require_NoError(t, s.Remove("*.>", 44))
|
||||
|
||||
require_NoError(t, s.Insert("*.bar", 55))
|
||||
require_NumInterest(t, "foo", 0)
|
||||
require_NumInterest(t, "foo.bar", 1)
|
||||
require_NumInterest(t, "foo.bar.baz", 0)
|
||||
require_NoError(t, s.Remove("*.bar", 55))
|
||||
|
||||
require_NoError(t, s.Insert("*", 66))
|
||||
require_NumInterest(t, "foo", 1)
|
||||
require_NumInterest(t, "foo.bar", 0)
|
||||
require_NoError(t, s.Remove("*", 66))
|
||||
}
|
||||
|
||||
// --- TEST HELPERS ---
|
||||
|
||||
func require_Matches[T comparable](t *testing.T, s *GenericSublist[T], sub string, c int) {
|
||||
t.Helper()
|
||||
matches := 0
|
||||
s.Match(sub, func(_ T) {
|
||||
matches++
|
||||
})
|
||||
require_Equal(t, matches, c)
|
||||
}
|
||||
|
||||
func require_True(t testing.TB, b bool) {
|
||||
t.Helper()
|
||||
if !b {
|
||||
antithesis.AssertUnreachable(t, "Failed require_True check", nil)
|
||||
t.Fatalf("require true, but got false")
|
||||
}
|
||||
}
|
||||
|
||||
func require_False(t testing.TB, b bool) {
|
||||
t.Helper()
|
||||
if b {
|
||||
antithesis.AssertUnreachable(t, "Failed require_False check", nil)
|
||||
t.Fatalf("require false, but got true")
|
||||
}
|
||||
}
|
||||
|
||||
func require_NoError(t testing.TB, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
antithesis.AssertUnreachable(t, "Failed require_NoError check", map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
t.Fatalf("require no error, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func require_Error(t testing.TB, err error, expected ...error) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
antithesis.AssertUnreachable(t, "Failed require_Error check (nil error)", nil)
|
||||
t.Fatalf("require error, but got none")
|
||||
}
|
||||
if len(expected) == 0 {
|
||||
return
|
||||
}
|
||||
// Try to strip nats prefix from Go library if present.
|
||||
const natsErrPre = "nats: "
|
||||
eStr := err.Error()
|
||||
if strings.HasPrefix(eStr, natsErrPre) {
|
||||
eStr = strings.Replace(eStr, natsErrPre, _EMPTY_, 1)
|
||||
}
|
||||
|
||||
for _, e := range expected {
|
||||
if err == e || strings.Contains(eStr, e.Error()) || strings.Contains(e.Error(), eStr) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
antithesis.AssertUnreachable(t, "Failed require_Error check (unexpected error)", map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
t.Fatalf("Expected one of %v, got '%v'", expected, err)
|
||||
}
|
||||
|
||||
func require_Equal[T comparable](t testing.TB, a, b T) {
|
||||
t.Helper()
|
||||
if a != b {
|
||||
antithesis.AssertUnreachable(t, "Failed require_Equal check", nil)
|
||||
t.Fatalf("require %T equal, but got: %v != %v", a, a, b)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user