Skip to content

Add non-cloning ctx.GetOASDefinitionRaw accessor for read-only hot paths #8484

Description

@emmanuelchaplais2

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

  1. Replace GetOASDefinition's implementation with the raw pointer: Breaks existing code that mutates the result. Rejected.

  2. 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.

  3. 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.

  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions