mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
fix(config): lock the chats limits that background goroutines read
go test -race on core/http/endpoints/localai reported data races between POST /api/settings and the goroutines that read the result: ApplyRuntimeSettings wrote chats_egress_audit_days and the chats_loop_* limits as plain fields while the audit prune and the loop runner read them through the Effective getters. Guard those six fields with one RWMutex, taken by the getters and by the registry setters. A new spec applies the settings while four goroutines read them and fails under -race without the lock. Assisted-by: Claude:claude-sonnet-5-5 go-test
This commit is contained in:
3 files changed
+104
-6
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -425,6 +426,12 @@ type ApplicationConfig struct {
|
||||
ChatsEgressAllowedHosts string
|
||||
ChatsEgressAuditDays int
|
||||
|
||||
// chatsLimitsMu guards ChatsEgressAuditDays and the chats_loop_* limits.
|
||||
// Background goroutines (the audit prune, the loop runner) read them
|
||||
// while a settings POST rewrites them; use the Effective getters and the
|
||||
// registry setters, which hold it.
|
||||
chatsLimitsMu sync.RWMutex
|
||||
|
||||
// ChatsMaxIterations is the step limit of the tool loop of one chat turn:
|
||||
// how many rounds of tool calls the model may make before it must answer.
|
||||
// A turn that reaches it ends as paused, and the person can let it go on.
|
||||
@@ -1602,6 +1609,8 @@ const (
|
||||
|
||||
// EffectiveChatsLoopMinInterval is chats_loop_min_interval as it applies now.
|
||||
func (o *ApplicationConfig) EffectiveChatsLoopMinInterval() time.Duration {
|
||||
o.chatsLimitsMu.RLock()
|
||||
defer o.chatsLimitsMu.RUnlock()
|
||||
if d := o.ChatsLoopMinInterval; d >= MinChatsLoopInterval && d <= MaxChatsLoopInterval {
|
||||
return d
|
||||
}
|
||||
@@ -1618,21 +1627,29 @@ func inRange(n, hi, def int) int {
|
||||
|
||||
// EffectiveChatsLoopMaxRuns is chats_loop_max_runs as it applies now.
|
||||
func (o *ApplicationConfig) EffectiveChatsLoopMaxRuns() int {
|
||||
o.chatsLimitsMu.RLock()
|
||||
defer o.chatsLimitsMu.RUnlock()
|
||||
return inRange(o.ChatsLoopMaxRuns, MaxChatsLoopMaxRuns, DefaultChatsLoopMaxRuns)
|
||||
}
|
||||
|
||||
// EffectiveChatsLoopMaxPerChat is chats_loop_max_per_chat as it applies now.
|
||||
func (o *ApplicationConfig) EffectiveChatsLoopMaxPerChat() int {
|
||||
o.chatsLimitsMu.RLock()
|
||||
defer o.chatsLimitsMu.RUnlock()
|
||||
return inRange(o.ChatsLoopMaxPerChat, MaxChatsLoopMaxPerChat, DefaultChatsLoopMaxPerChat)
|
||||
}
|
||||
|
||||
// EffectiveChatsLoopMaxPerUser is chats_loop_max_per_user as it applies now.
|
||||
func (o *ApplicationConfig) EffectiveChatsLoopMaxPerUser() int {
|
||||
o.chatsLimitsMu.RLock()
|
||||
defer o.chatsLimitsMu.RUnlock()
|
||||
return inRange(o.ChatsLoopMaxPerUser, MaxChatsLoopMaxPerUser, DefaultChatsLoopMaxPerUser)
|
||||
}
|
||||
|
||||
// EffectiveChatsLoopDailyRuns is chats_loop_daily_runs as it applies now.
|
||||
func (o *ApplicationConfig) EffectiveChatsLoopDailyRuns() int {
|
||||
o.chatsLimitsMu.RLock()
|
||||
defer o.chatsLimitsMu.RUnlock()
|
||||
return inRange(o.ChatsLoopDailyRuns, MaxChatsLoopDailyRuns, DefaultChatsLoopDailyRuns)
|
||||
}
|
||||
|
||||
@@ -1656,8 +1673,18 @@ func (o *ApplicationConfig) EffectiveChatsIndexMaxTextBytes() int {
|
||||
// line while chats_egress_audit_days is unset.
|
||||
const DefaultChatsEgressAuditDays = 30
|
||||
|
||||
// setChatsEgressAuditDays stores chats_egress_audit_days under the lock that
|
||||
// the prune goroutine's reads take.
|
||||
func (o *ApplicationConfig) setChatsEgressAuditDays(v int) {
|
||||
o.chatsLimitsMu.Lock()
|
||||
o.ChatsEgressAuditDays = v
|
||||
o.chatsLimitsMu.Unlock()
|
||||
}
|
||||
|
||||
// EffectiveChatsEgressAuditDays is chats_egress_audit_days as it applies now.
|
||||
func (o *ApplicationConfig) EffectiveChatsEgressAuditDays() int {
|
||||
o.chatsLimitsMu.RLock()
|
||||
defer o.chatsLimitsMu.RUnlock()
|
||||
if o.ChatsEgressAuditDays < 1 || o.ChatsEgressAuditDays > 365 {
|
||||
return DefaultChatsEgressAuditDays
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// The prune and loop goroutines read these limits while a settings POST
|
||||
// rewrites them. Under -race this fails when a getter or a setter skips the lock.
|
||||
var _ = Describe("chats limits read while settings are applied", func() {
|
||||
It("does not race", func() {
|
||||
o := &config.ApplicationConfig{}
|
||||
days, runs, perChat, perUser, daily := 10, 5, 2, 3, 4
|
||||
interval := "2m"
|
||||
rs := &config.RuntimeSettings{
|
||||
ChatsEgressAuditDays: &days, ChatsLoopMaxRuns: &runs, ChatsLoopMaxPerChat: &perChat,
|
||||
ChatsLoopMaxPerUser: &perUser, ChatsLoopDailyRuns: &daily, ChatsLoopMinInterval: &interval,
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
stop := make(chan struct{})
|
||||
for range 4 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
_ = o.EffectiveChatsEgressAuditDays()
|
||||
_ = o.EffectiveChatsLoopMinInterval()
|
||||
_ = o.EffectiveChatsLoopMaxRuns()
|
||||
_ = o.EffectiveChatsLoopMaxPerChat()
|
||||
_ = o.EffectiveChatsLoopMaxPerUser()
|
||||
_ = o.EffectiveChatsLoopDailyRuns()
|
||||
}
|
||||
}()
|
||||
}
|
||||
for range 200 {
|
||||
o.ApplyRuntimeSettings(rs)
|
||||
}
|
||||
close(stop)
|
||||
wg.Wait()
|
||||
Expect(o.EffectiveChatsEgressAuditDays()).To(Equal(10))
|
||||
Expect(o.EffectiveChatsLoopMaxRuns()).To(Equal(5))
|
||||
})
|
||||
})
|
||||
@@ -636,13 +636,13 @@ var runtimeSettingsFields = []fieldSpec{
|
||||
withApply(field("chats_egress_audit_days",
|
||||
func(s *RuntimeSettings) **int { return &s.ChatsEgressAuditDays },
|
||||
func(o *ApplicationConfig) int { return o.EffectiveChatsEgressAuditDays() },
|
||||
func(o *ApplicationConfig, v int) { o.ChatsEgressAuditDays = v }),
|
||||
func(o *ApplicationConfig, v int) { o.setChatsEgressAuditDays(v) }),
|
||||
func(o *ApplicationConfig, s *RuntimeSettings) bool {
|
||||
if v := *s.ChatsEgressAuditDays; v < 1 || v > 365 {
|
||||
xlog.Warn("invalid chats_egress_audit_days in runtime settings", "value", v)
|
||||
return false
|
||||
}
|
||||
o.ChatsEgressAuditDays = *s.ChatsEgressAuditDays
|
||||
o.setChatsEgressAuditDays(*s.ChatsEgressAuditDays)
|
||||
return true
|
||||
}),
|
||||
|
||||
@@ -695,7 +695,9 @@ var runtimeSettingsFields = []fieldSpec{
|
||||
func(o *ApplicationConfig) string { return o.EffectiveChatsLoopMinInterval().String() },
|
||||
func(o *ApplicationConfig, v string) {
|
||||
if d, err := time.ParseDuration(v); err == nil {
|
||||
o.chatsLimitsMu.Lock()
|
||||
o.ChatsLoopMinInterval = d
|
||||
o.chatsLimitsMu.Unlock()
|
||||
}
|
||||
},
|
||||
func(a, b string) bool { return a == b }),
|
||||
@@ -705,25 +707,43 @@ var runtimeSettingsFields = []fieldSpec{
|
||||
xlog.Warn("invalid chats_loop_min_interval in runtime settings", "value", *s.ChatsLoopMinInterval)
|
||||
return false
|
||||
}
|
||||
o.chatsLimitsMu.Lock()
|
||||
o.ChatsLoopMinInterval = d
|
||||
o.chatsLimitsMu.Unlock()
|
||||
return true
|
||||
}),
|
||||
loopIntField("chats_loop_max_runs", MaxChatsLoopMaxRuns,
|
||||
func(s *RuntimeSettings) **int { return &s.ChatsLoopMaxRuns },
|
||||
(*ApplicationConfig).EffectiveChatsLoopMaxRuns,
|
||||
func(o *ApplicationConfig, v int) { o.ChatsLoopMaxRuns = v }),
|
||||
func(o *ApplicationConfig, v int) {
|
||||
o.chatsLimitsMu.Lock()
|
||||
o.ChatsLoopMaxRuns = v
|
||||
o.chatsLimitsMu.Unlock()
|
||||
}),
|
||||
loopIntField("chats_loop_max_per_chat", MaxChatsLoopMaxPerChat,
|
||||
func(s *RuntimeSettings) **int { return &s.ChatsLoopMaxPerChat },
|
||||
(*ApplicationConfig).EffectiveChatsLoopMaxPerChat,
|
||||
func(o *ApplicationConfig, v int) { o.ChatsLoopMaxPerChat = v }),
|
||||
func(o *ApplicationConfig, v int) {
|
||||
o.chatsLimitsMu.Lock()
|
||||
o.ChatsLoopMaxPerChat = v
|
||||
o.chatsLimitsMu.Unlock()
|
||||
}),
|
||||
loopIntField("chats_loop_max_per_user", MaxChatsLoopMaxPerUser,
|
||||
func(s *RuntimeSettings) **int { return &s.ChatsLoopMaxPerUser },
|
||||
(*ApplicationConfig).EffectiveChatsLoopMaxPerUser,
|
||||
func(o *ApplicationConfig, v int) { o.ChatsLoopMaxPerUser = v }),
|
||||
func(o *ApplicationConfig, v int) {
|
||||
o.chatsLimitsMu.Lock()
|
||||
o.ChatsLoopMaxPerUser = v
|
||||
o.chatsLimitsMu.Unlock()
|
||||
}),
|
||||
loopIntField("chats_loop_daily_runs", MaxChatsLoopDailyRuns,
|
||||
func(s *RuntimeSettings) **int { return &s.ChatsLoopDailyRuns },
|
||||
(*ApplicationConfig).EffectiveChatsLoopDailyRuns,
|
||||
func(o *ApplicationConfig, v int) { o.ChatsLoopDailyRuns = v }),
|
||||
func(o *ApplicationConfig, v int) {
|
||||
o.chatsLimitsMu.Lock()
|
||||
o.ChatsLoopDailyRuns = v
|
||||
o.chatsLimitsMu.Unlock()
|
||||
}),
|
||||
|
||||
// Offers from the assistant: stored as the negation for UI clarity. Read
|
||||
// at each turn, so no restart.
|
||||
|
||||
Reference in new issue
Block a user