Files
crossbar/internal/config/route.go
T
kyle fa1c398fc4 A route may have its own listener: every request there is that route, paths unprefixed
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
2026-09-25 19:59:21 -07:00

169 lines
5.9 KiB
Go

package config
import (
"fmt"
"net"
"regexp"
"sort"
"strconv"
"strings"
)
// templateName matches a route template: a valid route name ending in "-*".
var templateName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*-\*$`)
// routeName matches a route (or template) name: the pattern a concrete or template route key must
// match, so a name with '*' or an invalid prefix never resolves.
var routeName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
// Route is an ordered list of hosts to try, with an optional default model, the peers allowed to
// reach it, how its requests are placed (affinity), and whether crossbar queues them.
type Route struct {
Hosts []string `toml:"hosts"`
DefaultModel string `toml:"default_model"`
Peers []string `toml:"peers"`
Affinity string `toml:"affinity"` // "" or "conversation" (the default), or "route"
Queue *bool `toml:"queue"` // nil means true
Listen string `toml:"listen"` // "" = none; else host:port of the route's own listener
}
// PerRoute reports affinity = "route": every request on the route (chat or control) shares one
// lease per model, so the route lives on one host.
func (r Route) PerRoute() bool {
return r.Affinity == "route"
}
// Queues reports whether the route's requests wait in (and can be refused by) crossbar's per-(host,
// model) queue; false only for queue = false, which leaves queueing to the client's own slot.
func (r Route) Queues() bool {
return r.Queue == nil || *r.Queue
}
// Route resolves a request route name: an exact entry wins; else the longest template
// "<prefix>-*" whose prefix (including the dash) starts name with a non-empty remainder;
// else ok is false. key is the config key that matched (the template's name for a template).
// A name that is not a valid route name (the pattern below) or contains '*' never matches.
func (c *Config) Route(name string) (r Route, key string, ok bool) {
if !routeName.MatchString(name) {
return Route{}, "", false
}
if rt, found := c.Routes[name]; found {
return rt, name, true
}
var (
best Route
bestKey string
bestLen int
)
for tmpl, rt := range c.Routes {
if !templateName.MatchString(tmpl) {
continue
}
prefix := tmpl[:len(tmpl)-1] // drop the trailing '*', keeping the dash
if len(prefix) > bestLen && strings.HasPrefix(name, prefix) && len(name) > len(prefix) {
best, bestKey, bestLen = rt, tmpl, len(prefix)
}
}
if bestKey == "" {
return Route{}, "", false
}
return best, bestKey, true
}
// checkRoutes validates and defaults one route's hosts, model, affinity and peers in a fixed order.
// A name that is neither a valid route nor a template, a missing or unknown host, a default model no
// host serves, an unrecognised affinity, or a peers list that breaks the identity contract each
// wins as the first error.
func (c *Config) checkRoutes(peersDefined map[string]bool, identityDefined bool) *Error {
if len(c.Routes) == 0 {
return &Error{Field: "routes", Msg: "at least one required"}
}
names := make([]string, 0, len(c.Routes))
for name := range c.Routes {
names = append(names, name)
}
sort.Strings(names)
// A route's own listener address, keyed for the uniqueness check: the value is the
// route that first claimed it, so the second route in sorted order reports the miss.
seenListen := make(map[string]string, len(c.Routes))
for _, name := range names {
r := c.Routes[name]
if !routeName.MatchString(name) && !templateName.MatchString(name) {
return &Error{Field: fmt.Sprintf("routes.%s", name), Msg: "must match [a-z0-9][a-z0-9-]*"}
}
hostsField := fmt.Sprintf("routes.%s.hosts", name)
if len(r.Hosts) == 0 {
return &Error{Field: hostsField, Msg: "at least one required"}
}
seen := make(map[string]bool, len(r.Hosts))
for _, h := range r.Hosts {
if seen[h] {
return &Error{Field: hostsField, Msg: "host listed twice"}
}
seen[h] = true
if _, ok := c.Hosts[h]; !ok {
return &Error{Field: hostsField, Msg: "unknown host"}
}
}
if r.DefaultModel != "" {
served := false
for _, h := range r.Hosts {
if _, ok := c.Hosts[h].Models[r.DefaultModel]; ok {
served = true
break
}
}
if !served {
return &Error{Field: fmt.Sprintf("routes.%s.default_model", name), Msg: "not served by any host in route"}
}
}
switch r.Affinity {
case "", "conversation", "route":
default:
return &Error{Field: fmt.Sprintf("routes.%s.affinity", name), Msg: `must be "conversation" or "route"`}
}
if e := checkPeers(name, r.Peers, peersDefined[name], identityDefined, c.Identity); e != nil {
return e
}
if e := checkListen(name, r.Listen, c.Listen, seenListen); e != nil {
return e
}
}
return nil
}
// checkListen validates one route's own listener. The rules, in order, each naming the key
// routes.<name>.listen: the value must split into host and a numeric port 1-65535, it must not be on
// a template route, it must not be the top-level listen, and it must be unique across routes (the
// second route in sorted name order reports the clash and the other route's name).
func checkListen(name, listen, mainListen string, seenListen map[string]string) *Error {
if listen == "" {
return nil
}
_, port, err := net.SplitHostPort(listen)
if err != nil {
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "must be host:port"}
}
n, err := strconv.Atoi(port)
if err != nil || n < 1 || n > 65535 {
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "port must be 1-65535"}
}
if templateName.MatchString(name) {
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "a template route cannot have its own listener"}
}
if listen == mainListen {
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "cannot be the main listen address"}
}
if other, dup := seenListen[listen]; dup {
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: fmt.Sprintf("already used by route %s", other)}
}
seenListen[listen] = name
return nil
}