mirror of
https://github.com/agent-substrate/substrate.git
synced 2026-11-11 10:23:17 +08:00
Resolves the `TODO: Introduce caching.` on `credbundle.Loader` / `credbundle.ClientLoader`. The TODO dates back to the package's introduction; since #237 established mTLS between all ate system components, every inter-component handshake is on this path. ## Problem `Loader`/`ClientLoader` re-read and re-parse the credential bundle from disk on every TLS handshake so that pod-certificate rotations get picked up. That costs ~125µs and ~120 allocations per handshake (RSA bundle) for a file that almost never changes. ## Approach: stat-validated cache Each loader keeps the last successful parse together with the `os.FileInfo` taken just before that parse. A handshake stats the file (~1µs) and serves the cached certificate while file identity (`os.SameFile`, i.e. device+inode on Unix), mtime, and size all match; any mismatch re-reads and re-parses. Invalidation and eviction: - The kubelet rotates projected-volume contents (including pod certificates) by writing a fresh timestamped directory and atomically swapping a symlink, so a rotation always surfaces as a change of file identity and is picked up on the **next handshake** — no staleness window. mtime and size additionally cover in-place rewrites. - **No TTL**: freshness is derived from the file, never from time, so an unchanged bundle is never re-parsed and a changed one is never served. (A refresh-interval floor to throttle stats was considered and rejected: it would introduce a staleness window to save a ~1µs syscall that only runs at connection-establishment rate. fsnotify was also considered and rejected: watching through symlink swaps is error-prone and adds watcher lifecycle for no latency benefit over the stat.) - **Memory**: exactly one parsed certificate per loader (one loader per `tls.Config`), replaced on rotation — nothing unbounded to evict. - **Failure semantics unchanged**: stat/read/parse errors are returned to the caller (never a stale certificate), the previous entry stays in place, and every later handshake retries until a parse succeeds — exactly the uncached behavior. - The file can change between the stat and the read; the parse is then stored against the older stat, so the next handshake detects the mismatch and re-reads. The cache never lags a rotation by more than one handshake. ## Benchmarks Apple M5 Pro, RSA-2048 bundle: | | ns/op | B/op | allocs/op | |---|---:|---:|---:| | `BenchmarkLoaderCached` | 1,001 | 304 | 2 | | `BenchmarkParseUncached` | 124,843 | 19,544 | 120 | `Parse` stays exported and uncached; `Loader`/`ClientLoader` signatures are unchanged, so no call sites move. - [x] Tests pass (`go test ./internal/credbundle/ -race`: cache-hit proof via unchanged-stat corruption, atomic-rename rotation, in-place rewrite, missing-file error, error recovery, concurrent handshakes under the race detector) - [x] Appropriate changes to documentation are included in the PR (doc comments on `Loader`/`ClientLoader`/`certCache` describe the caching, rotation, and failure semantics)