Files
llmgw/internal/mirror/sources.go
T
Ilker ManapandClaude Opus 5.5 8a76a53afb The control plane uses whichever mirror answers
mirror.peers names the mirrors kept in step with mirror.api. The
mirror's management calls go to the first that can be reached, and
each mirror is asked every fifteen seconds whether it answers: a shard
is sent its model's and adapters' address on one that does while the
mirror the catalog names is down.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 13:43:27 +01:00

103 lines
2.6 KiB
Go

package mirror
import (
"context"
"log/slog"
"strings"
"sync"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/tlsconf"
)
// Sources is where a node fetches a model while one mirror is down. A model
// in the catalog names one mirror's address; when that mirror does not
// answer, the address a node is given is the same file on one that does.
// SourcesInterval is how often each mirror is asked whether it answers.
const SourcesInterval = 15 * time.Second
// Sources watches mirrors kept in step, and rewrites a model's address to
// one that answers.
type Sources struct {
clients []*Client
log *slog.Logger
mu sync.Mutex
bases []string
healthy []bool
}
// NewSources watches the mirrors at these management addresses.
func NewSources(addrs []string, tc tlsconf.Config, log *slog.Logger) (*Sources, error) {
s := &Sources{log: log, bases: make([]string, len(addrs)), healthy: make([]bool, len(addrs))}
for _, a := range addrs {
c, err := NewClient(a, tc, 5*time.Second)
if err != nil {
return nil, err
}
s.clients = append(s.clients, c)
}
return s, nil
}
// Run asks each mirror, every while, until ctx ends.
func (s *Sources) Run(ctx context.Context) {
t := time.NewTicker(SourcesInterval)
defer t.Stop()
for {
s.Check(ctx)
select {
case <-ctx.Done():
return
case <-t.C:
}
}
}
// Check asks each mirror once: whether it answers, and the address its
// files are served at.
func (s *Sources) Check(ctx context.Context) {
for i, c := range s.clients {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
list, err := c.List(ctx)
cancel()
s.mu.Lock()
was := s.healthy[i]
s.healthy[i] = err == nil
if err == nil && list.Base != "" {
s.bases[i] = strings.TrimSuffix(list.Base, "/")
}
s.mu.Unlock()
if was && err != nil {
s.log.Warn("a mirror does not answer; models it holds are fetched from another", "mirror", c.Addr(), "err", err)
} else if !was && err == nil {
s.log.Info("a mirror answers", "mirror", c.Addr(), "files", list.Base)
}
}
}
// Rewrite is a model's address on a mirror that answers: as it is when its
// own mirror answers or no other does, else the same path on the first that
// does.
func (s *Sources) Rewrite(src string) string {
s.mu.Lock()
defer s.mu.Unlock()
from := -1
for i, b := range s.bases {
if b != "" && (src == b || strings.HasPrefix(src, b+"/")) {
from = i
break
}
}
if from < 0 || s.healthy[from] {
return src
}
for i, b := range s.bases {
if i != from && b != "" && s.healthy[i] {
return b + strings.TrimPrefix(src, s.bases[from])
}
}
return src
}