Updating router to provide new advanced routing logic

This commit is contained in:
2025-09-27 11:07:43 +00:00
parent f9e2bca7a1
commit b77efe8e55
8 changed files with 494 additions and 415 deletions

View File

@@ -1,181 +1,131 @@
// Package domain defines external message identifiers.
package domain package domain
import ( import (
"errors" "errors"
"fmt"
"regexp"
"sort" "sort"
"strings" "strings"
) )
const ( var ErrBadIdentifier = errors.New("identifier: invalid format")
prefixRaw = "raw::"
prefixInternal = "internal::"
)
// Identifier is a canonical representation of a data stream identifier. // Identifier is a lightweight wrapper around the canonical key.
type Identifier struct{ key string } type Identifier struct{ key string }
func (id Identifier) IsRaw() bool { return strings.HasPrefix(id.key, prefixRaw) } // NewIdentifier builds a canonical key: "namespace::l1.l2[param=v;...] .l3".
func (id Identifier) IsInternal() bool { return strings.HasPrefix(id.key, prefixInternal) } // Labels and params are sorted for determinism.
func (id Identifier) Key() string { return id.key } func NewIdentifier(namespace string, labels map[string]map[string]string) Identifier {
var b strings.Builder
// rough prealloc: ns + "::" + avg label + some params
b.Grow(len(namespace) + 2 + 10*len(labels) + 20)
func (id Identifier) ProviderSubject() (provider, subject string, ok bool) { // namespace
if !id.IsRaw() { b.WriteString(namespace)
return "", "", false b.WriteString("::")
// sort label names for stable output
labelNames := make([]string, 0, len(labels))
for name := range labels {
labelNames = append(labelNames, name)
} }
body := strings.TrimPrefix(id.key, prefixRaw) sort.Strings(labelNames)
prov, subj, ok := strings.Cut(body, ".")
return prov, subj, ok for i, name := range labelNames {
if i > 0 {
b.WriteByte('.')
}
b.WriteString(name)
// params (sorted)
params := labels[name]
if len(params) > 0 {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
b.WriteByte('[')
for j, k := range keys {
if j > 0 {
b.WriteByte(';')
}
b.WriteString(k)
b.WriteByte('=')
b.WriteString(params[k])
}
b.WriteByte(']')
}
}
return Identifier{key: b.String()}
} }
func (id Identifier) InternalParts() (venue, stream, symbol string, params map[string]string, ok bool) { // NewIdentifierFromRaw wraps a raw key without validation.
if !id.IsInternal() { func NewIdentifierFromRaw(raw string) Identifier { return Identifier{key: raw} }
return "", "", "", nil, false
// Key returns the canonical key string.
func (id Identifier) Key() string { return id.key }
// Parse returns namespace and labels from Key.
// Format: "namespace::label1.label2[param=a;foo=bar].label3"
func (id Identifier) Parse() (string, map[string]map[string]string, error) {
k := id.key
i := strings.Index(k, "::")
if i <= 0 {
return "", nil, ErrBadIdentifier
} }
body := strings.TrimPrefix(id.key, prefixInternal) ns := k[:i]
before, bracket, _ := strings.Cut(body, "[") if ns == "" {
parts := strings.Split(before, ".") return "", nil, ErrBadIdentifier
if len(parts) != 3 {
return "", "", "", nil, false
} }
return parts[0], parts[1], parts[2], decodeParams(strings.TrimSuffix(bracket, "]")), true raw := k[i+2:]
lbls := make(map[string]map[string]string, 8)
if raw == "" {
return ns, lbls, nil
}
for tok := range strings.SplitSeq(raw, ".") {
if tok == "" {
continue
}
name, params, err := parseLabel(tok)
if err != nil || name == "" {
return "", nil, ErrBadIdentifier
}
lbls[name] = params
}
return ns, lbls, nil
} }
func RawID(provider, subject string) (Identifier, error) { // parseLabel parses "name" or "name[k=v;...]" into (name, params).
p := strings.ToLower(strings.TrimSpace(provider)) func parseLabel(tok string) (string, map[string]string, error) {
s := strings.TrimSpace(subject) lb := strings.IndexByte(tok, '[')
if lb == -1 {
if err := validateComponent("provider", p, false); err != nil { return tok, map[string]string{}, nil
return Identifier{}, err
} }
if err := validateComponent("subject", s, true); err != nil { rb := strings.LastIndexByte(tok, ']')
return Identifier{}, err if rb == -1 || rb < lb {
} return "", nil, ErrBadIdentifier
return Identifier{key: prefixRaw + p + "." + s}, nil
}
func InternalID(venue, stream, symbol string, params map[string]string) (Identifier, error) {
v := strings.ToLower(strings.TrimSpace(venue))
t := strings.ToLower(strings.TrimSpace(stream))
sym := strings.ToUpper(strings.TrimSpace(symbol))
if err := validateComponent("venue", v, false); err != nil {
return Identifier{}, err
}
if err := validateComponent("stream", t, false); err != nil {
return Identifier{}, err
}
if err := validateComponent("symbol", sym, false); err != nil {
return Identifier{}, err
} }
paramStr, err := encodeParams(params) // "k=v;..." or "" name := tok[:lb]
if err != nil { paramStr := tok[lb+1 : rb]
return Identifier{}, err params := map[string]string{}
}
if paramStr == "" { if paramStr == "" {
paramStr = "[]" return name, params, nil
} else {
paramStr = "[" + paramStr + "]"
} }
return Identifier{key: prefixInternal + v + "." + t + "." + sym + paramStr}, nil
}
func ParseIdentifier(s string) (Identifier, error) { for pair := range strings.SplitSeq(paramStr, ";") {
s = strings.TrimSpace(s) if pair == "" {
switch {
case strings.HasPrefix(s, prefixRaw):
// raw::provider.subject
body := strings.TrimPrefix(s, prefixRaw)
prov, subj, ok := strings.Cut(body, ".")
if !ok {
return Identifier{}, errors.New("invalid raw identifier: missing '.'")
}
return RawID(prov, subj)
case strings.HasPrefix(s, prefixInternal):
// internal::venue.stream.symbol[...]
body := strings.TrimPrefix(s, prefixInternal)
before, bracket, _ := strings.Cut(body, "[")
parts := strings.Split(before, ".")
if len(parts) != 3 {
return Identifier{}, errors.New("invalid internal identifier: need venue.stream.symbol")
}
params := decodeParams(strings.TrimSuffix(bracket, "]"))
return InternalID(parts[0], parts[1], parts[2], params)
}
return Identifier{}, errors.New("unknown identifier prefix")
}
var (
segDisallow = regexp.MustCompile(`[ \t\r\n\[\]]`) // forbid whitespace/brackets in fixed segments
dotDisallow = regexp.MustCompile(`[.]`) // fixed segments cannot contain '.'
)
// allowAny=true (for subject) skips dot checks but still forbids whitespace/brackets.
func validateComponent(name, v string, allowAny bool) error {
if v == "" {
return fmt.Errorf("%s cannot be empty", name)
}
if allowAny {
if segDisallow.MatchString(v) {
return fmt.Errorf("%s contains illegal chars [] or whitespace", name)
}
return nil
}
if segDisallow.MatchString(v) || dotDisallow.MatchString(v) {
return fmt.Errorf("%s contains illegal chars (dot/brackets/whitespace)", name)
}
return nil
}
// encodeParams renders sorted k=v pairs separated by ';'.
func encodeParams(params map[string]string) (string, error) {
if len(params) == 0 {
return "", nil
}
keys := make([]string, 0, len(params))
for k := range params {
k = strings.ToLower(strings.TrimSpace(k))
if k == "" {
continue continue
} }
keys = append(keys, k) kv := strings.SplitN(pair, "=", 2)
} if len(kv) != 2 || kv[0] == "" {
sort.Strings(keys) return "", nil, ErrBadIdentifier
out := make([]string, 0, len(keys))
for _, k := range keys {
v := strings.TrimSpace(params[k])
// prevent breaking delimiters
if strings.ContainsAny(k, ";]") || strings.ContainsAny(v, ";]") {
return "", fmt.Errorf("param %q contains illegal ';' or ']'", k)
} }
out = append(out, k+"="+v) params[kv[0]] = kv[1]
} }
return strings.Join(out, ";"), nil return name, params, nil
}
func decodeParams(s string) map[string]string {
s = strings.TrimSpace(s)
if s == "" {
return map[string]string{}
}
out := make(map[string]string, 4)
for _, p := range strings.Split(s, ";") {
if p == "" {
continue
}
kv := strings.SplitN(p, "=", 2)
if len(kv) != 2 {
continue
}
k := strings.ToLower(strings.TrimSpace(kv[0]))
v := strings.TrimSpace(kv[1])
if k != "" {
out[k] = v
}
}
return out
} }

