From 8474296cb180ec3aa9416ddf93b6645ac324bb0c Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 27 Sep 2026 20:18:30 +0000 Subject: [PATCH] fix(failover): keep the aliased model of a warm target loaded The warm list held target names as the chain lists them. For an alias target that is the alias, but the preloader loads the alias stub (no backend, no model) and the eviction guard compares against loaded model names, which never include an alias. A warm alias target was neither preloaded nor protected from eviction. Report the model that serves each warm target instead. Signed-off-by: Ettore Di Giacinto Assisted-by: Claude:claude-opus-5-5 [Claude Code] --- core/services/failover/manager.go | 12 ++++++++---- core/services/failover/manager_test.go | 12 ++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/core/services/failover/manager.go b/core/services/failover/manager.go index ae997a5ad..8a9ea7e0b 100644 --- a/core/services/failover/manager.go +++ b/core/services/failover/manager.go @@ -87,7 +87,10 @@ type Manager struct { } type targetState struct { - name string + name string + // serving is the model that serves this target: its own name, or the + // model an alias points to. Model loading and eviction only know it. + serving string kind Kind warm bool state TargetState @@ -196,6 +199,7 @@ func (m *Manager) syncLocked() { continue } ts.kind = KindOf(tc) + ts.serving = tc.Name if t.Warm && ts.kind == KindLocal { ts.warm = true } @@ -224,9 +228,9 @@ func (m *Manager) syncLocked() { m.recomputeLocked(ch, "") } var warm []string - for name, ts := range m.targets { - if ts.warm { - warm = append(warm, name) + for _, ts := range m.targets { + if ts.warm && ts.serving != "" && !slices.Contains(warm, ts.serving) { + warm = append(warm, ts.serving) } } sort.Strings(warm) diff --git a/core/services/failover/manager_test.go b/core/services/failover/manager_test.go index 75a773f04..b74e32920 100644 --- a/core/services/failover/manager_test.go +++ b/core/services/failover/manager_test.go @@ -248,6 +248,18 @@ var _ = Describe("Manager", func() { Expect(m.WarmTargets()).To(Equal([]string{"b"})) }) + It("reports the model an alias target serves from as warm, not the alias", func() { + var got []string + m = New(src, WithClock(clock), WithOnWarmChanged(func(w []string) { got = w })) + // The preloader and the eviction guard work on loaded model names; an + // alias is never loaded under its own name. + src.Put(config.ModelConfig{Name: "al", Alias: "b"}) + src.Put(chainCfg("chain", nil, t("a"), warmT("al"))) + m.Sync() + Expect(got).To(Equal([]string{"b"})) + Expect(m.WarmTargets()).To(Equal([]string{"b"})) + }) + It("closes a subscription on cancel", func() { events, cancel := m.Subscribe(1) cancel()