mirror of
https://github.com/alibaba/higress.git
synced 2026-02-06 23:21:08 +08:00
feat: add higress wasm go plugin development skill for Claude (#3402)
This commit is contained in:
251
.claude/skills/higress-wasm-go-plugin/SKILL.md
Normal file
251
.claude/skills/higress-wasm-go-plugin/SKILL.md
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
---
|
||||||
|
name: higress-wasm-go-plugin
|
||||||
|
description: Develop Higress WASM plugins using Go 1.24+. Use when creating, modifying, or debugging Higress gateway plugins for HTTP request/response processing, external service calls, Redis integration, or custom gateway logic.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Higress WASM Go Plugin Development
|
||||||
|
|
||||||
|
Develop Higress gateway WASM plugins using Go language with the `wasm-go` SDK.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Project Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create project directory
|
||||||
|
mkdir my-plugin && cd my-plugin
|
||||||
|
|
||||||
|
# Initialize Go module
|
||||||
|
go mod init my-plugin
|
||||||
|
|
||||||
|
# Set proxy (China)
|
||||||
|
go env -w GOPROXY=https://proxy.golang.com.cn,direct
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
go get github.com/higress-group/proxy-wasm-go-sdk@go-1.24
|
||||||
|
go get github.com/higress-group/wasm-go@main
|
||||||
|
go get github.com/tidwall/gjson
|
||||||
|
```
|
||||||
|
|
||||||
|
### Minimal Plugin Template
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||||
|
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||||
|
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
wrapper.SetCtx(
|
||||||
|
"my-plugin",
|
||||||
|
wrapper.ParseConfig(parseConfig),
|
||||||
|
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MyConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||||
|
config.Enabled = json.Get("enabled").Bool()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||||
|
if config.Enabled {
|
||||||
|
proxywasm.AddHttpRequestHeader("x-my-header", "hello")
|
||||||
|
}
|
||||||
|
return types.HeaderContinue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Plugin Lifecycle
|
||||||
|
|
||||||
|
1. **init()** - Register plugin with `wrapper.SetCtx()`
|
||||||
|
2. **parseConfig** - Parse YAML config (auto-converted to JSON)
|
||||||
|
3. **HTTP processing phases** - Handle requests/responses
|
||||||
|
|
||||||
|
### HTTP Processing Phases
|
||||||
|
|
||||||
|
| Phase | Trigger | Handler |
|
||||||
|
|-------|---------|---------|
|
||||||
|
| Request Headers | Gateway receives client request headers | `ProcessRequestHeaders` |
|
||||||
|
| Request Body | Gateway receives client request body | `ProcessRequestBody` |
|
||||||
|
| Response Headers | Gateway receives backend response headers | `ProcessResponseHeaders` |
|
||||||
|
| Response Body | Gateway receives backend response body | `ProcessResponseBody` |
|
||||||
|
| Stream Done | HTTP stream completes | `ProcessStreamDone` |
|
||||||
|
|
||||||
|
### Action Return Values
|
||||||
|
|
||||||
|
| Action | Behavior |
|
||||||
|
|--------|----------|
|
||||||
|
| `types.HeaderContinue` | Continue to next filter |
|
||||||
|
| `types.HeaderStopIteration` | Stop header processing, wait for body |
|
||||||
|
| `types.HeaderStopAllIterationAndWatermark` | Stop all processing, buffer data, call `proxywasm.ResumeHttpRequest/Response()` to resume |
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### HttpContext Methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Request info (cached, safe to call in any phase)
|
||||||
|
ctx.Scheme() // :scheme
|
||||||
|
ctx.Host() // :authority
|
||||||
|
ctx.Path() // :path
|
||||||
|
ctx.Method() // :method
|
||||||
|
|
||||||
|
// Body handling
|
||||||
|
ctx.HasRequestBody() // Check if request has body
|
||||||
|
ctx.HasResponseBody() // Check if response has body
|
||||||
|
ctx.DontReadRequestBody() // Skip reading request body
|
||||||
|
ctx.DontReadResponseBody() // Skip reading response body
|
||||||
|
ctx.BufferRequestBody() // Buffer instead of stream
|
||||||
|
ctx.BufferResponseBody() // Buffer instead of stream
|
||||||
|
|
||||||
|
// Content detection
|
||||||
|
ctx.IsWebsocket() // Check WebSocket upgrade
|
||||||
|
ctx.IsBinaryRequestBody() // Check binary content
|
||||||
|
ctx.IsBinaryResponseBody() // Check binary content
|
||||||
|
|
||||||
|
// Context storage
|
||||||
|
ctx.SetContext(key, value)
|
||||||
|
ctx.GetContext(key)
|
||||||
|
ctx.GetStringContext(key, defaultValue)
|
||||||
|
ctx.GetBoolContext(key, defaultValue)
|
||||||
|
|
||||||
|
// Custom logging
|
||||||
|
ctx.SetUserAttribute(key, value)
|
||||||
|
ctx.WriteUserAttributeToLog()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Header/Body Operations (proxywasm)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Request headers
|
||||||
|
proxywasm.GetHttpRequestHeader(name)
|
||||||
|
proxywasm.AddHttpRequestHeader(name, value)
|
||||||
|
proxywasm.ReplaceHttpRequestHeader(name, value)
|
||||||
|
proxywasm.RemoveHttpRequestHeader(name)
|
||||||
|
proxywasm.GetHttpRequestHeaders()
|
||||||
|
proxywasm.ReplaceHttpRequestHeaders(headers)
|
||||||
|
|
||||||
|
// Response headers
|
||||||
|
proxywasm.GetHttpResponseHeader(name)
|
||||||
|
proxywasm.AddHttpResponseHeader(name, value)
|
||||||
|
proxywasm.ReplaceHttpResponseHeader(name, value)
|
||||||
|
proxywasm.RemoveHttpResponseHeader(name)
|
||||||
|
proxywasm.GetHttpResponseHeaders()
|
||||||
|
proxywasm.ReplaceHttpResponseHeaders(headers)
|
||||||
|
|
||||||
|
// Request body (only in body phase)
|
||||||
|
proxywasm.GetHttpRequestBody(start, size)
|
||||||
|
proxywasm.ReplaceHttpRequestBody(body)
|
||||||
|
proxywasm.AppendHttpRequestBody(data)
|
||||||
|
proxywasm.PrependHttpRequestBody(data)
|
||||||
|
|
||||||
|
// Response body (only in body phase)
|
||||||
|
proxywasm.GetHttpResponseBody(start, size)
|
||||||
|
proxywasm.ReplaceHttpResponseBody(body)
|
||||||
|
proxywasm.AppendHttpResponseBody(data)
|
||||||
|
proxywasm.PrependHttpResponseBody(data)
|
||||||
|
|
||||||
|
// Direct response
|
||||||
|
proxywasm.SendHttpResponse(statusCode, headers, body, grpcStatus)
|
||||||
|
|
||||||
|
// Flow control
|
||||||
|
proxywasm.ResumeHttpRequest() // Resume paused request
|
||||||
|
proxywasm.ResumeHttpResponse() // Resume paused response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### External HTTP Call
|
||||||
|
|
||||||
|
See [references/http-client.md](references/http-client.md) for complete HTTP client patterns.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||||
|
serviceName := json.Get("serviceName").String()
|
||||||
|
servicePort := json.Get("servicePort").Int()
|
||||||
|
config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||||
|
FQDN: serviceName,
|
||||||
|
Port: servicePort,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||||
|
err := config.client.Get("/api/check", nil, func(statusCode int, headers http.Header, body []byte) {
|
||||||
|
if statusCode != 200 {
|
||||||
|
proxywasm.SendHttpResponse(403, nil, []byte("Forbidden"), -1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proxywasm.ResumeHttpRequest()
|
||||||
|
}, 3000) // timeout ms
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return types.HeaderContinue // fallback on error
|
||||||
|
}
|
||||||
|
return types.HeaderStopAllIterationAndWatermark
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis Integration
|
||||||
|
|
||||||
|
See [references/redis-client.md](references/redis-client.md) for complete Redis patterns.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||||
|
config.redis = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
|
||||||
|
FQDN: json.Get("redisService").String(),
|
||||||
|
Port: json.Get("redisPort").Int(),
|
||||||
|
})
|
||||||
|
return config.redis.Init(
|
||||||
|
json.Get("username").String(),
|
||||||
|
json.Get("password").String(),
|
||||||
|
json.Get("timeout").Int(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-level Config
|
||||||
|
|
||||||
|
插件配置支持在控制台不同级别设置:全局、域名级、路由级。控制面会自动处理配置的优先级和匹配逻辑,插件代码中通过 `parseConfig` 解析到的就是当前请求匹配到的配置。
|
||||||
|
|
||||||
|
## Local Testing
|
||||||
|
|
||||||
|
See [references/local-testing.md](references/local-testing.md) for Docker Compose setup.
|
||||||
|
|
||||||
|
## Advanced Topics
|
||||||
|
|
||||||
|
See [references/advanced-patterns.md](references/advanced-patterns.md) for:
|
||||||
|
- Streaming body processing
|
||||||
|
- Route call pattern
|
||||||
|
- Tick functions (periodic tasks)
|
||||||
|
- Leader election
|
||||||
|
- Memory management
|
||||||
|
- Custom logging
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Never call Resume after SendHttpResponse** - Response auto-resumes
|
||||||
|
2. **Check HasRequestBody() before returning HeaderStopIteration** - Avoids blocking
|
||||||
|
3. **Use cached ctx methods** - `ctx.Path()` works in any phase, `GetHttpRequestHeader(":path")` only in header phase
|
||||||
|
4. **Handle external call failures gracefully** - Return `HeaderContinue` on error to avoid blocking
|
||||||
|
5. **Set appropriate timeouts** - Default HTTP call timeout is 500ms
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
# Advanced Patterns
|
||||||
|
|
||||||
|
## Streaming Body Processing
|
||||||
|
|
||||||
|
Process body chunks as they arrive without buffering:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
wrapper.SetCtx(
|
||||||
|
"streaming-plugin",
|
||||||
|
wrapper.ParseConfig(parseConfig),
|
||||||
|
wrapper.ProcessStreamingRequestBody(onStreamingRequestBody),
|
||||||
|
wrapper.ProcessStreamingResponseBody(onStreamingResponseBody),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func onStreamingRequestBody(ctx wrapper.HttpContext, config MyConfig, chunk []byte, isLastChunk bool) []byte {
|
||||||
|
// Modify chunk and return
|
||||||
|
modified := bytes.ReplaceAll(chunk, []byte("old"), []byte("new"))
|
||||||
|
return modified
|
||||||
|
}
|
||||||
|
|
||||||
|
func onStreamingResponseBody(ctx wrapper.HttpContext, config MyConfig, chunk []byte, isLastChunk bool) []byte {
|
||||||
|
// Can call external services with NeedPauseStreamingResponse()
|
||||||
|
return chunk
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Buffered Body Processing
|
||||||
|
|
||||||
|
Buffer entire body before processing:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
wrapper.SetCtx(
|
||||||
|
"buffered-plugin",
|
||||||
|
wrapper.ParseConfig(parseConfig),
|
||||||
|
wrapper.ProcessRequestBody(onRequestBody),
|
||||||
|
wrapper.ProcessResponseBody(onResponseBody),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func onRequestBody(ctx wrapper.HttpContext, config MyConfig, body []byte) types.Action {
|
||||||
|
// Full request body available
|
||||||
|
var data map[string]interface{}
|
||||||
|
json.Unmarshal(body, &data)
|
||||||
|
|
||||||
|
// Modify and replace
|
||||||
|
data["injected"] = "value"
|
||||||
|
newBody, _ := json.Marshal(data)
|
||||||
|
proxywasm.ReplaceHttpRequestBody(newBody)
|
||||||
|
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route Call Pattern
|
||||||
|
|
||||||
|
Call the current route's upstream with modified request:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func onRequestBody(ctx wrapper.HttpContext, config MyConfig, body []byte) types.Action {
|
||||||
|
err := ctx.RouteCall("POST", "/modified-path", [][2]string{
|
||||||
|
{"Content-Type", "application/json"},
|
||||||
|
{"X-Custom", "header"},
|
||||||
|
}, body, func(statusCode int, headers [][2]string, body []byte) {
|
||||||
|
// Handle response from upstream
|
||||||
|
proxywasm.SendHttpResponse(statusCode, headers, body, -1)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
proxywasm.SendHttpResponse(500, nil, []byte("Route call failed"), -1)
|
||||||
|
}
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tick Functions (Periodic Tasks)
|
||||||
|
|
||||||
|
Register periodic background tasks:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||||
|
// Register tick functions during config parsing
|
||||||
|
wrapper.RegisterTickFunc(1000, func() {
|
||||||
|
// Executes every 1 second
|
||||||
|
log.Info("1s tick")
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.RegisterTickFunc(5000, func() {
|
||||||
|
// Executes every 5 seconds
|
||||||
|
log.Info("5s tick")
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Leader Election
|
||||||
|
|
||||||
|
For tasks that should run on only one VM instance:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
wrapper.SetCtx(
|
||||||
|
"leader-plugin",
|
||||||
|
wrapper.PrePluginStartOrReload(onPluginStart),
|
||||||
|
wrapper.ParseConfig(parseConfig),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func onPluginStart(ctx wrapper.PluginContext) error {
|
||||||
|
ctx.DoLeaderElection()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||||
|
wrapper.RegisterTickFunc(10000, func() {
|
||||||
|
if ctx.IsLeader() {
|
||||||
|
// Only leader executes this
|
||||||
|
log.Info("Leader task")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin Context Storage
|
||||||
|
|
||||||
|
Store data across requests at plugin level:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type MyConfig struct {
|
||||||
|
// Config fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
wrapper.SetCtx(
|
||||||
|
"context-plugin",
|
||||||
|
wrapper.ParseConfigWithContext(parseConfigWithContext),
|
||||||
|
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfigWithContext(ctx wrapper.PluginContext, json gjson.Result, config *MyConfig) error {
|
||||||
|
// Store in plugin context (survives across requests)
|
||||||
|
ctx.SetContext("initTime", time.Now().Unix())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rule-Level Config Isolation
|
||||||
|
|
||||||
|
Enable graceful degradation when rule config parsing fails:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
wrapper.SetCtx(
|
||||||
|
"isolated-plugin",
|
||||||
|
wrapper.PrePluginStartOrReload(func(ctx wrapper.PluginContext) error {
|
||||||
|
ctx.EnableRuleLevelConfigIsolation()
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
wrapper.ParseOverrideConfig(parseGlobal, parseRule),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGlobal(json gjson.Result, config *MyConfig) error {
|
||||||
|
// Parse global config
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRule(json gjson.Result, global MyConfig, config *MyConfig) error {
|
||||||
|
// Parse per-rule config, inheriting from global
|
||||||
|
*config = global // Copy global defaults
|
||||||
|
// Override with rule-specific values
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Memory Management
|
||||||
|
|
||||||
|
Configure automatic VM rebuild to prevent memory leaks:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
wrapper.SetCtxWithOptions(
|
||||||
|
"memory-managed-plugin",
|
||||||
|
wrapper.ParseConfig(parseConfig),
|
||||||
|
wrapper.WithRebuildAfterRequests(10000), // Rebuild after 10k requests
|
||||||
|
wrapper.WithRebuildMaxMemBytes(100*1024*1024), // Rebuild at 100MB
|
||||||
|
wrapper.WithMaxRequestsPerIoCycle(20), // Limit concurrent requests
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Logging
|
||||||
|
|
||||||
|
Add structured fields to access logs:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||||
|
// Set custom attributes
|
||||||
|
ctx.SetUserAttribute("user_id", "12345")
|
||||||
|
ctx.SetUserAttribute("request_type", "api")
|
||||||
|
|
||||||
|
return types.HeaderContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||||
|
// Write to access log
|
||||||
|
ctx.WriteUserAttributeToLog()
|
||||||
|
|
||||||
|
// Or write to trace spans
|
||||||
|
ctx.WriteUserAttributeToTrace()
|
||||||
|
|
||||||
|
return types.HeaderContinue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Disable Re-routing
|
||||||
|
|
||||||
|
Prevent Envoy from recalculating routes after header modification:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||||
|
// Call BEFORE modifying headers
|
||||||
|
ctx.DisableReroute()
|
||||||
|
|
||||||
|
// Now safe to modify headers without triggering re-route
|
||||||
|
proxywasm.ReplaceHttpRequestHeader(":path", "/new-path")
|
||||||
|
|
||||||
|
return types.HeaderContinue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Buffer Limits
|
||||||
|
|
||||||
|
Set per-request buffer limits to control memory usage:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||||
|
// Allow larger request bodies for this request
|
||||||
|
ctx.SetRequestBodyBufferLimit(10 * 1024 * 1024) // 10MB
|
||||||
|
return types.HeaderContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||||
|
// Allow larger response bodies
|
||||||
|
ctx.SetResponseBodyBufferLimit(50 * 1024 * 1024) // 50MB
|
||||||
|
return types.HeaderContinue
|
||||||
|
}
|
||||||
|
```
|
||||||
179
.claude/skills/higress-wasm-go-plugin/references/http-client.md
Normal file
179
.claude/skills/higress-wasm-go-plugin/references/http-client.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# HTTP Client Reference
|
||||||
|
|
||||||
|
## Cluster Types
|
||||||
|
|
||||||
|
### FQDNCluster (Most Common)
|
||||||
|
|
||||||
|
For services registered in Higress with FQDN:
|
||||||
|
|
||||||
|
```go
|
||||||
|
wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||||
|
FQDN: "my-service.dns", // Service FQDN with suffix
|
||||||
|
Port: 8080,
|
||||||
|
Host: "optional-host-header", // Optional
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Common FQDN suffixes:
|
||||||
|
- `.dns` - DNS service
|
||||||
|
- `.static` - Static IP service (port defaults to 80)
|
||||||
|
- `.nacos` - Nacos service
|
||||||
|
|
||||||
|
### K8sCluster
|
||||||
|
|
||||||
|
For Kubernetes services:
|
||||||
|
|
||||||
|
```go
|
||||||
|
wrapper.NewClusterClient(wrapper.K8sCluster{
|
||||||
|
ServiceName: "my-service",
|
||||||
|
Namespace: "default",
|
||||||
|
Port: 8080,
|
||||||
|
Version: "", // Optional subset version
|
||||||
|
})
|
||||||
|
// Generates: outbound|8080||my-service.default.svc.cluster.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### NacosCluster
|
||||||
|
|
||||||
|
For Nacos registry services:
|
||||||
|
|
||||||
|
```go
|
||||||
|
wrapper.NewClusterClient(wrapper.NacosCluster{
|
||||||
|
ServiceName: "my-service",
|
||||||
|
Group: "DEFAULT-GROUP",
|
||||||
|
NamespaceID: "public",
|
||||||
|
Port: 8080,
|
||||||
|
IsExtRegistry: false, // true for EDAS/SAE
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### StaticIpCluster
|
||||||
|
|
||||||
|
For static IP services:
|
||||||
|
|
||||||
|
```go
|
||||||
|
wrapper.NewClusterClient(wrapper.StaticIpCluster{
|
||||||
|
ServiceName: "my-service",
|
||||||
|
Port: 8080,
|
||||||
|
})
|
||||||
|
// Generates: outbound|8080||my-service.static
|
||||||
|
```
|
||||||
|
|
||||||
|
### DnsCluster
|
||||||
|
|
||||||
|
For DNS-resolved services:
|
||||||
|
|
||||||
|
```go
|
||||||
|
wrapper.NewClusterClient(wrapper.DnsCluster{
|
||||||
|
ServiceName: "my-service",
|
||||||
|
Domain: "api.example.com",
|
||||||
|
Port: 443,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### RouteCluster
|
||||||
|
|
||||||
|
Use current route's upstream:
|
||||||
|
|
||||||
|
```go
|
||||||
|
wrapper.NewClusterClient(wrapper.RouteCluster{
|
||||||
|
Host: "optional-host-override",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### TargetCluster
|
||||||
|
|
||||||
|
Direct cluster name specification:
|
||||||
|
|
||||||
|
```go
|
||||||
|
wrapper.NewClusterClient(wrapper.TargetCluster{
|
||||||
|
Cluster: "outbound|8080||my-service.dns",
|
||||||
|
Host: "api.example.com",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP Methods
|
||||||
|
|
||||||
|
```go
|
||||||
|
client.Get(path, headers, callback, timeout...)
|
||||||
|
client.Post(path, headers, body, callback, timeout...)
|
||||||
|
client.Put(path, headers, body, callback, timeout...)
|
||||||
|
client.Patch(path, headers, body, callback, timeout...)
|
||||||
|
client.Delete(path, headers, body, callback, timeout...)
|
||||||
|
client.Head(path, headers, callback, timeout...)
|
||||||
|
client.Options(path, headers, callback, timeout...)
|
||||||
|
client.Call(method, path, headers, body, callback, timeout...)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Callback Signature
|
||||||
|
|
||||||
|
```go
|
||||||
|
func(statusCode int, responseHeaders http.Header, responseBody []byte)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
type MyConfig struct {
|
||||||
|
client wrapper.HttpClient
|
||||||
|
requestPath string
|
||||||
|
tokenHeader string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||||
|
config.tokenHeader = json.Get("tokenHeader").String()
|
||||||
|
if config.tokenHeader == "" {
|
||||||
|
return errors.New("missing tokenHeader")
|
||||||
|
}
|
||||||
|
|
||||||
|
config.requestPath = json.Get("requestPath").String()
|
||||||
|
if config.requestPath == "" {
|
||||||
|
return errors.New("missing requestPath")
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName := json.Get("serviceName").String()
|
||||||
|
servicePort := json.Get("servicePort").Int()
|
||||||
|
if servicePort == 0 {
|
||||||
|
if strings.HasSuffix(serviceName, ".static") {
|
||||||
|
servicePort = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||||
|
FQDN: serviceName,
|
||||||
|
Port: servicePort,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||||
|
err := config.client.Get(config.requestPath, nil,
|
||||||
|
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||||
|
if statusCode != http.StatusOK {
|
||||||
|
log.Errorf("http call failed, status: %d", statusCode)
|
||||||
|
proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
|
||||||
|
[]byte("http call failed"), -1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := responseHeaders.Get(config.tokenHeader)
|
||||||
|
if token != "" {
|
||||||
|
proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
|
||||||
|
}
|
||||||
|
proxywasm.ResumeHttpRequest()
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("http call dispatch failed: %v", err)
|
||||||
|
return types.HeaderContinue
|
||||||
|
}
|
||||||
|
return types.HeaderStopAllIterationAndWatermark
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
1. **Cannot use net/http** - Must use wrapper's HTTP client
|
||||||
|
2. **Default timeout is 500ms** - Pass explicit timeout for longer calls
|
||||||
|
3. **Callback is async** - Must return `HeaderStopAllIterationAndWatermark` and call `ResumeHttpRequest()` in callback
|
||||||
|
4. **Error handling** - If dispatch fails, return `HeaderContinue` to avoid blocking
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
# Local Testing with Docker Compose
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker installed
|
||||||
|
- Compiled `main.wasm` file
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Create these files in your plugin directory:
|
||||||
|
|
||||||
|
### docker-compose.yaml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.7'
|
||||||
|
services:
|
||||||
|
envoy:
|
||||||
|
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.1.5
|
||||||
|
entrypoint: /usr/local/bin/envoy
|
||||||
|
command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
|
||||||
|
depends_on:
|
||||||
|
- httpbin
|
||||||
|
networks:
|
||||||
|
- wasmtest
|
||||||
|
ports:
|
||||||
|
- "10000:10000"
|
||||||
|
volumes:
|
||||||
|
- ./envoy.yaml:/etc/envoy/envoy.yaml
|
||||||
|
- ./main.wasm:/etc/envoy/main.wasm
|
||||||
|
|
||||||
|
httpbin:
|
||||||
|
image: kennethreitz/httpbin:latest
|
||||||
|
networks:
|
||||||
|
- wasmtest
|
||||||
|
ports:
|
||||||
|
- "12345:80"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
wasmtest: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### envoy.yaml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
admin:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
protocol: TCP
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_value: 9901
|
||||||
|
|
||||||
|
static_resources:
|
||||||
|
listeners:
|
||||||
|
- name: listener_0
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
protocol: TCP
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_value: 10000
|
||||||
|
filter_chains:
|
||||||
|
- filters:
|
||||||
|
- name: envoy.filters.network.http_connection_manager
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||||
|
scheme_header_transformation:
|
||||||
|
scheme_to_overwrite: https
|
||||||
|
stat_prefix: ingress_http
|
||||||
|
route_config:
|
||||||
|
name: local_route
|
||||||
|
virtual_hosts:
|
||||||
|
- name: local_service
|
||||||
|
domains: ["*"]
|
||||||
|
routes:
|
||||||
|
- match:
|
||||||
|
prefix: "/"
|
||||||
|
route:
|
||||||
|
cluster: httpbin
|
||||||
|
http_filters:
|
||||||
|
- name: wasmdemo
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
|
||||||
|
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
|
||||||
|
value:
|
||||||
|
config:
|
||||||
|
name: wasmdemo
|
||||||
|
vm_config:
|
||||||
|
runtime: envoy.wasm.runtime.v8
|
||||||
|
code:
|
||||||
|
local:
|
||||||
|
filename: /etc/envoy/main.wasm
|
||||||
|
configuration:
|
||||||
|
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||||
|
value: |
|
||||||
|
{
|
||||||
|
"mockEnable": false
|
||||||
|
}
|
||||||
|
- name: envoy.filters.http.router
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||||
|
|
||||||
|
clusters:
|
||||||
|
- name: httpbin
|
||||||
|
connect_timeout: 30s
|
||||||
|
type: LOGICAL_DNS
|
||||||
|
dns_lookup_family: V4_ONLY
|
||||||
|
lb_policy: ROUND_ROBIN
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: httpbin
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: httpbin
|
||||||
|
port_value: 80
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start
|
||||||
|
docker compose up
|
||||||
|
|
||||||
|
# Test without gateway (baseline)
|
||||||
|
curl http://127.0.0.1:12345/get
|
||||||
|
|
||||||
|
# Test with gateway (plugin applied)
|
||||||
|
curl http://127.0.0.1:10000/get
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modifying Plugin Config
|
||||||
|
|
||||||
|
1. Edit the `configuration.value` section in `envoy.yaml`
|
||||||
|
2. Restart: `docker compose restart envoy`
|
||||||
|
|
||||||
|
## Viewing Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Follow Envoy logs
|
||||||
|
docker compose logs -f envoy
|
||||||
|
|
||||||
|
# WASM debug logs (enabled by --component-log-level wasm:debug)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding External Services
|
||||||
|
|
||||||
|
To test external HTTP/Redis calls, add services to docker-compose.yaml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
# ... existing services ...
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
networks:
|
||||||
|
- wasmtest
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
auth-service:
|
||||||
|
image: your-auth-service:latest
|
||||||
|
networks:
|
||||||
|
- wasmtest
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add clusters to envoy.yaml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
clusters:
|
||||||
|
# ... existing clusters ...
|
||||||
|
|
||||||
|
- name: outbound|6379||redis.static
|
||||||
|
connect_timeout: 5s
|
||||||
|
type: LOGICAL_DNS
|
||||||
|
dns_lookup_family: V4_ONLY
|
||||||
|
lb_policy: ROUND_ROBIN
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: redis
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: redis
|
||||||
|
port_value: 6379
|
||||||
|
```
|
||||||
215
.claude/skills/higress-wasm-go-plugin/references/redis-client.md
Normal file
215
.claude/skills/higress-wasm-go-plugin/references/redis-client.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# Redis Client Reference
|
||||||
|
|
||||||
|
## Initialization
|
||||||
|
|
||||||
|
```go
|
||||||
|
type MyConfig struct {
|
||||||
|
redis wrapper.RedisClient
|
||||||
|
qpm int
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||||
|
serviceName := json.Get("serviceName").String()
|
||||||
|
servicePort := json.Get("servicePort").Int()
|
||||||
|
if servicePort == 0 {
|
||||||
|
servicePort = 6379
|
||||||
|
}
|
||||||
|
|
||||||
|
config.redis = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
|
||||||
|
FQDN: serviceName,
|
||||||
|
Port: servicePort,
|
||||||
|
})
|
||||||
|
|
||||||
|
return config.redis.Init(
|
||||||
|
json.Get("username").String(),
|
||||||
|
json.Get("password").String(),
|
||||||
|
json.Get("timeout").Int(), // milliseconds
|
||||||
|
// Optional settings:
|
||||||
|
// wrapper.WithDataBase(1),
|
||||||
|
// wrapper.WithBufferFlushTimeout(3*time.Millisecond),
|
||||||
|
// wrapper.WithMaxBufferSizeBeforeFlush(1024),
|
||||||
|
// wrapper.WithDisableBuffer(), // For latency-sensitive scenarios
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Callback Signature
|
||||||
|
|
||||||
|
```go
|
||||||
|
func(response resp.Value)
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
if response.Error() != nil {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get values
|
||||||
|
response.Integer() // int
|
||||||
|
response.String() // string
|
||||||
|
response.Bool() // bool
|
||||||
|
response.Array() // []resp.Value
|
||||||
|
response.Bytes() // []byte
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
### Key Operations
|
||||||
|
|
||||||
|
```go
|
||||||
|
redis.Del(key, callback)
|
||||||
|
redis.Exists(key, callback)
|
||||||
|
redis.Expire(key, ttlSeconds, callback)
|
||||||
|
redis.Persist(key, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### String Operations
|
||||||
|
|
||||||
|
```go
|
||||||
|
redis.Get(key, callback)
|
||||||
|
redis.Set(key, value, callback)
|
||||||
|
redis.SetEx(key, value, ttlSeconds, callback)
|
||||||
|
redis.SetNX(key, value, ttlSeconds, callback) // ttl=0 means no expiry
|
||||||
|
redis.MGet(keys, callback)
|
||||||
|
redis.MSet(kvMap, callback)
|
||||||
|
redis.Incr(key, callback)
|
||||||
|
redis.Decr(key, callback)
|
||||||
|
redis.IncrBy(key, delta, callback)
|
||||||
|
redis.DecrBy(key, delta, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Operations
|
||||||
|
|
||||||
|
```go
|
||||||
|
redis.LLen(key, callback)
|
||||||
|
redis.RPush(key, values, callback)
|
||||||
|
redis.RPop(key, callback)
|
||||||
|
redis.LPush(key, values, callback)
|
||||||
|
redis.LPop(key, callback)
|
||||||
|
redis.LIndex(key, index, callback)
|
||||||
|
redis.LRange(key, start, stop, callback)
|
||||||
|
redis.LRem(key, count, value, callback)
|
||||||
|
redis.LInsertBefore(key, pivot, value, callback)
|
||||||
|
redis.LInsertAfter(key, pivot, value, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hash Operations
|
||||||
|
|
||||||
|
```go
|
||||||
|
redis.HExists(key, field, callback)
|
||||||
|
redis.HDel(key, fields, callback)
|
||||||
|
redis.HLen(key, callback)
|
||||||
|
redis.HGet(key, field, callback)
|
||||||
|
redis.HSet(key, field, value, callback)
|
||||||
|
redis.HMGet(key, fields, callback)
|
||||||
|
redis.HMSet(key, kvMap, callback)
|
||||||
|
redis.HKeys(key, callback)
|
||||||
|
redis.HVals(key, callback)
|
||||||
|
redis.HGetAll(key, callback)
|
||||||
|
redis.HIncrBy(key, field, delta, callback)
|
||||||
|
redis.HIncrByFloat(key, field, delta, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set Operations
|
||||||
|
|
||||||
|
```go
|
||||||
|
redis.SCard(key, callback)
|
||||||
|
redis.SAdd(key, values, callback)
|
||||||
|
redis.SRem(key, values, callback)
|
||||||
|
redis.SIsMember(key, value, callback)
|
||||||
|
redis.SMembers(key, callback)
|
||||||
|
redis.SDiff(key1, key2, callback)
|
||||||
|
redis.SDiffStore(dest, key1, key2, callback)
|
||||||
|
redis.SInter(key1, key2, callback)
|
||||||
|
redis.SInterStore(dest, key1, key2, callback)
|
||||||
|
redis.SUnion(key1, key2, callback)
|
||||||
|
redis.SUnionStore(dest, key1, key2, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sorted Set Operations
|
||||||
|
|
||||||
|
```go
|
||||||
|
redis.ZCard(key, callback)
|
||||||
|
redis.ZAdd(key, memberScoreMap, callback)
|
||||||
|
redis.ZCount(key, min, max, callback)
|
||||||
|
redis.ZIncrBy(key, member, delta, callback)
|
||||||
|
redis.ZScore(key, member, callback)
|
||||||
|
redis.ZRank(key, member, callback)
|
||||||
|
redis.ZRevRank(key, member, callback)
|
||||||
|
redis.ZRem(key, members, callback)
|
||||||
|
redis.ZRange(key, start, stop, callback)
|
||||||
|
redis.ZRevRange(key, start, stop, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lua Script
|
||||||
|
|
||||||
|
```go
|
||||||
|
redis.Eval(script, numkeys, keys, args, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Raw Command
|
||||||
|
|
||||||
|
```go
|
||||||
|
redis.Command([]interface{}{"SET", "key", "value"}, callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||||
|
now := time.Now()
|
||||||
|
minuteAligned := now.Truncate(time.Minute)
|
||||||
|
timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10)
|
||||||
|
|
||||||
|
err := config.redis.Incr(timeStamp, func(response resp.Value) {
|
||||||
|
if response.Error() != nil {
|
||||||
|
log.Errorf("redis error: %v", response.Error())
|
||||||
|
proxywasm.ResumeHttpRequest()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count := response.Integer()
|
||||||
|
ctx.SetContext("timeStamp", timeStamp)
|
||||||
|
ctx.SetContext("callTimeLeft", strconv.Itoa(config.qpm - count))
|
||||||
|
|
||||||
|
if count == 1 {
|
||||||
|
// First request in this minute, set expiry
|
||||||
|
config.redis.Expire(timeStamp, 60, func(response resp.Value) {
|
||||||
|
if response.Error() != nil {
|
||||||
|
log.Errorf("expire error: %v", response.Error())
|
||||||
|
}
|
||||||
|
proxywasm.ResumeHttpRequest()
|
||||||
|
})
|
||||||
|
} else if count > config.qpm {
|
||||||
|
proxywasm.SendHttpResponse(429, [][2]string{
|
||||||
|
{"timeStamp", timeStamp},
|
||||||
|
{"callTimeLeft", "0"},
|
||||||
|
}, []byte("Too many requests\n"), -1)
|
||||||
|
} else {
|
||||||
|
proxywasm.ResumeHttpRequest()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("redis call failed: %v", err)
|
||||||
|
return types.HeaderContinue
|
||||||
|
}
|
||||||
|
return types.HeaderStopAllIterationAndWatermark
|
||||||
|
}
|
||||||
|
|
||||||
|
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||||
|
if ts := ctx.GetContext("timeStamp"); ts != nil {
|
||||||
|
proxywasm.AddHttpResponseHeader("timeStamp", ts.(string))
|
||||||
|
}
|
||||||
|
if left := ctx.GetContext("callTimeLeft"); left != nil {
|
||||||
|
proxywasm.AddHttpResponseHeader("callTimeLeft", left.(string))
|
||||||
|
}
|
||||||
|
return types.HeaderContinue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
1. **Check Ready()** - `redis.Ready()` returns false if init failed
|
||||||
|
2. **Auto-reconnect** - Client handles NOAUTH errors and re-authenticates automatically
|
||||||
|
3. **Buffering** - Default 3ms flush timeout and 1024 byte buffer; use `WithDisableBuffer()` for latency-sensitive scenarios
|
||||||
|
4. **Error handling** - Always check `response.Error()` in callbacks
|
||||||
@@ -35,7 +35,8 @@ header:
|
|||||||
- 'hgctl/pkg/manifests'
|
- 'hgctl/pkg/manifests'
|
||||||
- 'pkg/ingress/kube/gateway/istio/testdata'
|
- 'pkg/ingress/kube/gateway/istio/testdata'
|
||||||
- 'release-notes/**'
|
- 'release-notes/**'
|
||||||
- '.cursor/**'
|
- '.cursor/**'
|
||||||
|
- '.claude/**'
|
||||||
|
|
||||||
comment: on-failure
|
comment: on-failure
|
||||||
dependency:
|
dependency:
|
||||||
|
|||||||
Reference in New Issue
Block a user