mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 09:57:57 -04:00
fix(watchdog): guard StopWatchdog with watchdogMutex to prevent double close StopWatchdog checked, closed and cleared a.watchdogStop without holding a.watchdogMutex, while startWatchdog and RestartWatchdog reassign and close the same channel under that lock. POST /api/settings dispatches to StopWatchdog or RestartWatchdog depending on ApplicationConfig.WatchdogShouldRun(), so both are reachable concurrently. Two callers can observe a non-nil watchdogStop and both close it, which panics with 'close of closed channel' and takes the server down. Take the mutex, matching the other two writers. StopWatchdog is only called from the settings handler, which holds no lock, so this cannot deadlock. Fixes #10841 Co-authored-by: Anai Guo <antai12232931@anaiguo.com>
79 lines
2.3 KiB
Go
79 lines
2.3 KiB
Go
package application
|
|
|
|
import (
|
|
"sync"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("extractModelGroupsFromConfigs", func() {
|
|
It("returns an empty map when no config declares groups", func() {
|
|
out := extractModelGroupsFromConfigs([]config.ModelConfig{
|
|
{Name: "a"},
|
|
{Name: "b"},
|
|
})
|
|
Expect(out).To(BeEmpty())
|
|
})
|
|
|
|
It("returns each model's normalized groups", func() {
|
|
out := extractModelGroupsFromConfigs([]config.ModelConfig{
|
|
{Name: "a", ConcurrencyGroups: []string{" heavy ", "vision", "heavy"}},
|
|
{Name: "b", ConcurrencyGroups: []string{"heavy"}},
|
|
{Name: "c"}, // no groups → omitted
|
|
})
|
|
Expect(out).To(HaveLen(2))
|
|
Expect(out["a"]).To(Equal([]string{"heavy", "vision"}))
|
|
Expect(out["b"]).To(Equal([]string{"heavy"}))
|
|
Expect(out).ToNot(HaveKey("c"))
|
|
})
|
|
|
|
It("omits models whose groups normalize to empty", func() {
|
|
out := extractModelGroupsFromConfigs([]config.ModelConfig{
|
|
{Name: "blanks", ConcurrencyGroups: []string{"", " "}},
|
|
})
|
|
Expect(out).To(BeEmpty())
|
|
})
|
|
|
|
It("skips disabled models so they cannot block loading after re-enable", func() {
|
|
disabled := true
|
|
out := extractModelGroupsFromConfigs([]config.ModelConfig{
|
|
{Name: "a", ConcurrencyGroups: []string{"heavy"}, Disabled: &disabled},
|
|
{Name: "b", ConcurrencyGroups: []string{"heavy"}},
|
|
})
|
|
Expect(out).To(HaveLen(1))
|
|
Expect(out).To(HaveKey("b"))
|
|
Expect(out).ToNot(HaveKey("a"))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("StopWatchdog", func() {
|
|
It("closes the stop channel exactly once under concurrent callers", func() {
|
|
// POST /api/settings routes to StopWatchdog whenever the new settings make
|
|
// WatchdogShouldRun() false, so concurrent requests land here in parallel.
|
|
// Before the mutex was taken, both could observe a non-nil watchdogStop and
|
|
// close it twice, panicking with "close of closed channel".
|
|
app := &Application{watchdogStop: make(chan bool, 1)}
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 64; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
defer wg.Done()
|
|
Expect(app.StopWatchdog()).To(Succeed())
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
Expect(app.watchdogStop).To(BeNil())
|
|
})
|
|
|
|
It("is a no-op when the watchdog was never started", func() {
|
|
app := &Application{}
|
|
Expect(app.StopWatchdog()).To(Succeed())
|
|
Expect(app.watchdogStop).To(BeNil())
|
|
})
|
|
})
|