├── .gitignore ├── .luacheckrc ├── .travis.yml ├── LICENSE ├── README.md ├── example └── client.lua ├── lib └── resty │ └── consul │ └── event.lua ├── lua-resty-consul-event-0.3.0-0.rockspec └── t ├── 01_sanity.t └── 02_watch.t /.gitignore: -------------------------------------------------------------------------------- 1 | t/servroot 2 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "ngx_lua" 2 | redefined = false 3 | max_line_length = 80 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # vim: st=2 sts=2 sw=2 et: 2 | 3 | sudo: true 4 | 5 | language: c 6 | 7 | compiler: gcc 8 | 9 | notifications: 10 | email: false 11 | 12 | cache: 13 | directories: 14 | - download-cache 15 | - perl5 16 | - $CONSUL_DIR 17 | 18 | env: 19 | global: 20 | - JOBS=2 21 | - LUAROCKS_VER=2.4.3 22 | - CONSUL_VERSION=1.3.0 23 | - CONSUL_DIR=$HOME/consul_$CONSUL_VERSION 24 | matrix: 25 | - OPENRESTY_VER=1.11.2.2 26 | - OPENRESTY_VER=1.11.2.3 27 | - OPENRESTY_VER=1.11.2.4 28 | - OPENRESTY_VER=1.11.2.5 29 | - OPENRESTY_VER=1.13.6.1 30 | - OPENRESTY_VER=1.13.6.2 31 | - LINT=1 32 | 33 | install: 34 | - mkdir -p download-cache 35 | - if [ -z "$OPENRESTY_VER" ]; then export OPENRESTY_VER=1.13.6.2; fi 36 | - if [ ! -f download-cache/openresty-$OPENRESTY_VER.tar.gz ]; then wget -O download-cache/openresty-$OPENRESTY_VER.tar.gz http://openresty.org/download/openresty-$OPENRESTY_VER.tar.gz; fi 37 | - if [ ! -f download-cache/luarocks-$LUAROCKS_VER.tar.gz ]; then wget -O download-cache/luarocks-$LUAROCKS_VER.tar.gz https://luarocks.github.io/luarocks/releases/luarocks-$LUAROCKS_VER.tar.gz; fi 38 | - if [ ! -f download-cache/cpanm ]; then wget -O download-cache/cpanm https://cpanmin.us/; fi 39 | - tar -zxf download-cache/openresty-$OPENRESTY_VER.tar.gz 40 | - tar -zxf download-cache/luarocks-$LUAROCKS_VER.tar.gz 41 | - chmod +x download-cache/cpanm 42 | - download-cache/cpanm --notest Test::Nginx > build.log 2>&1 || (cat build.log && exit 1) 43 | - download-cache/cpanm --notest --local-lib=$TRAVIS_BUILD_DIR/perl5 local::lib && eval $(perl -I $TRAVIS_BUILD_DIR/perl5/lib/perl5/ -Mlocal::lib) 44 | - pushd openresty-$OPENRESTY_VER 45 | - export OPENRESTY_PREFIX=$TRAVIS_BUILD_DIR/openresty-$OPENRESTY_VER 46 | - ./configure --prefix=$OPENRESTY_PREFIX --without-http_ssl_module -j$JOBS > build.log 2>&1 || (cat build.log && exit 1) 47 | - make -j$JOBS > build.log 2>&1 || (cat build.log && exit 1) 48 | - make install > build.log 2>&1 || (cat build.log && exit 1) 49 | - popd 50 | - pushd luarocks-$LUAROCKS_VER 51 | - ./configure --with-lua=$OPENRESTY_PREFIX/luajit --with-lua-include=$OPENRESTY_PREFIX/luajit/include/luajit-2.1 --lua-suffix=jit 52 | - make build 53 | - sudo make install 54 | - popd 55 | - export PATH=$OPENRESTY_PREFIX/nginx/sbin:$LUAROCKS_PREFIX/bin:$PATH 56 | - sudo luarocks install luacheck > build.log 2>&1 || (cat build.log && exit 1) 57 | - sudo luarocks install lua-resty-http 58 | - luarocks --version 59 | - nginx -V 60 | 61 | before_script: 62 | - 'if [[ ! -f $CONSUL_DIR/consul ]]; then (mkdir -p $CONSUL_DIR && cd $CONSUL_DIR && wget https://releases.hashicorp.com/consul/1.3.0/consul_${CONSUL_VERSION}_linux_amd64.zip && unzip consul_${CONSUL_VERSION}_linux_amd64.zip); fi' 63 | - $CONSUL_DIR/consul --version 64 | - $CONSUL_DIR/consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -bind=127.0.0.1 & 65 | # Wait for consul to elect itself as leader 66 | - sleep 10 67 | 68 | script: 69 | - if [ -z "$LINT" ]; then TEST_NGINX_RANDOMIZE=1 prove -j$JOBS -r t; else luacheck lib; fi 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2016-2018 Kong Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lua-resty-consul-events 2 | ======================= 3 | 4 | Consul Events HTTP API Wrapper 5 | 6 | # Table of Contents 7 | 8 | * [Overview](#overview) 9 | * [Dependencies](#dependencies) 10 | * [Synopsis](#synopsis) 11 | * [Usage](#usage) 12 | * [new](#new) 13 | * [watch](#watch) 14 | * [Testing](#testing) 15 | * [License](#license) 16 | 17 | # Overview 18 | 19 | This module provides an OpenResty client wrapper for the [Consul Events API](https://www.consul.io/api/event.html). This allows for OpenResty integration with Consul's custom user event mechanism, which can be used to build scripting infrastructure to do automated deploys, restart services, or perform any other orchestration action. 20 | 21 | This module leverages Consul's concept of [blocking queries](https://www.consul.io/api/index.html#blocking-queries) to watch for events broadcast on a given event name. 22 | 23 | # Dependencies 24 | 25 | * [lua-resty-http](https://github.com/pintsized/lua-resty-http) 26 | 27 | # Synopsis 28 | 29 | ```lua 30 | local event = require "resty.consul.event" 31 | 32 | local e, err = event.new({ 33 | host = "127.0.0.1", 34 | port = 8500, 35 | }) 36 | 37 | if err then 38 | ngx.log(ngx.ERR, err) 39 | end 40 | 41 | e:watch("foo", function(event) 42 | ngx.log(ngx.INFO, "i got ", ngx.decode_base64(event.payload)) 43 | end) 44 | ``` 45 | 46 | # Usage 47 | 48 | ## new 49 | 50 | `syntax: e, err = event.new(opts?)` 51 | 52 | Instantiates a new watch object. `opts` may be a table with the following options: 53 | 54 | * `host`: String defining the Consul host 55 | * `port`: Number defining the Consul port 56 | * `timeout`: Number, in seconds, to pass to Consul blocking query API via the `wait` parameter. This value is also used to to define TCP layer timeouts, which are set higher than the application-layer timeout. 57 | * `ssl_verify`: Boolean defining whether to validate the TLS certificate presented by the remote Consul server. 58 | * `token`: String defining the Consul ACL token to send via the `X-Consul-Token` request header. 59 | 60 | ## watch 61 | 62 | `syntax: e:watch(name, callback, initial_index, seen_ids)` 63 | 64 | Watch the Consul events API for events broadcast under a given `name`, and execute the function `callback` . `callback` is passed a single parameter `event`, which contains the body of a single event as defined by the [Consul Events API](https://www.consul.io/api/event.html). Callback functions are wrapped in `pcall`, so it is safe to throw an error from within this function. Callback functions may return a single value but this value is largely meaningless; currently, this single value is logged as a debug entry. 65 | 66 | The values `initial_index` and `seen_ids` are optional, and can be used to initialize the watch against a certain state in the Consul events ring. `initial_index` is expected to be a string output by a previous `X-Consul-Index` header. `seen_ids` is expected to be a list of Consul Event ID values for whom callback events should not be executed. For example, the current state of the event buffer can be used to initialize a given watch: 67 | 68 | ```lua 69 | local h = require("resty.http").new() 70 | 71 | -- get the current events 72 | local res, err = h:request_uri("http://127.0.0.1:8500/v1/event/list?name=foo") 73 | if err then 74 | ngx.log(ngx.ERR, err) 75 | end 76 | 77 | local event = require "resty.consul.event" 78 | 79 | local e, err = event.new({ 80 | host = "127.0.0.1", 81 | port = 8500, 82 | }) 83 | if err then 84 | ngx.log(ngx.ERR, err) 85 | end 86 | 87 | local l = {} 88 | 89 | for _, e in ipairs(require("cjson").decode(res.body)) do 90 | table.insert(l, e.ID) 91 | end 92 | 93 | ngx.timer.at(0, function() 94 | e:watch( 95 | "foo", 96 | function(p) ngx.log(ngx.DEBUG, p.payload) end, 97 | res.headers["X-Consul-Index"], 98 | l 99 | ) 100 | end) 101 | ``` 102 | 103 | *Note: This body of this function runs in an infinite loop in order to watch the Consul events API indefinitely. As a result, it is strongly recommended to call this function inside a background timer generated via ngx.timer.at* 104 | 105 | # Testing 106 | 107 | A test suite for this repo is provided. Tests are written using [Test::Nginx](https://metacpan.org/pod/Test::Nginx::Socket) and executed with `prove`. 108 | 109 | To best test library behavior, the suite expects a Consul server to be running and accessible. By default, Consul is accessed at `127.0.0.1:8500`; the Consul host and port can be overriden by defining the environmental variables `TEST_NGINX_CONSUL_ADDR` and `TEST_NGINX_CONSUL_PORT`, respectively. 110 | 111 | # License 112 | 113 | Copyright 2018 Kong Inc. 114 | 115 | Licensed under the Apache License, Version 2.0 (the "License"); 116 | you may not use this file except in compliance with the License. 117 | You may obtain a copy of the License at 118 | 119 | http://www.apache.org/licenses/LICENSE-2.0 120 | 121 | Unless required by applicable law or agreed to in writing, software 122 | distributed under the License is distributed on an "AS IS" BASIS, 123 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 124 | See the License for the specific language governing permissions and 125 | limitations under the License. 126 | -------------------------------------------------------------------------------- /example/client.lua: -------------------------------------------------------------------------------- 1 | local event = require "event" 2 | 3 | local e, err = event.new({ 4 | host = "127.0.0.1", 5 | port = 8500, 6 | timeout = 3, 7 | }) 8 | if not e then error(err) end 9 | 10 | e:watch( 11 | "foo", 12 | function(event) 13 | ngx.sleep(4) 14 | ngx.log(ngx.NOTICE, "i got " .. require("cjson").encode(event)) 15 | return "YEP YEP YEP" 16 | end 17 | ) 18 | -------------------------------------------------------------------------------- /lib/resty/consul/event.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | 4 | local bit = require "bit" 5 | local http = require "resty.http" 6 | local cjson = require "cjson.safe" 7 | 8 | 9 | local ngx_DEBUG = ngx.DEBUG 10 | local ngx_ERR = ngx.ERR 11 | local ngx_WARN = ngx.WARN 12 | 13 | 14 | local abs = math.abs 15 | local co_status = coroutine.status 16 | local ipairs = ipairs 17 | local insert = table.insert 18 | local lshift = bit.lshift 19 | local min = math.min 20 | local ngx_log = ngx.log 21 | local pairs = pairs 22 | local pcall = pcall 23 | local remove = table.remove 24 | local setmetatable = setmetatable 25 | local sleep = ngx.sleep 26 | local spawn = ngx.thread.spawn 27 | local tostring = tostring 28 | local wait = ngx.thread.wait 29 | local exiting = ngx.worker.exiting 30 | 31 | 32 | local EVENTS_ENDPOINT = "/v1/event/list" 33 | local INDEX_HEADER = "X-Consul-Index" 34 | local MAX_SLEEP = 30 -- 30s max sleep during failure backoff 35 | 36 | 37 | _M.version = "0.3" 38 | _M.user_agent = "lua-resty-consul-events/" .. _M.version .. 39 | "(Lua) ngx_lua/" .. ngx.config.ngx_lua_version 40 | 41 | 42 | local mt = { __index = _M } 43 | 44 | 45 | local function backoff(ctx) 46 | ctx.failures = ctx.failures + 1 47 | 48 | sleep(min(MAX_SLEEP, abs(lshift(125, min(MAX_SLEEP, ctx.failures)) / 1000))) 49 | end 50 | 51 | 52 | local function copy(o) 53 | local t = {} 54 | 55 | for k, v in pairs(o) do 56 | t[k] = v 57 | end 58 | 59 | return t 60 | end 61 | 62 | 63 | local function watch_event(ctx) 64 | local httpc = http.new() 65 | 66 | -- set the tcp timeout higher than the consul timeout 67 | httpc:set_timeout((ctx.timeout * 1000) * 2) 68 | 69 | local scheme = ctx.use_tls and "https" or "http" 70 | 71 | local path = scheme .. "://" .. ctx.host .. ":" .. 72 | tostring(ctx.port) .. EVENTS_ENDPOINT 73 | 74 | local res, err = httpc:request_uri(path, { 75 | query = { 76 | index = ctx.index, 77 | name = ctx.name, 78 | wait = ctx.timeout .. "s", 79 | }, 80 | headers = { 81 | ["User-Agent"] = _M.user_agent, 82 | ["Connection"] = "close", 83 | ["X-Consul-Token"] = ctx.token, 84 | }, 85 | ssl_verify = ctx.ssl_verify, 86 | }) 87 | 88 | if err then 89 | return { "watch", false, err } 90 | end 91 | 92 | if res.status ~= 200 then 93 | return { "watch", false, res.body } 94 | end 95 | 96 | -- check that the index has changed- if it hasn"t, 97 | -- return an empty events table 98 | if res.headers[INDEX_HEADER] == ctx.index then 99 | ngx_log(ngx_DEBUG, "returned identical index") 100 | return { "watch", true, {} } 101 | end 102 | 103 | -- new event(s). consul's interface isn't great here 104 | -- we have no way to know which events returned we have 105 | -- already processed, so we need to walk each event and 106 | -- confirm that the ID is newer than the most recent 107 | -- event we've seen 108 | local events, err = cjson.decode(res.body) 109 | if not events then 110 | return { "watch", false, err } 111 | end 112 | 113 | local new_events = {} 114 | 115 | for _, event in ipairs(events) do 116 | if not ctx.id_lru:get(event.ID) then 117 | ctx.id_lru:set(event.ID, true) 118 | 119 | insert(new_events, event) 120 | end 121 | end 122 | 123 | ctx.index = res.headers[INDEX_HEADER] 124 | 125 | return { "watch", true, new_events } 126 | end 127 | 128 | 129 | local function fire_callback(ctx, event) 130 | local ok, err = pcall(ctx.callback, event) 131 | 132 | return { "event", ok, err } 133 | end 134 | 135 | 136 | -- spawns a thread to listen for events 137 | -- when a response is received, respawn the listen thread, 138 | -- and spawn threads for each event callback 139 | function _M:watch(name, callback, initial_index, seen_ids) 140 | local t = {} -- threads table 141 | 142 | local ctx = copy(self) 143 | ctx.name = name 144 | ctx.callback = callback 145 | 146 | if initial_index then 147 | ctx.index = initial_index 148 | end 149 | 150 | if seen_ids then 151 | for i = 1, #seen_ids do 152 | ctx.id_lru:set(seen_ids[i], true) 153 | end 154 | end 155 | 156 | -- bootstrap the first event watch thread 157 | insert(t, spawn(watch_event, ctx)) 158 | 159 | while true do 160 | -- wait on our threads. if we got back a watch event, 161 | -- spawn the necessary callbacks and a new watch thread 162 | -- otherwise, we just wait again 163 | 164 | -- res is expected to be an array containing 3 values: 165 | -- 1. a string indicating the type of thread executed 166 | -- 2. a boolean indicating the function response status 167 | -- 3. a table containing context-specific return data 168 | -- in the case of "watch" return types, this value 169 | -- is a table containing the event details, if any. 170 | -- for "event" return types, this value is the return 171 | -- of the callback function. because callback functions 172 | -- are wrapped in pcall(), this value may either be the 173 | -- function return or a bubbled error 174 | local ok, res = wait(unpack(t)) 175 | 176 | if not ok then 177 | ngx_log(ngx_ERR, res) 178 | end 179 | 180 | for i, co in ipairs(t) do 181 | if co_status(co) == "dead" then 182 | remove(t, i) 183 | end 184 | end 185 | 186 | if exiting() then 187 | break 188 | end 189 | 190 | if res[1] == "watch" then 191 | if not res[2] then 192 | ngx_log(ngx_ERR, "error in fetching events: ", res[3]) 193 | 194 | backoff(ctx) 195 | 196 | else 197 | ctx.failures = 0 198 | 199 | for _, event in ipairs(res[3]) do 200 | insert(t, spawn(fire_callback, ctx, event)) 201 | end 202 | end 203 | 204 | -- after firing any potential callbacks, re-spawn a 205 | -- thread to watch for new events 206 | insert(t, spawn(watch_event, ctx)) 207 | 208 | elseif res[1] == "event" then 209 | ngx_log(ngx_DEBUG, "callback returned ", tostring(res[3])) 210 | 211 | else 212 | ngx_log(ngx_WARN, "invalid thread return type ", tostring(res[1])) 213 | end 214 | end 215 | end 216 | 217 | 218 | function _M.new(opts) 219 | if not opts then 220 | opts = {} 221 | end 222 | 223 | if type(opts) ~= "table" then 224 | return false, "opts must be a table" 225 | end 226 | 227 | opts.host = opts.host or "127.0.0.1" 228 | opts.port = opts.port or 8500 229 | opts.timeout = opts.timeout or 60 230 | opts.ssl_verify = opts.ssl or false 231 | 232 | if type(opts.host) ~= "string" then 233 | return false, "invalid host" 234 | end 235 | if type(opts.port) ~= "number" or opts.port < 0 or opts.port > 65535 then 236 | return false, "invalid port" 237 | end 238 | if type(opts.timeout) ~= "number" or opts.timeout < 0 then 239 | return false, "invalid timeout" 240 | end 241 | if type(opts.ssl_verify) ~= "boolean" then 242 | return false, "invalid ssl" 243 | end 244 | if opts.token ~= nil and type(opts.token) ~= "string" then 245 | return false, "invalid token" 246 | end 247 | 248 | local lrucache = require "resty.lrucache" 249 | local lru, err = lrucache.new(256) 250 | if err then 251 | return false, err 252 | end 253 | 254 | return setmetatable({ 255 | host = opts.host, 256 | port = opts.port, 257 | timeout = opts.timeout, 258 | ssl_verify = opts.ssl_verify, 259 | token = opts.token, 260 | 261 | -- index = nil, 262 | id_lru = lru, 263 | failures = 0, 264 | }, mt) 265 | end 266 | 267 | return _M 268 | -------------------------------------------------------------------------------- /lua-resty-consul-event-0.3.0-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-consul-event" 2 | version = "0.3.0-0" 3 | source = { 4 | url = "git://github.com/kong/lua-resty-consul-event", 5 | tag = "0.3.0" 6 | } 7 | description = { 8 | summary = "Consul Events HTTP API Wrapper for OpenResty", 9 | homepage = "https://github.com/kong/lua-resty-consul-event", 10 | license = "Apache 2.0" 11 | } 12 | dependencies = { 13 | "lua-resty-http == 0.12" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["resty.consul.event"] = "lib/resty/consul/event.lua", 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /t/01_sanity.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Cwd qw(cwd); 3 | 4 | plan tests => 3 * blocks(); 5 | 6 | my $pwd = cwd(); 7 | 8 | our $HttpConfig = <<"_EOC_"; 9 | lua_package_path "$pwd/lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; 10 | _EOC_ 11 | 12 | no_shuffle(); 13 | run_tests(); 14 | 15 | __DATA__ 16 | 17 | === TEST 1: nil new 18 | --- http_config eval: $::HttpConfig 19 | --- config 20 | location /t { 21 | content_by_lua_block { 22 | local e = require "resty.consul.event" 23 | 24 | local event, err = e.new() 25 | 26 | ngx.say(type(event) == "table") 27 | ngx.say(err) 28 | } 29 | } 30 | --- request 31 | GET /t 32 | --- error_code: 200 33 | --- response_body 34 | true 35 | nil 36 | --- no_error_log 37 | [error] 38 | 39 | 40 | === TEST 2: empty tab new 41 | --- http_config eval: $::HttpConfig 42 | --- config 43 | location /t { 44 | content_by_lua_block { 45 | local e = require "resty.consul.event" 46 | 47 | local event, err = e.new({}) 48 | 49 | ngx.say(type(event) == "table") 50 | ngx.say(err) 51 | } 52 | } 53 | --- request 54 | GET /t 55 | --- error_code: 200 56 | --- response_body 57 | true 58 | nil 59 | --- no_error_log 60 | [error] 61 | 62 | 63 | === TEST 3: new with a custom host 64 | --- http_config eval: $::HttpConfig 65 | --- config 66 | location /t { 67 | content_by_lua_block { 68 | local e = require "resty.consul.event" 69 | 70 | local event, err = e.new({ 71 | host = "consul", 72 | }) 73 | 74 | ngx.say(type(event) == "table") 75 | ngx.say(err) 76 | } 77 | } 78 | --- request 79 | GET /t 80 | --- error_code: 200 81 | --- response_body 82 | true 83 | nil 84 | --- no_error_log 85 | [error] 86 | 87 | 88 | === TEST 4: new with a custom port 89 | --- http_config eval: $::HttpConfig 90 | --- config 91 | location /t { 92 | content_by_lua_block { 93 | local e = require "resty.consul.event" 94 | 95 | local event, err = e.new({ 96 | port = 12355, 97 | }) 98 | 99 | ngx.say(type(event) == "table") 100 | ngx.say(err) 101 | } 102 | } 103 | --- request 104 | GET /t 105 | --- error_code: 200 106 | --- response_body 107 | true 108 | nil 109 | --- no_error_log 110 | [error] 111 | 112 | 113 | === TEST 5: new with custom timeout 114 | --- http_config eval: $::HttpConfig 115 | --- config 116 | location /t { 117 | content_by_lua_block { 118 | local e = require "resty.consul.event" 119 | 120 | local event, err = e.new({ 121 | timeout = 30, 122 | }) 123 | 124 | ngx.say(type(event) == "table") 125 | ngx.say(err) 126 | } 127 | } 128 | --- request 129 | GET /t 130 | --- error_code: 200 131 | --- response_body 132 | true 133 | nil 134 | --- no_error_log 135 | [error] 136 | 137 | 138 | === TEST 6: new with an invalid host 139 | --- http_config eval: $::HttpConfig 140 | --- config 141 | location /t { 142 | content_by_lua_block { 143 | local e = require "resty.consul.event" 144 | 145 | local event, err = e.new({ 146 | host = true, 147 | }) 148 | 149 | ngx.say(type(event) == "table") 150 | ngx.say(err) 151 | } 152 | } 153 | --- request 154 | GET /t 155 | --- error_code: 200 156 | --- response_body 157 | false 158 | invalid host 159 | --- no_error_log 160 | [error] 161 | 162 | 163 | === TEST 7: new with an invalid port 164 | --- http_config eval: $::HttpConfig 165 | --- config 166 | location /t { 167 | content_by_lua_block { 168 | local e = require "resty.consul.event" 169 | 170 | local event, err = e.new({ 171 | port = "nope", 172 | }) 173 | 174 | ngx.say(type(event) == "table") 175 | ngx.say(err) 176 | } 177 | } 178 | --- request 179 | GET /t 180 | --- error_code: 200 181 | --- response_body 182 | false 183 | invalid port 184 | --- no_error_log 185 | [error] 186 | 187 | 188 | === TEST 8: new with an invalid timeout type 189 | --- http_config eval: $::HttpConfig 190 | --- config 191 | location /t { 192 | content_by_lua_block { 193 | local e = require "resty.consul.event" 194 | 195 | local event, err = e.new({ 196 | timeout = "notathing", 197 | }) 198 | 199 | ngx.say(type(event) == "table") 200 | ngx.say(err) 201 | } 202 | } 203 | --- request 204 | GET /t 205 | --- error_code: 200 206 | --- response_body 207 | false 208 | invalid timeout 209 | --- no_error_log 210 | [error] 211 | 212 | 213 | === TEST 9: new with an invalid timeout value 214 | --- http_config eval: $::HttpConfig 215 | --- config 216 | location /t { 217 | content_by_lua_block { 218 | local e = require "resty.consul.event" 219 | 220 | local event, err = e.new({ 221 | timeout = -1 222 | }) 223 | 224 | ngx.say(type(event) == "table") 225 | ngx.say(err) 226 | } 227 | } 228 | --- request 229 | GET /t 230 | --- error_code: 200 231 | --- response_body 232 | false 233 | invalid timeout 234 | --- no_error_log 235 | [error] 236 | 237 | 238 | === TEST 10: new with an invalid token value 239 | --- http_config eval: $::HttpConfig 240 | --- config 241 | location /t { 242 | content_by_lua_block { 243 | local e = require "resty.consul.event" 244 | 245 | local event, err = e.new({ 246 | token = true 247 | }) 248 | 249 | ngx.say(type(event) == "table") 250 | ngx.say(err) 251 | } 252 | } 253 | --- request 254 | GET /t 255 | --- error_code: 200 256 | --- response_body 257 | false 258 | invalid token 259 | --- no_error_log 260 | [error] 261 | -------------------------------------------------------------------------------- /t/02_watch.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Cwd qw(cwd); 3 | 4 | plan tests => 6 * blocks(); 5 | 6 | 7 | $ENV{TEST_NGINX_CONSUL_ADDR} |= "127.0.0.1"; 8 | $ENV{TEST_NGINX_CONSUL_PORT} |= 8500; 9 | 10 | check_accum_error_log(); 11 | 12 | my $pwd = cwd(); 13 | 14 | our $HttpConfig = <<"_EOC_"; 15 | lua_package_path "$pwd/lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; 16 | 17 | lua_shared_dict hits 1m; 18 | 19 | init_by_lua_block { 20 | chost = "$ENV{TEST_NGINX_CONSUL_ADDR}" 21 | cport = $ENV{TEST_NGINX_CONSUL_PORT} 22 | } 23 | 24 | init_worker_by_lua_block { 25 | local event = require "resty.consul.event" 26 | 27 | local e, err = event.new({ 28 | host = chost, 29 | port = cport, 30 | }) 31 | if err then 32 | ngx.log(ngx.ERR, err) 33 | end 34 | 35 | ngx.timer.at(0, function() 36 | e:watch("foo", function() ngx.shared.hits:incr("foo", 1, 0) end ) 37 | end) 38 | ngx.timer.at(0, function() 39 | e:watch("bar", function() ngx.shared.hits:incr("bar", 1, 0) end ) 40 | end) 41 | ngx.timer.at(0, function() 42 | e:watch("error", function() error("whups") end ) 43 | end) 44 | } 45 | _EOC_ 46 | 47 | no_shuffle(); 48 | run_tests(); 49 | 50 | __DATA__ 51 | 52 | === TEST 1: Catches a single event (1/2) 53 | --- http_config eval: $::HttpConfig 54 | --- config 55 | location /s { 56 | content_by_lua_block { 57 | local h = require("resty.http").new() 58 | 59 | local res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 60 | "/v1/event/fire/foo", { 61 | method = "PUT", 62 | }) 63 | if err then 64 | ngx.log(ngx.ERR, err) 65 | end 66 | 67 | ngx.print("ok") 68 | } 69 | } 70 | location /t { 71 | content_by_lua_block { 72 | ngx.sleep(0.5) 73 | 74 | ngx.print(ngx.shared.hits:get("foo")) 75 | } 76 | } 77 | --- error_code eval 78 | [200, 200] 79 | --- request eval 80 | ["GET /s", "GET /t"] 81 | --- response_body eval 82 | ["ok", 1] 83 | --- no_error_log 84 | [error] 85 | 86 | 87 | === TEST 2: Catches a single event (2/2) 88 | --- http_config eval: $::HttpConfig 89 | --- config 90 | location /s { 91 | content_by_lua_block { 92 | local h = require("resty.http").new() 93 | 94 | local res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 95 | "/v1/event/fire/bar", { 96 | method = "PUT", 97 | }) 98 | if err then 99 | ngx.log(ngx.ERR, err) 100 | end 101 | 102 | ngx.print("ok") 103 | } 104 | } 105 | location /t { 106 | content_by_lua_block { 107 | ngx.sleep(0.5) 108 | 109 | ngx.print(ngx.shared.hits:get("bar")) 110 | } 111 | } 112 | --- error_code eval 113 | [200, 200] 114 | --- request eval 115 | ["GET /s", "GET /t"] 116 | --- response_body eval 117 | ["ok", 1] 118 | --- no_error_log 119 | [error] 120 | 121 | 122 | === TEST 3: Catches and swallows callback errors 123 | --- http_config eval: $::HttpConfig 124 | --- config 125 | location /s { 126 | content_by_lua_block { 127 | local h = require("resty.http").new() 128 | 129 | local res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 130 | "/v1/event/fire/error", { 131 | method = "PUT", 132 | }) 133 | if err then 134 | ngx.log(ngx.ERR, err) 135 | end 136 | 137 | ngx.print("ok") 138 | } 139 | } 140 | location /t { 141 | content_by_lua_block { 142 | ngx.sleep(0.5) 143 | 144 | ngx.print("ok") 145 | } 146 | } 147 | --- error_code eval 148 | [200, 200] 149 | --- request eval 150 | ["GET /s", "GET /t"] 151 | --- response_body eval 152 | ["ok", "ok"] 153 | --- no_error_log 154 | [error] 155 | 156 | 157 | === TEST 4: Catches a new event under a previously broadcast name 158 | --- http_config eval: $::HttpConfig 159 | --- config 160 | location /s { 161 | content_by_lua_block { 162 | local h = require("resty.http").new() 163 | 164 | local res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 165 | "/v1/event/fire/bar", { 166 | method = "PUT", 167 | }) 168 | if err then 169 | ngx.log(ngx.ERR, err) 170 | end 171 | 172 | ngx.print("ok") 173 | } 174 | } 175 | location /t { 176 | content_by_lua_block { 177 | ngx.sleep(0.5) 178 | 179 | ngx.print(ngx.shared.hits:get("bar")) 180 | } 181 | } 182 | --- error_code eval 183 | [200, 200] 184 | --- request eval 185 | ["GET /s", "GET /t"] 186 | --- response_body eval 187 | ["ok", 2] 188 | --- no_error_log 189 | [error] 190 | 191 | 192 | === TEST 5: Handles consul wait events appropriately 193 | --- http_config eval: $::HttpConfig 194 | --- config 195 | location /s { 196 | content_by_lua_block { 197 | local event = require "resty.consul.event" 198 | 199 | local e, err = event.new({ 200 | host = chost, 201 | port = cport, 202 | timeout = 1, 203 | }) 204 | if err then 205 | ngx.log(ngx.ERR, err) 206 | end 207 | 208 | ngx.timer.at(0, function() 209 | e:watch("foo", function() ngx.shared.hits:incr("foo", 1, 0) end ) 210 | end) 211 | 212 | ngx.print("ok") 213 | } 214 | } 215 | location /t { 216 | content_by_lua_block { 217 | ngx.sleep(4) 218 | 219 | ngx.print("ok") 220 | } 221 | } 222 | --- error_code eval 223 | [200, 200] 224 | --- request eval 225 | ["GET /s", "GET /t"] 226 | --- response_body eval 227 | ["ok", "ok"] 228 | --- error_log 229 | returned identical index 230 | --- no_error_log 231 | [error] 232 | --- timeout: 5s 233 | --- SKIP # error_log is only looking at the first request, triggering a false failure 234 | 235 | 236 | === TEST 6: Successfully executes a callback running longer than the watch timeout 237 | --- http_config eval: $::HttpConfig 238 | --- config 239 | location /s { 240 | content_by_lua_block { 241 | local event = require "resty.consul.event" 242 | 243 | local e, err = event.new({ 244 | host = chost, 245 | port = cport, 246 | timeout = 1, 247 | }) 248 | if err then 249 | ngx.log(ngx.ERR, err) 250 | end 251 | 252 | ngx.timer.at(0, function() 253 | e:watch("delay", function() ngx.sleep(3); ngx.shared.hits:incr("delay", 1, 0) end ) 254 | end) 255 | 256 | ngx.print("ok") 257 | } 258 | } 259 | location /t { 260 | content_by_lua_block { 261 | local h = require("resty.http").new() 262 | 263 | local res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 264 | "/v1/event/fire/delay", { 265 | method = "PUT", 266 | }) 267 | if err then 268 | ngx.log(ngx.ERR, err) 269 | end 270 | 271 | ngx.sleep(5) 272 | 273 | ngx.print(ngx.shared.hits:get("delay")) 274 | } 275 | } 276 | --- timeout: 10s 277 | --- error_code eval 278 | [200, 200] 279 | --- request eval 280 | ["GET /s", "GET /t"] 281 | --- response_body eval 282 | ["ok", 1] 283 | --- no_error_log 284 | [error] 285 | 286 | 287 | === TEST 7: Does not execute callbacks for past events 288 | --- http_config eval: $::HttpConfig 289 | --- config 290 | location /s { 291 | content_by_lua_block { 292 | local h = require("resty.http").new() 293 | 294 | local res, err 295 | 296 | for i = 1, 3 do 297 | res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 298 | "/v1/event/fire/baz", { 299 | method = "PUT", 300 | }) 301 | if err then 302 | ngx.log(ngx.ERR, err) 303 | end 304 | end 305 | 306 | -- get the current events 307 | res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 308 | "/v1/event/list?name=baz") 309 | if err then 310 | ngx.log(ngx.ERR, err) 311 | end 312 | 313 | local event = require "resty.consul.event" 314 | 315 | local e, err = event.new({ 316 | host = chost, 317 | port = cport, 318 | }) 319 | if err then 320 | ngx.log(ngx.ERR, err) 321 | end 322 | 323 | local l = {} 324 | 325 | for _, e in ipairs(require("cjson").decode(res.body)) do 326 | table.insert(l, e.ID) 327 | end 328 | 329 | ngx.timer.at(0, function() 330 | e:watch("baz", function() ngx.shared.hits:incr("baz", 1, 0) end, 331 | res.headers["X-Consul-Index"], 332 | l 333 | ) 334 | end) 335 | 336 | res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 337 | "/v1/event/fire/baz", { 338 | method = "PUT", 339 | }) 340 | if err then 341 | ngx.log(ngx.ERR, err) 342 | end 343 | 344 | ngx.print("ok") 345 | } 346 | } 347 | location /t { 348 | content_by_lua_block { 349 | ngx.sleep(0.5) 350 | 351 | ngx.print(ngx.shared.hits:get("baz")) 352 | } 353 | } 354 | --- timeout: 10s 355 | --- error_code eval 356 | [200, 200] 357 | --- request eval 358 | ["GET /s", "GET /t"] 359 | --- response_body eval 360 | ["ok", 1] 361 | --- no_error_log 362 | [error] 363 | 364 | 365 | === TEST 8: Does not execute callbacks for past events with no initial index 366 | --- http_config eval: $::HttpConfig 367 | --- config 368 | location /s { 369 | content_by_lua_block { 370 | local h = require("resty.http").new() 371 | 372 | local res, err 373 | 374 | for i = 1, 3 do 375 | res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 376 | "/v1/event/fire/bat", { 377 | method = "PUT", 378 | }) 379 | if err then 380 | ngx.log(ngx.ERR, err) 381 | end 382 | end 383 | 384 | -- get the current events 385 | res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 386 | "/v1/event/list?name=bat") 387 | if err then 388 | ngx.log(ngx.ERR, err) 389 | end 390 | 391 | local event = require "resty.consul.event" 392 | 393 | local e, err = event.new({ 394 | host = chost, 395 | port = cport, 396 | }) 397 | if err then 398 | ngx.log(ngx.ERR, err) 399 | end 400 | 401 | local l = {} 402 | 403 | for _, e in ipairs(require("cjson").decode(res.body)) do 404 | table.insert(l, e.ID) 405 | end 406 | 407 | ngx.timer.at(0, function() 408 | e:watch("bat", function() ngx.shared.hits:incr("bat", 1, 0) end, 409 | nil, 410 | l 411 | ) 412 | end) 413 | 414 | res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 415 | "/v1/event/fire/bat", { 416 | method = "PUT", 417 | }) 418 | if err then 419 | ngx.log(ngx.ERR, err) 420 | end 421 | 422 | ngx.print("ok") 423 | } 424 | } 425 | location /t { 426 | content_by_lua_block { 427 | ngx.sleep(0.5) 428 | 429 | ngx.print(ngx.shared.hits:get("bat")) 430 | } 431 | } 432 | --- timeout: 10s 433 | --- error_code eval 434 | [200, 200] 435 | --- request eval 436 | ["GET /s", "GET /t"] 437 | --- response_body eval 438 | ["ok", 1] 439 | --- no_error_log 440 | [error] 441 | 442 | 443 | === TEST 9: Does execute callbacks for past events with initial index but no seen_ids 444 | --- http_config eval: $::HttpConfig 445 | --- config 446 | location /s { 447 | content_by_lua_block { 448 | local h = require("resty.http").new() 449 | 450 | local res, err 451 | 452 | for i = 1, 3 do 453 | res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 454 | "/v1/event/fire/qux", { 455 | method = "PUT", 456 | }) 457 | if err then 458 | ngx.log(ngx.ERR, err) 459 | end 460 | end 461 | 462 | -- get the current events 463 | res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 464 | "/v1/event/list?name=qux") 465 | if err then 466 | ngx.log(ngx.ERR, err) 467 | end 468 | 469 | local event = require "resty.consul.event" 470 | 471 | local e, err = event.new({ 472 | host = chost, 473 | port = cport, 474 | }) 475 | if err then 476 | ngx.log(ngx.ERR, err) 477 | end 478 | 479 | local l = {} 480 | 481 | for _, e in ipairs(require("cjson").decode(res.body)) do 482 | table.insert(l, e.ID) 483 | end 484 | 485 | ngx.timer.at(0, function() 486 | e:watch("qux", function() ngx.shared.hits:incr("qux", 1, 0) end, 487 | res.headers["X-Consul-Index"] 488 | ) 489 | end) 490 | 491 | res, err = h:request_uri("http://" .. chost .. ":" .. cport .. 492 | "/v1/event/fire/qux", { 493 | method = "PUT", 494 | }) 495 | if err then 496 | ngx.log(ngx.ERR, err) 497 | end 498 | 499 | ngx.print("ok") 500 | } 501 | } 502 | location /t { 503 | content_by_lua_block { 504 | ngx.sleep(0.5) 505 | 506 | ngx.print(ngx.shared.hits:get("qux")) 507 | } 508 | } 509 | --- timeout: 10s 510 | --- error_code eval 511 | [200, 200] 512 | --- request eval 513 | ["GET /s", "GET /t"] 514 | --- response_body eval 515 | ["ok", 4] 516 | --- no_error_log 517 | [error] 518 | --------------------------------------------------------------------------------