caddyhttp: allocate the request UUID lazily instead of on every request (#7936)

addHTTPVarsToReplacer ran SetVar(ctx, "uuid", new(requestID)) for every
request during replacer setup, allocating a *requestID unconditionally even
though it is only ever read by the {http.request.uuid} placeholder. The UUID
value itself was already generated lazily (requestID.String caches on first
call), but the container was not.

Allocate the *requestID on first access instead: the uuid placeholder now
creates and stores it in the request's vars table when absent, so repeated
references within a request still share one instance (and thus one UUID),
while requests that never reference the UUID pay nothing.

Add a benchmark for the per-request replacer setup and a regression test for
the uuid placeholder, which previously had no coverage.

Benchmark (BenchmarkAddHTTPVarsToReplacer, linux/arm64, count=6), common path
where the UUID is never referenced:

    before: ~700 ns/op   352 B/op   9 allocs/op
    after:  ~690 ns/op   336 B/op   8 allocs/op

One 16-byte allocation removed per request; a GC-pressure win rather than a
latency one.
pull/7852/merge
Julien Voisin 2026-08-11 13:21:33 +02:00 committed by GitHub
parent c290397cda
commit 63f4e387c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 47 additions and 3 deletions

View File

@ -57,7 +57,6 @@ func NewTestReplacer(req *http.Request) *caddy.Replacer {
func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.ResponseWriter) {
SetVar(req.Context(), "start_time", time.Now())
SetVar(req.Context(), "uuid", new(requestID))
httpVars := func(key string) (any, bool) {
if req != nil {
@ -213,8 +212,14 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
return time.Since(start).Seconds() * 1e3, true // multiply seconds to preserve decimal (see #4666)
case "http.request.uuid":
// fetch the UUID for this request
id := GetVar(req.Context(), "uuid").(*requestID)
// fetch the UUID for this request, generating and caching it
// on first access so requests that never reference the UUID
// don't pay for the allocation
id, ok := GetVar(req.Context(), "uuid").(*requestID)
if !ok {
id = new(requestID)
SetVar(req.Context(), "uuid", id)
}
// set it to this request's access log
extra := req.Context().Value(ExtraLogFieldsCtxKey).(*ExtraLogFields)

View File

@ -296,3 +296,42 @@ func TestHTTPProtoNameNormalization(t *testing.T) {
}
}
}
// BenchmarkAddHTTPVarsToReplacer measures the per-request replacer setup, which
// is the common path where the request UUID is never referenced.
func BenchmarkAddHTTPVarsToReplacer(b *testing.B) {
req := httptest.NewRequest(http.MethodGet, "http://example.com/foo?a=b", nil)
req.Header.Set("User-Agent", "test-agent")
ctx := context.WithValue(req.Context(), VarsCtxKey, make(map[string]any))
ctx = context.WithValue(ctx, ExtraLogFieldsCtxKey, new(ExtraLogFields))
req = req.WithContext(ctx)
b.ReportAllocs()
for b.Loop() {
repl := caddy.NewReplacer()
addHTTPVarsToReplacer(repl, req, nil)
}
}
// TestHTTPVarReplacementUUID verifies the lazily-allocated request UUID is
// generated on first access and stays stable across references.
func TestHTTPVarReplacementUUID(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any))
ctx = context.WithValue(ctx, ExtraLogFieldsCtxKey, new(ExtraLogFields))
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, nil)
first, ok := repl.GetString("http.request.uuid")
if !ok || first == "" {
t.Fatalf("expected a non-empty uuid, got %q (ok=%t)", first, ok)
}
second, _ := repl.GetString("http.request.uuid")
if first != second {
t.Errorf("expected stable uuid across references: %q != %q", first, second)
}
}