diff --git a/core/config/application_config.go b/core/config/application_config.go index 306fbe78f1..d84d0405eb 100644 --- a/core/config/application_config.go +++ b/core/config/application_config.go @@ -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 } diff --git a/core/config/chats_limits_race_test.go b/core/config/chats_limits_race_test.go new file mode 100644 index 0000000000..1fcffcb4c5 --- /dev/null +++ b/core/config/chats_limits_race_test.go @@ -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)) + }) +}) diff --git a/core/config/runtime_settings_registry.go b/core/config/runtime_settings_registry.go index c77da71120..e150e79491 100644 --- a/core/config/runtime_settings_registry.go +++ b/core/config/runtime_settings_registry.go @@ -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.