mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-29 01:25:03 -04:00
fix(failover): emit chain.switched when leaving degraded in place
recomputeLocked only fired the event on an active-target change or on entering degraded. When the active target itself recovered while every target was down, the chain silently left degraded with no event, so SSE/realtime consumers tracking chain.switched.state got stuck on "degraded". Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
1 parent
3de6257609
commit
560ffee182
2 files changed
+25
No files matched your search
@@ -305,7 +305,13 @@ func (m *Manager) recomputeLocked(ch *chainState, override Reason) {
|
||||
ch.activeSince = now
|
||||
m.emitLocked(Event{Type: EventChainSwitched, Chain: ch.name, From: ch.targets[prev], To: ch.targets[next], State: string(state), Reason: reason, At: now})
|
||||
case state == ChainDegraded && ch.state != ChainDegraded:
|
||||
// Entering degraded with no active-target change (every target is down).
|
||||
m.emitLocked(Event{Type: EventChainSwitched, Chain: ch.name, From: ch.targets[prev], To: ch.targets[next], State: string(state), Reason: ReasonDegraded, At: now})
|
||||
case state != ChainDegraded && ch.state == ChainDegraded:
|
||||
// Leaving degraded with no active-target change (the active target
|
||||
// itself recovered): SSE/realtime consumers watch chain.switched.state,
|
||||
// so this must fire or they stay on "degraded" forever.
|
||||
m.emitLocked(Event{Type: EventChainSwitched, Chain: ch.name, From: ch.targets[prev], To: ch.targets[next], State: string(state), Reason: ReasonRecovery, At: now})
|
||||
}
|
||||
ch.state = state
|
||||
}
|
||||
|
||||
@@ -132,6 +132,25 @@ var _ = Describe("Manager", func() {
|
||||
Expect(att.Fail(errBoom)).To(BeFalse())
|
||||
})
|
||||
|
||||
It("emits chain.switched when leaving degraded without an active-target change", func() {
|
||||
m.ReportFailure("a", errBoom) // active moves to b
|
||||
m.ReportFailure("b", errBoom) // both down: degraded, active stays b
|
||||
st, _ := m.ChainStatus("chain")
|
||||
Expect(st.State).To(Equal(ChainDegraded))
|
||||
Expect(st.Active).To(Equal("b"))
|
||||
events, cancel := m.Subscribe(16)
|
||||
defer cancel()
|
||||
m.ReportSuccess("b") // b is cold local: one success recovers it in place
|
||||
st, _ = m.ChainStatus("chain")
|
||||
Expect(st.State).To(Equal(ChainFallback))
|
||||
Expect(st.Active).To(Equal("b"), "the active target itself recovered, no switch needed")
|
||||
sw := switched(drain(events))
|
||||
Expect(sw).To(HaveLen(1), "leaving degraded must still notify chain.switched listeners")
|
||||
Expect(sw[0]).To(MatchFields(IgnoreExtras, Fields{
|
||||
"Chain": Equal("chain"), "State": Equal("fallback"), "Reason": Equal(ReasonRecovery),
|
||||
}))
|
||||
})
|
||||
|
||||
It("pins a target regardless of health", func() {
|
||||
Expect(m.Pin("chain", "b")).To(Succeed())
|
||||
att, _ := m.Plan("chain")
|
||||
|
||||
Reference in new issue
Block a user