mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-29 17:44:30 -04:00
fix(failover): skip a target that has been edited into a chain
A chain is checked for nested chains when it is saved, but not when one of its targets is later edited into a chain. Requests then served the inner chain's config as the target, which has no backend and triggers backend auto-detection. Mark such a target missing so no plan picks it, and skip it without a trip in the HTTP path when it is pinned. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-5-5 [Claude Code]
This commit is contained in:
1 parent
8474296cb1
commit
00bd5ee121
3 files changed
+29
-2
No files matched your search
@@ -49,13 +49,15 @@ func (re *RequestExtractor) resolveFailover(c echo.Context, requested string, ch
|
||||
}
|
||||
for {
|
||||
cfg, err := re.loadFailoverTarget(st.attempt.Target())
|
||||
if err == nil && cfg.IsDisabled() {
|
||||
// A target that became a chain after its chain was saved has no
|
||||
// backend of its own; like a disabled one it is skipped, not tripped.
|
||||
if err == nil && (cfg.IsDisabled() || cfg.IsFailover()) {
|
||||
// Disabled on purpose, not broken: move on without a trip.
|
||||
if st.attempt.Skip() {
|
||||
continue
|
||||
}
|
||||
c.Set(ContextKeyFailoverAttempt, nil)
|
||||
return nil, fmt.Errorf("failover chain %q: target %q is disabled", chain.Name, cfg.Name)
|
||||
return nil, fmt.Errorf("failover chain %q: target %q is disabled or is itself a chain", chain.Name, cfg.Name)
|
||||
}
|
||||
if err == nil {
|
||||
failover.PrepareTarget(cfg) // cfg is a copy
|
||||
|
||||
@@ -198,6 +198,14 @@ func (m *Manager) syncLocked() {
|
||||
m.setTargetLocked(ts, StateMissing, ReasonMissing, "target config not found")
|
||||
continue
|
||||
}
|
||||
if tc.IsFailover() {
|
||||
// Nested chains are rejected when a chain is saved, but a
|
||||
// target edited into a chain later slips past that check.
|
||||
// Serving its chain config would load a model with no
|
||||
// backend, so treat it as unusable.
|
||||
m.setTargetLocked(ts, StateMissing, ReasonMissing, "target is itself a failover chain")
|
||||
continue
|
||||
}
|
||||
ts.kind = KindOf(tc)
|
||||
ts.serving = tc.Name
|
||||
if t.Warm && ts.kind == KindLocal {
|
||||
|
||||
@@ -260,6 +260,23 @@ var _ = Describe("Manager", func() {
|
||||
Expect(m.WarmTargets()).To(Equal([]string{"b"}))
|
||||
})
|
||||
|
||||
It("never plans a target that has since become a chain itself", func() {
|
||||
// Validation rejects a nested chain when the outer chain is saved,
|
||||
// but not when one of its targets is later edited into a chain.
|
||||
src.Put(chainCfg("inner", nil, t("a")))
|
||||
src.Put(chainCfg("chain", nil, t("a"), t("inner")))
|
||||
m.Sync()
|
||||
m.ReportFailure("a", errBoom)
|
||||
|
||||
st, ok := m.ChainStatus("chain")
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(st.Targets[1].State).To(Equal(StateMissing))
|
||||
att, err := m.Plan("chain")
|
||||
if err == nil {
|
||||
Expect(att.Target()).NotTo(Equal("inner"))
|
||||
}
|
||||
})
|
||||
|
||||
It("closes a subscription on cancel", func() {
|
||||
events, cancel := m.Subscribe(1)
|
||||
cancel()
|
||||
|
||||
Reference in new issue
Block a user