View File

@@ -0,0 +1,94 @@
package domain
import (
"sort"
"strings"
)
type Pattern struct {
Namespace string
Labels map[string]map[string]string
Exact bool
}
// Canonical returns a canonical string representation of the Pattern struct
// TODO: Ensure labels and namespaces are set to lowercase
func (p *Pattern) Canonical() string {
var b strings.Builder
b.Grow(len(p.Namespace) + 10*len(p.Labels) + 20) // preallocate a rough size estimate
b.WriteString(p.Namespace)
b.WriteString("::")
labelNames := make([]string, 0, len(p.Labels))
for name := range p.Labels {
labelNames = append(labelNames, name)
}
sort.Strings(labelNames) // sort the labels for determinism
for i, name := range labelNames {
if i > 0 {
b.WriteByte('|')
}
b.WriteString(name)
params := p.Labels[name]
if len(params) > 0 {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys) // sort params for determinism
b.WriteByte('[')
for j, k := range keys {
if j > 0 {
b.WriteByte(';')
}
b.WriteString(k)
b.WriteByte('=')
b.WriteString(params[k])
}
b.WriteByte(']')
}
}
b.WriteString("::")
if p.Exact {
b.WriteString("t")
} else {
b.WriteString("f")
}
return b.String()
}
// Satisfies checks if a domain.Identifier satisfies the pattern.
func (p *Pattern) Satisfies(id Identifier) bool {
ns, idLabels, err := id.Parse()
if err != nil || ns != p.Namespace {
return false
}
// Every pattern label must be present in the identifier.
for lname, wantParams := range p.Labels {
haveParams, ok := idLabels[lname]
if !ok {
return false
}
// If pattern specifies params, they must be a subset of identifier's params.
for k, v := range wantParams {
hv, ok := haveParams[k]
if !ok || hv != v {
return false
}
}
// If pattern has no params for this label, it matches any/none params in the identifier.
}
// Exact applies to label names only: no extras allowed.
if p.Exact && len(idLabels) != len(p.Labels) {
return false
}
return true
}

