Is your feature request related to a problem? Please describe.
The function ctx.GetOASDefinition() returns a deep clone of the OAS definition stored in the request context via reflect.Clone. This clone is performed on every request, even in hot paths where the clone is not mutated.
In production at ~100 RPS on a single auth plugin that calls GetOASDefinition() per request, reflect.Clone accounts for 80% of CPU time on the auth path, becoming a scaling bottleneck for gateway instances that route high volumes of requests.
Benchmark (1 call, with setup):
GetOASDefinition (with clone): 294 microseconds, 857 allocations, 75.6 KB
- Direct pointer access (no clone): 18.7 nanoseconds, 0 allocations
The deep copy exists to provide a mutable, request-private copy so callers can safely modify the definition without racing other concurrent requests. This is crucial for safety in the general case, but hot-path code that only reads the definition should not pay the clone cost.
Describe the solution you'd like
Add a new function ctx.GetOASDefinitionRaw(r *http.Request) *oas.OAS that returns the shared OAS definition pointer stored in context without cloning. This mirrors the existing "Raw" naming pattern in the codebase (log.GetRaw(), certificateManager.GetRaw()), both of which return the underlying unprocessed value.
- GetOASDefinitionRaw: returns the live
*oas.OAS pointer (no clone). Caller must not mutate; suitable for read-only inspection and hot paths.
- GetOASDefinition (unchanged): returns a deep-cloned copy. Safe default for callers that need to mutate.
The new function is additive and fully backward compatible. Existing code continues to use GetOASDefinition and gets the same safety guarantees.
Describe alternatives you've considered
-
Replace GetOASDefinition's implementation with the raw pointer: Breaks existing code that mutates the result. Rejected.
-
Add a parameter to GetOASDefinition (e.g. GetOASDefinition(r, clone=true)): Requires a flag/option at call sites. Rejected in favor of two distinct functions, which is clearer and mirrors the codebase's existing "Raw" precedent.
-
Lazy-clone GetOASDefinition (clone only if the caller mutates): Complex to detect mutation; Go's reflect.DeepEqual is itself expensive. The two-function approach is simpler and aligns with how other "raw" accessors work.
-
Cache the clone: Per-request cloning is already idempotent; a cache would only save the clone on repeated calls within one request. Not worth the context map churn.
Additional context
-
Root cause: Every call to reflect.Clone for the OAS definition (200–300 KB object) does a full deep traversal and allocation, which becomes hot for plugins/middleware that call GetOASDefinition() in their request path.
-
Benchmark setup: Single request hitting an auth plugin that calls GetOASDefinition() once; measured with testing.B.ReportMetric() and runtime.ReadMemStats(). Results consistent across 10k iterations.
-
Naming justification: "Raw" is already used in this codebase for "unprocessed/underlying" accessors:
log.GetRaw() returns the underlying zap logger without wrapping
certificateManager.GetRaw() returns the underlying certificate without processing
ctx.GetOASDefinitionRaw() returns the underlying shared OAS definition without cloning
-
Hazard documentation: The function's doc comment will explicitly warn that the returned pointer is shared with other requests and must not be mutated. This is the same ownership model as reading any global immutable data (e.g., API definitions loaded at boot); the hazard is documented, not hidden.
-
Scope: This PR adds only the non-cloning accessor. The equivalent hazard on classic-def ctx.GetDefinition() (which also clones) is mentioned as a follow-up investigation in the additional context, but not implemented here, to keep the PR focused on the profiled hot path.
Is your feature request related to a problem? Please describe.
The function
ctx.GetOASDefinition()returns a deep clone of the OAS definition stored in the request context viareflect.Clone. This clone is performed on every request, even in hot paths where the clone is not mutated.In production at ~100 RPS on a single auth plugin that calls
GetOASDefinition()per request,reflect.Cloneaccounts for 80% of CPU time on the auth path, becoming a scaling bottleneck for gateway instances that route high volumes of requests.Benchmark (1 call, with setup):
GetOASDefinition(with clone): 294 microseconds, 857 allocations, 75.6 KBThe deep copy exists to provide a mutable, request-private copy so callers can safely modify the definition without racing other concurrent requests. This is crucial for safety in the general case, but hot-path code that only reads the definition should not pay the clone cost.
Describe the solution you'd like
Add a new function
ctx.GetOASDefinitionRaw(r *http.Request) *oas.OASthat returns the shared OAS definition pointer stored in context without cloning. This mirrors the existing "Raw" naming pattern in the codebase (log.GetRaw(),certificateManager.GetRaw()), both of which return the underlying unprocessed value.*oas.OASpointer (no clone). Caller must not mutate; suitable for read-only inspection and hot paths.The new function is additive and fully backward compatible. Existing code continues to use
GetOASDefinitionand gets the same safety guarantees.Describe alternatives you've considered
Replace GetOASDefinition's implementation with the raw pointer: Breaks existing code that mutates the result. Rejected.
Add a parameter to GetOASDefinition (e.g.
GetOASDefinition(r, clone=true)): Requires a flag/option at call sites. Rejected in favor of two distinct functions, which is clearer and mirrors the codebase's existing "Raw" precedent.Lazy-clone GetOASDefinition (clone only if the caller mutates): Complex to detect mutation; Go's
reflect.DeepEqualis itself expensive. The two-function approach is simpler and aligns with how other "raw" accessors work.Cache the clone: Per-request cloning is already idempotent; a cache would only save the clone on repeated calls within one request. Not worth the context map churn.
Additional context
Root cause: Every call to
reflect.Clonefor the OAS definition (200–300 KB object) does a full deep traversal and allocation, which becomes hot for plugins/middleware that callGetOASDefinition()in their request path.Benchmark setup: Single request hitting an auth plugin that calls
GetOASDefinition()once; measured withtesting.B.ReportMetric()andruntime.ReadMemStats(). Results consistent across 10k iterations.Naming justification: "Raw" is already used in this codebase for "unprocessed/underlying" accessors:
log.GetRaw()returns the underlying zap logger without wrappingcertificateManager.GetRaw()returns the underlying certificate without processingctx.GetOASDefinitionRaw()returns the underlying shared OAS definition without cloningHazard documentation: The function's doc comment will explicitly warn that the returned pointer is shared with other requests and must not be mutated. This is the same ownership model as reading any global immutable data (e.g., API definitions loaded at boot); the hazard is documented, not hidden.
Scope: This PR adds only the non-cloning accessor. The equivalent hazard on classic-def
ctx.GetDefinition()(which also clones) is mentioned as a follow-up investigation in the additional context, but not implemented here, to keep the PR focused on the profiled hot path.