Skip to content

Commit ae3105b

Browse files
Jing-zemirror58229
andauthored
feat: Add response-cache plugin (alibaba#3061)
Co-authored-by: mirror58229 <674958229@qq.com>
1 parent 53ea959 commit ae3105b

File tree

11 files changed

+1649
-0
lines changed

11 files changed

+1649
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
## 简介
2+
---
3+
title: 通用响应缓存
4+
keywords: [higress,response cache]
5+
description: 通用响应缓存插件配置参考
6+
---
7+
8+
## 功能说明
9+
10+
通用响应缓存插件,支持从请求头/请求体中提取key,从响应体中提取value并缓存起来;下次请求时,如果请求头/请求体中携带了相同的key,则直接返回缓存中的value,而不会请求后端服务。
11+
12+
**提示**
13+
14+
携带请求头`x-higress-skip-response-cache: on`时,当前请求将不会使用缓存中的内容,而是直接转发给后端服务,同时也不会缓存该请求返回响应的内容
15+
16+
17+
## 运行属性
18+
19+
插件执行阶段:`认证阶段`
20+
插件执行优先级:`10`
21+
22+
## 配置说明
23+
配置包括 缓存数据库(cache)配置部分,以及配置缓存内容部分
24+
25+
## 配置说明
26+
27+
## 缓存服务(cache)
28+
| cache.type | string | required | "" | 缓存服务类型,例如 redis |
29+
| --- | --- | --- | --- | --- |
30+
| cache.serviceName | string | required | "" | 缓存服务名称 |
31+
| cache.serviceHost | string | required | "" | 缓存服务域名 |
32+
| cache.servicePort | int64 | optional | 6379 | 缓存服务端口 |
33+
| cache.username | string | optional | "" | 缓存服务用户名 |
34+
| cache.password | string | optional | "" | 缓存服务密码 |
35+
| cache.timeout | uint32 | optional | 10000 | 缓存服务的超时时间,单位为毫秒。默认值是10000,即10秒 |
36+
| cache.cacheTTL | int | optional | 0 | 缓存过期时间,单位为秒。默认值是 0,即 永不过期|
37+
| cacheKeyPrefix | string | optional | "higress-response-cache:" | 缓存 Key 的前缀,默认值为 "higress-response-cache:" |
38+
39+
40+
## 其他配置
41+
| Name | Type | Requirement | Default | Description |
42+
| --- | --- | --- | --- | --- |
43+
| cacheResponseCode | array of number | optional | 200 | 表示支持缓存的响应状态码列表;默认为200|
44+
| cacheKeyFromHeader | string | optional | "" | 表示提取header中的固定字段的值作为缓存key;配置此项时会从请求头提取key,不会读取请求body;cacheKeyFromHeader和cacheKeyFromBody**非空情况下只支持配置一项**|
45+
| cacheKeyFromBody | string | optional | "" | 配置为空时,表示提取所有body作为缓存key;否则按JSON响应格式,从请求 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串;仅在cacheKeyFromHeader为空或未配置时生效 |
46+
| cacheValueFromBodyType | string | optional | "application/json" | 表示缓存body的类型,命中cache时content-type会返回该值;默认为"application/json" |
47+
| cacheValueFromBody | string | optional | "" | 配置为空时,表示缓存所有body;当cacheValueFromBodyType为"application/json"时,支持从响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
48+
49+
其中,缓存key的拼接逻辑为以下中一个:
50+
1. `cacheKeyPrefix` + 从请求头中`cacheKeyFromHeader`对应字段提取的内容
51+
2. `cacheKeyPrefix` + 从请求体中`cacheKeyFromBody`对应字段提取的内容
52+
53+
**注意**`cacheKeyFromHeader``cacheKeyFromBody` 不能同时配置(非空情况下只支持配置一项)。如果同时配置,插件在配置解析阶段会报错。
54+
55+
56+
命中缓存插件的情况下,返回的响应头中有三种状态:
57+
- `x-cache-status: hit` ,表示命中缓存,直接返回缓存内容
58+
- `x-cache-status: miss` ,表示未命中缓存,返回后端响应结果
59+
- `x-cache-status: skip` ,表示跳过缓存检查
60+
61+
62+
## 配置示例
63+
### 基础配置
64+
```yaml
65+
cache:
66+
type: redis
67+
serviceName: my-redis.dns
68+
servicePort: 6379
69+
timeout: 2000
70+
71+
cacheKeyFromHeader: "x-http-cache-key"
72+
73+
cacheValueFromBodyType: "application/json"
74+
cacheValueFromBody: "messages.@reverse.0.content"
75+
```
76+
77+
假设请求为
78+
79+
```bash
80+
# Request
81+
curl -H "x-http-cache-key: abcd" <url>
82+
83+
# Response
84+
{"messages":[{"content":"1"}, {"content":"2"}, {"content":"3"}]}
85+
```
86+
87+
则缓存的key为`higress-response-cache:abcd`,缓存的value为`3`
88+
89+
后续请求命中缓存时,响应Content-type返回为 `application/json`
90+
91+
92+
### 响应body作为value
93+
94+
如果缓存所有响应body,则可以配置为
95+
96+
```yaml
97+
cacheValueFromBodyType: "text/html"
98+
cacheValueFromBody: ""
99+
100+
```
101+
102+
后续请求命中缓存时,响应Content-type返回为 `text/html`。
103+
104+
### 请求body作为key
105+
106+
使用请求body作为key,则可以配置为
107+
108+
```yaml
109+
cacheKeyFromBody: ""
110+
```
111+
112+
配置支持GJSON PATH语法。
113+
114+
## 进阶用法
115+
Body为`application/json`时,支持基于 GJSON PATH 语法:
116+
117+
比如表达式:`messages.@reverse.0.content` ,含义是把 messages 数组反转后取第一项的 content;
118+
119+
GJSON PATH 也支持条件判断语法,例如希望取最后一个 role 为 user 的 content 作为 key,可以写成: `messages.@reverse.#(role=="user").content`;
120+
121+
如果希望将所有 role 为 user 的 content 拼成一个数组作为 key,可以写成:`messages.@reverse.#(role=="user")#.content`;
122+
123+
还可以支持管道语法,例如希望取到数第二个 role 为 user 的 content 作为 key,可以写成:`messages.@reverse.#(role=="user")#.content|1`。
124+
125+
更多用法可以参考[官方文档](https://github.com/tidwall/gjson/blob/master/SYNTAX.md),可以使用 [GJSON Playground](https://gjson.dev/) 进行语法测试。
126+
127+
## 常见问题
128+
129+
1. 如果返回的错误为 `error status returned by host: bad argument`,请检查:
130+
- `serviceName`是否正确包含了服务的类型后缀(.dns等)
131+
- `servicePort`配置是否正确,尤其是 `static` 类型的服务端口现在固定为 80
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
title: Response Cache
3+
keywords: [higress,response cache]
4+
description: Response Cache Plugin Configuration Reference
5+
---
6+
## Function Description
7+
Response caching plugin supports extracting keys from request headers/request bodies and caching values extracted from response bodies. On subsequent requests, if the request headers/request bodies contain the same key, it directly returns the cached value without forwarding the request to the backend service.
8+
9+
**Hint**
10+
11+
When carrying the request header `x-higress-skip-response-cache: on`, the current request will not use content from the cache but will be directly forwarded to the backend service. Additionally, the response content from this request will not be cached.
12+
13+
## Runtime Properties
14+
Plugin Execution Phase: `Authentication Phase`
15+
Plugin Execution Priority: `10`
16+
17+
## Configuration Description
18+
19+
### Cache Service (cache)
20+
| Property | Type | Requirement | Default | Description |
21+
| --- | --- | --- | --- | --- |
22+
| cache.type | string | required | "" | Cache service type, e.g., redis |
23+
| cache.serviceName | string | required | "" | Cache service name |
24+
| cache.serviceHost | string | required | "" | Cache service domain |
25+
| cache.servicePort | int64 | optional | 6379 | Cache service port |
26+
| cache.username | string | optional | "" | Cache service username |
27+
| cache.password | string | optional | "" | Cache service password |
28+
| cache.timeout | uint32 | optional | 10000 | Timeout for cache service in milliseconds. Default is 10000, i.e., 10 seconds |
29+
| cache.cacheTTL | int | optional | 0 | Cache expiration time in seconds. Default is 0, meaning never expires |
30+
| cacheKeyPrefix | string | optional | "higress-response-cache:" | Prefix for cache keys, default is "higress-response-cache:" | |
31+
32+
### Other Configurations
33+
| Name | Type | Requirement | Default | Description |
34+
| --- | --- | --- | --- | --- |
35+
| cacheResponseCode | array of number | optional | 200 | Indicates the list of response status codes that support caching; the default is 200.|
36+
| cacheKeyFromHeader | string | optional | "" | Extracts a fixed field's value from headers as the cache key; when configured, extracts key from request headers without reading the request body; **only one of cacheKeyFromHeader and cacheKeyFromBody can be configured when both are non-empty**|
37+
| cacheKeyFromBody | string | optional | "" | If empty, extracts all body as the cache key; otherwise, extracts a string from the request body in JSON format based on [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md); only takes effect when cacheKeyFromHeader is empty or not configured |
38+
| cacheValueFromBodyType | string | optional | "application/json" | Indicates the type of cached body; the content-type returned on cache hit will be this value; default is "application/json" |
39+
| cacheValueFromBody | string | optional | "" | If empty, caches all body; when cacheValueFromBodyType is "application/json", supports extracting a string from the response body based on [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) |
40+
41+
42+
The logic for concatenating the cache key is one of the following:
43+
44+
1. `cacheKeyPrefix` + content extracted from the field corresponding to `cacheKeyFromHeader` in the request header
45+
2. `cacheKeyPrefix` + content extracted from the field corresponding to `cacheKeyFromBody` in the request body
46+
47+
**Note**: `cacheKeyFromHeader` and `cacheKeyFromBody` cannot be configured at the same time (only one of them can be configured when both are non-empty). If both are configured, the plugin will return an error during the configuration parsing phase.
48+
49+
In the case of hitting the cache plugin, there are three statuses in the returned response headers:
50+
51+
- `x-cache-status: hit` , indicating a cache hit and cached content is returned directly
52+
- `x-cache-status: miss` , indicating a cache miss and backend response results are returned
53+
- `x-cache-status: skip` , indicating skipping the cache check
54+
55+
## Configuration Example
56+
### Basic Configuration
57+
```yaml
58+
cache:
59+
type: redis
60+
serviceName: my-redis.dns
61+
servicePort: 6379
62+
timeout: 2000
63+
64+
cacheKeyFromHeader: "x-http-cache-key"
65+
66+
cacheValueFromBodyType: "application/json"
67+
cacheValueFromBody: "messages.@reverse.0.content"
68+
```
69+
70+
Assumed Request
71+
72+
```bash
73+
# Request
74+
curl -H "x-http-cache-key: abcd" <url>
75+
76+
# Response
77+
{"messages":[{"content":"1"}, {"content":"2"}, {"content":"3"}]}
78+
```
79+
80+
In this case, the cache key would be `higress-response-cache:abcd`, and the cached value would be `3`.
81+
82+
For subsequent requests that hit the cache, the response Content-Type returned is `application/json`.
83+
84+
### Response Body as Cache Value
85+
To cache all response bodies, configure as follows:
86+
87+
```yaml
88+
cacheValueFromBodyType: "text/html"
89+
cacheValueFromBody: ""
90+
```
91+
For subsequent requests that hit the cache, the response Content-Type returned is `text/html`.
92+
93+
94+
### Request Body as Cache Key
95+
To use the request body as the key, configure as follows:
96+
97+
```yaml
98+
99+
cacheKeyFromBody: ""
100+
```
101+
102+
The configuration supports GJSON PATH syntax.
103+
104+
105+
## Advanced Usage
106+
When the body is `application/json`, GJSON PATH syntax is supported:
107+
108+
For example, the expression `messages.@reverse.0.content` means taking the content of the first item after reversing the messages array.
109+
110+
GJSON PATH also supports conditional syntax. For instance, to take the content of the last message where role is "user", you can write: `messages.@reverse.#(role=="user").content`.
111+
112+
To concatenate all contents where role is "user" into an array, you can write: `messages.@reverse.#(role=="user")#.content`.
113+
114+
Pipeline syntax is also supported. For example, to take the second content where role is "user", you can write: `messages.@reverse.#(role=="user")#.content|1`.
115+
116+
Refer to the [official documentation](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for more usage examples, and test the syntax using the [GJSON Playground](https://gjson.dev/).
117+
118+
## Common Issues
119+
If the error `error status returned by host: bad argument` occurs, check:
120+
- Whether `serviceName` correctly includes the service type suffix (.dns, etc.)
121+
- Whether `servicePort` is configured correctly, especially that `static` type services now use a fixed port of 80
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package cache
2+
3+
import (
4+
"errors"
5+
"strings"
6+
7+
"github.com/higress-group/wasm-go/pkg/wrapper"
8+
"github.com/tidwall/gjson"
9+
)
10+
11+
const (
12+
PROVIDER_TYPE_REDIS = "redis"
13+
DEFAULT_CACHE_PREFIX = "higress-resp-cache:"
14+
)
15+
16+
type providerInitializer interface {
17+
ValidateConfig(ProviderConfig) error
18+
CreateProvider(ProviderConfig) (Provider, error)
19+
}
20+
21+
var (
22+
providerInitializers = map[string]providerInitializer{
23+
PROVIDER_TYPE_REDIS: &redisProviderInitializer{},
24+
}
25+
)
26+
27+
type ProviderConfig struct {
28+
// @Title zh-CN redis 缓存服务提供者类型
29+
// @Description zh-CN 缓存服务提供者类型,例如 redis
30+
typ string
31+
// @Title zh-CN redis 缓存服务名称
32+
// @Description zh-CN 缓存服务名称
33+
serviceName string
34+
// @Title zh-CN redis 缓存服务端口
35+
// @Description zh-CN 缓存服务端口,默认值为6379
36+
servicePort int
37+
// @Title zh-CN redis 缓存服务地址
38+
// @Description zh-CN Cache 缓存服务地址,非必填
39+
serviceHost string
40+
// @Title zh-CN 缓存服务用户名
41+
// @Description zh-CN 缓存服务用户名,非必填
42+
username string
43+
// @Title zh-CN 缓存服务密码
44+
// @Description zh-CN 缓存服务密码,非必填
45+
password string
46+
// @Title zh-CN 请求超时
47+
// @Description zh-CN 请求缓存服务的超时时间,单位为毫秒。默认值是10000,即10秒
48+
timeout uint32
49+
// @Title zh-CN 缓存过期时间
50+
// @Description zh-CN 缓存过期时间,单位为秒。默认值是0,即永不过期
51+
cacheTTL int
52+
// @Title 缓存 Key 前缀
53+
// @Description 缓存 Key 的前缀,默认值为 "higress-resp-cache:"
54+
cacheKeyPrefix string
55+
}
56+
57+
func (c *ProviderConfig) GetProviderType() string {
58+
return c.typ
59+
}
60+
61+
func (c *ProviderConfig) FromJson(json gjson.Result) {
62+
c.typ = json.Get("type").String()
63+
c.serviceName = json.Get("serviceName").String()
64+
c.servicePort = int(json.Get("servicePort").Int())
65+
if !json.Get("servicePort").Exists() {
66+
if strings.HasSuffix(c.serviceName, ".static") {
67+
// use default logic port which is 80 for static service
68+
c.servicePort = 80
69+
} else {
70+
c.servicePort = 6379
71+
}
72+
}
73+
c.serviceHost = json.Get("serviceHost").String()
74+
c.username = json.Get("username").String()
75+
c.password = json.Get("password").String()
76+
c.timeout = uint32(json.Get("timeout").Int())
77+
if !json.Get("timeout").Exists() {
78+
c.timeout = 10000
79+
}
80+
c.cacheTTL = int(json.Get("cacheTTL").Int())
81+
if !json.Get("cacheTTL").Exists() {
82+
c.cacheTTL = 0
83+
// c.cacheTTL = 3600000
84+
}
85+
if json.Get("cacheKeyPrefix").Exists() {
86+
c.cacheKeyPrefix = json.Get("cacheKeyPrefix").String()
87+
} else {
88+
c.cacheKeyPrefix = DEFAULT_CACHE_PREFIX
89+
}
90+
91+
}
92+
93+
func (c *ProviderConfig) Validate() error {
94+
if c.typ == "" {
95+
return errors.New("cache service type is required")
96+
}
97+
if c.serviceName == "" {
98+
return errors.New("cache service name is required")
99+
}
100+
if c.cacheTTL < 0 {
101+
return errors.New("cache TTL must be greater than or equal to 0")
102+
}
103+
initializer, has := providerInitializers[c.typ]
104+
if !has {
105+
return errors.New("unknown cache service provider type: " + c.typ)
106+
}
107+
if err := initializer.ValidateConfig(*c); err != nil {
108+
return err
109+
}
110+
return nil
111+
}
112+
113+
func CreateProvider(pc ProviderConfig) (Provider, error) {
114+
initializer, has := providerInitializers[pc.typ]
115+
if !has {
116+
return nil, errors.New("unknown provider type: " + pc.typ)
117+
}
118+
return initializer.CreateProvider(pc)
119+
}
120+
121+
type Provider interface {
122+
GetProviderType() string
123+
Init(username string, password string, timeout uint32) error
124+
Get(key string, cb wrapper.RedisResponseCallback) error
125+
Set(key string, value string, cb wrapper.RedisResponseCallback) error
126+
GetCacheKeyPrefix() string
127+
}

0 commit comments

Comments
 (0)