Files
LocalAI/core/services/messaging/match.go
Ettore Di Giacinto 2483c811d7 refactor(distributed): one subject matcher, shared by the carrier and the doubles
Three copies of "does this filter match this subject" lived in the tree: one
in testutil.FakeBus, a byte-identical second inside galleryop's own private
fakeBus, and a third in pkg/natsauth with different semantics. The first two
are doubles the specs publish through, and the carrier the pgbus work is about
to add needs the same rule in production. Two spellings drift, and the drift
reads as a peer that receives an event on one replica and not on another.

messaging.SubjectMatches is now the only definition either double uses. The
natsauth copy stays: it matches a NATS server allow list, so it has to
implement the '>' tail wildcard this one deliberately refuses, and Task 16
deletes that package anyway.

'>' is refused rather than implemented because no surviving subscription uses
it, and a caller who writes one must get no messages rather than silently
getting every message on the prefix. The refusal is checked BEFORE the
filter == subject fast path: a verbatim port checks equality first, and then
the filter "a.>" matches the literal subject "a.>", which is the contract
leaking. One table row pins that ordering and it is the only row that does.

messaging.ValidFilter refuses an empty filter, a '>' filter and an empty token
so a subscriber is told at subscribe time instead of staying silently empty for
the life of the process. FakeBus.Subscribe calls it, which is what keeps the
double honest about what the carrier will do, and three new testutil specs pin
that wiring: the previous state of the tree had no spec at all that failed when
the double's wildcard routing was replaced by exact matching in any package the
plan named.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-20 03:05:34 +00:00

72 lines
2.5 KiB
Go

package messaging
import (
"errors"
"fmt"
"strings"
)
// ErrUnsupportedFilter is the class every ValidFilter rejection belongs to, so a
// carrier can tell "this caller asked for something we do not implement" apart
// from "the store is unhappy" without string matching.
var ErrUnsupportedFilter = errors.New("unsupported subject filter")
// SubjectMatches reports whether a subscription filter matches a concrete
// subject, honoring the single-token `*` wildcard used by NATS.
//
// This is the single definition of subject matching for the whole tree: the
// carrier that delivers messages in production and the in-memory doubles the
// specs publish through both call it. When those were separate copies, a filter
// could match on one and not the other, and the difference read as a peer that
// received an event on one replica and missed it on another.
//
// A `>` tail wildcard is deliberately NOT implemented and makes the filter
// match nothing, so a caller who writes one receives no messages rather than
// silently receiving every message on the prefix. The refusal is checked before
// the exact-equality fast path, otherwise `a.>` would still match the literal
// subject `a.>`.
func SubjectMatches(filter, subject string) bool {
// sanitizeSubjectToken folds '>' out of every generated token, so a '>'
// anywhere in a filter can only be a hand-written tail wildcard.
if strings.Contains(filter, ">") {
return false
}
if filter == subject {
return true
}
fp := strings.Split(filter, ".")
sp := strings.Split(subject, ".")
if len(fp) != len(sp) {
return false
}
for i := range fp {
if fp[i] == "*" {
continue
}
if fp[i] != sp[i] {
return false
}
}
return true
}
// ValidFilter reports whether a filter is one SubjectMatches can act on, so a
// subscriber is refused at subscribe time instead of staying silently empty for
// the life of the process. It rejects an empty filter, a filter containing the
// unimplemented `>` tail wildcard, and a filter with an empty token (`a..b`),
// which no subject generator can ever produce.
func ValidFilter(filter string) error {
if filter == "" {
return fmt.Errorf("%w: filter is empty", ErrUnsupportedFilter)
}
if strings.Contains(filter, ">") {
return fmt.Errorf("%w: %q uses the tail wildcard '>', which is not implemented", ErrUnsupportedFilter, filter)
}
for _, token := range strings.Split(filter, ".") {
if token == "" {
return fmt.Errorf("%w: %q has an empty token", ErrUnsupportedFilter, filter)
}
}
return nil
}