maybeReload reloads c.entries from disk when the file mtime has advanced since our last load. Called from Lookup; a no-op when the cache is in-memory only or when the file can't be stat'd (in-memory state is preserved). Re-stats the file under the write lock to avoid TOCTOU races.
()
| 201 | // state is preserved). Re-stats the file under the write lock to avoid |
| 202 | // TOCTOU races. |
| 203 | func (c *Cache) maybeReload() { |
| 204 | if c.path == "" { |
| 205 | return |
| 206 | } |
| 207 | info, err := os.Stat(c.path) |
| 208 | if err != nil { |
| 209 | return |
| 210 | } |
| 211 | |
| 212 | c.mu.RLock() |
| 213 | upToDate := info.ModTime().Equal(c.mtime) |
| 214 | c.mu.RUnlock() |
| 215 | if upToDate { |
| 216 | return |
| 217 | } |
| 218 | |
| 219 | c.mu.Lock() |
| 220 | defer c.mu.Unlock() |
| 221 | // Re-stat under the write lock to avoid TOCTOU: the file could have |
| 222 | // been modified between the initial stat and the lock acquisition. |
| 223 | // Using the stale info would risk storing a mtime that doesn't match |
| 224 | // the content we're about to load. |
| 225 | info, err = os.Stat(c.path) |
| 226 | if err != nil || info.ModTime().Equal(c.mtime) { |
| 227 | // File disappeared or another goroutine already reloaded to this |
| 228 | // mtime; nothing to do. |
| 229 | return |
| 230 | } |
| 231 | fresh := make(map[string]string) |
| 232 | if err := loadFromFile(c.path, fresh); err != nil { |
| 233 | slog.Warn("cache reload failed; keeping in-memory state", |
| 234 | "path", c.path, "error", err) |
| 235 | return |
| 236 | } |
| 237 | c.entries = fresh |
| 238 | c.mtime = info.ModTime() |
| 239 | } |
| 240 | |
| 241 | // keyNormalizer returns a function that applies the configured |
| 242 | // normalization rules to a question before it is used as a cache key. |
no test coverage detected