mirror of
https://github.com/alibaba/higress.git
synced 2026-07-24 05:10:34 +08:00
Signed-off-by: 澄潭 <zty98751@alibaba-inc.com>
This commit is contained in:
156
AGENTS.md
Normal file
156
AGENTS.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Guidance for AI agents working in this repository.
|
||||||
|
|
||||||
|
Higress is a cloud-native API gateway built on Istio and Envoy. The control
|
||||||
|
plane extends Istio/pilot (Go); the data plane is Envoy extended with WASM
|
||||||
|
plugins (Go/Rust/C++/AssemblyScript) and a Go-based `golang-filter`. It supports
|
||||||
|
Ingress/Gateway API and ships a rich plugin ecosystem (including AI gateway
|
||||||
|
plugins).
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
Top-level directories (all paths relative to repo root):
|
||||||
|
|
||||||
|
- `cmd/higress/` — main entrypoint (`main.go`) for the Higress controller binary.
|
||||||
|
- `pkg/` — core Go control-plane packages: `bootstrap/`, `cert/`, `cmd/`,
|
||||||
|
`common/`, `config/`, `ingress/` (Ingress/Gateway config translation),
|
||||||
|
`kube/`.
|
||||||
|
- `api/` — protobuf/CRD API definitions; Higress CRDs live in
|
||||||
|
`api/extensions/v1alpha1` (e.g. the `WasmPlugin` type). Generated with
|
||||||
|
`make gen-api` / `make gen-client` (see `api/gen.sh`, `buf.*`).
|
||||||
|
- `client/` — generated Go clientset for Higress CRDs.
|
||||||
|
- `istio/` — git submodules of higress-group forks of Istio (`api`, `istio`,
|
||||||
|
`client-go`, `pkg`, `proxy`); see `.gitmodules`. Pulled via `make submodule`
|
||||||
|
(part of `prebuild`).
|
||||||
|
- `envoy/` — Envoy + `go-control-plane` submodules (higress-group forks).
|
||||||
|
- `external/` — vendored/external mirror dirs used during build (istio, envoy,
|
||||||
|
proxy, etc.).
|
||||||
|
- `plugins/` — all data-plane plugins (see "Plugins" below).
|
||||||
|
- `registry/` — service-discovery registry integrations (nacos, consul, eureka,
|
||||||
|
zookeeper, direct, mcp, ...).
|
||||||
|
- `hgctl/` — the `hgctl` CLI (separate Go module) for managing Higress.
|
||||||
|
- `helm/` — Helm charts: `helm/core` (the dev/install chart) and `helm/higress`.
|
||||||
|
- `test/` — `test/e2e/` (conformance/e2e, see "Build & test") and
|
||||||
|
`test/gateway/`.
|
||||||
|
- `tools/` — build/CI scripting: `tools/hack/` (build scripts), `tools/bin/`,
|
||||||
|
`tools/linter/`, `*.mk`.
|
||||||
|
- `samples/` — example manifests (gateway-api, hello-world, wasmplugin, ...).
|
||||||
|
- `docker/`, `docs/`, `release-notes/` — packaging, docs, and release notes.
|
||||||
|
- `Makefile` — istio common-files wrapper (supports `BUILD_WITH_CONTAINER`);
|
||||||
|
real targets live in `Makefile.core.mk` (+ `Makefile.overrides.mk`).
|
||||||
|
|
||||||
|
## Plugins
|
||||||
|
|
||||||
|
All plugins live under `plugins/`. See `plugins/README.md` for the contributor
|
||||||
|
overview. Prebuilt plugin images are published to
|
||||||
|
`higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins`.
|
||||||
|
|
||||||
|
### plugins/wasm-go/ (primary WASM plugin framework, Go)
|
||||||
|
|
||||||
|
- `extensions/<name>/` — one directory per plugin (~59 plugins, many `ai-*`).
|
||||||
|
Each plugin is its own Go module: `main.go`, `go.mod`/`go.sum`, `VERSION`,
|
||||||
|
`README.md`(+`README_EN.md`), often `config/`, `util/`, `main_test.go`.
|
||||||
|
Optional `.buildrc` sets `EXTRA_TAGS`; optional `prepare.sh`/`prepare.sh`.
|
||||||
|
`plugin.wasm` is a build artifact and is **not** committed.
|
||||||
|
- Shared SDK: plugins depend on external modules
|
||||||
|
`github.com/higress-group/wasm-go` and
|
||||||
|
`github.com/higress-group/proxy-wasm-go-sdk` (NOT an in-repo SDK dir).
|
||||||
|
In-repo, `plugins/wasm-go/pkg/mcp/` provides MCP helpers and
|
||||||
|
`plugins/wasm-go/mcp-servers/` holds MCP server plugins.
|
||||||
|
- `examples/` — minimal reference plugins (custom-log, custom-span-attribute,
|
||||||
|
test-foreign-function).
|
||||||
|
- Build: `plugins/wasm-go/Makefile`. `PLUGIN_NAME=<name> make build` builds a
|
||||||
|
wasm file (output to `extensions/<name>/plugin.wasm`) + image via
|
||||||
|
`Dockerfile`/`DockerfileBuilder` (uses a `wasm-go-builder` image, Go 1.24,
|
||||||
|
TinyGo optional). `make build-push` pushes the image; `make local-build`
|
||||||
|
builds locally with `GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared`.
|
||||||
|
- Conventions: `VERSION` is the image tag; the CI/e2e batch builder
|
||||||
|
(`tools/hack/build-wasm-plugins.sh`) only compiles a wasm-go plugin whose
|
||||||
|
`VERSION` ends in `-alpha` (see the section at the bottom of this file).
|
||||||
|
|
||||||
|
### plugins/wasm-rust/ (Rust WASM plugins)
|
||||||
|
|
||||||
|
- Workspace-style: root `Cargo.toml`/`Cargo.lock`, shared `src/`,
|
||||||
|
`extensions/<name>/` per plugin (e.g. `ai-data-masking`, `ai-intent`,
|
||||||
|
`request-block`, `say-hello`, `demo-wasm`), `example/`.
|
||||||
|
- Build via `plugins/wasm-rust/Makefile` (`PLUGIN_NAME=<name> make build`, plus
|
||||||
|
`lint`/`test`); the batch builder runs it when `PLUGIN_TYPE=RUST`.
|
||||||
|
|
||||||
|
### plugins/wasm-cpp/ (C++ WASM plugins, Bazel)
|
||||||
|
|
||||||
|
- Bazel project: `WORKSPACE`, `BUILD`, `bazel/`, `common/`, `scripts/`,
|
||||||
|
`extensions/<name>/` (e.g. `basic_auth`, `jwt_auth`, `key_rate_limit`,
|
||||||
|
`model_router`, ...). Build via `plugins/wasm-cpp/Makefile`
|
||||||
|
(`PLUGIN_NAME=<name> make build`), invoked with `PLUGIN_TYPE=CPP`.
|
||||||
|
|
||||||
|
### plugins/wasm-assemblyscript/ (AssemblyScript WASM plugins)
|
||||||
|
|
||||||
|
- Node/AssemblyScript project: `asconfig.json`, `package.json`, `assembly/`,
|
||||||
|
`extensions/`.
|
||||||
|
|
||||||
|
### plugins/golang-filter/ (Envoy Go HTTP filter, NOT WASM)
|
||||||
|
|
||||||
|
- A native Envoy Golang HTTP filter (`main.go`, `mcp-server/`, `mcp-session/`);
|
||||||
|
compiled as a shared object (`.so`) independent of Envoy — no Envoy rebuild
|
||||||
|
needed. Requires Higress >= 2.1.0. Plugins register in `main.go`'s `init()`
|
||||||
|
via `RegisterHttpFilterFactoryAndConfigParser`. See
|
||||||
|
`plugins/golang-filter/README.md`.
|
||||||
|
- Build: `plugins/golang-filter/Makefile` (docker build, outputs
|
||||||
|
`golang-filter_<arch>.so`). Wired into the gateway image build via
|
||||||
|
`Makefile.core.mk` targets `build-golang-filter[-amd64|-arm64]`.
|
||||||
|
|
||||||
|
### How plugins are loaded
|
||||||
|
|
||||||
|
`WasmPlugin` CRDs (`extensions.higress.io/v1alpha1`) reference a plugin by
|
||||||
|
`url:` — either `oci://.../plugins/<name>:<version>` (image) or
|
||||||
|
`file:///opt/plugins/.../plugin.wasm` (local mount used in e2e). The dev install
|
||||||
|
`make install-dev-wasmplugin` sets Helm `global.volumeWasmPlugins=true` to mount
|
||||||
|
locally built wasm files into the gateway.
|
||||||
|
|
||||||
|
## Build & test
|
||||||
|
|
||||||
|
Run targets from the repo root; `Makefile` delegates to `Makefile.core.mk`.
|
||||||
|
Common ones:
|
||||||
|
|
||||||
|
- `make build` / `make build-linux` — build the Higress controller binary
|
||||||
|
(`prebuild` first fetches submodules).
|
||||||
|
- `make build-hgctl` — build the `hgctl` CLI.
|
||||||
|
- `make build-gateway` / `make build-istio` / `make build-envoy` — data-plane
|
||||||
|
and control-plane images (gateway pulls in the golang-filter).
|
||||||
|
- `make build-wasmplugins` — runs `tools/hack/build-wasm-plugins.sh` to batch
|
||||||
|
build WASM plugins (respects `PLUGIN_TYPE` / `PLUGIN_NAME`; Go plugins require
|
||||||
|
a `-alpha` VERSION).
|
||||||
|
- `make gen-api` / `make gen-client` — regenerate API/client code.
|
||||||
|
|
||||||
|
### Conformance / e2e tests (`test/e2e/`)
|
||||||
|
|
||||||
|
- Entrypoint `test/e2e/e2e_test.go`, run with build tag `conformance` and
|
||||||
|
`--test-area` / `--execute-tests` flags.
|
||||||
|
- Cases live in `test/e2e/conformance/tests/` as **paired `<name>.go` +
|
||||||
|
`<name>.yaml`** files (~68 cases; WASM cases are prefixed by language, e.g.
|
||||||
|
`go-wasm-*`, `cpp-wasm-*`). Support code: `conformance/base/`,
|
||||||
|
`conformance/utils/`, `conformance/embed.go`.
|
||||||
|
- Key Make targets (each spins up a kind cluster):
|
||||||
|
- `make higress-conformance-test` — Ingress/Gateway conformance.
|
||||||
|
- `make higress-wasmplugin-test` — WASM plugin e2e (uses
|
||||||
|
`install-dev-wasmplugin`, which builds plugins and mounts them).
|
||||||
|
- `*-prepare` / `*-skip-docker-build` / `*-clean` variants exist for
|
||||||
|
iterating; `run-higress-e2e-test[-wasmplugin]` runs `go test` against an
|
||||||
|
already-prepared cluster (filter with `TEST_SHORTNAME`).
|
||||||
|
- For the specifics of authoring a wasm-go e2e test, see the section below.
|
||||||
|
|
||||||
|
## Writing e2e conformance tests with wasm-go plugins
|
||||||
|
|
||||||
|
When adding an e2e conformance test that ships its own wasm-go plugin under
|
||||||
|
`plugins/wasm-go/extensions/<name>/`:
|
||||||
|
|
||||||
|
- The plugin's `VERSION` file **must end in `-alpha`** (e.g. `1.0.0-alpha`).
|
||||||
|
CI's `tools/hack/build-wasm-plugins.sh` only compiles a wasm-go plugin when
|
||||||
|
its version ends in `-alpha`; otherwise it silently skips it.
|
||||||
|
- `plugin.wasm` is a build artifact and is **not** committed. If the plugin
|
||||||
|
isn't built, the `file:///opt/plugins/.../plugin.wasm` URL in the test's
|
||||||
|
`WasmPlugin` manifest resolves to a missing file, envoy rejects the wasm
|
||||||
|
config and fails closed, and every request on that route returns HTTP 500.
|
||||||
|
Locally this can be masked because a previously built `plugin.wasm` still
|
||||||
|
exists on disk — so a test can pass locally yet 500 in CI.
|
||||||
@@ -205,7 +205,7 @@ install: pre-install
|
|||||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||||
|
|
||||||
HIGRESS_LATEST_IMAGE_TAG ?= latest
|
HIGRESS_LATEST_IMAGE_TAG ?= latest
|
||||||
ENVOY_LATEST_IMAGE_TAG ?= fcc50202f47e27f6b8391a4bd9bbc0a9127d89d7
|
ENVOY_LATEST_IMAGE_TAG ?= 91244c578aef498af93cacb2cf353f3878b92fc4
|
||||||
ISTIO_LATEST_IMAGE_TAG ?= de2c9628294f51b13c4a70b3a862b4372890797a
|
ISTIO_LATEST_IMAGE_TAG ?= de2c9628294f51b13c4a70b3a862b4372890797a
|
||||||
|
|
||||||
install-dev: pre-install
|
install-dev: pre-install
|
||||||
|
|||||||
Submodule envoy/envoy updated: f49d591e67...f468a1a3ac
@@ -0,0 +1 @@
|
|||||||
|
1.0.0-alpha
|
||||||
20
plugins/wasm-go/extensions/test-redis-inject-spin/go.mod
Normal file
20
plugins/wasm-go/extensions/test-redis-inject-spin/go.mod
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
module github.com/alibaba/higress/plugins/wasm-go/extensions/test-redis-inject-spin
|
||||||
|
|
||||||
|
go 1.24.1
|
||||||
|
|
||||||
|
toolchain go1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2
|
||||||
|
github.com/higress-group/wasm-go v1.0.10-0.20260120033417-1c84f010156d
|
||||||
|
github.com/tidwall/gjson v1.18.0
|
||||||
|
github.com/tidwall/resp v0.1.1
|
||||||
|
google.golang.org/protobuf v1.36.6
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
|
)
|
||||||
32
plugins/wasm-go/extensions/test-redis-inject-spin/go.sum
Normal file
32
plugins/wasm-go/extensions/test-redis-inject-spin/go.sum
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2 h1:NY33OrWCJJ+DFiLc+lsBY4Ywor2Ik61ssk6qkGF8Ypo=
|
||||||
|
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||||
|
github.com/higress-group/wasm-go v1.0.10-0.20260120033417-1c84f010156d h1:LgYbzEBtg0+LEqoebQeMVgAB6H5SgqG+KN+gBhNfKbM=
|
||||||
|
github.com/higress-group/wasm-go v1.0.10-0.20260120033417-1c84f010156d/go.mod h1:uKVYICbRaxTlKqdm8E0dpjbysxM8uCPb9LV26hF3Km8=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||||
|
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
119
plugins/wasm-go/extensions/test-redis-inject-spin/main.go
Normal file
119
plugins/wasm-go/extensions/test-redis-inject-spin/main.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// Repro plugin for higress issue #4034: worker CPU spin in
|
||||||
|
// WasmBase::doAfterVmCallActions when a deferred async callback synchronously
|
||||||
|
// calls injectEncodedDataToFilterChain (nesting SaveRestoreContext) so that the
|
||||||
|
// after-vm-call action re-queues itself forever.
|
||||||
|
//
|
||||||
|
// Repro shape (dead-Redis only, self-contained — no external HTTP dependency):
|
||||||
|
// 1. In the response-header phase, pause the response.
|
||||||
|
// 2. Fire N Redis commands against an UNREACHABLE redis cluster.
|
||||||
|
// 3. Each failure callback (onRedisCallFailure) synchronously calls the
|
||||||
|
// inject_encoded_data_to_filter_chain foreign function. Because sibling
|
||||||
|
// failure callbacks are still queued as after-vm-call actions, the nested
|
||||||
|
// SaveRestoreContext leaves current_context_ != nullptr and the deferred
|
||||||
|
// action re-queues itself -> CPU spin on the pre-fix host.
|
||||||
|
//
|
||||||
|
// With the Layer A drain-to-local fix in place, the drain terminates over a
|
||||||
|
// snapshot and worker CPU stays bounded.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||||
|
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||||
|
"github.com/higress-group/wasm-go/pkg/log"
|
||||||
|
pb "github.com/higress-group/wasm-go/pkg/protos"
|
||||||
|
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"github.com/tidwall/resp"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
redisClient *wrapper.RedisClusterClient[wrapper.FQDNCluster]
|
||||||
|
redisKey string
|
||||||
|
injectBody string
|
||||||
|
injectCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
wrapper.SetCtx(
|
||||||
|
"test-redis-inject-spin",
|
||||||
|
wrapper.ParseConfig(parseConfig),
|
||||||
|
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig(json gjson.Result, config *Config) error {
|
||||||
|
serviceName := json.Get("service_name").String()
|
||||||
|
if serviceName == "" {
|
||||||
|
serviceName = "dead-redis.dead-redis.svc.cluster.local"
|
||||||
|
}
|
||||||
|
servicePort := json.Get("service_port").Int()
|
||||||
|
if servicePort == 0 {
|
||||||
|
servicePort = 6379
|
||||||
|
}
|
||||||
|
config.redisKey = json.Get("redis_key").String()
|
||||||
|
if config.redisKey == "" {
|
||||||
|
config.redisKey = "higress-4034-key"
|
||||||
|
}
|
||||||
|
config.injectBody = json.Get("inject_body").String()
|
||||||
|
if config.injectBody == "" {
|
||||||
|
config.injectBody = "injected-by-4034-repro\n"
|
||||||
|
}
|
||||||
|
config.injectCount = int(json.Get("inject_count").Int())
|
||||||
|
if config.injectCount == 0 {
|
||||||
|
config.injectCount = 16
|
||||||
|
}
|
||||||
|
|
||||||
|
config.redisClient = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
|
||||||
|
FQDN: serviceName,
|
||||||
|
Port: servicePort,
|
||||||
|
})
|
||||||
|
// Init never returns error for an unreachable host; commands' callbacks will
|
||||||
|
// fire with an error value, which is exactly the #4034 trigger path.
|
||||||
|
return config.redisClient.Init("", "", 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
func onHttpResponseHeaders(ctx wrapper.HttpContext, config Config) types.Action {
|
||||||
|
proxywasm.RemoveHttpResponseHeader("content-length")
|
||||||
|
ctx.DontReadResponseBody()
|
||||||
|
|
||||||
|
inject := func() {
|
||||||
|
d := &pb.InjectEncodedDataToFilterChainArguments{
|
||||||
|
Body: config.injectBody,
|
||||||
|
Endstream: true,
|
||||||
|
}
|
||||||
|
s, err := proto.Marshal(d)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("marshal inject args failed: %+v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := proxywasm.CallForeignFunction("inject_encoded_data_to_filter_chain_on_header", s); err != nil {
|
||||||
|
log.Errorf("call inject_encoded_data_to_filter_chain_on_header failed: %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduled := 0
|
||||||
|
for i := 0; i < config.injectCount; i++ {
|
||||||
|
err := config.redisClient.Get(config.redisKey, func(response resp.Value) {
|
||||||
|
// Fires on redis-unreachable failure; synchronously inject to nest
|
||||||
|
// SaveRestoreContext while sibling callbacks are still queued.
|
||||||
|
if response.Error() != nil {
|
||||||
|
log.Debugf("redis get failed as expected: %v", response.Error())
|
||||||
|
}
|
||||||
|
inject()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("redis Get dispatch failed: %+v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
scheduled++
|
||||||
|
}
|
||||||
|
|
||||||
|
if scheduled == 0 {
|
||||||
|
// Nothing dispatched (e.g. cluster missing) — don't hang the response.
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
return types.ActionPause
|
||||||
|
}
|
||||||
75
test/e2e/conformance/tests/go-wasm-test-redis-inject-spin.go
Normal file
75
test/e2e/conformance/tests/go-wasm-test-redis-inject-spin.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) 2026 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alibaba/higress/v2/test/e2e/conformance/utils/http"
|
||||||
|
"github.com/alibaba/higress/v2/test/e2e/conformance/utils/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(WasmPluginsRedisInjectSpin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WasmPluginsRedisInjectSpin reproduces issue #4034: a deferred async Redis
|
||||||
|
// failure callback synchronously calls injectEncodedDataToFilterChain, which
|
||||||
|
// (pre-fix) makes WasmBase::doAfterVmCallActions re-queue an action forever and
|
||||||
|
// spins a worker at 100% CPU. Redis is pointed at an unroutable endpoint so the
|
||||||
|
// failure callbacks fire under concurrency.
|
||||||
|
//
|
||||||
|
// With the Layer A drain-to-local fix in place, the after-vm-call drain
|
||||||
|
// terminates over a snapshot, deferred callbacks complete, and the gateway keeps
|
||||||
|
// serving requests. The test asserts the plugin route stays responsive (200)
|
||||||
|
// rather than hanging — a spinning worker would fail this via timeout.
|
||||||
|
var WasmPluginsRedisInjectSpin = suite.ConformanceTest{
|
||||||
|
ShortName: "WasmPluginsRedisInjectSpin",
|
||||||
|
Description: "Reproduce #4034: redis-failure callback + inject must not spin the worker; gateway stays responsive.",
|
||||||
|
Manifests: []string{"tests/go-wasm-test-redis-inject-spin.yaml"},
|
||||||
|
Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
|
||||||
|
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
|
||||||
|
testcases := []http.Assertion{
|
||||||
|
{
|
||||||
|
Meta: http.AssertionMeta{
|
||||||
|
TargetBackend: "infra-backend-v1",
|
||||||
|
TargetNamespace: "higress-conformance-infra",
|
||||||
|
},
|
||||||
|
Request: http.AssertionRequest{
|
||||||
|
ActualRequest: http.Request{
|
||||||
|
Host: "redis-inject-spin.com",
|
||||||
|
Path: "/",
|
||||||
|
UnfollowRedirect: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Response: http.AssertionResponse{
|
||||||
|
ExpectedResponse: http.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
t.Run("WasmPlugins redis-inject-spin (#4034 CPU spin repro)", func(t *testing.T) {
|
||||||
|
// Drive repeated requests: on the pre-fix host the deferred callback
|
||||||
|
// re-queues forever and a worker spins, so the route stops
|
||||||
|
// responding; on the fixed host every request completes.
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Copyright (c) 2026 Alibaba Group Holding Ltd.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# A "dead" Redis: the Service (with manual Endpoints) makes Higress create the
|
||||||
|
# envoy cluster so RedisInit succeeds and commands dispatch, but the endpoint IP
|
||||||
|
# is unroutable (240.0.0.1, class-E) so every connection fails -> the plugin's
|
||||||
|
# redis-failure callbacks fire, which is the #4034 trigger.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: dead-redis
|
||||||
|
namespace: higress-conformance-infra
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: redis
|
||||||
|
port: 6379
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 6379
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Endpoints
|
||||||
|
metadata:
|
||||||
|
name: dead-redis
|
||||||
|
namespace: higress-conformance-infra
|
||||||
|
subsets:
|
||||||
|
- addresses:
|
||||||
|
- ip: 240.0.0.1
|
||||||
|
ports:
|
||||||
|
- name: redis
|
||||||
|
port: 6379
|
||||||
|
protocol: TCP
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: redis-inject-spin
|
||||||
|
namespace: higress-conformance-infra
|
||||||
|
spec:
|
||||||
|
ingressClassName: higress
|
||||||
|
rules:
|
||||||
|
- host: "redis-inject-spin.com"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
path: "/"
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: infra-backend-v1
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
---
|
||||||
|
apiVersion: extensions.higress.io/v1alpha1
|
||||||
|
kind: WasmPlugin
|
||||||
|
metadata:
|
||||||
|
name: test-redis-inject-spin
|
||||||
|
namespace: higress-system
|
||||||
|
spec:
|
||||||
|
defaultConfig:
|
||||||
|
service_name: "dead-redis.higress-conformance-infra.svc.cluster.local"
|
||||||
|
service_port: 6379
|
||||||
|
redis_key: "higress-4034-key"
|
||||||
|
inject_body: "injected-by-4034-repro\n"
|
||||||
|
inject_count: 16
|
||||||
|
url: file:///opt/plugins/wasm-go/extensions/test-redis-inject-spin/plugin.wasm
|
||||||
Reference in New Issue
Block a user