perf: cache GeneralSetting singleton with sync.RWMutex#440
Open
DioCrafts wants to merge 1 commit intokite-org:mainfrom
Open
perf: cache GeneralSetting singleton with sync.RWMutex#440DioCrafts wants to merge 1 commit intokite-org:mainfrom
DioCrafts wants to merge 1 commit intokite-org:mainfrom
Conversation
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
perf: Cache GeneralSetting singleton with
sync.RWMutexProblem
GetGeneralSetting()executesDB.First(&setting, 1)— aSELECT * FROM general_settings WHERE id = 1— on every single call. Thegeneral_settingstable holds a singleton row (always ID=1) that only changes when an admin manually updates configuration via the UI.On top of the SELECT, each call runs normalisation logic with conditional writes: it checks for empty fields (
AIProvider,AIModel,KubectlImage,NodeTerminalImage) and issues anUPDATEback to the DB if any default is missing. This write-on-read pattern is only useful on first run after a migration, yet it executes thousands of times per day.Call sites (7 total, 4 on hot paths)
LoadRuntimeConfig()→HandleChatLoadRuntimeConfig()→HandleExecuteContinueLoadRuntimeConfig()→HandleAIStatusHandleGetGeneralSettingHandleUpdateGeneralSettingkubectl_terminal_handlernode_terminal_handlermain.gostartupDuring active AI conversations,
LoadRuntimeConfig()fires on every request — chat, execute, and status checks. Each one pays 1–5 ms of DB latency for a row that hasn't changed.Measured impact
Solution
sync.RWMutexwith double-checked lockingA singleton pointer guarded by
sync.RWMutex. The read path holdsRLockfor ~10–25 ns. On cache miss, it upgrades to a write lock with double-check to avoid thundering herd — only one goroutine loads from DB, all others get the cached result.Immediate invalidation on mutation
UpdateGeneralSetting()callsInvalidateGeneralSettingCache()after the DB write and re-read, ensuring the next read sees fresh data:Changes
pkg/model/general_setting.gogsMu sync.RWMutex+gsCache *GeneralSetting— package-level singleton cache.GetGeneralSettingCached()— RLock fast-path, double-checked write-lock on miss, stores result fromGetGeneralSetting().InvalidateGeneralSettingCache()— setsgsCache = nilunder write lock. Wired intoUpdateGeneralSetting().GetGeneralSetting()kept public — used by cache miss fallback, mutation paths, and startup.pkg/ai/config.goLoadRuntimeConfig()→model.GetGeneralSettingCached()(hot AI path — chat, execute, status).pkg/ai/handler.goHandleGetGeneralSetting→model.GetGeneralSettingCached()(read-only GET endpoint).HandleUpdateGeneralSetting→ keepsmodel.GetGeneralSetting()(mutation path needs guaranteed fresh data before update).pkg/handlers/kubectl_terminal_handler.gomodel.GetGeneralSettingCached().pkg/handlers/node_terminal_handler.gomodel.GetGeneralSettingCached().Design decisions
sync.RWMutexoveratomic.Valuenilcheck on*GeneralSettingis trivial and safe.syncis stdlib. No LRU needed for a single-entry cache.GetGeneralSetting()kept publicUpdateGeneralSetting), update handler (HandleUpdateGeneralSetting), cache fallback, and startup init. Not dead code.UpdateGeneralSettingdoes DB write → re-read →applyRuntimeGeneralSetting→ invalidate. Next cached read gets the freshly normalised row.Testing
Risk assessment
InvalidateGeneralSettingCache()called inUpdateGeneralSetting()sync.RWMutexguards all reads and writes