You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by sp...@apache.org on 2022/12/07 06:03:40 UTC

[apisix] branch master updated: feat: add consul discovery module (#8380)

This is an automated email from the ASF dual-hosted git repository.

spacewander 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 04dbc61f9 feat: add consul discovery module (#8380)
04dbc61f9 is described below

commit 04dbc61f9447898f37c7ebd63dacd9c83c594c34
Author: Fabriceli <li...@gmail.com>
AuthorDate: Wed Dec 7 14:03:34 2022 +0800

    feat: add consul discovery module (#8380)
---
 Makefile                           |   3 +-
 apisix/discovery/consul/init.lua   | 416 ++++++++++++++++++++++++++
 apisix/discovery/consul/schema.lua |  86 ++++++
 ci/pod/docker-compose.first.yml    |   4 +-
 conf/config-default.yaml           |  23 ++
 docs/en/latest/config.json         |   1 +
 docs/en/latest/discovery/consul.md | 298 +++++++++++++++++++
 t/discovery/consul.t               | 578 +++++++++++++++++++++++++++++++++++++
 t/discovery/consul_dump.t          | 453 +++++++++++++++++++++++++++++
 9 files changed, 1859 insertions(+), 3 deletions(-)

diff --git a/Makefile b/Makefile
index 740eb2717..f5908f71f 100644
--- a/Makefile
+++ b/Makefile
@@ -283,7 +283,8 @@ install: runtime
 
 	$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/discovery
 	$(ENV_INSTALL) apisix/discovery/*.lua $(ENV_INST_LUADIR)/apisix/discovery/
-	$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/discovery/{consul_kv,dns,eureka,nacos,kubernetes,tars}
+	$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/discovery/{consul,consul_kv,dns,eureka,nacos,kubernetes,tars}
+	$(ENV_INSTALL) apisix/discovery/consul/*.lua $(ENV_INST_LUADIR)/apisix/discovery/consul
 	$(ENV_INSTALL) apisix/discovery/consul_kv/*.lua $(ENV_INST_LUADIR)/apisix/discovery/consul_kv
 	$(ENV_INSTALL) apisix/discovery/dns/*.lua $(ENV_INST_LUADIR)/apisix/discovery/dns
 	$(ENV_INSTALL) apisix/discovery/eureka/*.lua $(ENV_INST_LUADIR)/apisix/discovery/eureka
diff --git a/apisix/discovery/consul/init.lua b/apisix/discovery/consul/init.lua
new file mode 100644
index 000000000..dd8275e7d
--- /dev/null
+++ b/apisix/discovery/consul/init.lua
@@ -0,0 +1,416 @@
+--
+-- 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 require            = require
+local local_conf         = require("apisix.core.config_local").local_conf()
+local core               = require("apisix.core")
+local core_sleep         = require("apisix.core.utils").sleep
+local resty_consul       = require('resty.consul')
+local http               = require('resty.http')
+local util               = require("apisix.cli.util")
+local ipairs             = ipairs
+local error              = error
+local ngx                = ngx
+local unpack             = unpack
+local tonumber           = tonumber
+local pairs              = pairs
+local ngx_timer_at       = ngx.timer.at
+local ngx_timer_every    = ngx.timer.every
+local log                = core.log
+local json_delay_encode  = core.json.delay_encode
+local ngx_worker_id      = ngx.worker.id
+
+local all_services = core.table.new(0, 5)
+local default_service
+local default_weight
+local skip_service_map = core.table.new(0, 1)
+local dump_params
+
+local events
+local events_list
+local consul_services
+
+local _M = {
+    version = 0.1,
+}
+
+
+local function discovery_consul_callback(data, event, source, pid)
+    all_services = data
+    log.notice("update local variable all_services, event is: ", event,
+            "source: ", source, "server pid:", pid,
+            ", all services: ", json_delay_encode(all_services, true))
+end
+
+
+function _M.all_nodes()
+    return all_services
+end
+
+
+function _M.nodes(service_name)
+    if not all_services then
+        log.error("all_services is nil, failed to fetch nodes for : ", service_name)
+        return
+    end
+
+    local resp_list = all_services[service_name]
+
+    if not resp_list then
+        log.error("fetch nodes failed by ", service_name, ", return default service")
+        return default_service and {default_service}
+    end
+
+    log.info("process id: ", ngx_worker_id(), ", all_services[", service_name, "] = ",
+            json_delay_encode(resp_list, true))
+
+    return resp_list
+end
+
+
+local function parse_instance(node)
+    local service_name, host, port = node.Service, node.Address, node.Port
+    -- if exist, skip special service name
+    if service_name and skip_service_map[service_name] then
+        return false
+    end
+    -- "" means metadata of the service
+    return true, host, tonumber(port), "", service_name
+end
+
+
+local function update_all_services(server_name_prefix, data)
+    local up_services = core.table.new(0, #data)
+    local weight = default_weight
+    for _, node in pairs(data) do
+        local succ, ip, port, metadata, server_name = parse_instance(node)
+        if succ then
+            local nodes = up_services[server_name]
+            if not nodes then
+                nodes = core.table.new(1, 0)
+                up_services[server_name] = nodes
+            end
+            core.table.insert(nodes, {
+                host = ip,
+                port = port,
+                weight = metadata and metadata.weight or weight,
+            })
+        end
+    end
+
+    -- clean old unused data
+    local old_services = consul_services[server_name_prefix] or {}
+    for k, _ in pairs(old_services) do
+        all_services[k] = nil
+    end
+    core.table.clear(old_services)
+
+    for k, v in pairs(up_services) do
+        all_services[k] = v
+    end
+    consul_services[server_name_prefix] = up_services
+
+    log.info("update all services: ", json_delay_encode(all_services, true))
+end
+
+
+local function read_dump_services()
+    local data, err = util.read_file(dump_params.path)
+    if not data then
+        log.error("read dump file get error: ", err)
+        return
+    end
+
+    log.info("read dump file: ", data)
+    data = util.trim(data)
+    if #data == 0 then
+        log.error("dump file is empty")
+        return
+    end
+
+    local entity, err = core.json.decode(data)
+    if not entity then
+        log.error("decoded dump data got error: ", err, ", file content: ", data)
+        return
+    end
+
+    if not entity.services or not entity.last_update then
+        log.warn("decoded dump data miss fields, file content: ", data)
+        return
+    end
+
+    local now_time = ngx.time()
+    log.info("dump file last_update: ", entity.last_update, ", dump_params.expire: ",
+            dump_params.expire, ", now_time: ", now_time)
+    if dump_params.expire ~= 0  and (entity.last_update + dump_params.expire) < now_time then
+        log.warn("dump file: ", dump_params.path, " had expired, ignored it")
+        return
+    end
+
+    all_services = entity.services
+    log.info("load dump file into memory success")
+end
+
+
+local function write_dump_services()
+    local entity = {
+        services = all_services,
+        last_update = ngx.time(),
+        expire = dump_params.expire, -- later need handle it
+    }
+    local data = core.json.encode(entity)
+    local succ, err =  util.write_file(dump_params.path, data)
+    if not succ then
+        log.error("write dump into file got error: ", err)
+    end
+end
+
+
+local function show_dump_file()
+    if not dump_params then
+        return 503, "dump params is nil"
+    end
+
+    local data, err = util.read_file(dump_params.path)
+    if not data then
+        return 503, err
+    end
+
+    return 200, data
+end
+
+local function get_retry_delay(retry_delay)
+    if not retry_delay then
+        retry_delay = 1
+    else
+        retry_delay = retry_delay * 4
+    end
+
+    return retry_delay
+end
+
+
+function _M.connect(premature, consul_server, retry_delay)
+    if premature then
+        return
+    end
+
+    local consul_client = resty_consul:new({
+        host = consul_server.host,
+        port = consul_server.port,
+        connect_timeout = consul_server.connect_timeout,
+        read_timeout = consul_server.read_timeout,
+        default_args = consul_server.default_args,
+    })
+
+    log.info("consul_server: ", json_delay_encode(consul_server, true))
+    local watch_result, watch_err = consul_client:get(consul_server.consul_watch_sub_url)
+    local watch_error_info = (watch_err ~= nil and watch_err)
+            or ((watch_result ~= nil and watch_result.status ~= 200)
+            and watch_result.status)
+    if watch_error_info then
+        log.error("connect consul: ", consul_server.consul_server_url,
+                " by sub url: ", consul_server.consul_watch_sub_url,
+                ", got watch result: ", json_delay_encode(watch_result, true),
+                 ", with error: ", watch_error_info)
+
+        retry_delay = get_retry_delay(retry_delay)
+        log.warn("retry connecting consul after ", retry_delay, " seconds")
+        core_sleep(retry_delay)
+
+        goto ERR
+    end
+
+    log.info("connect consul: ", consul_server.consul_server_url,
+            ", watch_result status: ", watch_result.status,
+            ", watch_result.headers.index: ", watch_result.headers['X-Consul-Index'],
+            ", consul_server.index: ", consul_server.index,
+            ", consul_server: ", json_delay_encode(consul_server, true))
+
+    -- if current index different last index then update service
+    if consul_server.index ~= watch_result.headers['X-Consul-Index'] then
+
+        -- fetch all services info
+        local result, err = consul_client:get(consul_server.consul_sub_url)
+
+        local error_info = (err ~= nil and err) or
+         ((result ~= nil and result.status ~= 200) and result.status)
+
+        if error_info then
+            log.error("connect consul: ", consul_server.consul_server_url,
+                    " by sub url: ", consul_server.consul_sub_url,
+                    ", got result: ", json_delay_encode(result, true),
+                    ", with error: ", error_info)
+
+            retry_delay = get_retry_delay(retry_delay)
+            log.warn("retry connecting consul after ", retry_delay, " seconds")
+            core_sleep(retry_delay)
+
+            goto ERR
+        end
+
+        consul_server.index = watch_result.headers['X-Consul-Index']
+        -- only long connect type use index
+        if consul_server.keepalive then
+            consul_server.default_args.index = watch_result.headers['X-Consul-Index']
+        end
+        -- decode body, decode json, update service, error handling
+        if result.body then
+            log.notice("server_name: ", consul_server.consul_server_url,
+                    ", header: ", json_delay_encode(result.headers, true),
+                    ", body: ", json_delay_encode(result.body, true))
+            update_all_services(consul_server.consul_server_url, result.body)
+            --update events
+            local ok, err = events.post(events_list._source, events_list.updating, all_services)
+            if not ok then
+                log.error("post_event failure with ", events_list._source,
+                        ", update all services error: ", err)
+            end
+
+            if dump_params then
+                ngx_timer_at(0, write_dump_services)
+            end
+        end
+    end
+
+    :: ERR ::
+    local keepalive = consul_server.keepalive
+    if keepalive then
+        local ok, err = ngx_timer_at(0, _M.connect, consul_server, retry_delay)
+        if not ok then
+            log.error("create ngx_timer_at got error: ", err)
+            return
+        end
+    end
+end
+
+
+local function format_consul_params(consul_conf)
+    local consul_server_list = core.table.new(0, #consul_conf.servers)
+    local args
+
+    if consul_conf.keepalive == false then
+        args = {}
+    elseif consul_conf.keepalive then
+        args = {
+            wait = consul_conf.timeout.wait, --blocked wait!=0; unblocked by wait=0
+            index = 0,
+        }
+    end
+
+    for _, v in pairs(consul_conf.servers) do
+        local scheme, host, port, path = unpack(http.parse_uri(nil, v))
+        if scheme ~= "http" then
+            return nil, "only support consul http schema address, eg: http://address:port"
+        elseif path ~= "/" or core.string.has_suffix(v, '/') then
+            return nil, "invalid consul server address, the valid format: http://address:port"
+        end
+
+        core.table.insert(consul_server_list, {
+            host = host,
+            port = port,
+            connect_timeout = consul_conf.timeout.connect,
+            read_timeout = consul_conf.timeout.read,
+            consul_sub_url = "/agent/services",
+            consul_watch_sub_url = "/catalog/services",
+            consul_server_url = v .. "/v1",
+            weight = consul_conf.weight,
+            keepalive = consul_conf.keepalive,
+            default_args = args,
+            index = 0,
+            fetch_interval = consul_conf.fetch_interval -- fetch interval to next connect consul
+        })
+    end
+
+    return consul_server_list, nil
+end
+
+
+function _M.init_worker()
+    local consul_conf = local_conf.discovery.consul
+
+    if consul_conf.dump then
+        local dump = consul_conf.dump
+        dump_params = dump
+
+        if dump.load_on_init then
+            read_dump_services()
+        end
+    end
+
+    events = require("resty.worker.events")
+    events_list = events.event_list(
+            "discovery_consul_update_all_services",
+            "updating"
+    )
+
+    if 0 ~= ngx_worker_id() then
+        events.register(discovery_consul_callback, events_list._source, events_list.updating)
+        return
+    end
+
+    log.notice("consul_conf: ", json_delay_encode(consul_conf, true))
+    default_weight = consul_conf.weight
+    -- set default service, used when the server node cannot be found
+    if consul_conf.default_service then
+        default_service = consul_conf.default_service
+        default_service.weight = default_weight
+    end
+    if consul_conf.skip_services then
+        skip_service_map = core.table.new(0, #consul_conf.skip_services)
+        for _, v in ipairs(consul_conf.skip_services) do
+            skip_service_map[v] = true
+        end
+    end
+
+    local consul_servers_list, err = format_consul_params(consul_conf)
+    if err then
+        error(err)
+    end
+    log.info("consul_server_list: ", json_delay_encode(consul_servers_list, true))
+
+    consul_services = core.table.new(0, 1)
+    -- success or failure
+    for _, server in ipairs(consul_servers_list) do
+        local ok, err = ngx_timer_at(0, _M.connect, server)
+        if not ok then
+            error("create consul got error: " .. err)
+        end
+
+        if server.keepalive == false then
+            ngx_timer_every(server.fetch_interval, _M.connect, server)
+        end
+    end
+end
+
+
+function _M.dump_data()
+    return {config = local_conf.discovery.consul, services = all_services }
+end
+
+
+function _M.control_api()
+    return {
+        {
+            methods = {"GET"},
+            uris = {"/show_dump_file"},
+            handler = show_dump_file,
+        }
+    }
+end
+
+
+return _M
diff --git a/apisix/discovery/consul/schema.lua b/apisix/discovery/consul/schema.lua
new file mode 100644
index 000000000..3e998b015
--- /dev/null
+++ b/apisix/discovery/consul/schema.lua
@@ -0,0 +1,86 @@
+--
+-- 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.
+--
+return {
+    type = "object",
+    properties = {
+        servers = {
+            type = "array",
+            minItems = 1,
+            items = {
+                type = "string",
+            }
+        },
+        fetch_interval = {type = "integer", minimum = 1, default = 3},
+        keepalive = {
+            type = "boolean",
+            default = true
+        },
+        weight = {type = "integer", minimum = 1, default = 1},
+        timeout = {
+            type = "object",
+            properties = {
+                connect = {type = "integer", minimum = 1, default = 2000},
+                read = {type = "integer", minimum = 1, default = 2000},
+                wait = {type = "integer", minimum = 1, default = 60}
+            },
+            default = {
+                connect = 2000,
+                read = 2000,
+                wait = 60,
+            }
+        },
+        skip_services = {
+            type = "array",
+            minItems = 1,
+            items = {
+                type = "string",
+            }
+        },
+        dump = {
+            type = "object",
+            properties = {
+                path = {type = "string", minLength = 1},
+                load_on_init = {type = "boolean", default = true},
+                expire = {type = "integer", default = 0},
+            },
+            required = {"path"},
+        },
+        default_service = {
+            type = "object",
+            properties = {
+                host = {type = "string"},
+                port = {type = "integer"},
+                metadata = {
+                    type = "object",
+                    properties = {
+                        fail_timeout = {type = "integer", default = 1},
+                        weight = {type = "integer", default = 1},
+                        max_fails = {type = "integer", default = 1}
+                    },
+                    default = {
+                        fail_timeout = 1,
+                        weight = 1,
+                        max_fails = 1
+                    }
+                }
+            }
+        }
+    },
+
+    required = {"servers"}
+}
+
diff --git a/ci/pod/docker-compose.first.yml b/ci/pod/docker-compose.first.yml
index a13ad3cf1..62ef7a328 100644
--- a/ci/pod/docker-compose.first.yml
+++ b/ci/pod/docker-compose.first.yml
@@ -33,7 +33,7 @@ services:
     restart: unless-stopped
     ports:
       - "8500:8500"
-    command: [ "consul", "agent", "-server", "-bootstrap-expect=1", "-client", "0.0.0.0", "-log-level", "info", "-data-dir=/consul/data" ]
+    command: [ "consul", "agent", "-server", "-bootstrap-expect=1", "-client", "0.0.0.0", "-log-level", "info", "-data-dir=/consul/data", "-enable-script-checks" ]
     networks:
       consul_net:
 
@@ -42,7 +42,7 @@ services:
     restart: unless-stopped
     ports:
       - "8600:8500"
-    command: [ "consul", "agent", "-server", "-bootstrap-expect=1", "-client", "0.0.0.0", "-log-level", "info", "-data-dir=/consul/data" ]
+    command: [ "consul", "agent", "-server", "-bootstrap-expect=1", "-client", "0.0.0.0", "-log-level", "info", "-data-dir=/consul/data", "-enable-script-checks" ]
     networks:
       consul_net:
 
diff --git a/conf/config-default.yaml b/conf/config-default.yaml
index f14db8ef2..59a6de6b0 100755
--- a/conf/config-default.yaml
+++ b/conf/config-default.yaml
@@ -316,6 +316,29 @@ nginx_config:                     # config for render the template to generate n
 #    dump:                         # if you need, when registered nodes updated can dump into file
 #       path: "logs/consul_kv.dump"
 #       expire: 2592000            # unit sec, here is 30 day
+#  consul:
+#    servers:
+#      - "http://127.0.0.1:8500"
+#      - "http://127.0.0.1:8600"
+#    skip_services:                    # if you need to skip special services
+#      - "service_a"
+#    timeout:
+#      connect: 2000               # default 2000 ms
+#      read: 2000                  # default 2000 ms
+#      wait: 60                    # default 60 sec
+#    weight: 1                     # default 1
+#    fetch_interval: 3             # default 3 sec, only take effect for keepalive: false way
+#    keepalive: true               # default true, use the long pull way to query consul servers
+#    default_service:               # you can define default server when missing hit
+#      host: "127.0.0.1"
+#      port: 20999
+#      metadata:
+#        fail_timeout: 1           # default 1 ms
+#        weight: 1                 # default 1
+#        max_fails: 1              # default 1
+#    dump:                         # if you need, when registered nodes updated can dump into file
+#       path: "logs/consul.dump"
+#       expire: 2592000            # unit sec, here is 30 day
 #  kubernetes:
 #    ### kubernetes service discovery both support single-cluster and multi-cluster mode
 #    ### applicable to the case where the service is distributed in a single or multiple kubernetes clusters.
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index b5dd26e1d..73836ab41 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -267,6 +267,7 @@
           "items": [
             "discovery",
             "discovery/dns",
+            "discovery/consul",
             "discovery/consul_kv",
             "discovery/nacos",
             "discovery/eureka",
diff --git a/docs/en/latest/discovery/consul.md b/docs/en/latest/discovery/consul.md
new file mode 100644
index 000000000..17e13b001
--- /dev/null
+++ b/docs/en/latest/discovery/consul.md
@@ -0,0 +1,298 @@
+---
+title: consul
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+## Summary
+
+APACHE APISIX supports Consul as a service discovery
+
+## Configuration for discovery client
+
+### Configuration for Consul
+
+First of all, we need to add following configuration in `conf/config.yaml` :
+
+```yaml
+discovery:
+  consul:
+    servers:                      # make sure service name is unique in these consul servers
+      - "http://127.0.0.1:8500"
+      - "http://127.0.0.1:8600"   # `http://127.0.0.1:8500` and `http://127.0.0.1:8600` are different clusters
+    skip_services:                # if you need to skip special services
+      - "service_a"
+    timeout:
+      connect: 1000               # default 2000 ms
+      read: 1000                  # default 2000 ms
+      wait: 60                    # default 60 sec
+    weight: 1                     # default 1
+    fetch_interval: 5             # default 3 sec, only take effect for keepalive: false way
+    keepalive: true               # default true, use the long pull way to query consul servers
+    default_service:              # you can define default service when missing hit
+      host: "127.0.0.1"
+      port: 20999
+      metadata:
+        fail_timeout: 1           # default 1 ms
+        weight: 1                 # default 1
+        max_fails: 1              # default 1
+    dump:                         # if you need, when registered nodes updated can dump into file
+       path: "logs/consul.dump"
+       expire: 2592000            # unit sec, here is 30 day
+```
+
+And you can config it in short by default value:
+
+```yaml
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:8500"
+```
+
+The `keepalive` has two optional values:
+
+- `true`, default and recommend value, use the long pull way to query consul servers
+- `false`, not recommend, it would use the short pull way to query consul servers, then you can set the `fetch_interval` for fetch interval
+
+#### Dump Data
+
+When we need reload `apisix` online, as the `consul` module maybe loads data from CONSUL slower than load routes from ETCD, and would get the log at the moment before load successfully from consul:
+
+```
+ http_access_phase(): failed to set upstream: no valid upstream node
+```
+
+So, we import the `dump` function for `consul` module. When reload, would load the dump file before from consul; when the registered nodes in consul been updated, would dump the upstream nodes into file automatically.
+
+The `dump` has three optional values now:
+
+- `path`, the dump file save path
+    - support relative path, eg: `logs/consul.dump`
+    - support absolute path, eg: `/tmp/consul.dump`
+    - make sure the dump file's parent path exist
+    - make sure the `apisix` has the dump file's read-write access permission,eg: add below config in `conf/config.yaml`
+
+```yaml
+nginx_config:                     # config for render the template to generate nginx.conf
+  user: root                     # specifies the execution user of the worker process.
+```
+
+- `load_on_init`, default value is `true`
+    - if `true`, just try to load the data from the dump file before loading data from  consul when starting, does not care the dump file exists or not
+    - if `false`, ignore loading data from the dump file
+    - Whether `true` or `false`, we don't need to prepare a dump file for apisix at anytime
+- `expire`, unit sec, avoiding load expired dump data when load
+    - default `0`, it is unexpired forever
+    - recommend 2592000, which is 30 days(equals 3600 \* 24 \* 30)
+
+### Register Http API Services
+
+Now, register nodes into consul:
+
+```shell
+curl -X PUT 'http://127.0.0.1:8500/v1/agent/service/register' \
+-d '{
+  "ID": "service_a1",
+  "Name": "service_a",
+  "Tags": ["primary", "v1"],
+  "Address": "127.0.0.1",
+  "Port": 8000,
+  "Meta": {
+    "service_a_version": "4.0"
+  },
+  "EnableTagOverride": false,
+  "Weights": {
+    "Passing": 10,
+    "Warning": 1
+  }
+}'
+
+curl -X PUT 'http://127.0.0.1:8500/v1/agent/service/register' \
+-d '{
+  "ID": "service_a1",
+  "Name": "service_a",
+  "Tags": ["primary", "v1"],
+  "Address": "127.0.0.1",
+  "Port": 8002,
+  "Meta": {
+    "service_a_version": "4.0"
+  },
+  "EnableTagOverride": false,
+  "Weights": {
+    "Passing": 10,
+    "Warning": 1
+  }
+}'
+```
+
+In some case, same service name exist in different consul servers.
+To avoid confusion, use the full consul key url path as service name in practice.
+
+### Upstream setting
+
+Here is an example of routing a request with a URL of "/*" to a service which named "service_a" and use consul discovery client in the registry :
+
+```shell
+$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
+{
+    "uri": "/*",
+    "upstream": {
+        "service_name": "service_a",
+        "type": "roundrobin",
+        "discovery_type": "consul"
+    }
+}'
+```
+
+The format response as below:
+
+```json
+{
+  "key": "/apisix/routes/1",
+  "value": {
+    "uri": "/*",
+    "priority": 0,
+    "id": "1",
+    "upstream": {
+      "scheme": "http",
+      "type": "roundrobin",
+      "hash_on": "vars",
+      "discovery_type": "consul",
+      "service_name": "service_a",
+      "pass_host": "pass"
+    },
+    "create_time": 1669267329,
+    "status": 1,
+    "update_time": 1669267329
+  }
+}
+```
+
+You could find more usage in the `apisix/t/discovery/consul.t` file.
+
+## Debugging API
+
+It also offers control api for debugging.
+
+### Memory Dump API
+
+```shell
+GET /v1/discovery/consul/dump
+```
+
+For example:
+
+```shell
+# curl http://127.0.0.1:9090/v1/discovery/consul/dump | jq
+{
+  "config": {
+    "fetch_interval": 3,
+    "timeout": {
+      "wait": 60,
+      "connect": 6000,
+      "read": 6000
+    },
+    "weight": 1,
+    "servers": [
+      "http://172.19.5.30:8500",
+      "http://172.19.5.31:8500"
+    ],
+    "keepalive": true,
+    "default_service": {
+      "host": "172.19.5.11",
+      "port": 8899,
+      "metadata": {
+        "fail_timeout": 1,
+        "weight": 1,
+        "max_fails": 1
+      }
+    },
+    "skip_services": [
+      "service_d"
+    ]
+  },
+  "services": {
+    "service_a": [
+      {
+        "host": "127.0.0.1",
+        "port": 30513,
+        "weight": 1
+      },
+      {
+        "host": "127.0.0.1",
+        "port": 30514,
+        "weight": 1
+      }
+    ],
+    "service_b": [
+      {
+        "host": "172.19.5.51",
+        "port": 50051,
+        "weight": 1
+      }
+    ],
+    "service_c": [
+      {
+        "host": "127.0.0.1",
+        "port": 30511,
+        "weight": 1
+      },
+      {
+        "host": "127.0.0.1",
+        "port": 30512,
+        "weight": 1
+      }
+    ]
+  }
+}
+```
+
+### Show Dump File API
+
+It offers another control api for dump file view now. Maybe would add more api for debugging in future.
+
+```shell
+GET /v1/discovery/consul/show_dump_file
+```
+
+For example:
+
+```shell
+curl http://127.0.0.1:9090/v1/discovery/consul/show_dump_file | jq
+{
+  "services": {
+    "service_a": [
+      {
+        "host": "172.19.5.12",
+        "port": 8000,
+        "weight": 120
+      },
+      {
+        "host": "172.19.5.13",
+        "port": 8000,
+        "weight": 120
+      }
+    ]
+  },
+  "expire": 0,
+  "last_update": 1615877468
+}
+```
diff --git a/t/discovery/consul.t b/t/discovery/consul.t
new file mode 100644
index 000000000..39c5ab287
--- /dev/null
+++ b/t/discovery/consul.t
@@ -0,0 +1,578 @@
+#
+# 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);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    my $http_config = $block->http_config // <<_EOC_;
+
+    server {
+        listen 20999;
+
+        location / {
+            content_by_lua_block {
+                ngx.say("missing consul services")
+            }
+        }
+    }
+
+    server {
+        listen 30511;
+
+        location /hello {
+            content_by_lua_block {
+                ngx.say("server 1")
+            }
+        }
+    }
+    server {
+        listen 30512;
+
+        location /hello {
+            content_by_lua_block {
+                ngx.say("server 2")
+            }
+        }
+    }
+    server {
+        listen 30513;
+
+        location /hello {
+            content_by_lua_block {
+                ngx.say("server 3")
+            }
+        }
+    }
+    server {
+        listen 30514;
+
+        location /hello {
+            content_by_lua_block {
+                ngx.say("server 4")
+            }
+        }
+    }
+_EOC_
+
+    $block->set_value("http_config", $http_config);
+});
+
+our $yaml_config = <<_EOC_;
+apisix:
+  node_listen: 1984
+  enable_control: true
+  control:
+    ip: 127.0.0.1
+    port: 9090
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:8500"
+      - "http://127.0.0.1:8600"
+    skip_services:
+      - "service_c"
+    timeout:
+      connect: 1000
+      read: 1000
+      wait: 60
+    weight: 1
+    fetch_interval: 1
+    keepalive: true
+    default_service:
+      host: "127.0.0.1"
+      port: 20999
+      metadata:
+        fail_timeout: 1
+        weight: 1
+        max_fails: 1
+_EOC_
+
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: prepare consul catalog register nodes
+--- config
+location /consul1 {
+    rewrite  ^/consul1/(.*) /v1/agent/service/$1 break;
+    proxy_pass http://127.0.0.1:8500;
+}
+
+location /consul2 {
+    rewrite  ^/consul2/(.*) /v1/agent/service/$1 break;
+    proxy_pass http://127.0.0.1:8600;
+}
+--- pipelined_requests eval
+[
+    "PUT /consul1/deregister/service_a1",
+    "PUT /consul1/deregister/service_b1",
+    "PUT /consul1/deregister/service_a2",
+    "PUT /consul1/deregister/service_b2",
+    "PUT /consul2/deregister/service_a1",
+    "PUT /consul2/deregister/service_b1",
+    "PUT /consul2/deregister/service_a2",
+    "PUT /consul2/deregister/service_b2",
+    "PUT /consul1/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30511,\"Meta\":{\"service_a_version\":\"4.0\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+    "PUT /consul1/register\n" . "{\"ID\":\"service_a2\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30512,\"Meta\":{\"service_a_version\":\"4.0\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+    "PUT /consul1/register\n" . "{\"ID\":\"service_b1\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30513,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+    "PUT /consul1/register\n" . "{\"ID\":\"service_b2\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30514,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+]
+--- error_code eval
+[200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200]
+
+
+
+=== TEST 2: test consul server 1
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_a
+      discovery_type: consul
+      type: roundrobin
+#END
+--- pipelined_requests eval
+[
+    "GET /hello",
+    "GET /hello",
+]
+--- response_body_like eval
+[
+    qr/server [1-2]\n/,
+    qr/server [1-2]\n/,
+]
+--- no_error_log
+[error, error]
+
+
+
+=== TEST 3: test consul server 2
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_b
+      discovery_type: consul
+      type: roundrobin
+#END
+--- pipelined_requests eval
+[
+    "GET /hello",
+    "GET /hello"
+]
+--- response_body_like eval
+[
+    qr/server [3-4]\n/,
+    qr/server [3-4]\n/,
+]
+--- no_error_log
+[error, error]
+
+
+
+=== TEST 4: test mini consul config
+--- yaml_config
+apisix:
+  node_listen: 1984
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:8500"
+      - "http://127.0.0.1:6500"
+#END
+--- apisix_yaml
+routes:
+  -
+    uri: /hello
+    upstream:
+      service_name: service_a
+      discovery_type: consul
+      type: roundrobin
+#END
+--- request
+GET /hello
+--- response_body_like eval
+qr/server [1-2]/
+--- ignore_error_log
+
+
+
+=== TEST 5: test invalid service name sometimes the consul key maybe deleted by mistake
+
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_c
+      discovery_type: consul
+      type: roundrobin
+#END
+--- pipelined_requests eval
+[
+    "GET /hello_api",
+    "GET /hello_api"
+]
+--- response_body eval
+[
+    "missing consul services\n",
+    "missing consul services\n"
+]
+--- ignore_error_log
+
+
+
+=== TEST 6: test skip keys
+skip some services, return default nodes, get response: missing consul services
+--- yaml_config
+apisix:
+  node_listen: 1984
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:8600"
+    prefix: "upstreams"
+    skip_services:
+      - "service_a"
+    default_service:
+      host: "127.0.0.1"
+      port: 20999
+      metadata:
+        fail_timeout: 1
+        weight: 1
+        max_fails: 1
+#END
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_a
+      discovery_type: consul
+      type: roundrobin
+#END
+--- request
+GET /hello
+--- response_body eval
+"missing consul services\n"
+--- ignore_error_log
+
+
+
+=== TEST 7: test register and unregister nodes
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_a
+      discovery_type: consul
+      type: roundrobin
+#END
+--- config
+location /v1/agent {
+    proxy_pass http://127.0.0.1:8500;
+}
+location /sleep {
+    content_by_lua_block {
+        local args = ngx.req.get_uri_args()
+        local sec = args.sec or "2"
+        ngx.sleep(tonumber(sec))
+        ngx.say("ok")
+    }
+}
+--- timeout: 6
+--- request eval
+[
+    "PUT /v1/agent/service/deregister/service_a1",
+    "PUT /v1/agent/service/deregister/service_a2",
+    "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30513,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+    "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a2\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30514,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+    "GET /sleep",
+
+    "GET /hello?random1",
+    "GET /hello?random2",
+    "GET /hello?random3",
+    "GET /hello?random4",
+
+    "PUT /v1/agent/service/deregister/service_a1",
+    "PUT /v1/agent/service/deregister/service_a2",
+    "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30511,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+    "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a2\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30512,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+    "GET /sleep?sec=5",
+
+    "GET /hello?random1",
+    "GET /hello?random2",
+    "GET /hello?random3",
+    "GET /hello?random4",
+
+]
+--- response_body_like eval
+[
+    qr//,
+    qr//,
+    qr//,
+    qr//,
+    qr/ok\n/,
+
+    qr/server [3-4]\n/,
+    qr/server [3-4]\n/,
+    qr/server [3-4]\n/,
+    qr/server [3-4]\n/,
+
+    qr//,
+    qr//,
+    qr//,
+    qr//,
+    qr/ok\n/,
+
+    qr/server [1-2]\n/,
+    qr/server [1-2]\n/,
+    qr/server [1-2]\n/,
+    qr/server [1-2]\n/
+]
+--- ignore_error_log
+
+
+
+=== TEST 8: clean nodes
+--- config
+location /v1/agent {
+    proxy_pass http://127.0.0.1:8500;
+}
+--- request eval
+[
+    "PUT /v1/agent/service/deregister/service_a1",
+    "PUT /v1/agent/service/deregister/service_a2",
+]
+--- error_code eval
+[200, 200]
+
+
+
+=== TEST 9: test consul short connect type
+--- yaml_config
+apisix:
+  node_listen: 1984
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:8500"
+    keepalive: false
+    fetch_interval: 3
+    default_service:
+      host: "127.0.0.1"
+      port: 20999
+#END
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_a
+      discovery_type: consul
+      type: roundrobin
+#END
+--- config
+location /v1/agent {
+    proxy_pass http://127.0.0.1:8500;
+}
+location /sleep {
+    content_by_lua_block {
+        local args = ngx.req.get_uri_args()
+        local sec = args.sec or "2"
+        ngx.sleep(tonumber(sec))
+        ngx.say("ok")
+    }
+}
+--- timeout: 6
+--- request eval
+[
+    "GET /hello",
+    "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30511,\"Meta\":{\"service_a_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+    "GET /sleep?sec=5",
+    "GET /hello",
+]
+--- response_body_like eval
+[
+    qr/missing consul services\n/,
+    qr//,
+    qr/ok\n/,
+    qr/server 1\n/
+]
+--- ignore_error_log
+
+
+
+=== TEST 10: retry when Consul can't be reached (long connect type)
+--- yaml_config
+apisix:
+  node_listen: 1984
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:8501"
+    keepalive: true
+    fetch_interval: 3
+    default_service:
+      host: "127.0.0.1"
+      port: 20999
+#END
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_a
+      discovery_type: consul
+      type: roundrobin
+#END
+--- timeout: 4
+--- config
+location /sleep {
+    content_by_lua_block {
+        local args = ngx.req.get_uri_args()
+        local sec = args.sec or "2"
+        ngx.sleep(tonumber(sec))
+        ngx.say("ok")
+    }
+}
+--- request
+GET /sleep?sec=3
+--- response_body
+ok
+--- grep_error_log eval
+qr/retry connecting consul after \d seconds/
+--- grep_error_log_out
+retry connecting consul after 1 seconds
+retry connecting consul after 4 seconds
+
+
+
+=== TEST 11: prepare healthy and unhealthy nodes
+--- config
+location /v1/agent {
+    proxy_pass http://127.0.0.1:8500;
+}
+--- request eval
+[
+    "PUT /v1/agent/service/deregister/service_a1",
+    "PUT /v1/agent/service/deregister/service_a2",
+    "PUT /v1/agent/service/deregister/service_b1",
+    "PUT /v1/agent/service/deregister/service_b2",
+    "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_b1\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30513,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+    "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_b2\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30514,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+]
+--- error_code eval
+[200, 200, 200, 200, 200, 200]
+
+
+
+=== TEST 12: test health checker
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+  -
+    uris:
+        - /hello
+    upstream_id: 1
+upstreams:
+    -
+      service_name: service_b
+      discovery_type: consul
+      type: roundrobin
+      id: 1
+      checks:
+        active:
+            http_path: "/hello"
+            healthy:
+                interval: 1
+                successes: 1
+            unhealthy:
+                interval: 1
+                http_failures: 1
+#END
+--- config
+    location /thc {
+        content_by_lua_block {
+            local json = require("toolkit.json")
+            local t = require("lib.test_admin")
+            local http = require "resty.http"
+            local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+            local httpc = http.new()
+            httpc:request_uri(uri, {method = "GET"})
+            ngx.sleep(3)
+
+            local code, body, res = t.test('/v1/healthcheck',
+                ngx.HTTP_GET)
+            res = json.decode(res)
+            local nodes = res[1].nodes
+            table.sort(nodes, function(a, b)
+                return a.port < b.port
+            end)
+            ngx.say(json.encode(nodes))
+
+            local code, body, res = t.test('/v1/healthcheck/upstreams/1',
+                ngx.HTTP_GET)
+            res = json.decode(res)
+            nodes = res.nodes
+            table.sort(nodes, function(a, b)
+                return a.port < b.port
+            end)
+            ngx.say(json.encode(nodes))
+        }
+    }
+--- request
+GET /thc
+--- response_body
+[{"host":"127.0.0.1","port":30513,"priority":0,"weight":1},{"host":"127.0.0.1","port":30514,"priority":0,"weight":1}]
+[{"host":"127.0.0.1","port":30513,"priority":0,"weight":1},{"host":"127.0.0.1","port":30514,"priority":0,"weight":1}]
+--- ignore_error_log
diff --git a/t/discovery/consul_dump.t b/t/discovery/consul_dump.t
new file mode 100644
index 000000000..b229fb7a4
--- /dev/null
+++ b/t/discovery/consul_dump.t
@@ -0,0 +1,453 @@
+#
+# 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_root_location();
+no_shuffle();
+log_level("info");
+
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    my $http_config = $block->http_config // <<_EOC_;
+
+    server {
+        listen 30511;
+
+        location /hello {
+            content_by_lua_block {
+                ngx.say("server 1")
+            }
+        }
+    }
+_EOC_
+
+    $block->set_value("http_config", $http_config);
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: prepare nodes
+--- config
+location /v1/agent {
+    proxy_pass http://127.0.0.1:8500;
+}
+--- request eval
+[
+    "PUT /v1/agent/service/deregister/service_a1",
+    "PUT /v1/agent/service/deregister/service_a2",
+    "PUT /v1/agent/service/deregister/service_b1",
+    "PUT /v1/agent/service/deregister/service_b2",
+    "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30511,\"Meta\":{\"service_a_version\":\"4.0\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+    "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_b1\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":8002,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+]
+--- response_body eval
+--- error_code eval
+[200, 200, 200, 200, 200, 200]
+
+
+
+=== TEST 2: show dump services
+--- yaml_config
+apisix:
+  node_listen: 1984
+  enable_control: true
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:8500"
+    dump:
+      path: "consul.dump"
+      load_on_init: true
+--- config
+    location /t {
+        content_by_lua_block {
+            local json = require("toolkit.json")
+            local t = require("lib.test_admin")
+            ngx.sleep(2)
+
+            local code, body, res = t.test('/v1/discovery/consul/show_dump_file',
+                ngx.HTTP_GET)
+            local entity = json.decode(res)
+            ngx.say(json.encode(entity.services))
+        }
+    }
+--- timeout: 3
+--- request
+GET /t
+--- response_body
+{"service_a":[{"host":"127.0.0.1","port":30511,"weight":1}],"service_b":[{"host":"127.0.0.1","port":8002,"weight":1}]}
+
+
+
+=== TEST 3: prepare dump file for next test
+--- yaml_config
+apisix:
+  node_listen: 1984
+  enable_control: true
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:8500"
+    dump:
+      path: "/tmp/consul.dump"
+      load_on_init: false
+#END
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_a
+      discovery_type: consul
+      type: roundrobin
+#END
+--- request
+GET /hello
+--- response_body
+server 1
+
+
+
+=== TEST 4: clean registered nodes
+--- config
+location /v1/agent {
+    proxy_pass http://127.0.0.1:8500;
+}
+--- request eval
+[
+    "PUT /v1/agent/service/deregister/service_a1",
+    "PUT /v1/agent/service/deregister/service_b1",
+]
+--- error_code eval
+[200, 200]
+
+
+
+=== TEST 5: test load dump on init
+Configure the invalid consul server addr, and loading the last test 3 generated /tmp/consul.dump file into memory when initializing
+--- yaml_config
+apisix:
+  node_listen: 1984
+  enable_control: true
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:38500"
+    dump:
+      path: "/tmp/consul.dump"
+      load_on_init: true
+#END
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_a
+      discovery_type: consul
+      type: roundrobin
+#END
+--- request
+GET /hello
+--- response_body
+server 1
+--- error_log
+connect consul
+
+
+
+=== TEST 6: delete dump file
+--- config
+    location /t {
+        content_by_lua_block {
+            local util = require("apisix.cli.util")
+            local succ, err = util.execute_cmd("rm -f /tmp/consul.dump")
+            ngx.say(succ and "success" or err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+success
+
+
+
+=== TEST 7: miss load dump on init
+--- yaml_config
+apisix:
+  node_listen: 1984
+  enable_control: true
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:38500"
+    dump:
+      path: "/tmp/consul.dump"
+      load_on_init: true
+#END
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_a
+      discovery_type: consul
+      type: roundrobin
+#END
+--- request
+GET /hello
+--- error_code: 503
+--- error_log
+connect consul
+fetch nodes failed
+failed to set upstream
+
+
+
+=== TEST 8: prepare expired dump file
+--- config
+    location /t {
+        content_by_lua_block {
+            local util = require("apisix.cli.util")
+            local json = require("toolkit.json")
+
+            local applications = json.decode('{"service_a":[{"host":"127.0.0.1","port":30511,"weight":1}]}')
+            local entity = {
+                services = applications,
+                last_update = ngx.time(),
+                expire = 10,
+            }
+            local succ, err =  util.write_file("/tmp/consul.dump", json.encode(entity))
+
+            ngx.sleep(2)
+            ngx.say(succ and "success" or err)
+        }
+    }
+--- timeout: 3
+--- request
+GET /t
+--- response_body
+success
+
+
+
+=== TEST 9: unexpired dump
+test load unexpired /tmp/consul.dump file generated by upper test when initializing
+ when initializing
+--- yaml_config
+apisix:
+  node_listen: 1984
+  enable_control: true
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:38500"
+    dump:
+      path: "/tmp/consul.dump"
+      load_on_init: true
+      expire: 5
+#END
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_a
+      discovery_type: consul
+      type: roundrobin
+#END
+--- request
+GET /hello
+--- response_body
+server 1
+--- error_log
+connect consul
+
+
+
+=== TEST 10: expired dump
+test load expired ( by check: (dump_file.last_update + dump.expire) < ngx.time ) ) /tmp/consul.dump file generated by upper test when initializing
+ when initializing
+--- yaml_config
+apisix:
+  node_listen: 1984
+  enable_control: true
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:38500"
+    dump:
+      path: "/tmp/consul.dump"
+      load_on_init: true
+      expire: 1
+#END
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: service_a
+      discovery_type: consul
+      type: roundrobin
+#END
+--- request
+GET /hello
+--- error_code: 503
+--- error_log
+dump file: /tmp/consul.dump had expired, ignored it
+
+
+
+=== TEST 11: delete dump file
+--- config
+    location /t {
+        content_by_lua_block {
+            local util = require("apisix.cli.util")
+            local succ, err = util.execute_cmd("rm -f /tmp/consul.dump")
+            ngx.say(succ and "success" or err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+success
+
+
+
+=== TEST 12: dump file inexistence
+--- yaml_config
+apisix:
+  node_listen: 1984
+  enable_control: true
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:38500"
+    dump:
+      path: "/tmp/consul.dump"
+#END
+--- request
+GET /v1/discovery/consul/show_dump_file
+--- error_code: 503
+--- error_log
+connect consul
+
+
+
+=== TEST 13: no dump config
+--- yaml_config
+apisix:
+  node_listen: 1984
+  enable_control: true
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:38500"
+#END
+--- request
+GET /v1/discovery/consul/show_dump_file
+--- error_code: 503
+--- error_log
+connect consul
+
+
+
+=== TEST 14: prepare nodes with different consul clusters
+--- config
+location /consul1 {
+    rewrite  ^/consul1/(.*) /v1/agent/service/$1 break;
+    proxy_pass http://127.0.0.1:8500;
+}
+
+location /consul2 {
+    rewrite  ^/consul2/(.*) /v1/agent/service/$1 break;
+    proxy_pass http://127.0.0.1:8600;
+}
+--- pipelined_requests eval
+[
+    "PUT /consul1/deregister/service_a1",
+    "PUT /consul1/deregister/service_b1",
+    "PUT /consul1/deregister/service_a2",
+    "PUT /consul1/deregister/service_b2",
+    "PUT /consul2/deregister/service_a1",
+    "PUT /consul2/deregister/service_b1",
+    "PUT /consul2/deregister/service_a2",
+    "PUT /consul2/deregister/service_b2",
+    "PUT /consul1/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30511,\"Meta\":{\"service_a_version\":\"4.0\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+    "PUT /consul2/register\n" . "{\"ID\":\"service_b1\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30517,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+]
+--- error_code eval
+[200, 200, 200, 200, 200, 200, 200, 200, 200, 200]
+
+
+
+=== TEST 15: show dump services with different consul clusters
+--- yaml_config
+apisix:
+  node_listen: 1984
+  enable_control: true
+discovery:
+  consul:
+    servers:
+      - "http://127.0.0.1:8500"
+      - "http://127.0.0.1:8600"
+    dump:
+      path: "consul.dump"
+      load_on_init: false
+--- config
+    location /bonjour {
+        content_by_lua_block {
+            local json = require("toolkit.json")
+            local t = require("lib.test_admin")
+            ngx.sleep(2)
+
+            local code, body, res = t.test('/v1/discovery/consul/show_dump_file',
+                ngx.HTTP_GET)
+            local entity = json.decode(res)
+            ngx.say(json.encode(entity.services))
+        }
+    }
+--- timeout: 3
+--- request
+GET /bonjour
+--- response_body
+{"service_a":[{"host":"127.0.0.1","port":30511,"weight":1}],"service_b":[{"host":"127.0.0.1","port":30517,"weight":1}]}