This is an automated email from the ASF dual-hosted git repository.
membphis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 864aa16 feature: support for proxy caching plugin based on disk.
(#1153)
864aa16 is described below
commit 864aa16e91abe559182982031931e54bb540aff5
Author: agile6v <[email protected]>
AuthorDate: Sun Mar 15 22:26:08 2020 +0800
feature: support for proxy caching plugin based on disk. (#1153)
---
bin/apisix | 47 ++-
conf/config.yaml | 16 +
doc/README.md | 1 +
doc/README_CN.md | 1 +
doc/plugins/proxy-cache-cn.md | 146 +++++++++
doc/plugins/proxy-cache.md | 146 +++++++++
lua/apisix/core/ctx.lua | 18 +-
lua/apisix/plugins/proxy-cache.lua | 272 ++++++++++++++++
t/APISIX.pm | 23 ++
t/admin/plugins.t | 2 +-
t/debug/debug-mode.t | 1 +
t/plugin/proxy-cache.t | 621 +++++++++++++++++++++++++++++++++++++
12 files changed, 1287 insertions(+), 7 deletions(-)
diff --git a/bin/apisix b/bin/apisix
index 1c4f266..2d21aa0 100755
--- a/bin/apisix
+++ b/bin/apisix
@@ -182,6 +182,22 @@ http {
lua_shared_dict jwks 1m; # cache for JWKs
lua_shared_dict introspection 10m; # cache for JWT verification
results
+ {% if proxy_cache then %}
+ # for proxy cache
+ {% for _, cache in ipairs(proxy_cache.zones) do %}
+ proxy_cache_path {* cache.disk_path *} levels={* cache.cache_levels *}
keys_zone={* cache.name *}:{* cache.memory_size *} inactive=1d max_size={*
cache.disk_size *};
+ {% end %}
+ {% end %}
+
+ {% if proxy_cache then %}
+ # for proxy cache
+ map $upstream_cache_zone $upstream_cache_zone_info {
+ {% for _, cache in ipairs(proxy_cache.zones) do %}
+ {* cache.name *} {* cache.disk_path *},{* cache.cache_levels *};
+ {% end %}
+ }
+ {% end %}
+
lua_ssl_verify_depth 5;
ssl_session_timeout 86400;
@@ -375,6 +391,8 @@ http {
proxy_pass_header Server;
proxy_pass_header Date;
+ ### the following x-forwarded-* headers is to send to upstream
server
+
set $var_x_forwarded_for $remote_addr;
set $var_x_forwarded_proto $scheme;
set $var_x_forwarded_host $host;
@@ -398,7 +416,34 @@ http {
proxy_set_header X-Forwarded-Host $var_x_forwarded_host;
proxy_set_header X-Forwarded-Port $var_x_forwarded_port;
- proxy_pass $upstream_scheme://apisix_backend$upstream_uri;
+ {% if proxy_cache then %}
+ ### the following configuration is to cache response content from
upstream server
+
+ set $upstream_cache_zone off;
+ set $upstream_cache_key '';
+ set $upstream_cache_bypass '';
+ set $upstream_no_cache '';
+ set $upstream_hdr_expires '';
+ set $upstream_hdr_cache_control '';
+
+ proxy_cache $upstream_cache_zone;
+ proxy_cache_valid any {% if proxy_cache and
proxy_cache.cache_ttl then %} {* proxy_cache.cache_ttl *} {% else %} 5s {% end
%};
+ proxy_cache_min_uses 1;
+ proxy_cache_methods GET HEAD;
+ proxy_cache_lock_timeout 5s;
+ proxy_cache_use_stale off;
+ proxy_cache_key $upstream_cache_key;
+ proxy_no_cache $upstream_no_cache;
+ proxy_cache_bypass $upstream_cache_bypass;
+
+ proxy_hide_header Cache-Control;
+ proxy_hide_header Expires;
+ add_header Cache-Control $upstream_hdr_cache_control;
+ add_header Expires $upstream_hdr_expires;
+ add_header Apisix-Cache-Status $upstream_cache_status always;
+ {% end %}
+
+ proxy_pass $upstream_scheme://apisix_backend$upstream_uri;
header_filter_by_lua_block {
apisix.http_header_filter_phase()
diff --git a/conf/config.yaml b/conf/config.yaml
index 8c1beb2..4bb942d 100644
--- a/conf/config.yaml
+++ b/conf/config.yaml
@@ -35,6 +35,21 @@ apisix:
# enable_tcp_pp: true # Enable the proxy protocol for tcp proxy,
it works for stream_proxy.tcp option
# enable_tcp_pp_to_upstream: true # Enables the proxy protocol to the
upstream server
+ proxy_cache: # Proxy Caching configuration
+ cache_ttl: 10s # The default caching time if the upstream
does not specify the cache time
+ zones: # The parameters of a cache
+ - name: disk_cache_one # The name of the cache, administrator can
be specify
+ # which cache to use by name in the admin
api
+ memory_size: 50m # The size of shared memory, it's used to
store the cache index
+ disk_size: 1G # The size of disk, it's used to store the
cache data
+ disk_path: "/tmp/disk_cache_one" # The path to store the cache data
+ cache_levels: "1:2" # The hierarchy levels of a cache
+ # - name: disk_cache_two
+ # memory_size: 50m
+ # disk_size: 1G
+ # disk_path: "/tmp/disk_cache_two"
+ # cache_levels: "1:2"
+
# allow_admin: #
http://nginx.org/en/docs/http/ngx_http_access_module.html#allow
# - 127.0.0.0/24 # If we don't set any IP list, then any IP
access is allowed by default.
# - "::/64"
@@ -124,6 +139,7 @@ plugins: # plugin list
- fault-injection
- udp-logger
- wolf-rbac
+ - proxy-cache
- tcp-logger
stream_plugins:
diff --git a/doc/README.md b/doc/README.md
index 63efcbf..cba2444 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -58,6 +58,7 @@ Plugins
* [redirect](plugins/redirect.md): URI redirect.
* [response-rewrite](plugins/response-rewrite.md): Set customized response
status code, body and header to the client.
* [fault-injection](plugins/fault-injection.md): The specified response body,
response code, and response time can be returned, which provides processing
capabilities in different failure scenarios, such as service failure, service
overload, and high service delay.
+* [proxy-cache](plugins/proxy-cache.md): Provides the ability to cache
upstream response data.
* [tcp-logger](plugins/tcp-logger.md): Log requests to TCP servers
Deploy to the Cloud
diff --git a/doc/README_CN.md b/doc/README_CN.md
index 07ff451..3fc9f9d 100644
--- a/doc/README_CN.md
+++ b/doc/README_CN.md
@@ -59,4 +59,5 @@ Reference document
* [redirect](plugins/redirect-cn.md): URI 重定向。
* [response-rewrite](plugins/response-rewrite-cn.md): 支持自定义修改返回内容的 `status
code`、`body`、`headers`。
*
[fault-injection](plugins/fault-injection-cn.md):故障注入,可以返回指定的响应体、响应码和响应时间,从而提供了不同的失败场景下处理的能力,例如服务失败、服务过载、服务高延时等。
+* [proxy-cache](plugins/proxy-cache-cn.md):代理缓存插件提供缓存后端响应数据的能力。
diff --git a/doc/plugins/proxy-cache-cn.md b/doc/plugins/proxy-cache-cn.md
new file mode 100644
index 0000000..95f3bda
--- /dev/null
+++ b/doc/plugins/proxy-cache-cn.md
@@ -0,0 +1,146 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+-->
+
+[English](proxy-cache.md)
+
+# proxy-cache
+
+代理缓存插件,该插件提供缓存后端响应数据的能力,它可以和其他插件一起使用。该插件支持基于磁盘的缓存,未来也会支持基于内存的缓存。目前可以根据响应码、请求
Method 来指定需要缓存的数据,另外也可以通过 no_cache 和 cache_bypass 配置更复杂的缓存策略。
+
+基于磁盘的缓存需要注意:
+1. 不能动态配置缓存的过期时间,只能通过后端服务响应头 Expires 或 Cache-Control 来设置过期时间,如果后端响应头中没有
Expires 或 Cache-Control,那么 APISIX 将默认只缓存10秒钟
+2. 如果后端服务不可用, APISIX 将返回502或504,那么502或504将被缓存10秒钟
+
+### 参数
+
+|名称 |必须|类型|描述|
+|------- |-----|------|------|
+|cache_zone|是|string|指定使用哪个缓存区域,不同的缓存区域可以配置不同的路径,在conf/config.yaml文件中可以预定义使用的缓存区域|
+|cache_key|是|array[string]|缓存key,可以使用变量。例如:["$host", "$uri", "-cache-id"]|
+|cache_bypass|否|array[string]|是否跳过缓存检索,即不在缓存中查找数据,可以使用变量,需要注意当此参数的值不为空或非'0'时将会跳过缓存的检索。例如:["$arg_bypass"]|
+|cache_method|否|array[string]|根据请求method决定是否需要缓存|
+|cache_http_status|否|array[integer]|根据响应码决定是否需要缓存|
+|hide_cache_headers|否|boolean|是否将 Expires 和 Cache-Control 响应头返回给客户端,默认为 false|
+|no_cache|否|array[string]|是否缓存数据,可以使用变量,需要注意当此参数的值不为空或非'0'时将不会缓存数据。|
+
+注:变量以$开头,也可以使用变量和字符串的结合,但是需要以数组的形式分开写,最终变量被解析后会和字符串拼接在一起。
+
+### 示例
+
+#### 启用插件
+
+示例1:为特定路由启用 `proxy-cache` 插件:
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d '
+{
+ "plugins": {
+ "proxy-cache": {
+ "cache_zone": "disk_cache_one",
+ "cache_key": ["$uri", "-cache-id"],
+ "cache_bypass": ["$arg_bypass"],
+ "cache_method": ["GET"],
+ "cache_http_status": [200],
+ "hide_cache_headers": true,
+ "no_cache": ["$arg_test"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1999": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+}'
+```
+
+测试:
+
+```shell
+$ curl http://127.0.0.1:9080/hello -i
+HTTP/1.1 200 OK
+Content-Type: application/octet-stream
+Content-Length: 6
+Connection: keep-alive
+Server: APISIX web server
+Date: Tue, 03 Mar 2020 10:45:36 GMT
+Last-Modified: Tue, 03 Mar 2020 10:36:38 GMT
+Apisix-Cache-Status: MISS
+
+hello
+```
+
+> http status 返回`200`并且响应头中包含`Apisix-Cache-Status`,表示该插件已启用。
+
+示例2:验证文件是否被缓存,再次请求上边的地址:
+
+测试:
+
+```shell
+$ curl http://127.0.0.1:9080/hello -i
+HTTP/1.1 200 OK
+Content-Type: application/octet-stream
+Content-Length: 6
+Connection: keep-alive
+Server: APISIX web server
+Date: Tue, 03 Mar 2020 11:14:46 GMT
+Last-Modified: Thu, 20 Feb 2020 14:21:41 GMT
+Apisix-Cache-Status: HIT
+
+hello
+```
+
+> 响应头 Apisix-Cache-Status 值变为了 HIT,说明文件已经被缓存
+
+示例3:如何清理缓存的文件,只需要指定请求的 method 为 PURGE:
+
+测试:
+
+```shell
+$ curl -i http://127.0.0.1:9080/hello -X PURGE
+HTTP/1.1 200 OK
+Date: Tue, 03 Mar 2020 11:17:35 GMT
+Content-Type: text/plain
+Transfer-Encoding: chunked
+Connection: keep-alive
+Server: APISIX web server
+```
+
+> 响应码为200即表示删除成功,如果文件未找到将返回404
+
+#### 禁用插件
+
+移除插件配置中相应的 JSON 配置可立即禁用该插件,无需重启服务:
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d '
+{
+ "uri": "/hello",
+ "plugins": {},
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1999": 1
+ }
+ }
+}'
+```
+
+这时该插件已被禁用。
diff --git a/doc/plugins/proxy-cache.md b/doc/plugins/proxy-cache.md
new file mode 100644
index 0000000..c8e668c
--- /dev/null
+++ b/doc/plugins/proxy-cache.md
@@ -0,0 +1,146 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+-->
+
+[Chinese](proxy-cache-cn.md)
+
+# proxy-cache
+
+The proxy-cache plugin, which provides the ability to cache upstream response
data and can be used with other plugins. The plugin supports disk-based caching
and will support the memory-based caching in the future. The data that needs to
be cached can be determined by the response code or request method and more
complex caching policies can be configured by no_cache and cache_bypass
attributes.
+
+*Note*:
+1. The cache expiration time cannot be configured dynamically. The expiration
time can only be set by the upstream response header `Expires` or
`Cache-Control`, and the default cache expiration time is 10s if there is no
`Expires` or `Cache-Control` in the upstream response header
+2. If the upstream service is not available and APISIX will return 502 or 504,
then 502 or 504 will be cached for 10s.
+
+## Attributes
+
+|Name |Requirement | Type |Description|
+|------- |-----|------|------|
+|cache_zone|required|string|Specify which cache area to use, each cache area
can be configured with different paths. In addition, cache areas can be
predefined in conf/config.yaml file|
+|cache_key|required|array[string]|key of a cache, can use variables. For
example: ["$host", "$uri", "-cache-id"]|
+|cache_bypass|optional|array[string]|Whether to skip cache retrieval. That is,
do not look for data in the cache. It can use variables, and note that cache
data retrieval will be skipped when the value of this attribute is not empty or
not '0'. For example: ["$arg_bypass"]|
+|cache_method|optional|array[string]|Decide whether to be cached according to
the request method|
+|cache_http_status|optional|array[integer]|Decide whether to be cached
according to the upstream response status|
+|hide_cache_headers|optional|boolean|Whether to return the Expires and
Cache-Control response headers to the client, the default is false|
+|no_cache|optional|array[string]|Whether to cache data, it can use variables,
and note that the data will not be cached when the value of this attribute is
not empty or not '0'.|
+
+Note:
+1. The variable starts with $.
+2. The attribute can use a combination of the variable and the string, but it
needs to be written separately as an array, and the final values are stitched
together after the variable is parsed.
+
+### Examples
+
+#### Enable the plugin
+
+1: enable the proxy-cache plugin for a specific route :
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d '
+{
+ "plugins": {
+ "proxy-cache": {
+ "cache_zone": "disk_cache_one",
+ "cache_key": ["$uri", "-cache-id"],
+ "cache_bypass": ["$arg_bypass"],
+ "cache_method": ["GET"],
+ "cache_http_status": [200],
+ "hide_cache_headers": true,
+ "no_cache": ["$arg_test"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1999": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+}'
+```
+
+Test Plugin:
+
+```shell
+$ curl http://127.0.0.1:9080/hello -i
+HTTP/1.1 200 OK
+Content-Type: application/octet-stream
+Content-Length: 6
+Connection: keep-alive
+Server: APISIX web server
+Date: Tue, 03 Mar 2020 10:45:36 GMT
+Last-Modified: Tue, 03 Mar 2020 10:36:38 GMT
+Apisix-Cache-Status: MISS
+
+hello
+```
+> http status is '200' and the response header contains 'Apisix-Cache-Status'
to indicate that the plug-in is enabled.
+
+2: Verify that the file is cached, request the address above again:
+
+
+```shell
+$ curl http://127.0.0.1:9080/hello -i
+HTTP/1.1 200 OK
+Content-Type: application/octet-stream
+Content-Length: 6
+Connection: keep-alive
+Server: APISIX web server
+Date: Tue, 03 Mar 2020 11:14:46 GMT
+Last-Modified: Thu, 20 Feb 2020 14:21:41 GMT
+Apisix-Cache-Status: HIT
+
+hello
+```
+
+> Response header Apisix-Cache-Status has changed to HIT, indicating that the
file has been cached.
+
+3: How to clean up the cached file, simply specify the request method as PURGE:
+
+
+```shell
+$ curl -i http://127.0.0.1:9080/hello -X PURGE
+HTTP/1.1 200 OK
+Date: Tue, 03 Mar 2020 11:17:35 GMT
+Content-Type: text/plain
+Transfer-Encoding: chunked
+Connection: keep-alive
+Server: APISIX web server
+```
+
+> The response status is 200, indicating that the file was deleted
successfully. And return 404 if the file is not found.
+
+## Disable Plugin
+
+Remove the corresponding JSON in the plugin configuration to disable the
plugin immediately without restarting the service:
+
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d '
+{
+ "uri": "/hello",
+ "plugins": {},
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1999": 1
+ }
+ }
+}'
+```
+
+The plugin has been disabled now.
diff --git a/lua/apisix/core/ctx.lua b/lua/apisix/core/ctx.lua
index 5e53b31..31d0d89 100644
--- a/lua/apisix/core/ctx.lua
+++ b/lua/apisix/core/ctx.lua
@@ -42,11 +42,19 @@ do
}
local ngx_var_names = {
- upstream_scheme = true,
- upstream_host = true,
- upstream_upgrade = true,
- upstream_connection = true,
- upstream_uri = true,
+ upstream_scheme = true,
+ upstream_host = true,
+ upstream_upgrade = true,
+ upstream_connection = true,
+ upstream_uri = true,
+
+ upstream_cache_zone = true,
+ upstream_cache_zone_info = true,
+ upstream_no_cache = true,
+ upstream_cache_key = true,
+ upstream_cache_bypass = true,
+ upstream_hdr_expires = true,
+ upstream_hdr_cache_control = true,
}
local mt = {
diff --git a/lua/apisix/plugins/proxy-cache.lua
b/lua/apisix/plugins/proxy-cache.lua
new file mode 100644
index 0000000..05c1f5a
--- /dev/null
+++ b/lua/apisix/plugins/proxy-cache.lua
@@ -0,0 +1,272 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You 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.
+--
+
+local core = require("apisix.core")
+local ngx_re = require("ngx.re")
+local tab_insert = table.insert
+local tab_concat = table.concat
+local string = string
+local io_open = io.open
+local io_close = io.close
+local ngx = ngx
+local os = os
+local ipairs = ipairs
+local pairs = pairs
+local tonumber = tonumber
+
+local plugin_name = "proxy-cache"
+
+local schema = {
+ type = "object",
+ properties = {
+ cache_zone = {
+ type = "string",
+ minLength = 1
+ },
+ cache_key = {
+ type = "array",
+ minItems = 1,
+ items = {
+ description = "a key for caching",
+ type = "string",
+ pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]],
+ },
+ default = {"$host", "$request_uri"}
+ },
+ cache_http_status = {
+ type = "array",
+ minItems = 1,
+ items = {
+ description = "http response status",
+ type = "integer",
+ minimum = 200,
+ maximum = 599,
+ },
+ uniqueItems = true,
+ default = {200, 301, 404},
+ },
+ cache_method = {
+ type = "array",
+ minItems = 1,
+ items = {
+ description = "http method",
+ type = "string",
+ enum = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD",
+ "OPTIONS", "CONNECT", "TRACE"},
+ },
+ uniqueItems = true,
+ default = {"GET", "HEAD"},
+ },
+ hide_cache_headers = {
+ type = "boolean",
+ default = false,
+ },
+ cache_bypass = {
+ type = "array",
+ minItems = 1,
+ items = {
+ type = "string",
+ pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]]
+ },
+ },
+ no_cache = {
+ type = "array",
+ minItems = 1,
+ items = {
+ type = "string",
+ pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]]
+ },
+ },
+ },
+ required = {"cache_zone"},
+}
+
+local _M = {
+ version = 0.1,
+ priority = 1009,
+ name = plugin_name,
+ schema = schema,
+}
+
+
+function _M.check_schema(conf)
+ local ok, err = core.schema.check(schema, conf)
+ if not ok then
+ return false, err
+ end
+
+ local found = false
+ local local_conf = core.config.local_conf()
+ if local_conf.apisix.proxy_cache then
+ for _, cache in ipairs(local_conf.apisix.proxy_cache.zones) do
+ if cache.name == conf.cache_zone then
+ found = true
+ end
+ end
+
+ if found == false then
+ return false, "cache_zone " .. conf.cache_zone .. " not found"
+ end
+ end
+ return true
+end
+
+
+local tmp = {}
+local function generate_complex_value(data, ctx)
+ core.table.clear(tmp)
+
+ core.log.info("proxy-cache complex value: ", core.json.delay_encode(data))
+ for i, value in ipairs(data) do
+ core.log.info("proxy-cache complex value index-", i, ": ", value)
+
+ if string.sub(value, 1, 1) == "$" then
+ tab_insert(tmp, ctx.var[string.sub(value, 2)])
+ else
+ tab_insert(tmp, value)
+ end
+ end
+
+ return tab_concat(tmp, "")
+end
+
+
+-- check whether the request method and response status
+-- match the user defined.
+local function match_method_and_status(conf, ctx)
+ local match_method, match_status = false, false
+
+ -- Maybe there is no need for optimization here.
+ for _, method in ipairs(conf.cache_method) do
+ if method == ctx.var.request_method then
+ match_method = true
+ break
+ end
+ end
+
+ for _, status in ipairs(conf.cache_http_status) do
+ if status == ngx.status then
+ match_status = true
+ break
+ end
+ end
+
+ if match_method and match_status then
+ return true
+ end
+
+ return false
+end
+
+
+local function file_exists(name)
+ local f = io_open(name, "r")
+ if f~=nil then io_close(f) return true else return false end
+end
+
+
+local function generate_cache_filename(cache_path, cache_levels, cache_key)
+ local md5sum = ngx.md5(cache_key)
+ local levels = ngx_re.split(cache_levels, ":")
+ local filename = ""
+
+ local index = string.len(md5sum)
+ for k, v in pairs(levels) do
+ local length = tonumber(v)
+ index = index - length
+ filename = filename .. md5sum:sub(index+1, index+length) .. "/"
+ end
+ if cache_path:sub(-1) ~= "/" then
+ cache_path = cache_path .. "/"
+ end
+ filename = cache_path .. filename .. md5sum
+ return filename
+end
+
+
+local function cache_purge(conf, ctx)
+ local cache_zone_info = ngx_re.split(ctx.var.upstream_cache_zone_info, ",")
+
+ local filename = generate_cache_filename(cache_zone_info[1],
cache_zone_info[2],
+ ctx.var.upstream_cache_key)
+ if file_exists(filename) then
+ os.remove(filename)
+ return nil
+ end
+
+ return "Not found"
+end
+
+
+function _M.rewrite(conf, ctx)
+ core.log.info("proxy-cache plugin rewrite phase, conf: ",
core.json.delay_encode(conf))
+
+ ctx.var.upstream_cache_zone = conf.cache_zone
+
+ local value = generate_complex_value(conf.cache_key, ctx)
+ ctx.var.upstream_cache_key = value
+ core.log.info("proxy-cache cache key value:", value)
+
+ if ctx.var.request_method == "PURGE" then
+ local err = cache_purge(conf, ctx)
+ if err ~= nil then
+ return 404
+ end
+
+ return 200
+ end
+
+ if conf.cache_bypass ~= nil then
+ local value = generate_complex_value(conf.cache_bypass, ctx)
+ ctx.var.upstream_cache_bypass = value
+ core.log.info("proxy-cache cache bypass value:", value)
+ end
+end
+
+
+function _M.header_filter(conf, ctx)
+ core.log.info("proxy-cache plugin header filter phase, conf: ",
core.json.delay_encode(conf))
+
+ local no_cache = "1"
+
+ if match_method_and_status(conf, ctx) then
+ no_cache = "0"
+ end
+
+ if conf.no_cache ~= nil then
+ local value = generate_complex_value(conf.no_cache, ctx)
+ core.log.info("proxy-cache no-cache value:", value)
+
+ if value ~= nil and value ~= "" and value ~= "0" then
+ no_cache = "1"
+ end
+ end
+
+ if conf.hide_cache_headers == true then
+ ctx.var.upstream_hdr_cache_control = ""
+ ctx.var.upstream_hdr_expires = ""
+ else
+ ctx.var.upstream_hdr_cache_control =
ctx.var.upstream_http_cache_control
+ ctx.var.upstream_hdr_expires = ctx.var.upstream_http_expires
+ end
+
+ ctx.var.upstream_no_cache = no_cache
+ core.log.info("proxy-cache no cache:", no_cache)
+end
+
+
+return _M
diff --git a/t/APISIX.pm b/t/APISIX.pm
index 8091ae5..4dd710d 100644
--- a/t/APISIX.pm
+++ b/t/APISIX.pm
@@ -269,6 +269,29 @@ _EOC_
set \$upstream_connection '';
set \$upstream_uri '';
+ set \$upstream_cache_zone off;
+ set \$upstream_cache_key '';
+ set \$upstream_cache_bypass '';
+ set \$upstream_no_cache '';
+ set \$upstream_hdr_expires '';
+ set \$upstream_hdr_cache_control '';
+
+ proxy_cache \$upstream_cache_zone;
+ proxy_cache_valid any 10s;
+ proxy_cache_min_uses 1;
+ proxy_cache_methods GET HEAD;
+ proxy_cache_lock_timeout 5s;
+ proxy_cache_use_stale off;
+ proxy_cache_key \$upstream_cache_key;
+ proxy_no_cache \$upstream_no_cache;
+ proxy_cache_bypass \$upstream_cache_bypass;
+
+ proxy_hide_header Cache-Control;
+ proxy_hide_header Expires;
+ add_header Cache-Control \$upstream_hdr_cache_control;
+ add_header Expires \$upstream_hdr_expires;
+ add_header Apisix-Cache-Status \$upstream_cache_status always;
+
access_by_lua_block {
-- wait for etcd sync
ngx.sleep($wait_etcd_sync)
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index b8430c7..c5f34b7 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -30,7 +30,7 @@ __DATA__
--- request
GET /apisix/admin/plugins/list
--- response_body_like eval
-qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac","tcp-logger"\]/
+qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac","proxy-cache","tcp-logger"\]/
--- no_error_log
[error]
diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t
index 012262a..ead99df 100644
--- a/t/debug/debug-mode.t
+++ b/t/debug/debug-mode.t
@@ -63,6 +63,7 @@ loaded plugin and sort by priority: 2555 name: wolf-rbac
loaded plugin and sort by priority: 2520 name: basic-auth
loaded plugin and sort by priority: 2510 name: jwt-auth
loaded plugin and sort by priority: 2500 name: key-auth
+loaded plugin and sort by priority: 1009 name: proxy-cache
loaded plugin and sort by priority: 1008 name: proxy-rewrite
loaded plugin and sort by priority: 1003 name: limit-conn
loaded plugin and sort by priority: 1002 name: limit-count
diff --git a/t/plugin/proxy-cache.t b/t/plugin/proxy-cache.t
new file mode 100644
index 0000000..cb48517
--- /dev/null
+++ b/t/plugin/proxy-cache.t
@@ -0,0 +1,621 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+log_level('info');
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ my $http_config = $block->http_config // <<_EOC_;
+
+ # for proxy cache
+ proxy_cache_path /tmp/disk_cache_one levels=1:2
keys_zone=disk_cache_one:50m inactive=1d max_size=1G;
+ proxy_cache_path /tmp/disk_cache_two levels=1:2
keys_zone=disk_cache_two:50m inactive=1d max_size=1G;
+
+ # for proxy cache
+ map \$upstream_cache_zone \$upstream_cache_zone_info {
+ disk_cache_one /tmp/disk_cache_one,1:2;
+ disk_cache_two /tmp/disk_cache_two,1:2;
+ }
+
+ server {
+ listen 1986;
+ server_tokens off;
+
+ location / {
+ expires 60s;
+ return 200 "hello world!";
+ }
+
+ location /hello-not-found {
+ return 404;
+ }
+ }
+_EOC_
+
+ $block->set_value("http_config", $http_config);
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: sanity check (missing required field)
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "proxy-cache": {
+ "cache_bypass": ["$arg_bypass"],
+ "cache_method": ["GET"],
+ "cache_http_status": [200],
+ "hide_cache_headers": true,
+ "no_cache": ["$arg_no_cache"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body eval
+qr/failed to check the configuration of plugin proxy-cache/
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: sanity check (invalid type for cache_method)
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "proxy-cache": {
+ "cache_zone": "disk_cache_one",
+ "cache_bypass": ["$arg_bypass"],
+ "cache_method": "GET",
+ "cache_http_status": [200],
+ "hide_cache_headers": true,
+ "no_cache": ["$arg_no_cache"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body eval
+qr/failed to check the configuration of plugin proxy-cache/
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: sanity check (invalid type for cache_key)
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "proxy-cache": {
+ "cache_zone": "disk_cache_one",
+ "cache_key": "${uri}-cache-key",
+ "cache_bypass": ["$arg_bypass"],
+ "cache_method": ["GET"],
+ "cache_http_status": [200],
+ "hide_cache_headers": true,
+ "no_cache": ["$arg_no_cache"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1985": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body eval
+qr/failed to check the configuration of plugin proxy-cache/
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: sanity check (invalid type for cache_bypass)
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "proxy-cache": {
+ "cache_zone": "disk_cache_one",
+ "cache_bypass": "$arg_bypass",
+ "cache_method": ["GET"],
+ "cache_http_status": [200],
+ "hide_cache_headers": true,
+ "no_cache": ["$arg_no_cache"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1985": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body eval
+qr/failed to check the configuration of plugin proxy-cache/
+--- no_error_log
+[error]
+
+
+
+=== TEST 5: sanity check (invalid type for no_cache)
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "proxy-cache": {
+ "cache_zone": "disk_cache_one",
+ "cache_bypass": ["$arg_bypass"],
+ "cache_method": ["GET"],
+ "cache_http_status": [200],
+ "hide_cache_headers": true,
+ "no_cache": "$arg_no_cache"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1985": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body eval
+qr/failed to check the configuration of plugin proxy-cache/
+--- no_error_log
+[error]
+
+
+
+=== TEST 6: sanity check (illegal character for cache_key)
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "proxy-cache": {
+ "cache_zone": "disk_cache_one",
+ "cache_key": ["$uri-", "-cache-id"],
+ "cache_bypass": ["$arg_bypass"],
+ "cache_method": ["GET"],
+ "cache_http_status": [200],
+ "hide_cache_headers": true,
+ "no_cache": ["$arg_no_cache"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1985": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body eval
+qr/failed to check the configuration of plugin proxy-cache/
+--- no_error_log
+[error]
+
+
+
+=== TEST 7: sanity check (normal case)
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "proxy-cache": {
+ "cache_zone": "disk_cache_one",
+ "cache_bypass": ["$arg_bypass"],
+ "cache_method": ["GET"],
+ "cache_http_status": [200],
+ "hide_cache_headers": true,
+ "no_cache": ["$arg_no_cache"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1986": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello*"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 200
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 8: hit route (cache miss)
+--- request
+GET /hello
+--- response_body chop
+hello world!
+--- response_headers
+Apisix-Cache-Status: MISS
+--- no_error_log
+[error]
+
+
+
+=== TEST 9: hit route (cache hit)
+--- request
+GET /hello
+--- response_body chop
+hello world!
+--- response_headers
+Apisix-Cache-Status: HIT
+--- raw_response_headers_unlike
+Expires:
+--- no_error_log
+[error]
+
+
+
+=== TEST 10: hit route (cache bypass)
+--- request
+GET /hello?bypass=1
+--- response_body chop
+hello world!
+--- response_headers
+Apisix-Cache-Status: BYPASS
+--- no_error_log
+[error]
+
+
+
+=== TEST 11: purge cache
+--- request
+PURGE /hello
+--- error_code: 200
+--- no_error_log
+[error]
+
+
+
+=== TEST 12: hit route (nocache)
+--- request
+GET /hello?no_cache=1
+--- response_body chop
+hello world!
+--- response_headers
+Apisix-Cache-Status: MISS
+--- no_error_log
+[error]
+
+
+
+=== TEST 13: hit route (there's no cache indeed)
+--- request
+GET /hello
+--- response_body chop
+hello world!
+--- response_headers
+Apisix-Cache-Status: MISS
+--- raw_response_headers_unlike
+Expires:
+--- no_error_log
+[error]
+
+
+
+=== TEST 14: hit route (will be cached)
+--- request
+GET /hello
+--- response_body chop
+hello world!
+--- response_headers
+Apisix-Cache-Status: HIT
+--- no_error_log
+[error]
+
+
+
+=== TEST 15: hit route (not found)
+--- request
+GET /hello-not-found
+--- error_code: 404
+--- response_body eval
+qr/404 Not Found/
+--- response_headers
+Apisix-Cache-Status: MISS
+--- no_error_log
+[error]
+
+
+
+=== TEST 16: hit route (404 there's no cache indeed)
+--- request
+GET /hello-not-found
+--- error_code: 404
+--- response_body eval
+qr/404 Not Found/
+--- response_headers
+Apisix-Cache-Status: MISS
+--- no_error_log
+[error]
+
+
+
+=== TEST 17: hit route (HEAD method)
+--- request
+HEAD /hello-world
+--- error_code: 200
+--- response_headers
+Apisix-Cache-Status: MISS
+--- no_error_log
+[error]
+
+
+
+=== TEST 18: hit route (HEAD method there's no cache)
+--- request
+HEAD /hello-world
+--- error_code: 200
+--- response_headers
+Apisix-Cache-Status: MISS
+--- no_error_log
+[error]
+
+
+
+=== TEST 19: hide cache headers = false
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "proxy-cache": {
+ "cache_zone": "disk_cache_one",
+ "cache_bypass": ["$arg_bypass"],
+ "cache_method": ["GET"],
+ "cache_http_status": [200],
+ "hide_cache_headers": false,
+ "no_cache": ["$arg_no_cache"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1986": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello*"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 200
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 20: hit route (catch the cache headers)
+--- request
+GET /hello
+--- response_body chop
+hello world!
+--- response_headers
+Apisix-Cache-Status: HIT
+--- response_headers_like
+Cache-Control:
+--- no_error_log
+[error]
+
+
+
+=== TEST 21: purge cache
+--- request
+PURGE /hello
+--- error_code: 200
+--- no_error_log
+[error]
+
+
+
+=== TEST 22: purge cache (not found)
+--- request
+PURGE /hello-world
+--- error_code: 404
+--- no_error_log
+[error]
+
+
+
+=== TEST 23: invalid cache zone
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "proxy-cache": {
+ "cache_zone": "invalid_disk_cache",
+ "cache_bypass": ["$arg_bypass"],
+ "cache_method": ["GET"],
+ "cache_http_status": [200],
+ "hide_cache_headers": false,
+ "no_cache": ["$arg_no_cache"]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1986": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello*"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- error_code: 400
+--- response_body eval
+qr/cache_zone invalid_disk_cache not found/
+--- no_error_log
+[error]
+