feat: cache merged /facts and /nodes in memory, stale on backend failure
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

## Why
A busy Puppetboard re-fans-out the same /facts query every few seconds, and a
502 is worse than 30-second-old facts when every PuppetDB is unreachable.

## How
- Add a `Cache` interface (get reports fresh/stale/miss, put, stats) keyed on
  `<path>?<params>` with keys and repeated values sorted, plus a no-op default
  so uncached paths behave exactly as before.
- Route serveMerged/serveUnion/serveSummed through `serveCached`, so the
  reports cache drops in at `cacheFor` without touching a handler.
- Back /facts and /nodes with a byte-bounded LRU: `facts_ttl` (default 30s,
  clamped to a 30s cap) and `facts_cache_bytes` (default 64 MiB); expired
  entries are kept and served only when every backend fails.
- Single-flight identical keys so N concurrent requests cause one fan-out.
- Surface `cache` state and `serving_stale` in /healthz and the cache settings
  in `config show`.
This commit is contained in:
2026-09-05 20:41:59 +10:00
parent 1ee7a2c07c
commit ac3c19143a
7 changed files with 1159 additions and 45 deletions
+14
View File
@@ -151,6 +151,18 @@ func runServer(cfg Config) error {
}
}
func factsTTLString(cfg Config) string {
s := durationString(cfg.FactsTTL)
switch {
case cfg.factsTTLClamped > 0:
return fmt.Sprintf("%s (clamped from %s, cap %s)",
s, durationString(cfg.factsTTLClamped), durationString(maxFactsTTL))
case !cfg.cacheEnabled():
return s + " (cache disabled)"
}
return s
}
func printConfig(cfg Config) {
if p := cfg.SourcePath(); p != "" {
fmt.Printf("config file : %s (loaded)\n", p)
@@ -161,6 +173,8 @@ func printConfig(cfg Config) {
fmt.Printf("merge : %s\n", cfg.Merge)
fmt.Printf("timeout : %s\n", durationString(cfg.Timeout))
fmt.Printf("freshness_ttl: %s\n", durationString(cfg.FreshnessTTL))
fmt.Printf("facts_ttl : %s\n", factsTTLString(cfg))
fmt.Printf("facts_cache : %d bytes\n", cfg.CacheBytes)
fmt.Println("backends:")
for _, b := range cfg.Backends {
fmt.Printf(" - %-8s %s\n", b.Name, b.URL)