Skip to content

perf(policy-engine): drop per-request deep copy of policy params#2617

Open
renuka-fernando wants to merge 1 commit into
wso2:mainfrom
renuka-fernando:perf
Open

perf(policy-engine): drop per-request deep copy of policy params#2617
renuka-fernando wants to merge 1 commit into
wso2:mainfrom
renuka-fernando:perf

Conversation

@renuka-fernando

Copy link
Copy Markdown
Contributor

Purpose

The policy chain executor deep-copied each policy's parameters on every request, at all six execution phases (request/response headers, request/response body, streaming request/response body). The copy was a full JSON marshal/unmarshal round-trip over spec.Parameters.Raw. Profiling attributed roughly 30% of the ext_proc CPU tree to this deep copy, along with the corresponding allocation churn.

Policy parameters are an immutable snapshot: they are published once at chain-build time (on the xDS push / config load) and shared read-only across concurrent request handlers. Config updates build entirely new chain objects and swap them in atomically, so a live chain's parameters are never mutated. The per-request copy was therefore pure overhead.

Goals

Eliminate the redundant per-request parameter copy and the allocations it produced, without changing observable behavior.

Approach

  • Pass spec.Parameters.Raw directly to each policy On* method instead of a per-request deep copy.
  • Remove the deepCopyParams helper and the now-unused encoding/json import.
  • Drop the three orphaned TestDeepCopyParams_* unit tests.
  • Add a comment at each call site documenting the invariant that parameters are an immutable, read-only shared snapshot and must not be mutated by policies (verified true for all current policies).

User stories

N/A

Documentation

N/A — internal performance optimization with no user-facing or API changes.

Automation tests

  • Unit tests

    Existing executor unit tests pass. Verified with go test -race ./internal/executor/... ./internal/kernel/... — the kernel package covers the production chain-build and atomic-publish path, confirming no data race from sharing the parameter map across concurrent requests.

  • Integration tests

    No new integration tests; behavior is unchanged. Existing suites remain applicable.

Security checks

Samples

N/A

Related PRs

N/A

Test environment

Go (policy-engine module); linux/amd64 and darwin/arm64.

Related Issues

Related #2615

Checklist

  • Tests added or updated (unit, integration, etc.)
  • Samples updated (if applicable)

Remarks

An A/B benchmark with a realistic nested parameter map (a combined JWT-validation + rate-limit config) through a body-buffering policy showed roughly 30-40x faster chain execution and ~14x fewer allocations per policy once the copy was removed. The repo's existing executor benchmarks use empty parameter maps, where the old code short-circuited before the JSON round-trip, so they did not surface this cost.

The safety of sharing the map now rests on the convention that policies treat parameters as read-only. A hard guardrail (e.g. a read-only accessor type in the SDK) would be a separate, breaking follow-up and is out of scope here.

deepCopyParams did a full JSON marshal/unmarshal round-trip on
spec.Parameters.Raw at all six execution phases on every request.
Params are an immutable snapshot published once at chain-build time
and shared read-only across concurrent requests, so the copy was
pure overhead — it accounted for ~30% of the ext_proc CPU tree.

- Pass spec.Parameters.Raw directly to each policy On* method
- Remove deepCopyParams and the now-unused encoding/json import
- Drop the three orphaned TestDeepCopyParams_* tests

Verified with go test -race on the executor and kernel packages
(kernel covers the production chain-build/publish path). An A/B
benchmark with a realistic nested param map showed ~30-40x faster
execution and ~14x fewer allocations per policy.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Policy execution no longer deep-copies parameters for request, response, or streaming callbacks. Each callback receives spec.Parameters.Raw directly, while the cloning helper, JSON import, and related tests are removed.

Changes

Policy parameter sharing

