You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by me...@apache.org on 2020/10/27 05:42:15 UTC

[apisix] branch master updated: feat: implement api breaker plugin. (#2455)

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/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 6a7dfa6  feat: implement api breaker plugin. (#2455)
6a7dfa6 is described below

commit 6a7dfa677562876538f3afe8b61f25ad2af39e20
Author: YuanSheng Wang <me...@gmail.com>
AuthorDate: Tue Oct 27 13:40:23 2020 +0800

    feat: implement api breaker plugin. (#2455)
    
    Co-authored-by: liuheng <li...@gmail.com>
---
 apisix/plugins/api-breaker.lua   | 248 +++++++++++++++
 bin/apisix                       |   1 +
 conf/config-default.yaml         |   1 +
 doc/plugins/api-breaker.md       | 117 +++++++
 doc/zh-cn/plugins/api-breaker.md | 116 +++++++
 t/APISIX.pm                      |   1 +
 t/admin/plugins.t                |   2 +-
 t/debug/debug-mode.t             |   1 +
 t/lib/server.lua                 |   9 +
 t/plugin/api-breaker.t           | 651 +++++++++++++++++++++++++++++++++++++++
 10 files changed, 1146 insertions(+), 1 deletion(-)

diff --git a/apisix/plugins/api-breaker.lua b/apisix/plugins/api-breaker.lua
new file mode 100644
index 0000000..d97577d
--- /dev/null
+++ b/apisix/plugins/api-breaker.lua
@@ -0,0 +1,248 @@
+--
+-- 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 plugin_name = "api-breaker"
+local ngx = ngx
+local math = math
+local error = error
+local ipairs = ipairs
+
+local shared_buffer = ngx.shared['plugin-'.. plugin_name]
+if not shared_buffer then
+    error("failed to get ngx.shared dict when load plugin " .. plugin_name)
+end
+
+
+local schema = {
+    type = "object",
+    properties = {
+        break_response_code = {
+            type = "integer",
+            minimum = 200,
+            maximum = 599,
+        },
+        max_breaker_sec = {
+            type = "integer",
+            minimum = 3,
+            default = 300,
+        },
+        unhealthy = {
+            type = "object",
+            properties = {
+                http_statuses = {
+                    type = "array",
+                    minItems = 1,
+                    items = {
+                        type = "integer",
+                        minimum = 500,
+                        maximum = 599,
+                    },
+                    uniqueItems = true,
+                    default = {500}
+                },
+                failures = {
+                    type = "integer",
+                    minimum = 1,
+                    default = 3,
+                }
+            },
+            default = {http_statuses = {500}, failures = 3}
+        },
+        healthy = {
+            type = "object",
+            properties = {
+                http_statuses = {
+                    type = "array",
+                    minItems = 1,
+                    items = {
+                        type = "integer",
+                        minimum = 200,
+                        maximum = 499,
+                    },
+                    uniqueItems = true,
+                    default = {200}
+                },
+                successes = {
+                    type = "integer",
+                    minimum = 1,
+                    default = 3,
+                }
+            },
+            default = {http_statuses = {200}, successes = 3}
+        }
+    },
+    required = {"break_response_code"},
+}
+
+
+-- todo: we can move this into `core.talbe`
+local function array_find(array, val)
+    for i, v in ipairs(array) do
+        if v == val then
+            return i
+        end
+    end
+
+    return nil
+end
+
+
+local function gen_healthy_key(ctx)
+    return "healthy-" .. core.request.get_host(ctx) .. ctx.var.uri
+end
+
+
+local function gen_unhealthy_key(ctx)
+    return "unhealthy-" .. core.request.get_host(ctx) .. ctx.var.uri
+end
+
+
+local function gen_lasttime_key(ctx)
+    return "unhealthy-lastime" .. core.request.get_host(ctx) .. ctx.var.uri
+end
+
+
+local _M = {
+    version = 0.1,
+    name = plugin_name,
+    priority = 1005,
+    schema = schema,
+}
+
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)
+end
+
+
+function _M.access(conf, ctx)
+    local unhealthy_key = gen_unhealthy_key(ctx)
+    -- unhealthy counts
+    local unhealthy_count, err = shared_buffer:get(unhealthy_key)
+    if err then
+        core.log.warn("failed to get unhealthy_key: ",
+                      unhealthy_key, " err: ", err)
+        return
+    end
+
+    if not unhealthy_count then
+        return
+    end
+
+    -- timestamp of the last time a unhealthy state was triggered
+    local lasttime_key = gen_lasttime_key(ctx)
+    local lasttime, err = shared_buffer:get(lasttime_key)
+    if err then
+        core.log.warn("failed to get lasttime_key: ",
+                      lasttime_key, " err: ", err)
+        return
+    end
+
+    if not lasttime then
+        return
+    end
+
+    local failure_times = math.ceil(unhealthy_count / conf.unhealthy.failures)
+    if failure_times < 1 then
+        failure_times = 1
+    end
+
+    -- cannot exceed the maximum value of the user configuration
+    local breaker_time = 2 ^ failure_times
+    if breaker_time > conf.max_breaker_sec then
+        breaker_time = conf.max_breaker_sec
+    end
+    core.log.info("breaker_time: ", breaker_time)
+
+    -- breaker
+    if lasttime + breaker_time >= ngx.time() then
+        return conf.break_response_code
+    end
+
+    return
+end
+
+
+function _M.log(conf, ctx)
+    local unhealthy_key = gen_unhealthy_key(ctx)
+    local healthy_key = gen_healthy_key(ctx)
+    local upstream_status = core.response.get_upstream_status(ctx)
+
+    if not upstream_status then
+        return
+    end
+
+    -- unhealth process
+    if array_find(conf.unhealthy.http_statuses, upstream_status) then
+        local unhealthy_count, err = shared_buffer:incr(unhealthy_key, 1, 0)
+        if err then
+            core.log.warn("failed to incr unhealthy_key: ", unhealthy_key,
+                          " err: ", err)
+        end
+        core.log.info("unhealthy_key: ", unhealthy_key, " count: ",
+                      unhealthy_count)
+
+        shared_buffer:delete(healthy_key)
+
+        -- whether the user-configured number of failures has been reached,
+        -- and if so, the timestamp for entering the unhealthy state.
+        if unhealthy_count % conf.unhealthy.failures == 0 then
+            shared_buffer:set(gen_lasttime_key(ctx), ngx.time(),
+                              conf.max_breaker_sec)
+            core.log.info("update unhealthy_key: ", unhealthy_key, " to ",
+                          unhealthy_count)
+        end
+
+        return
+    end
+
+    -- health process
+    if not array_find(conf.healthy.http_statuses, upstream_status) then
+        return
+    end
+
+    local unhealthy_count, err = shared_buffer:get(unhealthy_key)
+    if err then
+        core.log.warn("failed to `get` unhealthy_key: ", unhealthy_key,
+                      " err: ", err)
+    end
+
+    if not unhealthy_count then
+        return
+    end
+
+    local healthy_count, err = shared_buffer:incr(healthy_key, 1, 0)
+    if err then
+        core.log.warn("failed to `incr` healthy_key: ", healthy_key,
+                      " err: ", err)
+    end
+
+    -- clear related status
+    if healthy_count >= conf.healthy.successes then
+        -- stat change to normal
+        core.log.info("chagne to normal, ", healthy_key, " ", healthy_count)
+        shared_buffer:delete(gen_lasttime_key(ctx))
+        shared_buffer:delete(unhealthy_key)
+        shared_buffer:delete(healthy_key)
+    end
+
+    return
+end
+
+
+return _M
diff --git a/bin/apisix b/bin/apisix
index 5e24fac..efefcea 100755
--- a/bin/apisix
+++ b/bin/apisix
@@ -191,6 +191,7 @@ http {
     lua_shared_dict balancer_ewma_locks  10m;
     lua_shared_dict balancer_ewma_last_touched_at 10m;
     lua_shared_dict plugin-limit-count-redis-cluster-slot-lock 1m;
+    lua_shared_dict plugin-api-breaker 10m;
 
     # for openid-connect plugin
     lua_shared_dict discovery             1m; # cache for discovery metadata documents
diff --git a/conf/config-default.yaml b/conf/config-default.yaml
index 00e0cbc..688fa54 100644
--- a/conf/config-default.yaml
+++ b/conf/config-default.yaml
@@ -196,6 +196,7 @@ plugins:                          # plugin list
   - proxy-mirror
   - request-id
   - hmac-auth
+  - api-breaker
 
 stream_plugins:
   - mqtt-proxy
diff --git a/doc/plugins/api-breaker.md b/doc/plugins/api-breaker.md
new file mode 100644
index 0000000..5d94236
--- /dev/null
+++ b/doc/plugins/api-breaker.md
@@ -0,0 +1,117 @@
+<!--
+#
+# 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.
+#
+-->
+
+- [中文](../zh-cn/plugins/api-blocker.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+- [**Test Plugin**](#test-plugin)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+The plugin implements API fuse functionality to help us protect our upstream business services.
+
+> About the breaker timeout logic
+
+the code logic automatically **triggers the unhealthy state** incrementation of the number of operations.
+
+Whenever the upstream service returns a status code from the `unhealthy.http_statuses` configuration (e.g., 500), up to `unhealthy.failures` (e.g., three times) and considers the upstream service to be in an unhealthy state.
+
+The first time unhealthy status is triggered, **breaken for 2 seconds**.
+
+Then, the request is forwarded to the upstream service again after 2 seconds, and if the `unhealthy.http_statuses` status code is returned, and the count reaches `unhealthy.failures` again, **broken for 4 seconds**.
+
+and so on, 2, 4, 8, 16, 32, 64, ..., 256, 300. `300` is the maximum value of `max_breaker_sec`, allow users to specify.
+
+In an unhealthy state, when a request is forwarded to an upstream service and the status code in the `healthy.http_statuses` configuration is returned (e.g., 200) that `healthy.successes` is reached (e.g., three times), and the upstream service is considered healthy again.
+
+## Attributes
+
+| Name          | Type          | Requirement | Default | Valid      | Description                                                                 |
+| ------------- | ------------- | ----------- | ------- | ---------- | --------------------------------------------------------------------------- |
+| break_response_code           | integer | required |          | [200, ..., 600] | return error code when unhealthy |
+| max_breaker_sec | integer | optional | 300 | >=60 | Maximum breaker time(seconds) |
+| unhealthy.http_statuses | array[integer] | optional | {500}      | [500, ..., 599] | Status codes when unhealthy |
+| unhealthy.failures      | integer        | optional | 1          | >=1             | Number of consecutive error requests that triggered an unhealthy state |
+| healthy.http_statuses   | array[integer] | optional | {200, 206} | [200, ..., 499] | Status codes when healthy |
+| healthy.successes | integer        | optional | 1          | >=1             | Number of consecutive normal requests that trigger health status |
+
+## How To Enable
+
+Here's an example, enable the `api-breaker` plugin on the specified route.
+
+Response 500 or 503 three times in a row to trigger a unhealthy. Response 200 once in a row to restore healthy.
+
+```shell
+curl "http://127.0.0.1:9080/apisix/admin/routes/1" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "plugins": {
+        "api-breaker": {
+            "break_response_code": 502,
+            "unhealthy": {
+                "http_statuses": [500, 503],
+                "failures": 3
+            },
+            "healthy": {
+                "http_statuses": [200],
+                "successes": 1
+            }
+        }
+    },
+    "uri": "/hello",
+    "host": "127.0.0.1",
+}'
+```
+
+## Test Plugin
+
+Then. Like the configuration above, if your upstream service returns 500. 3 times in a row. The client will receive a 502 (break_response_code) response.
+
+```shell
+$ curl -i -X POST "http://127.0.0.1:9080/get"
+HTTP/1.1 502 Bad Gateway
+Content-Type: application/octet-stream
+Connection: keep-alive
+Server: APISIX/1.5
+
+... ...
+```
+
+## Disable Plugin
+
+When you want to disable the `api-breader` plugin, it is very simple, you can delete the corresponding json configuration in the plugin configuration, no need to restart the service, it will take effect immediately:
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "/hello",
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:1980": 1
+        }
+    }
+}'
+```
+
+The `api-breaker` plugin has been disabled now. It works for other plugins.
diff --git a/doc/zh-cn/plugins/api-breaker.md b/doc/zh-cn/plugins/api-breaker.md
new file mode 100644
index 0000000..5b90bb6
--- /dev/null
+++ b/doc/zh-cn/plugins/api-breaker.md
@@ -0,0 +1,116 @@
+<!--
+#
+# 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](../../plugins/api-blocker.md)
+
+# 目录
+
+- [**定义**](#定义)
+- [**属性列表**](#属性列表)
+- [**启用方式**](#启用方式)
+- [**测试插件**](#测试插件)
+- [**禁用插件**](#禁用插件)
+
+## 定义
+
+该插件实现 API 熔断功能,帮助我们保护上游业务服务。
+
+> 关于熔断超时逻辑
+
+由代码逻辑自动按**触发不健康状态**的次数递增运算:
+
+每当上游服务返回`unhealthy.http_statuses`配置中的状态码(比如:500),达到`unhealthy.failures`次时(比如:3 次),认为上游服务处于不健康状态。
+
+第一次触发不健康状态,**熔断 2 秒**。
+
+然后,2 秒过后重新开始转发请求到上游服务,如果继续返回`unhealthy.http_statuses`状态码,记数再次达到`unhealthy.failures`次时,**熔断 4 秒**(倍数方式)。
+
+依次类推,2, 4, 8, 16, 32, 64, ..., 256, 最大到 300。 300 是 `max_breaker_sec` 的最大值,允许自定义修改。
+
+在不健康状态时,当转发请求到上游服务并返回`healthy.http_statuses`配置中的状态码(比如:200),达到`healthy.successes`次时(比如:3 次),认为上游服务恢复健康状态。
+
+## 属性列表
+
+| 名称                    | 类型           | 必选项 | 默认值     | 有效值          | 描述                             |
+| ----------------------- | -------------- | ------ | ---------- | --------------- | -------------------------------- |
+| break_response_code | integer        | 必须   | 无         | [200, ..., 600] | 不健康返回错误码                 |
+| max_breaker_sec     | integer        | 可选   | 300        | >=60            | 最大熔断持续时间                 |
+| unhealthy.http_statuses | array[integer] | 可选   | {500}      | [500, ..., 599] | 不健康时候的状态码               |
+| unhealthy.failures      | integer        | 可选   | 1          | >=1             | 触发不健康状态的连续错误请求次数 |
+| healthy.http_statuses   | array[integer] | 可选   | {200, 206} | [200, ..., 499] | 健康时候的状态码                 |
+| healthy.successes       | integer        | 可选   | 1          | >=1             | 触发健康状态的连续正常请求次数   |
+
+## 启用方式
+
+这是一个示例,在指定的路由上启用`api-breaker`插件。
+应答 500 或 503 连续 3 次,触发熔断。应答 200 连续 1 次,恢复健康。
+
+```shell
+curl "http://127.0.0.1:9080/apisix/admin/routes/1" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "plugins": {
+        "api-breaker": {
+            "break_response_code": 502,
+            "unhealthy": {
+                "http_statuses": [500, 503],
+                "failures": 3
+            },
+            "healthy": {
+                "http_statuses": [200],
+                "successes": 1
+            }
+        }
+    },
+    "uri": "/hello",
+    "host": "127.0.0.1"
+}'
+```
+
+## 测试插件
+
+使用上游的配置,如果你的上流服务返回 500,连续 3 次。客户端将会收到 502(break_response_code)应答。
+
+```shell
+$ curl -i "http://127.0.0.1:9080/get"
+HTTP/1.1 502 Bad Gateway
+Content-Type: application/octet-stream
+Connection: keep-alive
+Server: APISIX/1.5
+
+... ...
+```
+
+## 禁用插件
+
+当想禁用`api-breaker`插件时,非常简单,只需要在插件配置中删除相应的 json 配置,无需重启服务,即可立即生效:
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+    "uri": "/hello",
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:1980": 1
+        }
+    }
+}'
+```
+
+`api-breaker` 插件现在已被禁用,它也适用于其他插件。
diff --git a/t/APISIX.pm b/t/APISIX.pm
index 17448e7..66fd0cc 100644
--- a/t/APISIX.pm
+++ b/t/APISIX.pm
@@ -241,6 +241,7 @@ _EOC_
     lua_shared_dict balancer_ewma_locks   1m;
     lua_shared_dict balancer_ewma_last_touched_at  1m;
     lua_shared_dict plugin-limit-count-redis-cluster-slot-lock 1m;
+    lua_shared_dict plugin-api-breaker 10m;
 
     resolver $dns_addrs_str;
     resolver_timeout 5;
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index 4caca83..5f3b3c6 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/\["request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","wolf-rbac","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","limit-conn","limit-count","limit-req","node-status","redirect","response-rewrite","grpc-transcode","prometheus","echo","http-logger","tcp-logger","kafka-logger","syslog", [...]
+qr/\["request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","wolf-rbac","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","node-status","redirect","response-rewrite","grpc-transcode","prometheus","echo","http-logger","tcp-logger","kafka-log [...]
 --- no_error_log
 [error]
 
diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t
index 9ad669f..0ad80ce 100644
--- a/t/debug/debug-mode.t
+++ b/t/debug/debug-mode.t
@@ -66,6 +66,7 @@ loaded plugin and sort by priority: 2000 name: authz-keycloak
 loaded plugin and sort by priority: 1010 name: proxy-mirror
 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: 1005 name: api-breaker
 loaded plugin and sort by priority: 1003 name: limit-conn
 loaded plugin and sort by priority: 1002 name: limit-count
 loaded plugin and sort by priority: 1001 name: limit-req
diff --git a/t/lib/server.lua b/t/lib/server.lua
index afed16e..0919d2a 100644
--- a/t/lib/server.lua
+++ b/t/lib/server.lua
@@ -262,6 +262,15 @@ function _M.websocket_handshake()
 end
 _M.websocket_handshake_route = _M.websocket_handshake
 
+function _M.api_breaker()
+    ngx.exit(tonumber(ngx.var.arg_code))
+end
+
+function _M.mysleep()
+    ngx.sleep(tonumber(ngx.var.arg_seconds))
+    ngx.say(ngx.var.arg_seconds)
+end
+
 local function print_uri()
     ngx.say(ngx.var.uri)
 end
diff --git a/t/plugin/api-breaker.t b/t/plugin/api-breaker.t
new file mode 100644
index 0000000..9b5ff4e
--- /dev/null
+++ b/t/plugin/api-breaker.t
@@ -0,0 +1,651 @@
+#
+# 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');
+run_tests;
+
+__DATA__
+
+=== TEST 1: sanity
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.api-breaker")
+            local ok, err = plugin.check_schema({
+                break_response_code = 502,
+                unhealthy = {
+                    http_statuses = {500},
+                    failures = 1,
+                },
+                healthy = {
+                    http_statuses = {200},
+                    successes = 1,
+                },
+            })
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say("done")
+        }
+    }
+--- request
+GET /t
+--- response_body
+done
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: default configuration
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.api-breaker")
+            local conf = {
+                break_response_code = 502
+            }
+
+            local ok, err = plugin.check_schema(conf)
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say(require("lib.json_sort").encode(conf))
+        }
+    }
+--- request
+GET /t
+--- response_body
+{"break_response_code":502,"healthy":{"http_statuses":[200],"successes":3},"max_breaker_sec":300,"unhealthy":{"failures":3,"http_statuses":[500]}}
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: default `healthy`
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.api-breaker")
+            local conf = {
+                break_response_code = 502,
+                healthy = {}
+            }
+
+            local ok, err = plugin.check_schema(conf)
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say(require("lib.json_sort").encode(conf))
+        }
+    }
+--- request
+GET /t
+--- response_body
+{"break_response_code":502,"healthy":{"http_statuses":[200],"successes":3},"max_breaker_sec":300,"unhealthy":{"failures":3,"http_statuses":[500]}}
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: default `unhealthy`
+--- config
+    location /t {
+        content_by_lua_block {
+            local plugin = require("apisix.plugins.api-breaker")
+            local conf = {
+                break_response_code = 502,
+                unhealthy = {}
+            }
+
+            local ok, err = plugin.check_schema(conf)
+            if not ok then
+                ngx.say(err)
+            end
+
+            ngx.say(require("lib.json_sort").encode(conf))
+        }
+    }
+--- request
+GET /t
+--- response_body
+{"break_response_code":502,"healthy":{"http_statuses":[200],"successes":3},"max_breaker_sec":300,"unhealthy":{"failures":3,"http_statuses":[500]}}
+--- no_error_log
+[error]
+
+
+
+=== TEST 5: bad break_response_code
+--- 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": {
+                        "api-breaker": {
+                            "break_response_code": 199,
+                            "unhealthy": {
+                                "http_statuses": [500, 503],
+                                "failures": 3
+                            },
+                            "healthy": {
+                                "http_statuses": [200, 206],
+                                "successes": 3
+                            }
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/api_breaker"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin api-breaker err: property \"break_response_code\" validation failed: expected 199 to be greater than 200"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 6: bad max_breaker_sec
+--- 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": {
+                        "api-breaker": {
+                            "break_response_code": 200,
+                            "max_breaker_sec": -1
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/api_breaker"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- no_error_log
+[error]
+
+
+
+=== TEST 7: bad unhealthy.http_statuses
+--- 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": {
+                        "api-breaker": {
+                            "break_response_code": 200,
+                            "max_breaker_sec": 40,
+                            "unhealthy": {
+                                "http_statuses": [500, 603],
+                                "failures": 3
+                            },
+                            "healthy": {
+                                "http_statuses": [200, 206],
+                                "successes": 3
+                            }
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/api_breaker"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- no_error_log
+[error]
+
+
+
+=== TEST 8: same http_statuses in healthy
+--- 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": {
+                        "api-breaker": {
+                            "break_response_code": 500,
+                            "unhealthy": {
+                                "http_statuses": [500, 503],
+                                "failures": 3
+                            },
+                            "healthy": {
+                                "http_statuses": [206, 206],
+                                "successes": 3
+                            }
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/api_breaker"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin api-breaker err: property \"healthy\" validation failed: property \"http_statuses\" validation failed: expected unique items but items 2 and 1 are equal"}
+--- no_error_log
+[error]
+
+
+
+=== TEST 9: set route, http_statuses: [500, 503]
+--- 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": {
+                        "api-breaker": {
+                            "break_response_code": 599,
+                            "unhealthy": {
+                                "http_statuses": [500, 503],
+                                "failures": 3
+                            },
+                            "healthy": {
+                                "http_statuses": [200, 206],
+                                "successes": 3
+                            }
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/api_breaker"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 10: trigger breaker
+--- request eval
+[
+    "GET /api_breaker?code=200", "GET /api_breaker?code=500",
+    "GET /api_breaker?code=503", "GET /api_breaker?code=500",
+    "GET /api_breaker?code=500", "GET /api_breaker?code=500"
+]
+--- error_code eval
+[200, 500, 503, 500, 599, 599]
+--- no_error_log
+[error]
+
+
+
+=== TEST 11: trigger reset status
+--- request eval
+[
+    "GET /api_breaker?code=500", "GET /api_breaker?code=500",
+
+    "GET /api_breaker?code=200", "GET /api_breaker?code=200",
+    "GET /api_breaker?code=200",
+
+    "GET /api_breaker?code=500", "GET /api_breaker?code=500"
+]
+--- error_code eval
+[
+    500, 500,
+    200, 200, 200,
+    500, 500
+]
+--- no_error_log
+[error]
+
+
+
+=== TEST 12: trigger del healthy numeration
+--- config
+location /t {
+    content_by_lua_block {
+        local t = require("lib.test_admin").test
+        local json = require("lib.json_sort")
+
+        -- trigger to unhealth
+        for i = 1, 4 do
+            local code = t('/api_breaker?code=500', ngx.HTTP_GET)
+            ngx.say("code: ", code)
+        end
+
+        -- break for 3 seconds
+        ngx.sleep(3)
+
+        -- make a try
+        for i = 1, 4 do
+            local code = t('/api_breaker?code=200', ngx.HTTP_GET)
+            ngx.say("code: ", code)
+        end
+
+        for i = 1, 4 do
+            local code = t('/api_breaker?code=500', ngx.HTTP_GET)
+            ngx.say("code: ", code)
+        end
+    }
+}
+--- request
+GET /t
+--- response_body
+code: 500
+code: 500
+code: 500
+code: 599
+code: 200
+code: 200
+code: 200
+code: 200
+code: 500
+code: 500
+code: 500
+code: 599
+--- no_error_log
+[error]
+breaker_time: 4
+--- error_log
+breaker_time: 2
+
+
+
+=== TEST 13: add plugin with default config value
+--- 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": {
+                        "api-breaker": {
+                            "break_response_code": 502,
+                            "unhealthy": {
+                                "failures": 3
+                            },
+                            "healthy": {
+                                "successes": 3
+                            }
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/api_breaker"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 14: default value
+--- request
+GET /api_breaker?code=500
+--- error_code: 500
+--- no_error_log
+[error]
+
+
+
+=== TEST 15: trigger default value of unhealthy.http_statuses breaker
+--- request eval
+[
+    "GET /api_breaker?code=200", "GET /api_breaker?code=500",
+    "GET /api_breaker?code=503", "GET /api_breaker?code=500",
+    "GET /api_breaker?code=500", "GET /api_breaker?code=500"
+]
+--- error_code eval
+[200, 500, 503, 500, 500, 502]
+--- no_error_log
+[error]
+
+
+
+=== TEST 16: unhealthy -> timeout -> normal
+--- config
+    location /sleep1 {
+        proxy_pass "http://127.0.0.1:1980/sleep1";
+    }
+--- request eval
+[
+    "GET /api_breaker?code=500",
+    "GET /api_breaker?code=500",
+    "GET /api_breaker?code=500",
+    "GET /api_breaker?code=200",
+
+    "GET /sleep1",
+    "GET /sleep1",
+    "GET /sleep1",
+
+    "GET /api_breaker?code=200",
+    "GET /api_breaker?code=200",
+    "GET /api_breaker?code=200",
+    "GET /api_breaker?code=200",
+    "GET /api_breaker?code=200"]
+--- error_code eval
+[
+    500, 500, 500, 502,
+    200, 200, 200,
+    200, 200, 200, 200,200
+]
+--- no_error_log
+[error]
+
+
+
+=== TEST 17: unhealthy -> timeout -> unhealthy
+--- config
+location /sleep1 {
+    proxy_pass "http://127.0.0.1:1980/sleep1";
+}
+--- request eval
+[
+    "GET /api_breaker?code=500", "GET /api_breaker?code=500",
+    "GET /api_breaker?code=500", "GET /api_breaker?code=200",
+
+    "GET /sleep1", "GET /sleep1", "GET /sleep1",
+
+    "GET /api_breaker?code=500","GET /api_breaker?code=500",
+    "GET /api_breaker?code=500","GET /api_breaker?code=500"
+    ]
+--- error_code eval
+[
+    500, 500, 500, 502,
+    200, 200, 200,
+    500,502,502,502
+]
+--- no_error_log
+[error]
+
+
+
+=== TEST 18: enable plugin, unhealthy.failures=1
+--- 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": {
+                        "api-breaker": {
+                            "break_response_code": 502,
+                            "max_breaker_sec": 10,
+                            "unhealthy": {
+                                "http_statuses": [500, 503],
+                                "failures": 1
+                            },
+                            "healthy": {
+                                "successes": 3
+                            }
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/api_breaker"
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+--- no_error_log
+[error]
+
+
+
+=== TEST 19: hit route 20 times, confirm the breaker time
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local json = require("lib.json_sort")
+
+            local status_count = {}
+            for i = 1, 20 do
+                local code = t('/api_breaker?code=500', ngx.HTTP_GET)
+                code = tostring(code)
+                status_count[code] = (status_count[code] or 0) + 1
+                ngx.sleep(1)
+            end
+
+            ngx.say(json.encode(status_count))
+        }
+    }
+--- request
+GET /t
+--- no_error_log
+[error]
+phase_func(): breaker_time: 16
+--- error_log
+phase_func(): breaker_time: 2
+phase_func(): breaker_time: 4
+phase_func(): breaker_time: 8
+phase_func(): breaker_time: 10
+--- response_body
+{"500":4,"502":16}
+--- timeout: 25