From 757c673de365a84283933bb73cef9bd2c9a982d5 Mon Sep 17 00:00:00 2001 From: DioCrafts Date: Sat, 21 Mar 2026 17:07:47 +0100 Subject: [PATCH] perf: cache GeneralSetting singleton with sync.RWMutex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetGeneralSetting() executes a SELECT + normalisation logic on every call. Since the general_settings row (ID=1) is a singleton that only changes when an admin updates it via the UI, every AI request (/ai/chat, /ai/execute, /ai/status), GET /general-setting, and terminal handler was paying 1-5ms of unnecessary DB latency. Changes: - pkg/model/general_setting.go: - Add GetGeneralSettingCached(): RWMutex-guarded cache with double-checked locking on miss. Read-path cost: ~10-25ns. - Add InvalidateGeneralSettingCache(): clears the cached pointer. - Wire InvalidateGeneralSettingCache() in UpdateGeneralSetting() so admin changes take effect immediately. - pkg/ai/config.go: - LoadRuntimeConfig() → GetGeneralSettingCached() (hot AI path). - pkg/ai/handler.go: - HandleGetGeneralSetting → GetGeneralSettingCached() (read-only). - HandleUpdateGeneralSetting keeps GetGeneralSetting() (mutation path needs guaranteed fresh data). - pkg/handlers/kubectl_terminal_handler.go: - Terminal open → GetGeneralSettingCached(). - pkg/handlers/node_terminal_handler.go: - Terminal open → GetGeneralSettingCached(). GetGeneralSetting() is intentionally kept public for: - Cache-miss fallback inside GetGeneralSettingCached() - UpdateGeneralSetting() (mutation path, needs fresh DB read) - HandleUpdateGeneralSetting() (reads current state before update) - main.go startup (ensures row exists, runs once) --- pkg/ai/config.go | 2 +- pkg/ai/handler.go | 2 +- pkg/handlers/kubectl_terminal_handler.go | 2 +- pkg/handlers/node_terminal_handler.go | 2 +- pkg/model/general_setting.go | 42 ++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/pkg/ai/config.go b/pkg/ai/config.go index ea5c20dc..4dd21c9b 100644 --- a/pkg/ai/config.go +++ b/pkg/ai/config.go @@ -38,7 +38,7 @@ func providerLabel(provider string) string { } func LoadRuntimeConfig() (*RuntimeConfig, error) { - setting, err := model.GetGeneralSetting() + setting, err := model.GetGeneralSettingCached() if err != nil { return nil, err } diff --git a/pkg/ai/handler.go b/pkg/ai/handler.go index 1cd0b417..49852d7c 100644 --- a/pkg/ai/handler.go +++ b/pkg/ai/handler.go @@ -133,7 +133,7 @@ func HandleExecuteContinue(c *gin.Context) { } func HandleGetGeneralSetting(c *gin.Context) { - setting, err := model.GetGeneralSetting() + setting, err := model.GetGeneralSettingCached() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to load general setting: %v", err)}) return diff --git a/pkg/handlers/kubectl_terminal_handler.go b/pkg/handlers/kubectl_terminal_handler.go index fb51b0a6..66b3b225 100644 --- a/pkg/handlers/kubectl_terminal_handler.go +++ b/pkg/handlers/kubectl_terminal_handler.go @@ -50,7 +50,7 @@ func (h *KubectlTerminalHandler) HandleKubectlTerminalWebSocket(c *gin.Context) return } - setting, err := model.GetGeneralSetting() + setting, err := model.GetGeneralSettingCached() if err != nil { h.sendErrorMessage(conn, fmt.Sprintf("Failed to load settings: %v", err)) return diff --git a/pkg/handlers/node_terminal_handler.go b/pkg/handlers/node_terminal_handler.go index fbf22bca..7df3a225 100644 --- a/pkg/handlers/node_terminal_handler.go +++ b/pkg/handlers/node_terminal_handler.go @@ -61,7 +61,7 @@ func (h *NodeTerminalHandler) HandleNodeTerminalWebSocket(c *gin.Context) { h.sendErrorMessage(conn, fmt.Sprintf("Node %s not found", nodeName)) return } - setting, err := model.GetGeneralSetting() + setting, err := model.GetGeneralSettingCached() if err != nil { log.Printf("Failed to load general setting: %v", err) h.sendErrorMessage(conn, fmt.Sprintf("Failed to load settings: %v", err)) diff --git a/pkg/model/general_setting.go b/pkg/model/general_setting.go index 36ede60b..62dcf775 100644 --- a/pkg/model/general_setting.go +++ b/pkg/model/general_setting.go @@ -3,6 +3,7 @@ package model import ( "errors" "strings" + "sync" "github.com/zxh326/kite/pkg/common" "gorm.io/gorm" @@ -63,6 +64,46 @@ func DefaultGeneralAIModelByProvider(provider string) string { } } +// gsMu + gsCache implement a singleton cache for the GeneralSetting row. +// Read-path uses RLock (~10-25 ns); miss falls through to DB with double-check. +var ( + gsMu sync.RWMutex + gsCache *GeneralSetting +) + +// GetGeneralSettingCached returns the cached GeneralSetting singleton. +// On cache miss it falls back to GetGeneralSetting (DB + normalisation) +// and stores the result. Thread-safe via double-checked locking. +func GetGeneralSettingCached() (*GeneralSetting, error) { + gsMu.RLock() + if c := gsCache; c != nil { + gsMu.RUnlock() + return c, nil + } + gsMu.RUnlock() + + // Cache miss — acquire write lock and double-check. + gsMu.Lock() + defer gsMu.Unlock() + if gsCache != nil { + return gsCache, nil + } + s, err := GetGeneralSetting() + if err != nil { + return nil, err + } + gsCache = s + return s, nil +} + +// InvalidateGeneralSettingCache clears the cached singleton so the next +// read reloads from the database. Called from UpdateGeneralSetting. +func InvalidateGeneralSettingCache() { + gsMu.Lock() + gsCache = nil + gsMu.Unlock() +} + func GetGeneralSetting() (*GeneralSetting, error) { var setting GeneralSetting err := DB.First(&setting, 1).Error @@ -132,6 +173,7 @@ func UpdateGeneralSetting(updates map[string]interface{}) (*GeneralSetting, erro return nil, err } applyRuntimeGeneralSetting(setting) + InvalidateGeneralSettingCache() return setting, nil }