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'
|
||||
|
||||
HIGRESS_LATEST_IMAGE_TAG ?= latest
|
||||
ENVOY_LATEST_IMAGE_TAG ?= fcc50202f47e27f6b8391a4bd9bbc0a9127d89d7
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 91244c578aef498af93cacb2cf353f3878b92fc4
|
||||
ISTIO_LATEST_IMAGE_TAG ?= de2c9628294f51b13c4a70b3a862b4372890797a
|
||||
|
||||
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