View File

@@ -60,7 +60,7 @@ type detachResult struct {
type configureCmd struct { type configureCmd struct {
sid uuid.UUID sid uuid.UUID
next []domain.Identifier next []domain.Pattern
resp chan configureResult resp chan configureResult
} }

View File

@@ -1,36 +1 @@
package manager package manager
import (
"gitlab.michelsen.id/phillmichelsen/tessera/services/data_service/internal/domain"
)
func identifierSetDifferences(oldIDs, nextIDs []domain.Identifier) (toAdd, toDel []domain.Identifier) {
oldSet := make(map[domain.Identifier]struct{}, len(oldIDs))
for _, id := range oldIDs {
oldSet[id] = struct{}{}
}
newSet := make(map[domain.Identifier]struct{}, len(nextIDs))
for _, id := range nextIDs {
newSet[id] = struct{}{}
if _, ok := oldSet[id]; !ok {
toAdd = append(toAdd, id)
}
}
for _, id := range oldIDs {
if _, ok := newSet[id]; !ok {
toDel = append(toDel, id)
}
}
return
}
func identifierMapToSlice(m map[domain.Identifier]struct{}) []domain.Identifier {
ids := make([]domain.Identifier, 0, len(m))
for id := range m {
ids = append(ids, id)
}
return ids
}

View File

@@ -43,7 +43,7 @@ func NewManager(router *router.Router, workerRegistry *worker.Registry) *Manager
sessions: make(map[uuid.UUID]*session), sessions: make(map[uuid.UUID]*session),
router: router, router: router,
} }
go router.Run() go router.Start()
go m.run() go m.run()
slog.Default().Info("manager started", slog.String("cmp", "manager")) slog.Default().Info("manager started", slog.String("cmp", "manager"))
@@ -113,9 +113,9 @@ func (m *Manager) DetachClient(id uuid.UUID) error {
return r.err return r.err
} }
// ConfigureSession sets the next set of identifiers for the session, starting and stopping streams as needed. // ConfigureSession sets the next set of patterns for the session, starting and stopping streams as needed.
func (m *Manager) ConfigureSession(id uuid.UUID, next []domain.Identifier) error { func (m *Manager) ConfigureSession(id uuid.UUID, next []domain.Pattern) error {
slog.Default().Debug("configure session request", slog.String("cmp", "manager"), slog.String("session", id.String()), slog.Int("idents", len(next))) slog.Default().Debug("configure session request", slog.String("cmp", "manager"), slog.String("session", id.String()), slog.Int("patterns", len(next)))
resp := make(chan configureResult, 1) resp := make(chan configureResult, 1)
m.cmdCh <- configureCmd{sid: id, next: next, resp: resp} m.cmdCh <- configureCmd{sid: id, next: next, resp: resp}
@@ -218,10 +218,6 @@ func (m *Manager) handleAttach(cmd attachCmd) {
s.attached = true s.attached = true
s.disarmIdleTimer() s.disarmIdleTimer()
for id := range s.bound {
m.router.RegisterRoute(id, cout)
}
cmd.resp <- attachResult{cin: cin, cout: cout, err: nil} cmd.resp <- attachResult{cin: cin, cout: cout, err: nil}
} }
@@ -237,10 +233,6 @@ func (m *Manager) handleDetach(cmd detachCmd) {
return return
} }
for id := range s.bound {
m.router.DeregisterRoute(id, s.outChannel)
}
s.clearChannels() s.clearChannels()
// Only rearm the idle timer if the timeout is positive. // Only rearm the idle timer if the timeout is positive.
@@ -260,109 +252,20 @@ func (m *Manager) handleDetach(cmd detachCmd) {
// handleConfigure updates the session bindings, starting and stopping streams as needed. Currently only supports Raw streams. // handleConfigure updates the session bindings, starting and stopping streams as needed. Currently only supports Raw streams.
// TODO: Change this configuration to be an atomic operation, so that partial failures do not end in a half-configured state. // TODO: Change this configuration to be an atomic operation, so that partial failures do not end in a half-configured state.
func (m *Manager) handleConfigure(cmd configureCmd) { func (m *Manager) handleConfigure(cmd configureCmd) {
s, ok := m.sessions[cmd.sid] _, ok := m.sessions[cmd.sid]
if !ok { if !ok {
cmd.resp <- configureResult{ErrSessionNotFound} cmd.resp <- configureResult{ErrSessionNotFound}
return return
} }
toAdd, toRemove := identifierSetDifferences(identifierMapToSlice(s.bound), cmd.next)
pendingSub := make(map[domain.Identifier]<-chan error)
pendingUnsub := make(map[domain.Identifier]<-chan error)
var added, removed []domain.Identifier
var errs error var errs error
// Adds
for _, id := range toAdd {
pName, subject, ok := id.ProviderSubject()
if !ok || subject == "" || pName == "" {
errs = errors.Join(errs, fmt.Errorf("invalid identifier: %s", id.Key()))
continue
}
p, ok := m.providers[pName]
if !ok {
errs = errors.Join(errs, fmt.Errorf("provider not found: %s", pName))
continue
}
if p.IsStreamActive(subject) {
s.bound[id] = struct{}{}
added = append(added, id)
continue
}
pendingSub[id] = p.Subscribe(subject)
}
// Removes
for _, id := range toRemove {
pName, subject, ok := id.ProviderSubject()
if !ok || subject == "" || pName == "" {
errs = errors.Join(errs, fmt.Errorf("invalid identifier: %s", id.Key()))
continue
}
p, ok := m.providers[pName]
if !ok {
errs = errors.Join(errs, fmt.Errorf("provider not found: %s", pName))
continue
}
stillNeeded := false
for _, other := range m.sessions {
if other.id == s.id {
continue
}
if _, bound := other.bound[id]; bound {
stillNeeded = true
break
}
}
if stillNeeded {
delete(s.bound, id)
removed = append(removed, id)
continue
}
pendingUnsub[id] = p.Unsubscribe(subject)
}
// Wait for subscribes
for id, ch := range pendingSub {
if err := <-ch; err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to subscribe to %s: %w", id.Key(), err))
continue
}
s.bound[id] = struct{}{}
added = append(added, id)
}
// Wait for unsubscribes
for id, ch := range pendingUnsub {
if err := <-ch; err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to unsubscribe from %s: %w", id.Key(), err))
continue
}
delete(s.bound, id)
removed = append(removed, id)
}
if s.attached {
if s.inChannel == nil || s.outChannel == nil {
errs = errors.Join(errs, fmt.Errorf("channels do not exist despite attached state")) // error should never be hit
slog.Default().Error("no channels despite attached state", slog.String("cmp", "manager"), slog.String("session", cmd.sid.String()))
} else {
for _, id := range added {
m.router.RegisterRoute(id, s.outChannel)
}
for _, id := range removed {
m.router.DeregisterRoute(id, s.outChannel)
}
}
}
cmd.resp <- configureResult{err: errs} cmd.resp <- configureResult{err: errs}
} }
// handleCloseSession closes and removes the session, cleaning up all bindings. // handleCloseSession closes and removes the session, cleaning up all bindings.
func (m *Manager) handleCloseSession(cmd closeSessionCmd) { func (m *Manager) handleCloseSession(cmd closeSessionCmd) {
s, ok := m.sessions[cmd.sid] _, ok := m.sessions[cmd.sid]
if !ok { if !ok {
cmd.resp <- closeSessionResult{err: ErrSessionNotFound} cmd.resp <- closeSessionResult{err: ErrSessionNotFound}
return return
@@ -370,60 +273,5 @@ func (m *Manager) handleCloseSession(cmd closeSessionCmd) {
var errs error var errs error
// Deregister attached routes
if s.attached {
if s.outChannel == nil {
errs = errors.Join(errs, fmt.Errorf("channels do not exist despite attached state"))
slog.Default().Error("no channels despite attached state", slog.String("cmp", "manager"), slog.String("session", cmd.sid.String()))
} else {
for id := range s.bound {
m.router.DeregisterRoute(id, s.outChannel)
}
}
}
// Unsubscribe from all streams if no other session needs them.
pendingUnsub := make(map[domain.Identifier]<-chan error)
for id := range s.bound {
pName, subject, ok := id.ProviderSubject()
if !ok || subject == "" || pName == "" {
errs = errors.Join(errs, fmt.Errorf("invalid identifier: %s", id.Key()))
continue
}
p, ok := m.providers[pName]
if !ok {
errs = errors.Join(errs, fmt.Errorf("provider not found: %s", pName))
continue
}
stillNeeded := false
for _, other := range m.sessions {
if other.id == s.id {
continue
}
if _, bound := other.bound[id]; bound {
stillNeeded = true
break
}
}
if stillNeeded {
continue
}
pendingUnsub[id] = p.Unsubscribe(subject)
}
for id, ch := range pendingUnsub {
if err := <-ch; err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to unsubscribe from %s: %w", id.Key(), err))
}
}
// Stop timers and channels, remove session.
s.disarmIdleTimer()
s.clearChannels()
delete(m.sessions, s.id)
cmd.resp <- closeSessionResult{err: errs} cmd.resp <- closeSessionResult{err: errs}
} }

View File

@@ -0,0 +1,165 @@
package router
import (
"slices"
"sync"
"gitlab.michelsen.id/phillmichelsen/tessera/services/data_service/internal/domain"
)
// actorPartition: single goroutine owns state.
type actorPartition struct {
opCh chan any
wg sync.WaitGroup
memo map[domain.Identifier][]chan<- domain.Message
rules map[string]*ruleEntry // the string is to be a pattern.Canonical()
}
type ruleEntry struct {
pattern domain.Pattern
channels map[chan<- domain.Message]struct{}
}
func newActorPartition(bufferSize int) *actorPartition {
return &actorPartition{
opCh: make(chan any, bufferSize),
memo: make(map[domain.Identifier][]chan<- domain.Message),
rules: make(map[string]*ruleEntry),
}
}
// External (though not exported) methods to implement the pattern interface
func (p *actorPartition) start() {
p.wg.Go(p.loop)
}
func (p *actorPartition) stop() {
close(p.opCh)
p.wg.Wait()
}
func (p *actorPartition) registerRoute(pat domain.Pattern, ch chan<- domain.Message) {
done := make(chan struct{}, 1)
p.opCh <- opRegister{pattern: pat, channel: ch, done: done}
<-done
}
func (p *actorPartition) deregisterRoute(pat domain.Pattern, ch chan<- domain.Message) {
done := make(chan struct{}, 1)
p.opCh <- opDeregister{pattern: pat, channel: ch, done: done}
<-done
}
func (p *actorPartition) deliver(msg domain.Message) {
p.opCh <- opDeliver{msg: msg}
}
// Internal
type opRegister struct {
pattern domain.Pattern
channel chan<- domain.Message
done chan struct{}
}
type opDeregister struct {
pattern domain.Pattern
channel chan<- domain.Message
done chan struct{}
}
type opDeliver struct{ msg domain.Message }
func (p *actorPartition) loop() {
for op := range p.opCh {
switch v := op.(type) {
case opDeliver:
id := v.msg.Identifier
subs, exists := p.memo[id]
if !exists {
uniqueChannels := make(map[chan<- domain.Message]struct{})
for _, e := range p.rules {
if e.pattern.Satisfies(id) {
for ch := range e.channels {
uniqueChannels[ch] = struct{}{}
}
}
}
if len(uniqueChannels) > 0 {
uniqueChannelsSlice := make([]chan<- domain.Message, 0, len(uniqueChannels))
for ch := range uniqueChannels {
uniqueChannelsSlice = append(uniqueChannelsSlice, ch)
}
p.memo[id] = uniqueChannelsSlice
subs = uniqueChannelsSlice
} else {
p.memo[id] = nil // cache "no subscribers", fast hot-path.
subs = nil
}
}
for _, ch := range subs {
select {
case ch <- v.msg:
default: // drop on full ch
}
}
case opRegister:
key := v.pattern.Canonical()
e, exists := p.rules[key]
if !exists {
e = &ruleEntry{pattern: v.pattern, channels: make(map[chan<- domain.Message]struct{})}
p.rules[key] = e
}
if _, exists := e.channels[v.channel]; exists {
v.done <- struct{}{}
continue
}
e.channels[v.channel] = struct{}{}
for id, subs := range p.memo {
if v.pattern.Satisfies(id) && !slices.Contains(subs, v.channel) {
p.memo[id] = append(subs, v.channel)
}
}
v.done <- struct{}{}
case opDeregister:
key := v.pattern.Canonical()
e, ok := p.rules[key]
if !ok {
v.done <- struct{}{}
continue
}
if _, ok := e.channels[v.channel]; !ok {
v.done <- struct{}{}
continue
}
delete(e.channels, v.channel)
if len(e.channels) == 0 {
delete(p.rules, key)
}
for id, subs := range p.memo {
if v.pattern.Satisfies(id) {
for i := range subs {
if subs[i] == v.channel {
last := len(subs) - 1
subs[i] = subs[last]
subs[last] = nil // help GC
p.memo[id] = subs[:last]
break
}
}
}
}
v.done <- struct{}{}
}
}
}

View File

@@ -1,70 +0,0 @@
package router
import (
"log/slog"
"sync"
"gitlab.michelsen.id/phillmichelsen/tessera/services/data_service/internal/domain"
)
type Router struct {
incoming chan domain.Message
routes map[domain.Identifier][]chan<- domain.Message
mu sync.RWMutex
}
func NewRouter(buffer int) *Router {
return &Router{
incoming: make(chan domain.Message, buffer), // Buffered channel for incoming messages
routes: make(map[domain.Identifier][]chan<- domain.Message),
}
}
func (r *Router) IncomingChannel() chan<- domain.Message {
return r.incoming
}
func (r *Router) Run() {
slog.Default().Info("router started", "cmp", "router")
for msg := range r.incoming {
r.mu.RLock()
channels := r.routes[msg.Identifier]
for _, ch := range channels {
select {
case ch <- msg:
default:
slog.Default().Warn("dropping message due to backpressure", "cmp", "router", "identifier", msg.Identifier.Key())
}
}
r.mu.RUnlock()
}
}
func (r *Router) RegisterRoute(id domain.Identifier, ch chan<- domain.Message) {
r.mu.Lock()
r.routes[id] = append(r.routes[id], ch)
r.mu.Unlock()
slog.Default().Debug("registered route", "cmp", "router", "identifier", id.Key(), "channel", ch)
}
func (r *Router) DeregisterRoute(id domain.Identifier, ch chan<- domain.Message) {
r.mu.Lock()
slice := r.routes[id]
for i := 0; i < len(slice); i++ {
if slice[i] == ch {
slice[i] = slice[len(slice)-1]
slice = slice[:len(slice)-1]
i--
}
}
if len(slice) == 0 {
delete(r.routes, id)
} else {
r.routes[id] = slice
}
r.mu.Unlock()
slog.Default().Debug("deregistered route", "cmp", "router", "identifier", id.Key(), "channel", ch)
}

View File

@@ -0,0 +1,127 @@
// Package router for routing!
package router
import (
"fmt"
"sync"
"gitlab.michelsen.id/phillmichelsen/tessera/services/data_service/internal/domain"
)
type partition interface {
registerRoute(domain.Pattern, chan<- domain.Message)
deregisterRoute(domain.Pattern, chan<- domain.Message)
deliver(domain.Message)
start()
stop()
}
// Factories take a buffer size (internal), router stores a zero-arg thunk.
var partitionFactories = map[string]func(int) partition{
"actor": func(buf int) partition { return newActorPartition(buf) },
}
type Router struct {
incoming chan domain.Message
mu sync.RWMutex
partitions map[string]partition
newPart func() partition // zero-arg thunk
wg sync.WaitGroup
started bool
}
func NewRouter(kind string, incomingBuf, partBuf int) (*Router, error) {
if incomingBuf <= 0 {
incomingBuf = 2048
}
if partBuf <= 0 {
partBuf = 1024
}
makePartWithBuf, ok := partitionFactories[kind]
if !ok {
return nil, fmt.Errorf("unknown partition kind %q", kind)
}
// Curry (!!!) to zero-arg
makePart := func() partition { return makePartWithBuf(partBuf) }
return &Router{
incoming: make(chan domain.Message, incomingBuf),
partitions: make(map[string]partition),
newPart: makePart,
}, nil
}
func (r *Router) Start() {
r.mu.Lock()
if r.started {
r.mu.Unlock()
return
}
r.started = true
r.mu.Unlock()
r.wg.Go(func() {
for msg := range r.incoming {
ns, _, _ := msg.Identifier.Parse()
r.mu.RLock()
p, exists := r.partitions[ns]
r.mu.RUnlock()
if exists {
p.deliver(msg)
}
}
})
}
func (r *Router) Stop() {
r.mu.Lock()
if !r.started {
r.mu.Unlock()
return
}
r.started = false
close(r.incoming)
ps := make([]partition, 0, len(r.partitions))
for _, p := range r.partitions {
ps = append(ps, p)
}
r.partitions = make(map[string]partition)
r.mu.Unlock()
for _, p := range ps {
p.stop()
}
r.wg.Wait()
}
func (r *Router) Incoming() chan<- domain.Message { return r.incoming }
func (r *Router) RegisterPattern(pat domain.Pattern, ch chan<- domain.Message) {
// Inline ensurePartition
ns := pat.Namespace
r.mu.RLock()
p := r.partitions[ns]
r.mu.RUnlock()
if p == nil {
r.mu.Lock()
// recheck under write lock
if p = r.partitions[ns]; p == nil {
p = r.newPart()
p.start()
r.partitions[ns] = p
}
r.mu.Unlock()
}
p.registerRoute(pat, ch)
}
func (r *Router) DeregisterPattern(pat domain.Pattern, ch chan<- domain.Message) {
r.mu.RLock()
p := r.partitions[pat.Namespace]
r.mu.RUnlock()
if p != nil {
p.deregisterRoute(pat, ch)
}
}