Files
LocalAI/core/config/application_config_test.go
Ettore Di Giacinto 50f9c9a058 feat(watchdog): add Memory resource reclaimer (#7583)
* feat(watchdog): add GPU reclaimer

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Handle vram calculation for unified memory devices

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Support RAM eviction, set watchdog interval from runtime settings

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-12-16 09:15:18 +01:00

578 lines
17 KiB
Go

package config
import (
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("ApplicationConfig RuntimeSettings Conversion", func() {
Describe("ToRuntimeSettings", func() {
It("should convert all fields correctly", func() {
appConfig := &ApplicationConfig{
WatchDog: true,
WatchDogIdle: true,
WatchDogBusy: true,
WatchDogIdleTimeout: 20 * time.Minute,
WatchDogBusyTimeout: 10 * time.Minute,
SingleBackend: false,
MaxActiveBackends: 5,
ParallelBackendRequests: true,
MemoryReclaimerEnabled: true,
MemoryReclaimerThreshold: 0.85,
Threads: 8,
ContextSize: 4096,
F16: true,
Debug: true,
CORS: true,
CSRF: true,
CORSAllowOrigins: "https://example.com",
P2PToken: "test-token",
P2PNetworkID: "test-network",
Federated: true,
Galleries: []Gallery{{Name: "test-gallery", URL: "https://example.com"}},
BackendGalleries: []Gallery{{Name: "backend-gallery", URL: "https://example.com/backend"}},
AutoloadGalleries: true,
AutoloadBackendGalleries: true,
ApiKeys: []string{"key1", "key2"},
AgentJobRetentionDays: 30,
}
rs := appConfig.ToRuntimeSettings()
Expect(rs.WatchdogEnabled).ToNot(BeNil())
Expect(*rs.WatchdogEnabled).To(BeTrue())
Expect(rs.WatchdogIdleEnabled).ToNot(BeNil())
Expect(*rs.WatchdogIdleEnabled).To(BeTrue())
Expect(rs.WatchdogBusyEnabled).ToNot(BeNil())
Expect(*rs.WatchdogBusyEnabled).To(BeTrue())
Expect(rs.WatchdogIdleTimeout).ToNot(BeNil())
Expect(*rs.WatchdogIdleTimeout).To(Equal("20m0s"))
Expect(rs.WatchdogBusyTimeout).ToNot(BeNil())
Expect(*rs.WatchdogBusyTimeout).To(Equal("10m0s"))
Expect(rs.SingleBackend).ToNot(BeNil())
Expect(*rs.SingleBackend).To(BeFalse())
Expect(rs.MaxActiveBackends).ToNot(BeNil())
Expect(*rs.MaxActiveBackends).To(Equal(5))
Expect(rs.ParallelBackendRequests).ToNot(BeNil())
Expect(*rs.ParallelBackendRequests).To(BeTrue())
Expect(rs.MemoryReclaimerEnabled).ToNot(BeNil())
Expect(*rs.MemoryReclaimerEnabled).To(BeTrue())
Expect(rs.MemoryReclaimerThreshold).ToNot(BeNil())
Expect(*rs.MemoryReclaimerThreshold).To(Equal(0.85))
Expect(rs.Threads).ToNot(BeNil())
Expect(*rs.Threads).To(Equal(8))
Expect(rs.ContextSize).ToNot(BeNil())
Expect(*rs.ContextSize).To(Equal(4096))
Expect(rs.F16).ToNot(BeNil())
Expect(*rs.F16).To(BeTrue())
Expect(rs.Debug).ToNot(BeNil())
Expect(*rs.Debug).To(BeTrue())
Expect(rs.CORS).ToNot(BeNil())
Expect(*rs.CORS).To(BeTrue())
Expect(rs.CSRF).ToNot(BeNil())
Expect(*rs.CSRF).To(BeTrue())
Expect(rs.CORSAllowOrigins).ToNot(BeNil())
Expect(*rs.CORSAllowOrigins).To(Equal("https://example.com"))
Expect(rs.P2PToken).ToNot(BeNil())
Expect(*rs.P2PToken).To(Equal("test-token"))
Expect(rs.P2PNetworkID).ToNot(BeNil())
Expect(*rs.P2PNetworkID).To(Equal("test-network"))
Expect(rs.Federated).ToNot(BeNil())
Expect(*rs.Federated).To(BeTrue())
Expect(rs.Galleries).ToNot(BeNil())
Expect(*rs.Galleries).To(HaveLen(1))
Expect((*rs.Galleries)[0].Name).To(Equal("test-gallery"))
Expect(rs.BackendGalleries).ToNot(BeNil())
Expect(*rs.BackendGalleries).To(HaveLen(1))
Expect((*rs.BackendGalleries)[0].Name).To(Equal("backend-gallery"))
Expect(rs.AutoloadGalleries).ToNot(BeNil())
Expect(*rs.AutoloadGalleries).To(BeTrue())
Expect(rs.AutoloadBackendGalleries).ToNot(BeNil())
Expect(*rs.AutoloadBackendGalleries).To(BeTrue())
Expect(rs.ApiKeys).ToNot(BeNil())
Expect(*rs.ApiKeys).To(HaveLen(2))
Expect(*rs.ApiKeys).To(ContainElements("key1", "key2"))
Expect(rs.AgentJobRetentionDays).ToNot(BeNil())
Expect(*rs.AgentJobRetentionDays).To(Equal(30))
})
It("should use default timeouts when not set", func() {
appConfig := &ApplicationConfig{}
rs := appConfig.ToRuntimeSettings()
Expect(rs.WatchdogIdleTimeout).ToNot(BeNil())
Expect(*rs.WatchdogIdleTimeout).To(Equal("15m"))
Expect(rs.WatchdogBusyTimeout).ToNot(BeNil())
Expect(*rs.WatchdogBusyTimeout).To(Equal("5m"))
})
})
Describe("ApplyRuntimeSettings", func() {
It("should return false when settings is nil", func() {
appConfig := &ApplicationConfig{}
changed := appConfig.ApplyRuntimeSettings(nil)
Expect(changed).To(BeFalse())
})
It("should only apply non-nil fields", func() {
appConfig := &ApplicationConfig{
WatchDog: false,
Threads: 4,
ContextSize: 2048,
}
watchdogEnabled := true
rs := &RuntimeSettings{
WatchdogEnabled: &watchdogEnabled,
// Leave other fields nil
}
changed := appConfig.ApplyRuntimeSettings(rs)
Expect(changed).To(BeTrue())
Expect(appConfig.WatchDog).To(BeTrue())
// Unchanged fields should remain
Expect(appConfig.Threads).To(Equal(4))
Expect(appConfig.ContextSize).To(Equal(2048))
})
It("should apply watchdog settings and return changed=true", func() {
appConfig := &ApplicationConfig{}
watchdogEnabled := true
watchdogIdle := true
watchdogBusy := true
idleTimeout := "30m"
busyTimeout := "15m"
rs := &RuntimeSettings{
WatchdogEnabled: &watchdogEnabled,
WatchdogIdleEnabled: &watchdogIdle,
WatchdogBusyEnabled: &watchdogBusy,
WatchdogIdleTimeout: &idleTimeout,
WatchdogBusyTimeout: &busyTimeout,
}
changed := appConfig.ApplyRuntimeSettings(rs)
Expect(changed).To(BeTrue())
Expect(appConfig.WatchDog).To(BeTrue())
Expect(appConfig.WatchDogIdle).To(BeTrue())
Expect(appConfig.WatchDogBusy).To(BeTrue())
Expect(appConfig.WatchDogIdleTimeout).To(Equal(30 * time.Minute))
Expect(appConfig.WatchDogBusyTimeout).To(Equal(15 * time.Minute))
})
It("should enable watchdog when idle is enabled", func() {
appConfig := &ApplicationConfig{WatchDog: false}
watchdogIdle := true
rs := &RuntimeSettings{
WatchdogIdleEnabled: &watchdogIdle,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.WatchDog).To(BeTrue())
Expect(appConfig.WatchDogIdle).To(BeTrue())
})
It("should enable watchdog when busy is enabled", func() {
appConfig := &ApplicationConfig{WatchDog: false}
watchdogBusy := true
rs := &RuntimeSettings{
WatchdogBusyEnabled: &watchdogBusy,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.WatchDog).To(BeTrue())
Expect(appConfig.WatchDogBusy).To(BeTrue())
})
It("should handle MaxActiveBackends and update SingleBackend accordingly", func() {
appConfig := &ApplicationConfig{}
maxBackends := 1
rs := &RuntimeSettings{
MaxActiveBackends: &maxBackends,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.MaxActiveBackends).To(Equal(1))
Expect(appConfig.SingleBackend).To(BeTrue())
// Test with multiple backends
maxBackends = 5
rs = &RuntimeSettings{
MaxActiveBackends: &maxBackends,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.MaxActiveBackends).To(Equal(5))
Expect(appConfig.SingleBackend).To(BeFalse())
})
It("should handle SingleBackend and update MaxActiveBackends accordingly", func() {
appConfig := &ApplicationConfig{}
singleBackend := true
rs := &RuntimeSettings{
SingleBackend: &singleBackend,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.SingleBackend).To(BeTrue())
Expect(appConfig.MaxActiveBackends).To(Equal(1))
// Test disabling single backend
singleBackend = false
rs = &RuntimeSettings{
SingleBackend: &singleBackend,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.SingleBackend).To(BeFalse())
Expect(appConfig.MaxActiveBackends).To(Equal(0))
})
It("should enable watchdog when memory reclaimer is enabled", func() {
appConfig := &ApplicationConfig{WatchDog: false}
memoryEnabled := true
threshold := 0.90
rs := &RuntimeSettings{
MemoryReclaimerEnabled: &memoryEnabled,
MemoryReclaimerThreshold: &threshold,
}
changed := appConfig.ApplyRuntimeSettings(rs)
Expect(changed).To(BeTrue())
Expect(appConfig.WatchDog).To(BeTrue())
Expect(appConfig.MemoryReclaimerEnabled).To(BeTrue())
Expect(appConfig.MemoryReclaimerThreshold).To(Equal(0.90))
})
It("should reject invalid memory threshold values", func() {
appConfig := &ApplicationConfig{MemoryReclaimerThreshold: 0.50}
// Test threshold > 1.0
invalidThreshold := 1.5
rs := &RuntimeSettings{
MemoryReclaimerThreshold: &invalidThreshold,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.MemoryReclaimerThreshold).To(Equal(0.50)) // Should remain unchanged
// Test threshold <= 0
invalidThreshold = 0.0
rs = &RuntimeSettings{
MemoryReclaimerThreshold: &invalidThreshold,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.MemoryReclaimerThreshold).To(Equal(0.50)) // Should remain unchanged
// Test negative threshold
invalidThreshold = -0.5
rs = &RuntimeSettings{
MemoryReclaimerThreshold: &invalidThreshold,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.MemoryReclaimerThreshold).To(Equal(0.50)) // Should remain unchanged
})
It("should accept valid memory threshold at boundary", func() {
appConfig := &ApplicationConfig{}
// Test threshold = 1.0 (maximum valid)
threshold := 1.0
rs := &RuntimeSettings{
MemoryReclaimerThreshold: &threshold,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.MemoryReclaimerThreshold).To(Equal(1.0))
// Test threshold just above 0
threshold = 0.01
rs = &RuntimeSettings{
MemoryReclaimerThreshold: &threshold,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.MemoryReclaimerThreshold).To(Equal(0.01))
})
It("should apply performance settings without triggering watchdog change", func() {
appConfig := &ApplicationConfig{}
threads := 16
contextSize := 8192
f16 := true
debug := true
rs := &RuntimeSettings{
Threads: &threads,
ContextSize: &contextSize,
F16: &f16,
Debug: &debug,
}
changed := appConfig.ApplyRuntimeSettings(rs)
// These settings don't require watchdog restart
Expect(changed).To(BeFalse())
Expect(appConfig.Threads).To(Equal(16))
Expect(appConfig.ContextSize).To(Equal(8192))
Expect(appConfig.F16).To(BeTrue())
Expect(appConfig.Debug).To(BeTrue())
})
It("should apply CORS and security settings", func() {
appConfig := &ApplicationConfig{}
cors := true
csrf := true
origins := "https://example.com,https://other.com"
rs := &RuntimeSettings{
CORS: &cors,
CSRF: &csrf,
CORSAllowOrigins: &origins,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.CORS).To(BeTrue())
Expect(appConfig.CSRF).To(BeTrue())
Expect(appConfig.CORSAllowOrigins).To(Equal("https://example.com,https://other.com"))
})
It("should apply P2P settings", func() {
appConfig := &ApplicationConfig{}
token := "p2p-test-token"
networkID := "p2p-test-network"
federated := true
rs := &RuntimeSettings{
P2PToken: &token,
P2PNetworkID: &networkID,
Federated: &federated,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.P2PToken).To(Equal("p2p-test-token"))
Expect(appConfig.P2PNetworkID).To(Equal("p2p-test-network"))
Expect(appConfig.Federated).To(BeTrue())
})
It("should apply gallery settings", func() {
appConfig := &ApplicationConfig{}
galleries := []Gallery{
{Name: "gallery1", URL: "https://gallery1.com"},
{Name: "gallery2", URL: "https://gallery2.com"},
}
backendGalleries := []Gallery{
{Name: "backend-gallery", URL: "https://backend.com"},
}
autoload := true
autoloadBackend := true
rs := &RuntimeSettings{
Galleries: &galleries,
BackendGalleries: &backendGalleries,
AutoloadGalleries: &autoload,
AutoloadBackendGalleries: &autoloadBackend,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.Galleries).To(HaveLen(2))
Expect(appConfig.Galleries[0].Name).To(Equal("gallery1"))
Expect(appConfig.BackendGalleries).To(HaveLen(1))
Expect(appConfig.AutoloadGalleries).To(BeTrue())
Expect(appConfig.AutoloadBackendGalleries).To(BeTrue())
})
It("should apply agent settings", func() {
appConfig := &ApplicationConfig{}
retentionDays := 14
rs := &RuntimeSettings{
AgentJobRetentionDays: &retentionDays,
}
appConfig.ApplyRuntimeSettings(rs)
Expect(appConfig.AgentJobRetentionDays).To(Equal(14))
})
})
Describe("Round-trip conversion", func() {
It("should maintain values through ToRuntimeSettings -> ApplyRuntimeSettings", func() {
original := &ApplicationConfig{
WatchDog: true,
WatchDogIdle: true,
WatchDogBusy: false,
WatchDogIdleTimeout: 25 * time.Minute,
WatchDogBusyTimeout: 12 * time.Minute,
SingleBackend: false,
MaxActiveBackends: 3,
ParallelBackendRequests: true,
MemoryReclaimerEnabled: true,
MemoryReclaimerThreshold: 0.92,
Threads: 12,
ContextSize: 6144,
F16: true,
Debug: false,
CORS: true,
CSRF: false,
CORSAllowOrigins: "https://test.com",
P2PToken: "round-trip-token",
P2PNetworkID: "round-trip-network",
Federated: true,
AutoloadGalleries: true,
AutoloadBackendGalleries: false,
AgentJobRetentionDays: 60,
}
// Convert to RuntimeSettings
rs := original.ToRuntimeSettings()
// Apply to a new ApplicationConfig
target := &ApplicationConfig{}
target.ApplyRuntimeSettings(&rs)
// Verify all values match
Expect(target.WatchDog).To(Equal(original.WatchDog))
Expect(target.WatchDogIdle).To(Equal(original.WatchDogIdle))
Expect(target.WatchDogBusy).To(Equal(original.WatchDogBusy))
Expect(target.WatchDogIdleTimeout).To(Equal(original.WatchDogIdleTimeout))
Expect(target.WatchDogBusyTimeout).To(Equal(original.WatchDogBusyTimeout))
Expect(target.MaxActiveBackends).To(Equal(original.MaxActiveBackends))
Expect(target.ParallelBackendRequests).To(Equal(original.ParallelBackendRequests))
Expect(target.MemoryReclaimerEnabled).To(Equal(original.MemoryReclaimerEnabled))
Expect(target.MemoryReclaimerThreshold).To(Equal(original.MemoryReclaimerThreshold))
Expect(target.Threads).To(Equal(original.Threads))
Expect(target.ContextSize).To(Equal(original.ContextSize))
Expect(target.F16).To(Equal(original.F16))
Expect(target.Debug).To(Equal(original.Debug))
Expect(target.CORS).To(Equal(original.CORS))
Expect(target.CSRF).To(Equal(original.CSRF))
Expect(target.CORSAllowOrigins).To(Equal(original.CORSAllowOrigins))
Expect(target.P2PToken).To(Equal(original.P2PToken))
Expect(target.P2PNetworkID).To(Equal(original.P2PNetworkID))
Expect(target.Federated).To(Equal(original.Federated))
Expect(target.AutoloadGalleries).To(Equal(original.AutoloadGalleries))
Expect(target.AutoloadBackendGalleries).To(Equal(original.AutoloadBackendGalleries))
Expect(target.AgentJobRetentionDays).To(Equal(original.AgentJobRetentionDays))
})
It("should handle empty galleries correctly in round-trip", func() {
original := &ApplicationConfig{
Galleries: []Gallery{},
BackendGalleries: []Gallery{},
ApiKeys: []string{},
}
rs := original.ToRuntimeSettings()
target := &ApplicationConfig{}
target.ApplyRuntimeSettings(&rs)
Expect(target.Galleries).To(BeEmpty())
Expect(target.BackendGalleries).To(BeEmpty())
})
})
Describe("Edge cases", func() {
It("should handle invalid timeout string in ApplyRuntimeSettings", func() {
appConfig := &ApplicationConfig{
WatchDogIdleTimeout: 10 * time.Minute,
}
invalidTimeout := "not-a-duration"
rs := &RuntimeSettings{
WatchdogIdleTimeout: &invalidTimeout,
}
appConfig.ApplyRuntimeSettings(rs)
// Should remain unchanged due to parse error
Expect(appConfig.WatchDogIdleTimeout).To(Equal(10 * time.Minute))
})
It("should handle zero values in ApplicationConfig", func() {
appConfig := &ApplicationConfig{
// All zero values
}
rs := appConfig.ToRuntimeSettings()
// Should still have non-nil pointers with zero/default values
Expect(rs.WatchdogEnabled).ToNot(BeNil())
Expect(*rs.WatchdogEnabled).To(BeFalse())
Expect(rs.Threads).ToNot(BeNil())
Expect(*rs.Threads).To(Equal(0))
Expect(rs.MemoryReclaimerThreshold).ToNot(BeNil())
Expect(*rs.MemoryReclaimerThreshold).To(Equal(0.0))
})
It("should prefer MaxActiveBackends over SingleBackend when both are set", func() {
appConfig := &ApplicationConfig{}
maxBackends := 3
singleBackend := true
rs := &RuntimeSettings{
MaxActiveBackends: &maxBackends,
SingleBackend: &singleBackend,
}
appConfig.ApplyRuntimeSettings(rs)
// MaxActiveBackends should take precedence
Expect(appConfig.MaxActiveBackends).To(Equal(3))
Expect(appConfig.SingleBackend).To(BeFalse()) // 3 != 1, so single backend is false
})
})
})