Layer / File(s) Summary
Direct parameter passing across policy phases
gateway/gateway-runtime/policy-engine/internal/executor/chain.go
Request, response, and streaming policy callbacks now receive spec.Parameters.Raw directly, with comments identifying the snapshot as non-mutable.
Deep-copy removal and test updates
gateway/gateway-runtime/policy-engine/internal/executor/chain.go, gateway/gateway-runtime/policy-engine/internal/executor/chain_test.go
The JSON-based deepCopyParams helper and import are removed, along with tests for mutation, empty-map, and slice isolation.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, specific, and matches the main change: removing per-request deep copies of policy params.
Description check ✅ Passed The description follows the template closely and covers purpose, goals, approach, tests, security, and environment.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain modules listed in go.work or their selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gateway/gateway-runtime/policy-engine/internal/executor/chain.go`:
- Around line 143-145: Update the policy dispatch around OnRequestHeaders so
policies cannot mutate the shared spec.Parameters.Raw map: replace the raw-map
argument with the established read-only accessor, or create a defensive
per-request clone before invoking the callback. Preserve the existing parameter
values while ensuring concurrent requests never share a mutable map instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1498b86e-8d1b-4be6-b24b-db619f6e8ef6

📥 Commits

Reviewing files that changed from the base of the PR and between 7a66828 and 578e49e.

📒 Files selected for processing (2)
  • gateway/gateway-runtime/policy-engine/internal/executor/chain.go
  • gateway/gateway-runtime/policy-engine/internal/executor/chain_test.go
💤 Files with no reviewable changes (1)
  • gateway/gateway-runtime/policy-engine/internal/executor/chain_test.go

Comment on lines +143 to +145
// spec.Parameters.Raw is an immutable snapshot published at chain-build time and
// shared read-only across concurrent requests; policies must not mutate it.
action := headerPol.OnRequestHeaders(ctx, reqCtx, spec.Parameters.Raw)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the concrete type of Parameters.Raw and look for any existing immutability guard
rg -nP -g '*.go' '\btype\s+Parameters\b' -A 10
rg -nP -g '*.go' 'Raw\s+map\[string\]interface\{\}'

# Search for any policy implementation (outside tests) that writes into a params-like map argument
rg -nP -g '*.go' -g '!*_test.go' '\b(params|parameters)\s*\[[^\]]+\]\s*='

Repository: wso2/api-platform

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files before reading them
git ls-files 'gateway/gateway-runtime/policy-engine/internal/executor/chain.go' \
             'gateway/gateway-runtime/policy-engine/internal/**' \
             | sed -n '1,200p'

# Inspect the chain implementation around the cited lines
wc -l gateway/gateway-runtime/policy-engine/internal/executor/chain.go
cat -n gateway/gateway-runtime/policy-engine/internal/executor/chain.go | sed -n '120,180p'

# Find the Parameters type and the Raw field definition
rg -n --hidden --glob '*.go' '\btype\s+Parameters\b|Raw\s+map\[string\]interface\{\}|type\s+.*Parameters' gateway/gateway-runtime/policy-engine/internal

# Find policy callback signatures to see whether the map is exposed directly everywhere
rg -n --hidden --glob '*.go' 'OnRequestHeaders|OnRequestBody|OnResponseHeaders|OnResponseBody|OnRequestBodyChunk|OnResponseBodyChunk' gateway/gateway-runtime/policy-engine/internal

# Look for any code in the policy engine that mutates the shared map or nested values
rg -n --hidden --glob '*.go' --glob '!**/*_test.go' 'spec\.Parameters\.Raw|Parameters\.Raw|\[[^]]+\]\s*=|append\(' gateway/gateway-runtime/policy-engine/internal

Repository: wso2/api-platform

Length of output: 11339


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the Parameters type and the Raw field
rg -n --hidden --glob '*.go' 'type\s+Parameters\b|Raw\s+map\[string\]interface\{\}' gateway/gateway-runtime/policy-engine/internal

# Read the file(s) that define Parameters in full enough to inspect the surrounding context
for f in $(rg -l --hidden --glob '*.go' 'type\s+Parameters\b|Raw\s+map\[string\]interface\{\}' gateway/gateway-runtime/policy-engine/internal); do
  echo "===== $f ====="
  wc -l "$f"
  cat -n "$f" | sed -n '1,260p'
done

# Inspect the policy callback interfaces and any wrappers around the params argument
rg -n --hidden --glob '*.go' 'type\s+.*interface\b|OnRequestHeaders|OnRequestBody|OnResponseHeaders|OnResponseBody|OnRequestBodyChunk|OnResponseBodyChunk' gateway/gateway-runtime/policy-engine/internal

Repository: wso2/api-platform

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where spec.Parameters is defined and how Raw is typed
rg -n --hidden --glob '*.go' 'spec\.Parameters|Parameters\s+struct|Raw\s+map\[string\]interface\{\}|map\[string\]any|map\[string\]interface\{\}' gateway/gateway-runtime/policy-engine

# Find policy callback interface definitions and implementations
rg -n --hidden --glob '*.go' 'OnRequestHeaders\(|OnRequestBody\(|OnResponseHeaders\(|OnResponseBody\(|OnRequestBodyChunk\(|OnResponseBodyChunk\(' gateway/gateway-runtime/policy-engine

# Inspect likely definition files if found
for f in \
  $(rg -l --hidden --glob '*.go' 'spec\.Parameters|Raw\s+map\[string\]interface\{\}|map\[string\]any' gateway/gateway-runtime/policy-engine | head -n 20); do
  echo "===== $f ====="
  wc -l "$f"
  cat -n "$f" | sed -n '1,220p'
done

Repository: wso2/api-platform

Length of output: 50374


Shared spec.Parameters.Raw is still a mutable map[string]interface{}
chain.go passes the same map instance into every policy callback, but the policy API exposes it as a raw map with no defensive copy or read-only wrapper. Any mutation can leak state across requests or trigger concurrent map writes in the gateway. Replace it with a read-only accessor or clone/freeze params before dispatch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-runtime/policy-engine/internal/executor/chain.go` around
lines 143 - 145, Update the policy dispatch around OnRequestHeaders so policies
cannot mutate the shared spec.Parameters.Raw map: replace the raw-map argument
with the established read-only accessor, or create a defensive per-request clone
before invoking the callback. Preserve the existing parameter values while
ensuring concurrent requests never share a mutable map instance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant