- users_file.go: maybeReload uses reloadMu.TryLock() instead of Lock().
A full reload (stat+read+parse+replaceFileUsers, incl. per-user
resolveSecret file reads) could otherwise block every concurrent
ServeHTTP request for its whole duration, negating the buildMu/mu
lock split. A request that finds a reload in flight now skips this
cycle; reload stays throttled and best-effort.
- users_file.go: mtime check changed from ModTime().After(lastModTime)
to ModTime().Equal(lastModTime), so a backward mtime move (restore
from backup, cp -p) still triggers a reload instead of being
skipped. Added a regression test with os.Chtimes into the past.
- llmgateway.go: attachUsersFile now stats the file before loading it,
matching maybeReload's ordering, so a write landing between stat and
load is caught on the next check instead of being lost. Documented
why the ignored statErr is safe (falls back to zero lastModTime,
causing one harmless extra reload).
Add usersFile (newUsersFile/load) for the JSON users-file shape
{"users":[{name,group,apiKey,limits?}]}, and authStore.maybeReload
for throttled hot reload: checks mtime at most every reloadEvery
(5s), reloads on change, keeps the last good set and logs via
errorf on any error (stat, parse, or a rejected replace), and logs
via logf when the reload changes the file-sourced user count.
nowFn is injectable for tests.
Wire-up in llmgateway.go: newGateway does a synchronous initial
load when Users.File is set (a bad initial file fails the
constructor); ServeHTTP calls g.auth.maybeReload() at entry
(no-op when no file is configured).
Also fixes replaceFileUsers's merge semantics: a file user now
overrides an inline user of the same name, even when their keys
differ, dropping the inline entry from the rebuilt set. The prior
digest-only keying let a same-named inline/file pair with
different keys coexist instead of the file version winning.