Files
substrate/internal/credbundle
botengyao 1861c99708 Cache parsed credential bundles in credbundle loaders (#537)
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)
2026-07-28 09:32:34 -07:00
..