├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .luacheckrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist.ini ├── lib └── resty │ ├── mlcache.lua │ └── mlcache │ └── ipc.lua ├── lua-resty-mlcache-2.6.1-1.rockspec └── t ├── 00-ipc.t ├── 01-new.t ├── 02-get.t ├── 03-peek.t ├── 04-update.t ├── 05-set.t ├── 06-delete.t ├── 07-l1_serializer.t ├── 08-purge.t ├── 09-isolation.t ├── 10-ipc_shm.t ├── 11-locks_shm.t ├── 12-resurrect-stale.t ├── 13-get_bulk.t ├── 14-bulk-and-res.t └── TestMLCache.pm /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | workflow_dispatch: 9 | inputs: 10 | openresty: 11 | description: 'OpenResty version (e.g. 1.21.4.1rc2)' 12 | required: true 13 | 14 | defaults: 15 | run: 16 | shell: bash 17 | 18 | jobs: 19 | tests: 20 | name: Tests 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | matrix: 24 | os: [ubuntu-latest] 25 | openresty: 26 | - 1.25.3.1 27 | - 1.21.4.3 28 | - 1.19.9.1 29 | - 1.19.3.2 30 | - 1.17.8.2 31 | - 1.15.8.3 32 | - 1.13.6.2 33 | - 1.11.2.5 34 | steps: 35 | - if: ${{ github.event_name == 'workflow_dispatch' }} 36 | run: echo "OPENRESTY_VER=${{ github.event.inputs.openresty }}" >> $GITHUB_ENV 37 | - if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }} 38 | run: echo "OPENRESTY_VER=${{ matrix.openresty }}" >> $GITHUB_ENV 39 | - uses: actions/checkout@v2 40 | - name: Setup OpenResty 41 | uses: thibaultcha/setup-openresty@main 42 | with: 43 | version: ${{ env.OPENRESTY_VER }} 44 | - run: prove -r t/ 45 | 46 | lint: 47 | name: Lint 48 | runs-on: ${{ matrix.os }} 49 | strategy: 50 | matrix: 51 | os: [ubuntu-latest] 52 | openresty: [1.19.9.1] 53 | steps: 54 | - uses: actions/checkout@v2 55 | - name: Setup OpenResty 56 | uses: thibaultcha/setup-openresty@main 57 | with: 58 | version: ${{ matrix.openresty }} 59 | - run: | 60 | echo "luarocks check" 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | t/servroot* 2 | lua-resty-mlcache-*/ 3 | *.tar.gz 4 | *.rock 5 | work/ 6 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "ngx_lua" 2 | redefined = false 3 | max_line_length = 80 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | - [2.6.1](#261) 4 | - [2.6.0](#260) 5 | - [2.5.0](#250) 6 | - [2.4.1](#241) 7 | - [2.4.0](#240) 8 | - [2.3.0](#230) 9 | - [2.2.1](#221) 10 | - [2.2.0](#220) 11 | - [2.1.0](#210) 12 | - [2.0.2](#202) 13 | - [2.0.1](#201) 14 | - [2.0.0](#200) 15 | - [1.0.1](#101) 16 | - [1.0.0](#100) 17 | 18 | ## [2.6.1] 19 | 20 | > Released on: 2024/01/30 21 | 22 | #### Fixed 23 | 24 | - Ensure the `l1_serializer` callback option is properly invoked in a couple of 25 | `get()` edge-cases. 26 | [#123](https://github.com/thibaultcha/lua-resty-mlcache/pull/123) 27 | 28 | [Back to TOC](#table-of-contents) 29 | 30 | ## [2.6.0] 31 | 32 | > Released on: 2022/08/22 33 | 34 | #### Added 35 | 36 | - Use the new LuaJIT `string.buffer` API for L2 (shm layer) encoding/decoding 37 | when available. 38 | [#110](https://github.com/thibaultcha/lua-resty-mlcache/pull/110) 39 | 40 | [Back to TOC](#table-of-contents) 41 | 42 | ## [2.5.0] 43 | 44 | > Released on: 2020/11/18 45 | 46 | #### Added 47 | 48 | - `get()` callback functions are now optional. Without a callback, `get()` now 49 | still performs on-cpu L1/L2 lookups (no yielding). This allows implementing 50 | new cache lookup patterns guaranteed to be on-cpu for a more constant, 51 | smoother latency tail end (e.g. values are refreshed in background timers with 52 | `set()`). 53 | Thanks Hamish Forbes and Corina Purcarea for proposing this feature and 54 | participating in its development! 55 | [#96](https://github.com/thibaultcha/lua-resty-mlcache/pull/96) 56 | 57 | #### Fixed 58 | 59 | - Improve `update()` robustness to worker crashes. Now, the library behind 60 | `cache:update()` is much more robust to re-spawned workers when initialized in 61 | the `init_by_lua` phase. 62 | [#97](https://github.com/thibaultcha/lua-resty-mlcache/pull/97) 63 | - Document the `peek()` method `stale` argument which was not mentioned, as well 64 | as the possibility of negative TTL return values for expired items. 65 | 66 | [Back to TOC](#table-of-contents) 67 | 68 | ## [2.4.1] 69 | 70 | > Released on: 2020/01/17 71 | 72 | #### Fixed 73 | 74 | - The IPC module now avoids replaying all events when spawning new workers, and 75 | gets initialized with the latest event index instead. 76 | [#88](https://github.com/thibaultcha/lua-resty-mlcache/pull/88) 77 | 78 | [Back to TOC](#table-of-contents) 79 | 80 | ## [2.4.0] 81 | 82 | > Released on: 2019/03/28 83 | 84 | #### Added 85 | 86 | - A new `get_bulk()` API allows for fetching several values from the layered 87 | caches in a single call, and will execute all L3 callback functions 88 | concurrently, in a configurable pool of threads. 89 | [#77](https://github.com/thibaultcha/lua-resty-mlcache/pull/77) 90 | - `purge()` now clears the L1 LRU cache with the new `flush_all()` method when 91 | used in OpenResty >= 1.13.6.2. 92 | Thanks [@Crack](https://github.com/Crack) for the patch! 93 | [#78](https://github.com/thibaultcha/lua-resty-mlcache/pull/78) 94 | 95 | #### Fixed 96 | 97 | - `get()` is now resilient to L3 callback functions calling `error()` with 98 | non-string arguments. Such functions could result in a runtime error when 99 | LuaJIT is compiled with `-DLUAJIT_ENABLE_LUA52COMPAT`. 100 | Thanks [@MartinAmps](https://github.com/MartinAmps) for the patch! 101 | [#75](https://github.com/thibaultcha/lua-resty-mlcache/pull/75) 102 | - Instances using a custom L1 LRU cache in OpenResty < 1.13.6.2 are now 103 | restricted from calling `purge()`, since doing so would result in the LRU 104 | cache being overwritten. 105 | [#79](https://github.com/thibaultcha/lua-resty-mlcache/pull/79) 106 | 107 | [Back to TOC](#table-of-contents) 108 | 109 | ## [2.3.0] 110 | 111 | > Released on: 2019/01/17 112 | 113 | #### Added 114 | 115 | - Returning a negative `ttl` value from the L3 callback will now make the 116 | fetched data bypass the cache (it will still be returned by `get()`). 117 | This is useful when some fetched data indicates that it is not cacheable. 118 | Thanks [@eaufavor](https://github.com/eaufavor) for the patch! 119 | [#68](https://github.com/thibaultcha/lua-resty-mlcache/pull/68) 120 | 121 | [Back to TOC](#table-of-contents) 122 | 123 | ## [2.2.1] 124 | 125 | > Released on: 2018/07/28 126 | 127 | #### Fixed 128 | 129 | - When `get()` returns a value from L2 (shm) during its last millisecond of 130 | freshness, we do not erroneously cache the value in L1 (LRU) indefinitely 131 | anymore. Thanks [@jdesgats](https://github.com/jdesgats) and 132 | [@javierguerragiraldez](https://github.com/javierguerragiraldez) for the 133 | report and initial fix. 134 | [#58](https://github.com/thibaultcha/lua-resty-mlcache/pull/58) 135 | - When `get()` returns a previously resurrected value from L2 (shm), we now 136 | correctly set the `hit_lvl` return value to `4`, instead of `2`. 137 | [307feca](https://github.com/thibaultcha/lua-resty-mlcache/commit/307fecad6adac8755d4fcd931bbb498da23d069c) 138 | 139 | [Back to TOC](#table-of-contents) 140 | 141 | ## [2.2.0] 142 | 143 | > Released on: 2018/06/29 144 | 145 | #### Added 146 | 147 | - Implement a new `resurrect_ttl` option. When specified, `get()` will behave 148 | in a more resilient way upon errors, and in particular callback errors. 149 | [#52](https://github.com/thibaultcha/lua-resty-mlcache/pull/52) 150 | - New `stale` argument to `peek()`. When specified, `peek()` will return stale 151 | shm values. 152 | [#52](https://github.com/thibaultcha/lua-resty-mlcache/pull/52) 153 | 154 | [Back to TOC](#table-of-contents) 155 | 156 | ## [2.1.0] 157 | 158 | > Released on: 2018/06/14 159 | 160 | #### Added 161 | 162 | - Implement a new `shm_locks` option. This option receives the name of a 163 | lua_shared_dict, and, when specified, the mlcache instance will store 164 | lua-resty-lock objects in it instead of storing them in the cache hits 165 | lua_shared_dict. This can help reducing LRU churning in some workloads. 166 | [#55](https://github.com/thibaultcha/lua-resty-mlcache/pull/55) 167 | - Provide stack traceback in `err` return value when the L3 callback throws an 168 | error. 169 | [#56](https://github.com/thibaultcha/lua-resty-mlcache/pull/56) 170 | 171 | #### Fixed 172 | 173 | - Ensure `no memory` errors returned by shm insertions are properly returned 174 | by `set()`. 175 | [#53](https://github.com/thibaultcha/lua-resty-mlcache/pull/53) 176 | 177 | [Back to TOC](#table-of-contents) 178 | 179 | ## [2.0.2] 180 | 181 | > Released on: 2018/04/09 182 | 183 | #### Fixed 184 | 185 | - Make `get()` lookup in shm after lock timeout. This prevents a possible (but 186 | rare) race condition under high load. Thanks to 187 | [@jdesgats](https://github.com/jdesgats) for the report and initial fix. 188 | [#49](https://github.com/thibaultcha/lua-resty-mlcache/pull/49) 189 | 190 | [Back to TOC](#table-of-contents) 191 | 192 | ## [2.0.1] 193 | 194 | > Released on: 2018/03/27 195 | 196 | #### Fixed 197 | 198 | - Ensure the `set()`, `delete()`, `peek()`, and `purge()` method properly 199 | support the new `shm_miss` option. 200 | [#45](https://github.com/thibaultcha/lua-resty-mlcache/pull/45) 201 | 202 | [Back to TOC](#table-of-contents) 203 | 204 | ## [2.0.0] 205 | 206 | > Released on: 2018/03/18 207 | 208 | This release implements numerous new features. The major version digit has been 209 | bumped to ensure that the changes to the interpretation of the callback return 210 | values (documented below) do not break any dependent application. 211 | 212 | #### Added 213 | 214 | - Implement a new `purge()` method to clear all cached items in both 215 | the L1 and L2 caches. 216 | [#34](https://github.com/thibaultcha/lua-resty-mlcache/pull/34) 217 | - Implement a new `shm_miss` option. This option receives the name 218 | of a lua_shared_dict, and when specified, will cache misses there instead of 219 | the instance's `shm` shared dict. This is particularly useful for certain 220 | types of workload where a large number of misses can be triggered and 221 | eventually evict too many cached values (hits) from the instance's `shm`. 222 | [#42](https://github.com/thibaultcha/lua-resty-mlcache/pull/42) 223 | - Implement a new `l1_serializer` callback option. It allows the 224 | deserialization of data from L2 or L3 into arbitrary Lua data inside the LRU 225 | cache (L1). This includes userdata, cdata, functions, etc... 226 | Thanks to [@jdesgats](https://github.com/jdesgats) for the contribution. 227 | [#29](https://github.com/thibaultcha/lua-resty-mlcache/pull/29) 228 | - Implement a new `shm_set_tries` option to retry `shm:set()` 229 | operations and ensure LRU eviction when caching values of disparate sizes. 230 | [#41](https://github.com/thibaultcha/lua-resty-mlcache/issues/41) 231 | - The L3 callback can now return `nil + err`, which will be bubbled up 232 | to the caller of `get()`. Prior to this change, the second return value of 233 | callbacks was ignored, and users had to throw hard Lua errors from inside 234 | their callbacks. 235 | [#35](https://github.com/thibaultcha/lua-resty-mlcache/pull/35) 236 | - Support for custom IPC module. 237 | [#31](https://github.com/thibaultcha/lua-resty-mlcache/issues/31) 238 | 239 | #### Fixed 240 | 241 | - In the event of a `no memory` error returned by the L2 lua_shared_dict cache 242 | (after the number of `shm_set_tries` failed), we do not interrupt the `get()` 243 | flow to return an error anymore. Instead, the retrieved value is now bubbled 244 | up for insertion in L1, and returned to the caller. A warning log is (by 245 | default) printed in the nginx error logs. 246 | [#41](https://github.com/thibaultcha/lua-resty-mlcache/issues/41) 247 | 248 | [Back to TOC](#table-of-contents) 249 | 250 | ## [1.0.1] 251 | 252 | > Released on: 2017/08/26 253 | 254 | #### Fixed 255 | 256 | - Do not rely on memory address of mlcache instance in invalidation events 257 | channel names. This ensures invalidation events are properly broadcasted to 258 | sibling instances in other workers. 259 | [#27](https://github.com/thibaultcha/lua-resty-mlcache/pull/27) 260 | 261 | [Back to TOC](#table-of-contents) 262 | 263 | ## [1.0.0] 264 | 265 | > Released on: 2017/08/23 266 | 267 | Initial release. 268 | 269 | [Back to TOC](#table-of-contents) 270 | 271 | [2.6.1]: https://github.com/thibaultcha/lua-resty-mlcache/compare/2.6.0...2.6.1 272 | [2.6.0]: https://github.com/thibaultcha/lua-resty-mlcache/compare/2.5.0...2.6.0 273 | [2.5.0]: https://github.com/thibaultcha/lua-resty-mlcache/compare/2.4.1...2.5.0 274 | [2.4.1]: https://github.com/thibaultcha/lua-resty-mlcache/compare/2.4.0...2.4.1 275 | [2.4.0]: https://github.com/thibaultcha/lua-resty-mlcache/compare/2.3.0...2.4.0 276 | [2.3.0]: https://github.com/thibaultcha/lua-resty-mlcache/compare/2.2.1...2.3.0 277 | [2.2.1]: https://github.com/thibaultcha/lua-resty-mlcache/compare/2.2.0...2.2.1 278 | [2.2.0]: https://github.com/thibaultcha/lua-resty-mlcache/compare/2.1.0...2.2.0 279 | [2.1.0]: https://github.com/thibaultcha/lua-resty-mlcache/compare/2.0.2...2.1.0 280 | [2.0.2]: https://github.com/thibaultcha/lua-resty-mlcache/compare/2.0.1...2.0.2 281 | [2.0.1]: https://github.com/thibaultcha/lua-resty-mlcache/compare/2.0.0...2.0.1 282 | [2.0.0]: https://github.com/thibaultcha/lua-resty-mlcache/compare/1.0.1...2.0.0 283 | [1.0.1]: https://github.com/thibaultcha/lua-resty-mlcache/compare/1.0.0...1.0.1 284 | [1.0.0]: https://github.com/thibaultcha/lua-resty-mlcache/tree/1.0.0 285 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2024 Thibault Charbonnier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name = lua-resty-mlcache 2 | abstract = Layered caching library for OpenResty 3 | author = Thibault Charbonnier (thibaultcha) 4 | is_original = yes 5 | license = mit 6 | lib_dir = lib 7 | repo_link = https://github.com/thibaultcha/lua-resty-mlcache 8 | main_module = lib/resty/mlcache.lua 9 | requires = openresty 10 | -------------------------------------------------------------------------------- /lib/resty/mlcache/ipc.lua: -------------------------------------------------------------------------------- 1 | -- vim: ts=4 sts=4 sw=4 et: 2 | 3 | local ERR = ngx.ERR 4 | local WARN = ngx.WARN 5 | local INFO = ngx.INFO 6 | local sleep = ngx.sleep 7 | local shared = ngx.shared 8 | local worker_pid = ngx.worker.pid 9 | local ngx_log = ngx.log 10 | local fmt = string.format 11 | local sub = string.sub 12 | local find = string.find 13 | local min = math.min 14 | local type = type 15 | local pcall = pcall 16 | local error = error 17 | local insert = table.insert 18 | local tonumber = tonumber 19 | local setmetatable = setmetatable 20 | 21 | 22 | local INDEX_KEY = "lua-resty-ipc:index" 23 | local FORCIBLE_KEY = "lua-resty-ipc:forcible" 24 | local POLL_SLEEP_RATIO = 2 25 | 26 | 27 | local function marshall(worker_pid, channel, data) 28 | return fmt("%d:%d:%s%s", worker_pid, #data, channel, data) 29 | end 30 | 31 | 32 | local function unmarshall(str) 33 | local sep_1 = find(str, ":", nil , true) 34 | local sep_2 = find(str, ":", sep_1 + 1, true) 35 | 36 | local pid = tonumber(sub(str, 1 , sep_1 - 1)) 37 | local data_len = tonumber(sub(str, sep_1 + 1, sep_2 - 1)) 38 | 39 | local channel_last_pos = #str - data_len 40 | 41 | local channel = sub(str, sep_2 + 1, channel_last_pos) 42 | local data = sub(str, channel_last_pos + 1) 43 | 44 | return pid, channel, data 45 | end 46 | 47 | 48 | local function log(lvl, ...) 49 | return ngx_log(lvl, "[ipc] ", ...) 50 | end 51 | 52 | 53 | local _M = {} 54 | local mt = { __index = _M } 55 | 56 | 57 | function _M.new(shm, debug) 58 | local dict = shared[shm] 59 | if not dict then 60 | return nil, "no such lua_shared_dict: " .. shm 61 | end 62 | 63 | local self = { 64 | dict = dict, 65 | pid = debug and 0 or worker_pid(), 66 | idx = 0, 67 | callbacks = {}, 68 | } 69 | 70 | return setmetatable(self, mt) 71 | end 72 | 73 | 74 | function _M:subscribe(channel, cb) 75 | if type(channel) ~= "string" then 76 | error("channel must be a string", 2) 77 | end 78 | 79 | if type(cb) ~= "function" then 80 | error("callback must be a function", 2) 81 | end 82 | 83 | if not self.callbacks[channel] then 84 | self.callbacks[channel] = { cb } 85 | 86 | else 87 | insert(self.callbacks[channel], cb) 88 | end 89 | end 90 | 91 | 92 | function _M:broadcast(channel, data) 93 | if type(channel) ~= "string" then 94 | error("channel must be a string", 2) 95 | end 96 | 97 | if type(data) ~= "string" then 98 | error("data must be a string", 2) 99 | end 100 | 101 | local marshalled_event = marshall(worker_pid(), channel, data) 102 | 103 | local idx, err = self.dict:incr(INDEX_KEY, 1, 0) 104 | if not idx then 105 | return nil, "failed to increment index: " .. err 106 | end 107 | 108 | local ok, err, forcible = self.dict:set(idx, marshalled_event) 109 | if not ok then 110 | return nil, "failed to insert event in shm: " .. err 111 | end 112 | 113 | if forcible then 114 | -- take note that eviction has started 115 | -- we repeat this flagging to avoid this key from ever being 116 | -- evicted itself 117 | local ok, err = self.dict:set(FORCIBLE_KEY, true) 118 | if not ok then 119 | return nil, "failed to set forcible flag in shm: " .. err 120 | end 121 | end 122 | 123 | return true 124 | end 125 | 126 | 127 | -- Note: if this module were to be used by users (that is, users can implement 128 | -- their own pub/sub events and thus, callbacks), this method would then need 129 | -- to consider the time spent in callbacks to prevent long running callbacks 130 | -- from penalizing the worker. 131 | -- Since this module is currently only used by mlcache, whose callback is an 132 | -- shm operation, we only worry about the time spent waiting for events 133 | -- between the 'incr()' and 'set()' race condition. 134 | function _M:poll(timeout) 135 | if timeout ~= nil and type(timeout) ~= "number" then 136 | error("timeout must be a number", 2) 137 | end 138 | 139 | local shm_idx, err = self.dict:get(INDEX_KEY) 140 | if err then 141 | return nil, "failed to get index: " .. err 142 | end 143 | 144 | if shm_idx == nil then 145 | -- no events to poll yet 146 | return true 147 | end 148 | 149 | if type(shm_idx) ~= "number" then 150 | return nil, "index is not a number, shm tampered with" 151 | end 152 | 153 | if not timeout then 154 | timeout = 0.3 155 | end 156 | 157 | if self.idx == 0 then 158 | local forcible, err = self.dict:get(FORCIBLE_KEY) 159 | if err then 160 | return nil, "failed to get forcible flag from shm: " .. err 161 | end 162 | 163 | if forcible then 164 | -- shm lru eviction occurred, we are likely a new worker 165 | -- skip indexes that may have been evicted and resume current 166 | -- polling idx 167 | self.idx = shm_idx - 1 168 | end 169 | 170 | else 171 | -- guard: self.idx <= shm_idx 172 | self.idx = min(self.idx, shm_idx) 173 | end 174 | 175 | local elapsed = 0 176 | 177 | for _ = self.idx, shm_idx - 1 do 178 | -- fetch event from shm with a retry policy in case 179 | -- we run our :get() in between another worker's 180 | -- :incr() and :set() 181 | 182 | local v 183 | local idx = self.idx + 1 184 | 185 | do 186 | local perr 187 | local pok = true 188 | local sleep_step = 0.001 189 | 190 | while elapsed < timeout do 191 | v, err = self.dict:get(idx) 192 | if v ~= nil or err then 193 | break 194 | end 195 | 196 | if pok then 197 | log(INFO, "no event data at index '", idx, "', ", 198 | "retrying in: ", sleep_step, "s") 199 | 200 | -- sleep is not available in all ngx_lua contexts 201 | -- if we fail once, never retry to sleep 202 | pok, perr = pcall(sleep, sleep_step) 203 | if not pok then 204 | log(WARN, "could not sleep before retry: ", perr, 205 | " (note: it is safer to call this function ", 206 | "in contexts that support the ngx.sleep() ", 207 | "API)") 208 | end 209 | end 210 | 211 | elapsed = elapsed + sleep_step 212 | sleep_step = min(sleep_step * POLL_SLEEP_RATIO, 213 | timeout - elapsed) 214 | end 215 | end 216 | 217 | -- fetch next event on next iteration 218 | -- even if we timeout, we might miss 1 event (we return in timeout and 219 | -- we don't retry that event), but it's better than being stuck forever 220 | -- on an event that might have been evicted from the shm. 221 | self.idx = idx 222 | 223 | if elapsed >= timeout then 224 | return nil, "timeout" 225 | end 226 | 227 | if err then 228 | log(ERR, "could not get event at index '", self.idx, "': ", err) 229 | 230 | elseif type(v) ~= "string" then 231 | log(ERR, "event at index '", self.idx, "' is not a string, ", 232 | "shm tampered with") 233 | 234 | else 235 | local pid, channel, data = unmarshall(v) 236 | 237 | if self.pid ~= pid then 238 | -- coming from another worker 239 | local cbs = self.callbacks[channel] 240 | if cbs then 241 | for j = 1, #cbs do 242 | local pok, perr = pcall(cbs[j], data) 243 | if not pok then 244 | log(ERR, "callback for channel '", channel, 245 | "' threw a Lua error: ", perr) 246 | end 247 | end 248 | end 249 | end 250 | end 251 | end 252 | 253 | return true 254 | end 255 | 256 | 257 | return _M 258 | -------------------------------------------------------------------------------- /lua-resty-mlcache-2.6.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-mlcache" 2 | version = "2.6.1-1" 3 | source = { 4 | url = "git+https://github.com/thibaultcha/lua-resty-mlcache", 5 | tag = "2.6.1" 6 | } 7 | description = { 8 | summary = "Layered caching library for OpenResty", 9 | detailed = [[ 10 | This library can be manipulated as a key/value store caching scalar Lua 11 | types and tables, combining the power of the lua_shared_dict API and 12 | lua-resty-lrucache, which results in an extremely performant and flexible 13 | layered caching solution. 14 | 15 | Features: 16 | 17 | - Caching and negative caching with TTLs. 18 | - Built-in mutex via lua-resty-lock to prevent dog-pile effects to your 19 | database/backend on cache misses. 20 | - Built-in inter-worker communication to propagate cache invalidations and 21 | allow workers to update their L1 (lua-resty-lrucache) caches upon changes 22 | (`set()`, `delete()`). 23 | - Support for split hits and misses caching queues. 24 | - Multiple isolated instances can be created to hold various types of data 25 | while relying on the *same* `lua_shared_dict` L2 cache. 26 | ]], 27 | homepage = "https://github.com/thibaultcha/lua-resty-mlcache", 28 | license = "MIT" 29 | } 30 | build = { 31 | type = "builtin", 32 | modules = { 33 | ["resty.mlcache.ipc"] = "lib/resty/mlcache/ipc.lua", 34 | ["resty.mlcache"] = "lib/resty/mlcache.lua" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /t/00-ipc.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | plan tests => repeat_each() * blocks() * 5; 8 | 9 | run_tests(); 10 | 11 | __DATA__ 12 | 13 | === TEST 1: new() ensures shm exists 14 | --- config 15 | location /t { 16 | content_by_lua_block { 17 | local mlcache_ipc = require "resty.mlcache.ipc" 18 | 19 | local ipc, err = mlcache_ipc.new("foo") 20 | ngx.say(err) 21 | } 22 | } 23 | --- response_body 24 | no such lua_shared_dict: foo 25 | --- no_error_log 26 | [error] 27 | [crit] 28 | [alert] 29 | 30 | 31 | 32 | === TEST 2: broadcast() sends an event through shm 33 | --- http_config 34 | init_worker_by_lua_block { 35 | local mlcache_ipc = require "resty.mlcache.ipc" 36 | 37 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 38 | 39 | ipc:subscribe("my_channel", function(data) 40 | ngx.log(ngx.NOTICE, "received event from my_channel: ", data) 41 | end) 42 | } 43 | --- config 44 | location /t { 45 | content_by_lua_block { 46 | assert(ipc:broadcast("my_channel", "hello world")) 47 | assert(ipc:poll()) 48 | } 49 | } 50 | --- ignore_response_body 51 | --- error_log 52 | received event from my_channel: hello world 53 | --- no_error_log 54 | [error] 55 | [crit] 56 | [alert] 57 | 58 | 59 | 60 | === TEST 3: broadcast() runs event callback in protected mode 61 | --- http_config 62 | init_worker_by_lua_block { 63 | local mlcache_ipc = require "resty.mlcache.ipc" 64 | 65 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 66 | 67 | ipc:subscribe("my_channel", function(data) 68 | error("my callback had an error") 69 | end) 70 | } 71 | --- config 72 | location /t { 73 | content_by_lua_block { 74 | assert(ipc:broadcast("my_channel", "hello world")) 75 | assert(ipc:poll()) 76 | } 77 | } 78 | --- ignore_response_body 79 | --- error_log eval 80 | qr/\[error\] .*? \[ipc\] callback for channel 'my_channel' threw a Lua error: init_worker_by_lua(.*?)?:\d: my callback had an error/ 81 | --- no_error_log 82 | lua entry thread aborted: runtime error 83 | [crit] 84 | [alert] 85 | 86 | 87 | 88 | === TEST 4: poll() catches invalid timeout arg 89 | --- http_config 90 | init_worker_by_lua_block { 91 | local mlcache_ipc = require "resty.mlcache.ipc" 92 | 93 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 94 | } 95 | --- config 96 | location /t { 97 | content_by_lua_block { 98 | local ok, err = pcall(ipc.poll, ipc, false) 99 | if not ok then 100 | ngx.say(err) 101 | end 102 | } 103 | } 104 | --- response_body 105 | timeout must be a number 106 | --- no_error_log 107 | [error] 108 | [crit] 109 | [alert] 110 | 111 | 112 | 113 | === TEST 5: poll() catches up with all events 114 | --- http_config 115 | init_worker_by_lua_block { 116 | local mlcache_ipc = require "resty.mlcache.ipc" 117 | 118 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 119 | 120 | ipc:subscribe("my_channel", function(data) 121 | ngx.log(ngx.NOTICE, "received event from my_channel: ", data) 122 | end) 123 | } 124 | --- config 125 | location /t { 126 | content_by_lua_block { 127 | assert(ipc:broadcast("my_channel", "msg 1")) 128 | assert(ipc:broadcast("my_channel", "msg 2")) 129 | assert(ipc:broadcast("my_channel", "msg 3")) 130 | assert(ipc:poll()) 131 | } 132 | } 133 | --- ignore_response_body 134 | --- error_log 135 | received event from my_channel: msg 1 136 | received event from my_channel: msg 2 137 | received event from my_channel: msg 3 138 | --- no_error_log 139 | [error] 140 | 141 | 142 | 143 | === TEST 6: poll() resumes to current idx if events were previously evicted 144 | This ensures new workers spawned during a master process' lifecycle do not 145 | attempt to replay all events from index 0. 146 | https://github.com/thibaultcha/lua-resty-mlcache/issues/87 147 | https://github.com/thibaultcha/lua-resty-mlcache/issues/93 148 | --- http_config 149 | lua_shared_dict ipc_shm 32k; 150 | 151 | init_by_lua_block { 152 | require "resty.core" 153 | local mlcache_ipc = require "resty.mlcache.ipc" 154 | 155 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 156 | 157 | ipc:subscribe("my_channel", function(data) 158 | ngx.log(ngx.NOTICE, "my_channel event: ", data) 159 | end) 160 | 161 | for i = 1, 32 do 162 | -- fill shm, simulating busy workers 163 | -- this must trigger eviction for this test to succeed 164 | assert(ipc:broadcast("my_channel", string.rep(".", 2^10))) 165 | end 166 | } 167 | --- config 168 | location /t { 169 | content_by_lua_block { 170 | ngx.say("ipc.idx: ", ipc.idx) 171 | 172 | assert(ipc:broadcast("my_channel", "first broadcast")) 173 | assert(ipc:broadcast("my_channel", "second broadcast")) 174 | 175 | -- first poll without new() to simulate new worker 176 | assert(ipc:poll()) 177 | 178 | -- ipc.idx set to shm_idx-1 ("second broadcast") 179 | ngx.say("ipc.idx: ", ipc.idx) 180 | } 181 | } 182 | --- response_body 183 | ipc.idx: 0 184 | ipc.idx: 34 185 | --- error_log 186 | my_channel event: second broadcast 187 | --- no_error_log 188 | my_channel event: first broadcast 189 | [error] 190 | 191 | 192 | 193 | === TEST 7: poll() does not execute events from self (same pid) 194 | --- http_config 195 | init_worker_by_lua_block { 196 | local mlcache_ipc = require "resty.mlcache.ipc" 197 | 198 | ipc = assert(mlcache_ipc.new("ipc_shm")) 199 | 200 | ipc:subscribe("my_channel", function(data) 201 | ngx.log(ngx.NOTICE, "received event from my_channel: ", data) 202 | end) 203 | } 204 | --- config 205 | location /t { 206 | content_by_lua_block { 207 | assert(ipc:broadcast("my_channel", "hello world")) 208 | assert(ipc:poll()) 209 | } 210 | } 211 | --- ignore_response_body 212 | --- no_error_log 213 | received event from my_channel: hello world 214 | [error] 215 | [crit] 216 | [alert] 217 | 218 | 219 | 220 | === TEST 8: poll() runs all registered callbacks for a channel 221 | --- http_config 222 | init_worker_by_lua_block { 223 | local mlcache_ipc = require "resty.mlcache.ipc" 224 | 225 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 226 | 227 | ipc:subscribe("my_channel", function(data) 228 | ngx.log(ngx.NOTICE, "callback 1 from my_channel: ", data) 229 | end) 230 | 231 | ipc:subscribe("my_channel", function(data) 232 | ngx.log(ngx.NOTICE, "callback 2 from my_channel: ", data) 233 | end) 234 | 235 | ipc:subscribe("my_channel", function(data) 236 | ngx.log(ngx.NOTICE, "callback 3 from my_channel: ", data) 237 | end) 238 | } 239 | --- config 240 | location /t { 241 | content_by_lua_block { 242 | assert(ipc:broadcast("my_channel", "hello world")) 243 | assert(ipc:poll()) 244 | } 245 | } 246 | --- ignore_response_body 247 | --- error_log 248 | callback 1 from my_channel: hello world 249 | callback 2 from my_channel: hello world 250 | callback 3 from my_channel: hello world 251 | --- no_error_log 252 | [error] 253 | 254 | 255 | 256 | === TEST 9: poll() exits when no event to poll 257 | --- http_config 258 | init_worker_by_lua_block { 259 | local mlcache_ipc = require "resty.mlcache.ipc" 260 | 261 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 262 | 263 | ipc:subscribe("my_channel", function(data) 264 | ngx.log(ngx.NOTICE, "callback from my_channel: ", data) 265 | end) 266 | } 267 | --- config 268 | location /t { 269 | content_by_lua_block { 270 | assert(ipc:poll()) 271 | } 272 | } 273 | --- ignore_response_body 274 | --- no_error_log 275 | callback from my_channel: hello world 276 | [error] 277 | [crit] 278 | [alert] 279 | 280 | 281 | 282 | === TEST 10: poll() runs all callbacks from all channels 283 | --- http_config 284 | init_worker_by_lua_block { 285 | local mlcache_ipc = require "resty.mlcache.ipc" 286 | 287 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 288 | 289 | ipc:subscribe("my_channel", function(data) 290 | ngx.log(ngx.NOTICE, "callback 1 from my_channel: ", data) 291 | end) 292 | 293 | ipc:subscribe("my_channel", function(data) 294 | ngx.log(ngx.NOTICE, "callback 2 from my_channel: ", data) 295 | end) 296 | 297 | ipc:subscribe("other_channel", function(data) 298 | ngx.log(ngx.NOTICE, "callback 1 from other_channel: ", data) 299 | end) 300 | 301 | ipc:subscribe("other_channel", function(data) 302 | ngx.log(ngx.NOTICE, "callback 2 from other_channel: ", data) 303 | end) 304 | } 305 | --- config 306 | location /t { 307 | content_by_lua_block { 308 | assert(ipc:broadcast("my_channel", "hello world")) 309 | assert(ipc:broadcast("other_channel", "hello ipc")) 310 | assert(ipc:broadcast("other_channel", "hello ipc 2")) 311 | assert(ipc:poll()) 312 | } 313 | } 314 | --- ignore_response_body 315 | --- grep_error_log eval: qr/callback \d+ from [^,]*/ 316 | --- grep_error_log_out 317 | callback 1 from my_channel: hello world 318 | callback 2 from my_channel: hello world 319 | callback 1 from other_channel: hello ipc 320 | callback 2 from other_channel: hello ipc 321 | callback 1 from other_channel: hello ipc 2 322 | callback 2 from other_channel: hello ipc 2 323 | --- no_error_log 324 | [error] 325 | [crit] 326 | [alert] 327 | 328 | 329 | 330 | === TEST 11: poll() catches tampered shm (by third-party users) 331 | --- http_config 332 | init_worker_by_lua_block { 333 | local mlcache_ipc = require "resty.mlcache.ipc" 334 | 335 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 336 | } 337 | --- config 338 | location /t { 339 | content_by_lua_block { 340 | assert(ipc:broadcast("my_channel", "msg 1")) 341 | 342 | assert(ngx.shared.ipc_shm:set("lua-resty-ipc:index", false)) 343 | 344 | local ok, err = ipc:poll() 345 | if not ok then 346 | ngx.say(err) 347 | end 348 | } 349 | } 350 | --- response_body 351 | index is not a number, shm tampered with 352 | --- no_error_log 353 | [error] 354 | [crit] 355 | [alert] 356 | 357 | 358 | 359 | === TEST 12: poll() retries getting an event until timeout 360 | --- http_config 361 | init_worker_by_lua_block { 362 | local mlcache_ipc = require "resty.mlcache.ipc" 363 | 364 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 365 | } 366 | --- config 367 | location /t { 368 | content_by_lua_block { 369 | assert(ipc:broadcast("my_channel", "msg 1")) 370 | 371 | ngx.shared.ipc_shm:delete(1) 372 | ngx.shared.ipc_shm:flush_expired() 373 | 374 | local ok, err = ipc:poll() 375 | if not ok then 376 | ngx.log(ngx.ERR, "could not poll: ", err) 377 | end 378 | } 379 | } 380 | --- ignore_response_body 381 | --- grep_error_log eval: qr/((\[error\] .*?)|(\[ipc\] no event data at index '\d+', retrying .*?))[^,]*/ 382 | --- grep_error_log_out eval 383 | qr/\[ipc\] no event data at index '1', retrying in: 0\.001s 384 | \[ipc\] no event data at index '1', retrying in: 0\.002s 385 | \[ipc\] no event data at index '1', retrying in: 0\.004s 386 | \[ipc\] no event data at index '1', retrying in: 0\.008s 387 | \[ipc\] no event data at index '1', retrying in: 0\.016s 388 | \[ipc\] no event data at index '1', retrying in: 0\.032s 389 | \[ipc\] no event data at index '1', retrying in: 0\.064s 390 | \[ipc\] no event data at index '1', retrying in: 0\.128s 391 | \[ipc\] no event data at index '1', retrying in: 0\.045s 392 | \[error\] .*? could not poll: timeout/ 393 | --- no_error_log 394 | [warn] 395 | [crit] 396 | [alert] 397 | 398 | 399 | 400 | === TEST 13: poll() reaches custom timeout 401 | --- http_config 402 | init_worker_by_lua_block { 403 | local mlcache_ipc = require "resty.mlcache.ipc" 404 | 405 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 406 | } 407 | --- config 408 | location /t { 409 | content_by_lua_block { 410 | assert(ipc:broadcast("my_channel", "msg 1")) 411 | 412 | ngx.shared.ipc_shm:delete(1) 413 | ngx.shared.ipc_shm:flush_expired() 414 | 415 | local ok, err = ipc:poll(0.01) 416 | if not ok then 417 | ngx.log(ngx.ERR, "could not poll: ", err) 418 | end 419 | } 420 | } 421 | --- ignore_response_body 422 | --- grep_error_log eval: qr/((\[error\] .*?)|(\[ipc\] no event data at index '\d+', retrying .*?))[^,]*/ 423 | --- grep_error_log_out eval 424 | qr/\[ipc\] no event data at index '1', retrying in: 0\.001s 425 | \[ipc\] no event data at index '1', retrying in: 0\.002s 426 | \[ipc\] no event data at index '1', retrying in: 0\.004s 427 | \[ipc\] no event data at index '1', retrying in: 0\.003s 428 | \[error\] .*? could not poll: timeout/ 429 | --- no_error_log 430 | [warn] 431 | [crit] 432 | [alert] 433 | 434 | 435 | 436 | === TEST 14: poll() logs errors and continue if event has been tampered with 437 | --- http_config 438 | init_worker_by_lua_block { 439 | local mlcache_ipc = require "resty.mlcache.ipc" 440 | 441 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 442 | 443 | ipc:subscribe("my_channel", function(data) 444 | ngx.log(ngx.NOTICE, "callback from my_channel: ", data) 445 | end) 446 | } 447 | --- config 448 | location /t { 449 | content_by_lua_block { 450 | assert(ipc:broadcast("my_channel", "msg 1")) 451 | assert(ipc:broadcast("my_channel", "msg 2")) 452 | 453 | assert(ngx.shared.ipc_shm:set(1, false)) 454 | 455 | assert(ipc:poll()) 456 | } 457 | } 458 | --- ignore_response_body 459 | --- error_log eval 460 | [ 461 | qr/\[error\] .*? \[ipc\] event at index '1' is not a string, shm tampered with/, 462 | qr/\[notice\] .*? callback from my_channel: msg 2/, 463 | ] 464 | --- no_error_log 465 | [warn] 466 | [crit] 467 | 468 | 469 | 470 | === TEST 15: poll() is safe to be called in contexts that don't support ngx.sleep() 471 | --- http_config 472 | init_worker_by_lua_block { 473 | local mlcache_ipc = require "resty.mlcache.ipc" 474 | 475 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 476 | 477 | ipc:subscribe("my_channel", function(data) 478 | ngx.log(ngx.NOTICE, "callback from my_channel: ", data) 479 | end) 480 | } 481 | --- config 482 | location /t { 483 | return 200; 484 | 485 | log_by_lua_block { 486 | assert(ipc:broadcast("my_channel", "msg 1")) 487 | 488 | ngx.shared.ipc_shm:delete(1) 489 | ngx.shared.ipc_shm:flush_expired() 490 | 491 | local ok, err = ipc:poll() 492 | if not ok then 493 | ngx.log(ngx.ERR, "could not poll: ", err) 494 | end 495 | } 496 | } 497 | --- ignore_response_body 498 | --- error_log eval 499 | [ 500 | qr/\[info\] .*? \[ipc\] no event data at index '1', retrying in: 0\.001s/, 501 | qr/\[warn\] .*? \[ipc\] could not sleep before retry: API disabled in the context of log_by_lua/, 502 | qr/\[error\] .*? could not poll: timeout/, 503 | ] 504 | --- no_error_log 505 | [crit] 506 | 507 | 508 | 509 | === TEST 16: poll() guards self.idx from growing beyond the current shm idx 510 | --- http_config 511 | init_worker_by_lua_block { 512 | local mlcache_ipc = require "resty.mlcache.ipc" 513 | 514 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 515 | 516 | ipc:subscribe("my_channel", function(data) 517 | ngx.log(ngx.NOTICE, "callback from my_channel: ", data) 518 | end) 519 | } 520 | --- config 521 | location /t { 522 | content_by_lua_block { 523 | assert(ipc:broadcast("other_channel", "")) 524 | assert(ipc:poll()) 525 | assert(ipc:broadcast("my_channel", "fist broadcast")) 526 | assert(ipc:broadcast("other_channel", "")) 527 | assert(ipc:broadcast("my_channel", "second broadcast")) 528 | 529 | -- shm idx is 5, let's mess with the instance's idx 530 | ipc.idx = 10 531 | assert(ipc:poll()) 532 | 533 | -- we may have skipped the above events, but we are able to resume polling 534 | assert(ipc:broadcast("other_channel", "")) 535 | assert(ipc:broadcast("my_channel", "third broadcast")) 536 | assert(ipc:poll()) 537 | } 538 | } 539 | --- ignore_response_body 540 | --- error_log 541 | callback from my_channel: third broadcast 542 | --- no_error_log 543 | callback from my_channel: first broadcast 544 | callback from my_channel: second broadcast 545 | [error] 546 | 547 | 548 | 549 | === TEST 17: poll() JITs 550 | --- http_config 551 | init_worker_by_lua_block { 552 | local mlcache_ipc = require "resty.mlcache.ipc" 553 | 554 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 555 | 556 | ipc:subscribe("my_channel", function(data) 557 | ngx.log(ngx.NOTICE, "callback from my_channel: ", data) 558 | end) 559 | } 560 | --- config 561 | location /t { 562 | content_by_lua_block { 563 | for i = 1, 10e3 do 564 | assert(ipc:poll()) 565 | end 566 | } 567 | } 568 | --- ignore_response_body 569 | --- error_log eval 570 | qr/\[TRACE\s+\d+ content_by_lua\(nginx\.conf:\d+\):2 loop\]/ 571 | --- no_error_log 572 | [warn] 573 | [error] 574 | [crit] 575 | 576 | 577 | 578 | === TEST 18: broadcast() JITs 579 | --- http_config 580 | init_worker_by_lua_block { 581 | local mlcache_ipc = require "resty.mlcache.ipc" 582 | 583 | ipc = assert(mlcache_ipc.new("ipc_shm", true)) 584 | 585 | ipc:subscribe("my_channel", function(data) 586 | ngx.log(ngx.NOTICE, "callback from my_channel: ", data) 587 | end) 588 | } 589 | --- config 590 | location /t { 591 | content_by_lua_block { 592 | for i = 1, 10e3 do 593 | assert(ipc:broadcast("my_channel", "hello world")) 594 | end 595 | } 596 | } 597 | --- ignore_response_body 598 | --- error_log eval 599 | qr/\[TRACE\s+\d+ content_by_lua\(nginx\.conf:\d+\):2 loop\]/ 600 | --- no_error_log 601 | [warn] 602 | [error] 603 | [crit] 604 | -------------------------------------------------------------------------------- /t/01-new.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | repeat_each(2); 8 | 9 | plan tests => repeat_each() * blocks() * 3; 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: module has version number 16 | --- config 17 | location /t { 18 | content_by_lua_block { 19 | local mlcache = require "resty.mlcache" 20 | 21 | ngx.say(mlcache._VERSION) 22 | } 23 | } 24 | --- response_body_like 25 | \d+\.\d+\.\d+ 26 | --- no_error_log 27 | [error] 28 | 29 | 30 | 31 | === TEST 2: new() validates name 32 | --- config 33 | location /t { 34 | content_by_lua_block { 35 | local mlcache = require "resty.mlcache" 36 | 37 | local ok, err = pcall(mlcache.new) 38 | if not ok then 39 | ngx.say(err) 40 | end 41 | } 42 | } 43 | --- response_body 44 | name must be a string 45 | --- no_error_log 46 | [error] 47 | 48 | 49 | 50 | === TEST 3: new() validates shm 51 | --- config 52 | location /t { 53 | content_by_lua_block { 54 | local mlcache = require "resty.mlcache" 55 | 56 | local ok, err = pcall(mlcache.new, "name") 57 | if not ok then 58 | ngx.say(err) 59 | end 60 | } 61 | } 62 | --- response_body 63 | shm must be a string 64 | --- no_error_log 65 | [error] 66 | 67 | 68 | 69 | === TEST 4: new() validates opts 70 | --- config 71 | location /t { 72 | content_by_lua_block { 73 | local mlcache = require "resty.mlcache" 74 | 75 | local ok, err = pcall(mlcache.new, "name", "cache_shm", "foo") 76 | if not ok then 77 | ngx.say(err) 78 | end 79 | } 80 | } 81 | --- response_body 82 | opts must be a table 83 | --- no_error_log 84 | [error] 85 | 86 | 87 | 88 | === TEST 5: new() ensures shm exists 89 | --- config 90 | location /t { 91 | content_by_lua_block { 92 | local mlcache = require "resty.mlcache" 93 | 94 | local cache, err = mlcache.new("name", "foo") 95 | if not cache then 96 | ngx.say(err) 97 | end 98 | } 99 | } 100 | --- response_body 101 | no such lua_shared_dict: foo 102 | --- no_error_log 103 | [error] 104 | 105 | 106 | 107 | === TEST 6: new() supports ipc_shm option and validates it 108 | --- config 109 | location /t { 110 | content_by_lua_block { 111 | local mlcache = require "resty.mlcache" 112 | 113 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { ipc_shm = 1 }) 114 | if not ok then 115 | ngx.say(err) 116 | end 117 | } 118 | } 119 | --- response_body 120 | opts.ipc_shm must be a string 121 | --- no_error_log 122 | [error] 123 | 124 | 125 | 126 | === TEST 7: new() supports opts.ipc_shm and ensures it exists 127 | --- config 128 | location /t { 129 | content_by_lua_block { 130 | local mlcache = require "resty.mlcache" 131 | 132 | local cache, err = mlcache.new("name", "cache_shm", { ipc_shm = "ipc" }) 133 | if not cache then 134 | ngx.log(ngx.ERR, err) 135 | end 136 | } 137 | } 138 | --- ignore_response_body 139 | --- error_log eval 140 | qr/\[error\] .*? no such lua_shared_dict: ipc/ 141 | --- no_error_log 142 | [crit] 143 | 144 | 145 | 146 | === TEST 8: new() supports ipc options and validates it 147 | --- config 148 | location /t { 149 | content_by_lua_block { 150 | local mlcache = require "resty.mlcache" 151 | 152 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { ipc = false }) 153 | if not ok then 154 | ngx.say(err) 155 | end 156 | } 157 | } 158 | --- response_body 159 | opts.ipc must be a table 160 | --- no_error_log 161 | [error] 162 | 163 | 164 | 165 | === TEST 9: new() prevents both opts.ipc_shm and opts.ipc to be given 166 | --- config 167 | location /t { 168 | content_by_lua_block { 169 | local mlcache = require "resty.mlcache" 170 | 171 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { 172 | ipc_shm = "ipc", 173 | ipc = {} 174 | }) 175 | if not ok then 176 | ngx.say(err) 177 | end 178 | } 179 | } 180 | --- response_body 181 | cannot specify both of opts.ipc_shm and opts.ipc 182 | --- no_error_log 183 | [error] 184 | 185 | 186 | 187 | === TEST 10: new() validates ipc.register_listeners + ipc.broadcast + ipc.poll (type: custom) 188 | --- config 189 | location /t { 190 | content_by_lua_block { 191 | local mlcache = require "resty.mlcache" 192 | 193 | local args = { 194 | "register_listeners", 195 | "broadcast", 196 | "poll", 197 | } 198 | 199 | for _, arg in ipairs(args) do 200 | local ipc_opts = { 201 | register_listeners = function() end, 202 | broadcast = function() end, 203 | poll = function() end, 204 | } 205 | 206 | ipc_opts[arg] = false 207 | 208 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { 209 | ipc = ipc_opts, 210 | }) 211 | if not ok then 212 | ngx.say(err) 213 | end 214 | end 215 | } 216 | } 217 | --- response_body 218 | opts.ipc.register_listeners must be a function 219 | opts.ipc.broadcast must be a function 220 | opts.ipc.poll must be a function 221 | --- no_error_log 222 | [error] 223 | 224 | 225 | 226 | === TEST 11: new() ipc.register_listeners can return nil + err (type: custom) 227 | --- config 228 | location /t { 229 | content_by_lua_block { 230 | local mlcache = require "resty.mlcache" 231 | 232 | local cache, err = mlcache.new("name", "cache_shm", { 233 | ipc = { 234 | register_listeners = function() 235 | return nil, "something happened" 236 | end, 237 | broadcast = function() end, 238 | poll = function() end, 239 | } 240 | }) 241 | if not cache then 242 | ngx.say(err) 243 | end 244 | } 245 | } 246 | --- response_body_like 247 | failed to initialize custom IPC \(opts\.ipc\.register_listeners returned an error\): something happened 248 | --- no_error_log 249 | [error] 250 | 251 | 252 | 253 | === TEST 12: new() calls ipc.register_listeners with events array (type: custom) 254 | --- config 255 | location /t { 256 | content_by_lua_block { 257 | local mlcache = require "resty.mlcache" 258 | 259 | local cache, err = mlcache.new("name", "cache_shm", { 260 | ipc = { 261 | register_listeners = function(events) 262 | local res = {} 263 | for ev_name, ev in pairs(events) do 264 | table.insert(res, string.format("%s | channel: %s | handler: %s", 265 | ev_name, ev.channel, type(ev.handler))) 266 | end 267 | 268 | table.sort(res) 269 | 270 | for i = 1, #res do 271 | ngx.say(res[i]) 272 | end 273 | end, 274 | broadcast = function() end, 275 | poll = function() end, 276 | } 277 | }) 278 | if not cache then 279 | ngx.say(err) 280 | end 281 | } 282 | } 283 | --- response_body 284 | invalidation | channel: mlcache:invalidations:name | handler: function 285 | purge | channel: mlcache:purge:name | handler: function 286 | --- no_error_log 287 | [error] 288 | 289 | 290 | 291 | === TEST 13: new() ipc.poll is optional (some IPC libraries might not need it 292 | --- config 293 | location /t { 294 | content_by_lua_block { 295 | local mlcache = require "resty.mlcache" 296 | 297 | local cache, err = mlcache.new("name", "cache_shm", { 298 | ipc = { 299 | register_listeners = function() end, 300 | broadcast = function() end, 301 | poll = nil 302 | } 303 | }) 304 | if not cache then 305 | ngx.say(err) 306 | end 307 | 308 | ngx.say("ok") 309 | } 310 | } 311 | --- response_body 312 | ok 313 | --- no_error_log 314 | [error] 315 | 316 | 317 | 318 | === TEST 14: new() validates opts.lru_size 319 | --- config 320 | location /t { 321 | content_by_lua_block { 322 | local mlcache = require "resty.mlcache" 323 | 324 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { 325 | lru_size = "", 326 | }) 327 | if not ok then 328 | ngx.log(ngx.ERR, err) 329 | end 330 | } 331 | } 332 | --- response_body 333 | 334 | --- error_log 335 | opts.lru_size must be a number 336 | 337 | 338 | 339 | === TEST 15: new() validates opts.ttl 340 | --- config 341 | location /t { 342 | content_by_lua_block { 343 | local mlcache = require "resty.mlcache" 344 | 345 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { 346 | ttl = "" 347 | }) 348 | if not ok then 349 | ngx.say(err) 350 | end 351 | 352 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { 353 | ttl = -1 354 | }) 355 | if not ok then 356 | ngx.say(err) 357 | end 358 | } 359 | } 360 | --- response_body 361 | opts.ttl must be a number 362 | opts.ttl must be >= 0 363 | --- no_error_log 364 | [error] 365 | 366 | 367 | 368 | === TEST 16: new() validates opts.neg_ttl 369 | --- config 370 | location /t { 371 | content_by_lua_block { 372 | local mlcache = require "resty.mlcache" 373 | 374 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { 375 | neg_ttl = "" 376 | }) 377 | if not ok then 378 | ngx.say(err) 379 | end 380 | 381 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { 382 | neg_ttl = -1 383 | }) 384 | if not ok then 385 | ngx.say(err) 386 | end 387 | } 388 | } 389 | --- response_body 390 | opts.neg_ttl must be a number 391 | opts.neg_ttl must be >= 0 392 | --- no_error_log 393 | [error] 394 | 395 | 396 | 397 | === TEST 17: new() validates opts.resty_lock_opts 398 | --- config 399 | location /t { 400 | content_by_lua_block { 401 | local mlcache = require "resty.mlcache" 402 | 403 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { 404 | resty_lock_opts = false, 405 | }) 406 | if not ok then 407 | ngx.say(err) 408 | end 409 | } 410 | } 411 | --- response_body 412 | opts.resty_lock_opts must be a table 413 | --- no_error_log 414 | [error] 415 | 416 | 417 | 418 | === TEST 18: new() validates opts.shm_set_tries 419 | --- config 420 | location /t { 421 | content_by_lua_block { 422 | local mlcache = require "resty.mlcache" 423 | 424 | local values = { 425 | false, 426 | -1, 427 | 0, 428 | } 429 | 430 | for _, v in ipairs(values) do 431 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { 432 | shm_set_tries = v, 433 | }) 434 | if not ok then 435 | ngx.say(err) 436 | end 437 | end 438 | } 439 | } 440 | --- response_body 441 | opts.shm_set_tries must be a number 442 | opts.shm_set_tries must be >= 1 443 | opts.shm_set_tries must be >= 1 444 | --- no_error_log 445 | [error] 446 | 447 | 448 | 449 | === TEST 19: new() validates opts.shm_miss 450 | --- config 451 | location /t { 452 | content_by_lua_block { 453 | local mlcache = require "resty.mlcache" 454 | 455 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { 456 | shm_miss = false, 457 | }) 458 | if not ok then 459 | ngx.say(err) 460 | end 461 | } 462 | } 463 | --- response_body 464 | opts.shm_miss must be a string 465 | --- no_error_log 466 | [error] 467 | 468 | 469 | 470 | === TEST 20: new() ensures opts.shm_miss exists 471 | --- config 472 | location /t { 473 | content_by_lua_block { 474 | local mlcache = require "resty.mlcache" 475 | 476 | local ok, err = mlcache.new("name", "cache_shm", { 477 | shm_miss = "foo", 478 | }) 479 | if not ok then 480 | ngx.say(err) 481 | end 482 | } 483 | } 484 | --- response_body 485 | no such lua_shared_dict for opts.shm_miss: foo 486 | --- no_error_log 487 | [error] 488 | 489 | 490 | 491 | === TEST 21: new() creates an mlcache object with default attributes 492 | --- config 493 | location /t { 494 | content_by_lua_block { 495 | local mlcache = require "resty.mlcache" 496 | 497 | local cache, err = mlcache.new("name", "cache_shm") 498 | if not cache then 499 | ngx.log(ngx.ERR, err) 500 | end 501 | 502 | ngx.say(type(cache)) 503 | ngx.say(type(cache.ttl)) 504 | ngx.say(type(cache.neg_ttl)) 505 | } 506 | } 507 | --- response_body 508 | table 509 | number 510 | number 511 | --- no_error_log 512 | [error] 513 | 514 | 515 | 516 | === TEST 22: new() accepts user-provided LRU instances via opts.lru 517 | --- config 518 | location /t { 519 | content_by_lua_block { 520 | local mlcache = require "resty.mlcache" 521 | local pureffi_lrucache = require "resty.lrucache.pureffi" 522 | 523 | local my_lru = pureffi_lrucache.new(100) 524 | 525 | local cache = assert(mlcache.new("name", "cache_shm", { lru = my_lru })) 526 | 527 | ngx.say("lru is user-provided: ", cache.lru == my_lru) 528 | } 529 | } 530 | --- response_body 531 | lru is user-provided: true 532 | --- no_error_log 533 | [error] 534 | -------------------------------------------------------------------------------- /t/03-peek.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | workers(2); 8 | #repeat_each(2); 9 | 10 | plan tests => repeat_each() * (blocks() * 4); 11 | 12 | run_tests(); 13 | 14 | __DATA__ 15 | 16 | === TEST 1: peek() validates key 17 | --- config 18 | location /t { 19 | content_by_lua_block { 20 | local mlcache = require "resty.mlcache" 21 | 22 | local cache, err = mlcache.new("my_mlcache", "cache_shm") 23 | if not cache then 24 | ngx.log(ngx.ERR, err) 25 | return 26 | end 27 | 28 | local ok, err = pcall(cache.peek, cache) 29 | if not ok then 30 | ngx.say(err) 31 | end 32 | } 33 | } 34 | --- response_body 35 | key must be a string 36 | --- no_error_log 37 | [error] 38 | [crit] 39 | 40 | 41 | 42 | === TEST 2: peek() returns nil if a key has never been fetched before 43 | --- config 44 | location /t { 45 | content_by_lua_block { 46 | local mlcache = require "resty.mlcache" 47 | 48 | local cache, err = mlcache.new("my_mlcache", "cache_shm") 49 | if not cache then 50 | ngx.log(ngx.ERR, err) 51 | return 52 | end 53 | 54 | local ttl, err = cache:peek("my_key") 55 | if err then 56 | ngx.log(ngx.ERR, err) 57 | return 58 | end 59 | 60 | ngx.say("ttl: ", ttl) 61 | } 62 | } 63 | --- response_body 64 | ttl: nil 65 | --- no_error_log 66 | [error] 67 | [crit] 68 | 69 | 70 | 71 | === TEST 3: peek() returns the remaining ttl if a key has been fetched before 72 | --- main_config 73 | timer_resolution 10ms; 74 | --- config 75 | location /t { 76 | content_by_lua_block { 77 | local mlcache = require "resty.mlcache" 78 | 79 | local cache, err = mlcache.new("my_mlcache", "cache_shm") 80 | if not cache then 81 | ngx.log(ngx.ERR, err) 82 | return 83 | end 84 | 85 | local function cb() 86 | return nil 87 | end 88 | 89 | local val, err = cache:get("my_key", { neg_ttl = 19 }, cb) 90 | if err then 91 | ngx.log(ngx.ERR, err) 92 | return 93 | end 94 | 95 | local ttl, err = cache:peek("my_key") 96 | if err then 97 | ngx.log(ngx.ERR, err) 98 | return 99 | end 100 | 101 | ngx.say("ttl: ", math.ceil(ttl)) 102 | 103 | ngx.sleep(1) 104 | 105 | local ttl, err = cache:peek("my_key") 106 | if err then 107 | ngx.log(ngx.ERR, err) 108 | return 109 | end 110 | 111 | ngx.say("ttl: ", math.ceil(ttl)) 112 | } 113 | } 114 | --- response_body 115 | ttl: 19 116 | ttl: 18 117 | --- no_error_log 118 | [error] 119 | [crit] 120 | 121 | 122 | 123 | === TEST 4: peek() returns a 0 remaining_ttl if the ttl was 0 124 | --- config 125 | location /t { 126 | content_by_lua_block { 127 | local mlcache = require "resty.mlcache" 128 | 129 | local cache = assert(mlcache.new("my_mlcache", "cache_shm")) 130 | 131 | local function cb() 132 | return nil 133 | end 134 | 135 | local val, err = cache:get("my_key", { neg_ttl = 0 }, cb) 136 | if err then 137 | ngx.log(ngx.ERR, err) 138 | return 139 | end 140 | 141 | ngx.sleep(1) 142 | 143 | local ttl = assert(cache:peek("my_key")) 144 | ngx.say("ttl: ", math.ceil(ttl)) 145 | 146 | ngx.sleep(1) 147 | 148 | local ttl = assert(cache:peek("my_key")) 149 | ngx.say("ttl: ", math.ceil(ttl)) 150 | } 151 | } 152 | --- response_body 153 | ttl: 0 154 | ttl: 0 155 | --- no_error_log 156 | [error] 157 | [crit] 158 | 159 | 160 | 161 | === TEST 5: peek() returns remaining ttl if shm_miss is specified 162 | --- main_config 163 | timer_resolution 10ms; 164 | --- config 165 | location /t { 166 | content_by_lua_block { 167 | local mlcache = require "resty.mlcache" 168 | 169 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 170 | shm_miss = "cache_shm_miss", 171 | })) 172 | 173 | local function cb() 174 | return nil 175 | end 176 | 177 | local val, err = cache:get("my_key", { neg_ttl = 19 }, cb) 178 | if err then 179 | ngx.log(ngx.ERR, err) 180 | return 181 | end 182 | 183 | local ttl, err = cache:peek("my_key") 184 | if err then 185 | ngx.log(ngx.ERR, err) 186 | return 187 | end 188 | 189 | ngx.say("ttl: ", math.ceil(ttl)) 190 | 191 | ngx.sleep(1) 192 | 193 | local ttl, err = cache:peek("my_key") 194 | if err then 195 | ngx.log(ngx.ERR, err) 196 | return 197 | end 198 | 199 | ngx.say("ttl: ", math.ceil(ttl)) 200 | } 201 | } 202 | --- response_body 203 | ttl: 19 204 | ttl: 18 205 | --- no_error_log 206 | [error] 207 | [crit] 208 | 209 | 210 | 211 | === TEST 6: peek() returns the value if a key has been fetched before 212 | --- config 213 | location /t { 214 | content_by_lua_block { 215 | local mlcache = require "resty.mlcache" 216 | 217 | local cache, err = mlcache.new("my_mlcache", "cache_shm") 218 | if not cache then 219 | ngx.log(ngx.ERR, err) 220 | return 221 | end 222 | 223 | local function cb_number() 224 | return 123 225 | end 226 | 227 | local function cb_nil() 228 | return nil 229 | end 230 | 231 | local val, err = cache:get("my_key", nil, cb_number) 232 | if err then 233 | ngx.log(ngx.ERR, err) 234 | return 235 | end 236 | 237 | local val, err = cache:get("my_nil_key", nil, cb_nil) 238 | if err then 239 | ngx.log(ngx.ERR, err) 240 | return 241 | end 242 | 243 | local ttl, err, val = cache:peek("my_key") 244 | if err then 245 | ngx.log(ngx.ERR, err) 246 | return 247 | end 248 | 249 | ngx.say("ttl: ", math.ceil(ttl), " val: ", val) 250 | 251 | local ttl, err, val = cache:peek("my_nil_key") 252 | if err then 253 | ngx.log(ngx.ERR, err) 254 | return 255 | end 256 | 257 | ngx.say("ttl: ", math.ceil(ttl), " nil_val: ", val) 258 | } 259 | } 260 | --- response_body_like 261 | ttl: \d* val: 123 262 | ttl: \d* nil_val: nil 263 | --- no_error_log 264 | [error] 265 | [crit] 266 | 267 | 268 | 269 | === TEST 7: peek() returns the value if shm_miss is specified 270 | --- config 271 | location /t { 272 | content_by_lua_block { 273 | local mlcache = require "resty.mlcache" 274 | 275 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 276 | shm_miss = "cache_shm_miss", 277 | })) 278 | 279 | local function cb_nil() 280 | return nil 281 | end 282 | 283 | local val, err = cache:get("my_nil_key", nil, cb_nil) 284 | if err then 285 | ngx.log(ngx.ERR, err) 286 | return 287 | end 288 | 289 | local ttl, err, val = cache:peek("my_nil_key") 290 | if err then 291 | ngx.log(ngx.ERR, err) 292 | return 293 | end 294 | 295 | ngx.say("ttl: ", math.ceil(ttl), " nil_val: ", val) 296 | } 297 | } 298 | --- response_body_like 299 | ttl: \d* nil_val: nil 300 | --- no_error_log 301 | [error] 302 | [crit] 303 | 304 | 305 | 306 | === TEST 8: peek() JITs on hit 307 | --- config 308 | location /t { 309 | content_by_lua_block { 310 | local mlcache = require "resty.mlcache" 311 | 312 | local cache = assert(mlcache.new("my_mlcache", "cache_shm")) 313 | 314 | local function cb() 315 | return 123456 316 | end 317 | 318 | local val = assert(cache:get("key", nil, cb)) 319 | ngx.say("val: ", val) 320 | 321 | for i = 1, 10e3 do 322 | assert(cache:peek("key")) 323 | end 324 | } 325 | } 326 | --- response_body 327 | val: 123456 328 | --- error_log eval 329 | qr/\[TRACE\s+\d+ content_by_lua\(nginx\.conf:\d+\):13 loop\]/ 330 | --- no_error_log 331 | [error] 332 | 333 | 334 | 335 | === TEST 9: peek() JITs on miss 336 | --- config 337 | location /t { 338 | content_by_lua_block { 339 | local mlcache = require "resty.mlcache" 340 | 341 | local cache = assert(mlcache.new("my_mlcache", "cache_shm")) 342 | 343 | for i = 1, 10e3 do 344 | local ttl, err, val = cache:peek("key") 345 | assert(err == nil) 346 | assert(ttl == nil) 347 | assert(val == nil) 348 | end 349 | } 350 | } 351 | --- ignore_response_body 352 | --- error_log eval 353 | qr/\[TRACE\s+\d+ content_by_lua\(nginx\.conf:\d+\):6 loop\]/ 354 | --- no_error_log 355 | [error] 356 | [crit] 357 | 358 | 359 | 360 | === TEST 10: peek() returns nil if a value expired 361 | --- config 362 | location /t { 363 | content_by_lua_block { 364 | local mlcache = require "resty.mlcache" 365 | 366 | local cache, err = mlcache.new("my_mlcache", "cache_shm") 367 | if not cache then 368 | ngx.log(ngx.ERR, err) 369 | return 370 | end 371 | 372 | assert(cache:get("my_key", { ttl = 0.3 }, function() 373 | return 123 374 | end)) 375 | 376 | ngx.sleep(0.3) 377 | 378 | local ttl, err, data, stale = cache:peek("my_key") 379 | if err then 380 | ngx.log(ngx.ERR, err) 381 | return 382 | end 383 | 384 | ngx.say("ttl: ", ttl) 385 | ngx.say("data: ", data) 386 | ngx.say("stale: ", stale) 387 | } 388 | } 389 | --- response_body 390 | ttl: nil 391 | data: nil 392 | stale: nil 393 | --- no_error_log 394 | [error] 395 | [crit] 396 | 397 | 398 | 399 | === TEST 11: peek() returns nil if a value expired in 'shm_miss' 400 | --- config 401 | location /t { 402 | content_by_lua_block { 403 | local mlcache = require "resty.mlcache" 404 | 405 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 406 | shm_miss = "cache_shm_miss" 407 | }) 408 | if not cache then 409 | ngx.log(ngx.ERR, err) 410 | return 411 | end 412 | 413 | local data, err = cache:get("my_key", { neg_ttl = 0.3 }, function() 414 | return nil 415 | end) 416 | if err then 417 | ngx.log(ngx.ERR, err) 418 | return 419 | end 420 | 421 | ngx.sleep(0.3) 422 | 423 | local ttl, err, data, stale = cache:peek("my_key") 424 | if err then 425 | ngx.log(ngx.ERR, err) 426 | return 427 | end 428 | 429 | ngx.say("ttl: ", ttl) 430 | ngx.say("data: ", data) 431 | ngx.say("stale: ", stale) 432 | } 433 | } 434 | --- response_body 435 | ttl: nil 436 | data: nil 437 | stale: nil 438 | --- no_error_log 439 | [error] 440 | [crit] 441 | 442 | 443 | 444 | === TEST 12: peek() accepts stale arg and returns stale values 445 | --- config 446 | location /t { 447 | content_by_lua_block { 448 | local mlcache = require "resty.mlcache" 449 | 450 | local cache, err = mlcache.new("my_mlcache", "cache_shm") 451 | if not cache then 452 | ngx.log(ngx.ERR, err) 453 | return 454 | end 455 | 456 | assert(cache:get("my_key", { ttl = 0.3 }, function() 457 | return 123 458 | end)) 459 | 460 | ngx.sleep(0.3) 461 | 462 | local ttl, err, data, stale = cache:peek("my_key", true) 463 | if err then 464 | ngx.log(ngx.ERR, err) 465 | return 466 | end 467 | 468 | ngx.say("ttl: ", ttl) 469 | ngx.say("data: ", data) 470 | ngx.say("stale: ", stale) 471 | } 472 | } 473 | --- response_body_like chomp 474 | ttl: -0\.\d+ 475 | data: 123 476 | stale: true 477 | --- no_error_log 478 | [error] 479 | [crit] 480 | 481 | 482 | 483 | === TEST 13: peek() accepts stale arg and returns stale values from 'shm_miss' 484 | --- config 485 | location /t { 486 | content_by_lua_block { 487 | local mlcache = require "resty.mlcache" 488 | 489 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 490 | shm_miss = "cache_shm_miss" 491 | }) 492 | if not cache then 493 | ngx.log(ngx.ERR, err) 494 | return 495 | end 496 | 497 | local data, err = cache:get("my_key", { neg_ttl = 0.3 }, function() 498 | return nil 499 | end) 500 | if err then 501 | ngx.log(ngx.ERR, err) 502 | return 503 | end 504 | 505 | ngx.sleep(0.3) 506 | 507 | local ttl, err, data, stale = cache:peek("my_key", true) 508 | if err then 509 | ngx.log(ngx.ERR, err) 510 | return 511 | end 512 | 513 | ngx.say("ttl: ", ttl) 514 | ngx.say("data: ", data) 515 | ngx.say("stale: ", stale) 516 | } 517 | } 518 | --- response_body_like chomp 519 | ttl: -0\.\d+ 520 | data: nil 521 | stale: true 522 | --- no_error_log 523 | [error] 524 | [crit] 525 | 526 | 527 | 528 | === TEST 14: peek() does not evict stale items from L2 shm 529 | --- config 530 | location /t { 531 | content_by_lua_block { 532 | local mlcache = require "resty.mlcache" 533 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 534 | ttl = 0.3, 535 | })) 536 | 537 | local data, err = cache:get("key", nil, function() 538 | return 123 539 | end) 540 | if err then 541 | ngx.log(ngx.ERR, err) 542 | return 543 | end 544 | 545 | ngx.sleep(0.3) 546 | 547 | for i = 1, 3 do 548 | remaining_ttl, err, data = cache:peek("key", true) 549 | if err then 550 | ngx.log(ngx.ERR, err) 551 | return 552 | end 553 | ngx.say("remaining_ttl: ", remaining_ttl) 554 | ngx.say("data: ", data) 555 | end 556 | } 557 | } 558 | --- response_body_like chomp 559 | remaining_ttl: -\d\.\d+ 560 | data: 123 561 | remaining_ttl: -\d\.\d+ 562 | data: 123 563 | remaining_ttl: -\d\.\d+ 564 | data: 123 565 | --- no_error_log 566 | [error] 567 | [crit] 568 | 569 | 570 | 571 | === TEST 15: peek() does not evict stale negative data from L2 shm_miss 572 | --- config 573 | location /t { 574 | content_by_lua_block { 575 | local mlcache = require "resty.mlcache" 576 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 577 | neg_ttl = 0.3, 578 | shm_miss = "cache_shm_miss", 579 | })) 580 | 581 | local data, err = cache:get("key", nil, function() 582 | return nil 583 | end) 584 | if err then 585 | ngx.log(ngx.ERR, err) 586 | return 587 | end 588 | 589 | ngx.sleep(0.3) 590 | 591 | for i = 1, 3 do 592 | remaining_ttl, err, data = cache:peek("key", true) 593 | if err then 594 | ngx.log(ngx.ERR, err) 595 | return 596 | end 597 | ngx.say("remaining_ttl: ", remaining_ttl) 598 | ngx.say("data: ", data) 599 | end 600 | } 601 | } 602 | --- response_body_like chomp 603 | remaining_ttl: -\d\.\d+ 604 | data: nil 605 | remaining_ttl: -\d\.\d+ 606 | data: nil 607 | remaining_ttl: -\d\.\d+ 608 | data: nil 609 | --- no_error_log 610 | [error] 611 | [crit] 612 | -------------------------------------------------------------------------------- /t/04-update.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | workers(2); 8 | #repeat_each(2); 9 | 10 | plan tests => repeat_each() * blocks() * 3; 11 | 12 | run_tests(); 13 | 14 | __DATA__ 15 | 16 | === TEST 1: update() errors if no ipc 17 | --- config 18 | location /t { 19 | content_by_lua_block { 20 | local mlcache = require "resty.mlcache" 21 | 22 | local cache = assert(mlcache.new("my_mlcache", "cache_shm")) 23 | 24 | local ok, err = pcall(cache.update, cache, "foo") 25 | ngx.say(err) 26 | } 27 | } 28 | --- response_body 29 | no polling configured, specify opts.ipc_shm or opts.ipc.poll 30 | --- no_error_log 31 | [error] 32 | 33 | 34 | 35 | === TEST 2: update() calls ipc poll() with timeout arg 36 | --- config 37 | location /t { 38 | content_by_lua_block { 39 | local mlcache = require "resty.mlcache" 40 | 41 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 42 | ipc = { 43 | register_listeners = function() end, 44 | broadcast = function() end, 45 | poll = function(...) 46 | ngx.say("called poll() with args: ", ...) 47 | return true 48 | end, 49 | } 50 | })) 51 | 52 | assert(cache:update(3.5, "not me")) 53 | } 54 | } 55 | --- response_body 56 | called poll() with args: 3.5 57 | --- no_error_log 58 | [error] 59 | 60 | 61 | 62 | === TEST 3: update() JITs when no events to catch up 63 | --- config 64 | location /t { 65 | content_by_lua_block { 66 | local mlcache = require "resty.mlcache" 67 | 68 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 69 | ipc_shm = "ipc_shm", 70 | })) 71 | 72 | for i = 1, 10e3 do 73 | assert(cache:update()) 74 | end 75 | } 76 | } 77 | --- ignore_response_body 78 | --- error_log eval 79 | qr/\[TRACE\s+\d+ content_by_lua\(nginx\.conf:\d+\):8 loop\]/ 80 | --- no_error_log 81 | [error] 82 | -------------------------------------------------------------------------------- /t/05-set.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | #repeat_each(2); 8 | 9 | plan tests => repeat_each() * blocks() * 4; 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: set() errors if no ipc 16 | --- config 17 | location /t { 18 | content_by_lua_block { 19 | local mlcache = require "resty.mlcache" 20 | 21 | local cache = assert(mlcache.new("my_mlcache", "cache_shm")) 22 | 23 | local ok, err = pcall(cache.set, cache, "foo") 24 | ngx.say(err) 25 | } 26 | } 27 | --- response_body 28 | no ipc to propagate update, specify opts.ipc_shm or opts.ipc 29 | --- no_error_log 30 | [error] 31 | [crit] 32 | 33 | 34 | 35 | === TEST 2: set() validates key 36 | --- config 37 | location /t { 38 | content_by_lua_block { 39 | local mlcache = require "resty.mlcache" 40 | 41 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 42 | ipc_shm = "ipc_shm", 43 | })) 44 | 45 | local ok, err = pcall(cache.set, cache) 46 | if not ok then 47 | ngx.say(err) 48 | end 49 | } 50 | } 51 | --- response_body 52 | key must be a string 53 | --- no_error_log 54 | [error] 55 | [crit] 56 | 57 | 58 | 59 | === TEST 3: set() puts a value directly in shm 60 | --- config 61 | location /t { 62 | content_by_lua_block { 63 | local mlcache = require "resty.mlcache" 64 | 65 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 66 | ipc_shm = "ipc_shm", 67 | })) 68 | 69 | -- setting a value in shm 70 | 71 | assert(cache:set("my_key", nil, 123)) 72 | 73 | -- declaring a callback that MUST NOT be called 74 | 75 | local function cb() 76 | ngx.log(ngx.ERR, "callback was called but should not have") 77 | end 78 | 79 | -- try to get() 80 | 81 | local value = assert(cache:get("my_key", nil, cb)) 82 | 83 | ngx.say("value from get(): ", value) 84 | 85 | -- value MUST BE in lru 86 | 87 | local value_lru = cache.lru:get("my_key") 88 | 89 | ngx.say("cache lru value after get(): ", value_lru) 90 | } 91 | } 92 | --- response_body 93 | value from get(): 123 94 | cache lru value after get(): 123 95 | --- no_error_log 96 | [error] 97 | [crit] 98 | 99 | 100 | 101 | === TEST 4: set() puts a negative hit directly in shm_miss if specified 102 | --- config 103 | location /t { 104 | content_by_lua_block { 105 | local mlcache = require "resty.mlcache" 106 | 107 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 108 | ipc_shm = "ipc_shm", 109 | shm_miss = "cache_shm_miss", 110 | })) 111 | 112 | -- setting a value in shm 113 | 114 | assert(cache:set("my_key", nil, nil)) 115 | 116 | -- declaring a callback that MUST NOT be called 117 | 118 | local function cb() 119 | ngx.log(ngx.ERR, "callback was called but should not have") 120 | end 121 | 122 | -- try to get() 123 | 124 | local value, err = cache:get("my_key", nil, cb) 125 | if err then 126 | ngx.log(ngx.ERR, err) 127 | return 128 | end 129 | 130 | ngx.say("value from get(): ", value) 131 | } 132 | } 133 | --- response_body 134 | value from get(): nil 135 | --- no_error_log 136 | [error] 137 | [crit] 138 | 139 | 140 | 141 | === TEST 5: set() puts a value directly in its own LRU 142 | --- config 143 | location /t { 144 | content_by_lua_block { 145 | local mlcache = require "resty.mlcache" 146 | 147 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 148 | ipc_shm = "ipc_shm", 149 | })) 150 | 151 | -- setting a value in shm 152 | 153 | assert(cache:set("my_key", nil, 123)) 154 | 155 | -- value MUST BE be in lru 156 | 157 | local value_lru = cache.lru:get("my_key") 158 | 159 | ngx.say("cache lru value after set(): ", value_lru) 160 | } 161 | } 162 | --- response_body 163 | cache lru value after set(): 123 164 | --- no_error_log 165 | [error] 166 | [crit] 167 | 168 | 169 | 170 | === TEST 6: set() respects 'ttl' for non-nil values 171 | --- config 172 | location /t { 173 | content_by_lua_block { 174 | local mlcache = require "resty.mlcache" 175 | 176 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 177 | ipc_shm = "ipc_shm", 178 | })) 179 | 180 | -- setting a non-nil value in shm 181 | 182 | assert(cache:set("my_key", { 183 | ttl = 0.2, 184 | neg_ttl = 1, 185 | }, 123)) 186 | 187 | -- declaring a callback that logs accesses 188 | 189 | local function cb() 190 | ngx.say("callback called") 191 | return 123 192 | end 193 | 194 | -- try to get() (callback MUST NOT be called) 195 | 196 | ngx.say("calling get()") 197 | local value = assert(cache:get("my_key", nil, cb)) 198 | ngx.say("value from get(): ", value) 199 | 200 | -- wait until expiry 201 | 202 | ngx.say("waiting until expiry...") 203 | ngx.sleep(0.3) 204 | ngx.say("waited 0.3s") 205 | 206 | -- try to get() (callback MUST be called) 207 | 208 | ngx.say("calling get()") 209 | local value = assert(cache:get("my_key", nil, cb)) 210 | ngx.say("value from get(): ", value) 211 | } 212 | } 213 | --- response_body 214 | calling get() 215 | value from get(): 123 216 | waiting until expiry... 217 | waited 0.3s 218 | calling get() 219 | callback called 220 | value from get(): 123 221 | --- no_error_log 222 | [error] 223 | [crit] 224 | 225 | 226 | 227 | === TEST 7: set() respects 'neg_ttl' for nil values 228 | --- config 229 | location /t { 230 | content_by_lua_block { 231 | local mlcache = require "resty.mlcache" 232 | 233 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 234 | ipc_shm = "ipc_shm", 235 | })) 236 | 237 | -- setting a nil value in shm 238 | 239 | assert(cache:set("my_key", { 240 | ttl = 1, 241 | neg_ttl = 0.2, 242 | }, nil)) 243 | 244 | -- declaring a callback that logs accesses 245 | 246 | local function cb() 247 | ngx.say("callback called") 248 | return nil 249 | end 250 | 251 | -- try to get() (callback MUST NOT be called) 252 | 253 | ngx.say("calling get()") 254 | local value, err = cache:get("my_key", nil, cb) 255 | if err then 256 | ngx.log(ngx.ERR, err) 257 | end 258 | ngx.say("value from get(): ", value) 259 | 260 | -- wait until expiry 261 | 262 | ngx.say("waiting until expiry...") 263 | ngx.sleep(0.3) 264 | ngx.say("waited 0.3s") 265 | 266 | -- try to get() (callback MUST be called) 267 | 268 | ngx.say("calling get()") 269 | local value, err = cache:get("my_key", nil, cb) 270 | if err then 271 | ngx.log(ngx.ERR, err) 272 | end 273 | ngx.say("value from get(): ", value) 274 | } 275 | } 276 | --- response_body 277 | calling get() 278 | value from get(): nil 279 | waiting until expiry... 280 | waited 0.3s 281 | calling get() 282 | callback called 283 | value from get(): nil 284 | --- no_error_log 285 | [error] 286 | [crit] 287 | 288 | 289 | 290 | === TEST 8: set() respects 'set_shm_tries' 291 | --- config 292 | location /t { 293 | content_by_lua_block { 294 | local dict = ngx.shared.cache_shm 295 | dict:flush_all() 296 | dict:flush_expired() 297 | local mlcache = require "resty.mlcache" 298 | 299 | -- fill up shm 300 | 301 | local idx = 0 302 | 303 | while true do 304 | local ok, err, forcible = dict:set(idx, string.rep("a", 2^2)) 305 | if not ok then 306 | ngx.log(ngx.ERR, err) 307 | return 308 | end 309 | 310 | if forcible then 311 | break 312 | end 313 | 314 | idx = idx + 1 315 | end 316 | 317 | -- shm:set() will evict up to 30 items when the shm is full 318 | -- now, trigger a hit with a larger value which should trigger LRU 319 | -- eviction and force the slab allocator to free pages 320 | 321 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 322 | ipc_shm = "ipc_shm", 323 | })) 324 | 325 | local data, err = cache:set("key", { 326 | shm_set_tries = 5, 327 | }, string.rep("a", 2^12)) 328 | if err then 329 | ngx.log(ngx.ERR, err) 330 | return 331 | end 332 | 333 | -- from shm 334 | 335 | cache.lru:delete("key") 336 | 337 | local cb_called 338 | local function cb() 339 | cb_called = true 340 | end 341 | 342 | local data, err = cache:get("key", nil, cb) 343 | if err then 344 | ngx.log(ngx.ERR, err) 345 | return 346 | end 347 | 348 | ngx.say("type of data in shm: ", type(data)) 349 | ngx.say("callback was called: ", cb_called ~= nil) 350 | } 351 | } 352 | --- response_body 353 | type of data in shm: string 354 | callback was called: false 355 | --- no_error_log 356 | [warn] 357 | [error] 358 | 359 | 360 | 361 | === TEST 9: set() with shm_miss can set a nil where a value was 362 | --- config 363 | location /t { 364 | content_by_lua_block { 365 | local mlcache = require "resty.mlcache" 366 | 367 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 368 | ipc_shm = "ipc_shm", 369 | shm_miss = "cache_shm_miss", 370 | })) 371 | 372 | local function cb() 373 | return 123 374 | end 375 | 376 | -- install a non-nil value in the cache 377 | 378 | local value, err = cache:get("my_key", nil, cb) 379 | if err then 380 | ngx.log(ngx.ERR, err) 381 | return 382 | end 383 | 384 | ngx.say("initial value from get(): ", value) 385 | 386 | -- override that value with a negative hit that 387 | -- must go in the shm_miss (and the shm value must be 388 | -- erased) 389 | 390 | assert(cache:set("my_key", nil, nil)) 391 | 392 | -- and remove it from the LRU 393 | 394 | cache.lru:delete("my_key") 395 | 396 | -- ok, now we should be getting nil from the cache 397 | 398 | local value, err = cache:get("my_key", nil, cb) 399 | if err then 400 | ngx.log(ngx.ERR, err) 401 | return 402 | end 403 | 404 | ngx.say("value from get() after set(): ", value) 405 | } 406 | } 407 | --- response_body 408 | initial value from get(): 123 409 | value from get() after set(): nil 410 | --- no_error_log 411 | [error] 412 | [crit] 413 | 414 | 415 | 416 | === TEST 10: set() with shm_miss can set a value where a nil was 417 | --- config 418 | location /t { 419 | content_by_lua_block { 420 | local mlcache = require "resty.mlcache" 421 | 422 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 423 | ipc_shm = "ipc_shm", 424 | shm_miss = "cache_shm_miss", 425 | })) 426 | 427 | local function cb() 428 | return nil 429 | end 430 | 431 | -- install a non-nil value in the cache 432 | 433 | local value, err = cache:get("my_key", nil, cb) 434 | if err then 435 | ngx.log(ngx.ERR, err) 436 | return 437 | end 438 | 439 | ngx.say("initial value from get(): ", value) 440 | 441 | -- override that value with a negative hit that 442 | -- must go in the shm_miss (and the shm value must be 443 | -- erased) 444 | 445 | assert(cache:set("my_key", nil, 123)) 446 | 447 | -- and remove it from the LRU 448 | 449 | cache.lru:delete("my_key") 450 | 451 | -- ok, now we should be getting nil from the cache 452 | 453 | local value, err = cache:get("my_key", nil, cb) 454 | if err then 455 | ngx.log(ngx.ERR, err) 456 | return 457 | end 458 | 459 | ngx.say("value from get() after set(): ", value) 460 | } 461 | } 462 | --- response_body 463 | initial value from get(): nil 464 | value from get() after set(): 123 465 | --- no_error_log 466 | [error] 467 | [crit] 468 | 469 | 470 | 471 | === TEST 11: set() returns 'no memory' errors upon fragmentation in the shm 472 | --- config 473 | location /t { 474 | content_by_lua_block { 475 | local mlcache = require "resty.mlcache" 476 | 477 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 478 | ipc_shm = "ipc_shm", 479 | })) 480 | 481 | -- fill shm 482 | 483 | local idx = 0 484 | 485 | while true do 486 | local ok, err, forcible = ngx.shared.cache_shm:set(idx, true) 487 | if not ok then 488 | ngx.log(ngx.ERR, err) 489 | return 490 | end 491 | 492 | if forcible then 493 | break 494 | end 495 | 496 | idx = idx + 1 497 | end 498 | 499 | -- set large value 500 | 501 | local ok, err = cache:set("my_key", { shm_set_tries = 1 }, string.rep("a", 2^10)) 502 | ngx.say(ok) 503 | ngx.say(err) 504 | } 505 | } 506 | --- response_body 507 | nil 508 | could not write to lua_shared_dict 'cache_shm': no memory 509 | --- no_error_log 510 | [warn] 511 | [error] 512 | 513 | 514 | 515 | === TEST 12: set() does not set LRU upon shm insertion error 516 | --- config 517 | location /t { 518 | content_by_lua_block { 519 | local mlcache = require "resty.mlcache" 520 | 521 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 522 | ipc_shm = "ipc_shm", 523 | })) 524 | 525 | -- fill shm 526 | 527 | local idx = 0 528 | 529 | while true do 530 | local ok, err, forcible = ngx.shared.cache_shm:set(idx, true) 531 | if not ok then 532 | ngx.log(ngx.ERR, err) 533 | return 534 | end 535 | 536 | if forcible then 537 | break 538 | end 539 | 540 | idx = idx + 1 541 | end 542 | 543 | -- set large value 544 | 545 | local ok = cache:set("my_key", { shm_set_tries = 1 }, string.rep("a", 2^10)) 546 | assert(ok == nil) 547 | 548 | local data = cache.lru:get("my_key") 549 | ngx.say(data) 550 | } 551 | } 552 | --- response_body 553 | nil 554 | --- no_error_log 555 | [error] 556 | [crit] 557 | 558 | 559 | 560 | === TEST 13: set() calls broadcast() with invalidated key 561 | --- config 562 | location /t { 563 | content_by_lua_block { 564 | local mlcache = require "resty.mlcache" 565 | 566 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 567 | ipc = { 568 | register_listeners = function() end, 569 | broadcast = function(channel, data, ...) 570 | ngx.say("channel: ", channel) 571 | ngx.say("data: ", data) 572 | ngx.say("other args:", ...) 573 | return true 574 | end, 575 | poll = function() end, 576 | } 577 | })) 578 | 579 | assert(cache:set("my_key", nil, nil)) 580 | } 581 | } 582 | --- response_body 583 | channel: mlcache:invalidations:my_mlcache 584 | data: my_key 585 | other args: 586 | --- no_error_log 587 | [error] 588 | [crit] 589 | -------------------------------------------------------------------------------- /t/06-delete.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | workers(2); 8 | #repeat_each(2); 9 | 10 | plan tests => repeat_each() * blocks() * 3; 11 | 12 | run_tests(); 13 | 14 | __DATA__ 15 | 16 | === TEST 1: delete() errors if no ipc 17 | --- config 18 | location /t { 19 | content_by_lua_block { 20 | local mlcache = require "resty.mlcache" 21 | 22 | local cache = assert(mlcache.new("my_mlcache", "cache_shm")) 23 | 24 | local ok, err = pcall(cache.delete, cache, "foo") 25 | ngx.say(err) 26 | } 27 | } 28 | --- response_body 29 | no ipc to propagate deletion, specify opts.ipc_shm or opts.ipc 30 | --- no_error_log 31 | [error] 32 | 33 | 34 | 35 | === TEST 2: delete() validates key 36 | --- config 37 | location /t { 38 | content_by_lua_block { 39 | local mlcache = require "resty.mlcache" 40 | 41 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 42 | ipc_shm = "ipc_shm", 43 | })) 44 | 45 | local ok, err = pcall(cache.delete, cache, 123) 46 | ngx.say(err) 47 | } 48 | } 49 | --- response_body 50 | key must be a string 51 | --- no_error_log 52 | [error] 53 | 54 | 55 | 56 | === TEST 3: delete() removes a cached value from LRU + shm 57 | --- config 58 | location /t { 59 | content_by_lua_block { 60 | local mlcache = require "resty.mlcache" 61 | 62 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 63 | ipc_shm = "ipc_shm", 64 | })) 65 | 66 | local value = 123 67 | 68 | local function cb() 69 | ngx.say("in callback") 70 | return value 71 | end 72 | 73 | -- set a value (callback call) 74 | 75 | local data = assert(cache:get("key", nil, cb)) 76 | ngx.say("from callback: ", data) 77 | 78 | -- get a value (no callback call) 79 | 80 | data = assert(cache:get("key", nil, cb)) 81 | ngx.say("from LRU: ", data) 82 | 83 | -- test if value is set from shm (safer to check due to the key) 84 | 85 | local v = ngx.shared.cache_shm:get(cache.name .. "key") 86 | ngx.say("shm has value before delete: ", v ~= nil) 87 | 88 | -- delete the value 89 | 90 | assert(cache:delete("key")) 91 | 92 | local v = ngx.shared.cache_shm:get(cache.name .. "key") 93 | ngx.say("shm has value after delete: ", v ~= nil) 94 | 95 | -- ensure LRU was also deleted 96 | 97 | v = cache.lru:get("key") 98 | ngx.say("from LRU: ", v) 99 | 100 | -- start over from callback again 101 | 102 | value = 456 103 | 104 | data = assert(cache:get("key", nil, cb)) 105 | ngx.say("from callback: ", data) 106 | } 107 | } 108 | --- response_body 109 | in callback 110 | from callback: 123 111 | from LRU: 123 112 | shm has value before delete: true 113 | shm has value after delete: false 114 | from LRU: nil 115 | in callback 116 | from callback: 456 117 | --- no_error_log 118 | [error] 119 | 120 | 121 | 122 | === TEST 4: delete() removes a cached nil from shm_miss if specified 123 | --- config 124 | location /t { 125 | content_by_lua_block { 126 | local mlcache = require "resty.mlcache" 127 | 128 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 129 | ipc_shm = "ipc_shm", 130 | shm_miss = "cache_shm_miss", 131 | })) 132 | 133 | local value = nil 134 | 135 | local function cb() 136 | ngx.say("in callback") 137 | return value 138 | end 139 | 140 | -- set a value (callback call) 141 | 142 | local data, err = cache:get("key", nil, cb) 143 | if err then 144 | ngx.log(ngx.ERR, err) 145 | return 146 | end 147 | ngx.say("from callback: ", data) 148 | 149 | -- get a value (no callback call) 150 | 151 | data, err = cache:get("key", nil, cb) 152 | if err then 153 | ngx.log(ngx.ERR, err) 154 | return 155 | end 156 | ngx.say("from LRU: ", data) 157 | 158 | -- test if value is set from shm (safer to check due to the key) 159 | 160 | local v = ngx.shared.cache_shm_miss:get(cache.name .. "key") 161 | ngx.say("shm_miss has value before delete: ", v ~= nil) 162 | 163 | -- delete the value 164 | 165 | assert(cache:delete("key")) 166 | 167 | local v = ngx.shared.cache_shm_miss:get(cache.name .. "key") 168 | ngx.say("shm_miss has value after delete: ", v ~= nil) 169 | 170 | -- ensure LRU was also deleted 171 | 172 | v = cache.lru:get("key") 173 | ngx.say("from LRU: ", v) 174 | 175 | -- start over from callback again 176 | 177 | value = 456 178 | 179 | data, err = cache:get("key", nil, cb) 180 | if err then 181 | ngx.log(ngx.ERR, err) 182 | return 183 | end 184 | ngx.say("from callback again: ", data) 185 | } 186 | } 187 | --- response_body 188 | in callback 189 | from callback: nil 190 | from LRU: nil 191 | shm_miss has value before delete: true 192 | shm_miss has value after delete: false 193 | from LRU: nil 194 | in callback 195 | from callback again: 456 196 | --- no_error_log 197 | [error] 198 | 199 | 200 | 201 | === TEST 5: delete() calls broadcast with invalidated key 202 | --- config 203 | location /t { 204 | content_by_lua_block { 205 | local mlcache = require "resty.mlcache" 206 | 207 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 208 | ipc = { 209 | register_listeners = function() end, 210 | broadcast = function(channel, data, ...) 211 | ngx.say("channel: ", channel) 212 | ngx.say("data: ", data) 213 | ngx.say("other args:", ...) 214 | return true 215 | end, 216 | poll = function() end, 217 | } 218 | })) 219 | 220 | assert(cache:delete("my_key")) 221 | } 222 | } 223 | --- response_body 224 | channel: mlcache:invalidations:my_mlcache 225 | data: my_key 226 | other args: 227 | --- no_error_log 228 | [error] 229 | -------------------------------------------------------------------------------- /t/07-l1_serializer.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | workers(2); 8 | #repeat_each(2); 9 | 10 | plan tests => repeat_each() * blocks() * 3; 11 | 12 | run_tests(); 13 | 14 | __DATA__ 15 | 16 | === TEST 1: l1_serializer is validated by the constructor 17 | --- config 18 | location /t { 19 | content_by_lua_block { 20 | local mlcache = require "resty.mlcache" 21 | 22 | local ok, err = pcall(mlcache.new, "my_mlcache", "cache_shm", { 23 | l1_serializer = false, 24 | }) 25 | if not ok then 26 | ngx.say(err) 27 | end 28 | } 29 | } 30 | --- response_body 31 | opts.l1_serializer must be a function 32 | --- no_error_log 33 | [error] 34 | 35 | 36 | 37 | === TEST 2: l1_serializer is called on L1+L2 cache misses 38 | --- config 39 | location /t { 40 | content_by_lua_block { 41 | local mlcache = require "resty.mlcache" 42 | 43 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 44 | l1_serializer = function(s) 45 | return string.format("transform(%q)", s) 46 | end, 47 | }) 48 | if not cache then 49 | ngx.log(ngx.ERR, err) 50 | return 51 | end 52 | 53 | local data, err = cache:get("key", nil, function() return "foo" end) 54 | if not data then 55 | ngx.log(ngx.ERR, err) 56 | return 57 | end 58 | 59 | ngx.say(data) 60 | } 61 | } 62 | --- response_body 63 | transform("foo") 64 | --- no_error_log 65 | [error] 66 | 67 | 68 | 69 | === TEST 3: get() JITs when hit of scalar value coming from shm with l1_serializer 70 | --- config 71 | location /t { 72 | content_by_lua_block { 73 | local mlcache = require "resty.mlcache" 74 | 75 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 76 | l1_serializer = function(i) 77 | return i + 2 78 | end, 79 | }) 80 | if not cache then 81 | ngx.log(ngx.ERR, err) 82 | return 83 | end 84 | 85 | local function cb_number() 86 | return 123456 87 | end 88 | 89 | for i = 1, 10e2 do 90 | local data = assert(cache:get("number", nil, cb_number)) 91 | assert(data == 123458) 92 | 93 | cache.lru:delete("number") 94 | end 95 | } 96 | } 97 | --- ignore_response_body 98 | --- error_log eval 99 | qr/\[TRACE\s+\d+ content_by_lua\(nginx\.conf:\d+\):18 loop\]/ 100 | --- no_error_log 101 | [error] 102 | 103 | 104 | 105 | === TEST 4: l1_serializer is not called on L1 hits 106 | --- config 107 | location /t { 108 | content_by_lua_block { 109 | local mlcache = require "resty.mlcache" 110 | 111 | local calls = 0 112 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 113 | l1_serializer = function(s) 114 | calls = calls + 1 115 | return string.format("transform(%q)", s) 116 | end, 117 | }) 118 | if not cache then 119 | ngx.log(ngx.ERR, err) 120 | return 121 | end 122 | 123 | for i = 1, 3 do 124 | local data, err = cache:get("key", nil, function() return "foo" end) 125 | if not data then 126 | ngx.log(ngx.ERR, err) 127 | return 128 | end 129 | 130 | ngx.say(data) 131 | end 132 | 133 | ngx.say("calls: ", calls) 134 | } 135 | } 136 | --- response_body 137 | transform("foo") 138 | transform("foo") 139 | transform("foo") 140 | calls: 1 141 | --- no_error_log 142 | [error] 143 | 144 | 145 | 146 | === TEST 5: l1_serializer is called on each L2 hit 147 | --- config 148 | location /t { 149 | content_by_lua_block { 150 | local mlcache = require "resty.mlcache" 151 | 152 | local calls = 0 153 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 154 | l1_serializer = function(s) 155 | calls = calls + 1 156 | return string.format("transform(%q)", s) 157 | end, 158 | }) 159 | if not cache then 160 | ngx.log(ngx.ERR, err) 161 | return 162 | end 163 | 164 | for i = 1, 3 do 165 | local data, err = cache:get("key", nil, function() return "foo" end) 166 | if not data then 167 | ngx.log(ngx.ERR, err) 168 | return 169 | end 170 | 171 | ngx.say(data) 172 | cache.lru:delete("key") 173 | end 174 | 175 | ngx.say("calls: ", calls) 176 | } 177 | } 178 | --- response_body 179 | transform("foo") 180 | transform("foo") 181 | transform("foo") 182 | calls: 3 183 | --- no_error_log 184 | [error] 185 | 186 | 187 | 188 | === TEST 6: l1_serializer is called on boolean false hits 189 | --- config 190 | location /t { 191 | content_by_lua_block { 192 | local mlcache = require "resty.mlcache" 193 | 194 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 195 | l1_serializer = function(s) 196 | return string.format("transform_boolean(%q)", s) 197 | end, 198 | }) 199 | if not cache then 200 | ngx.log(ngx.ERR, err) 201 | return 202 | end 203 | 204 | local function cb() 205 | return false 206 | end 207 | 208 | local data, err = cache:get("key", nil, cb) 209 | if not data then 210 | ngx.log(ngx.ERR, err) 211 | return 212 | end 213 | 214 | ngx.say(data) 215 | } 216 | } 217 | --- response_body 218 | transform_boolean("false") 219 | --- no_error_log 220 | [error] 221 | 222 | 223 | 224 | === TEST 7: l1_serializer is called on lock timeout 225 | --- config 226 | location /t { 227 | content_by_lua_block { 228 | -- insert 2 dummy values to ensure that lock acquisition (which 229 | -- uses shm:set) will _not_ evict out stale cached value 230 | ngx.shared.cache_shm:set(1, true, 0.2) 231 | ngx.shared.cache_shm:set(2, true, 0.2) 232 | 233 | local mlcache = require "resty.mlcache" 234 | local cache_1 = assert(mlcache.new("my_mlcache", "cache_shm", { 235 | ttl = 0.3, 236 | resurrect_ttl = 0.3, 237 | l1_serializer = function(s) 238 | return "from cache_1" 239 | end, 240 | })) 241 | local cache_2 = assert(mlcache.new("my_mlcache", "cache_shm", { 242 | ttl = 0.3, 243 | resurrect_ttl = 0.3, 244 | l1_serializer = function(s) 245 | return "from cache_2" 246 | end, 247 | resty_lock_opts = { 248 | timeout = 0.2 249 | } 250 | })) 251 | 252 | local function cb(delay, return_val) 253 | if delay then 254 | ngx.sleep(delay) 255 | end 256 | 257 | return return_val or 123 258 | end 259 | 260 | -- cache in shm 261 | 262 | local data, err, hit_lvl = cache_1:get("my_key", nil, cb) 263 | assert(data == "from cache_1") 264 | assert(err == nil) 265 | assert(hit_lvl == 3) 266 | 267 | -- make shm + LRU expire 268 | 269 | ngx.sleep(0.3) 270 | 271 | local t1 = ngx.thread.spawn(function() 272 | -- trigger L3 callback again, but slow to return this time 273 | 274 | cache_1:get("my_key", nil, cb, 0.3, 456) 275 | end) 276 | 277 | local t2 = ngx.thread.spawn(function() 278 | -- make this mlcache wait on other's callback, and timeout 279 | 280 | local data, err, hit_lvl = cache_2:get("my_key", nil, cb) 281 | ngx.say("data: ", data) 282 | ngx.say("err: ", err) 283 | ngx.say("hit_lvl: ", hit_lvl) 284 | end) 285 | 286 | assert(ngx.thread.wait(t1)) 287 | assert(ngx.thread.wait(t2)) 288 | 289 | ngx.say() 290 | ngx.say("-> subsequent get()") 291 | data, err, hit_lvl = cache_2:get("my_key", nil, cb, nil, 123) 292 | ngx.say("data: ", data) 293 | ngx.say("err: ", err) 294 | ngx.say("hit_lvl: ", hit_lvl) -- should be 1 since LRU instances are shared by mlcache namespace, and t1 finished 295 | } 296 | } 297 | --- response_body 298 | data: from cache_2 299 | err: nil 300 | hit_lvl: 4 301 | 302 | -> subsequent get() 303 | data: from cache_1 304 | err: nil 305 | hit_lvl: 1 306 | --- error_log eval 307 | qr/\[warn\] .*? could not acquire callback lock: timeout/ 308 | 309 | 310 | 311 | === TEST 8: l1_serializer is called when value has < 1ms remaining_ttl 312 | --- config 313 | location /t { 314 | content_by_lua_block { 315 | local forced_now = ngx.now() 316 | ngx.now = function() 317 | return forced_now 318 | end 319 | 320 | local mlcache = require "resty.mlcache" 321 | 322 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 323 | ttl = 0.2, 324 | l1_serializer = function(s) 325 | return "override" 326 | end, 327 | })) 328 | 329 | local function cb(v) 330 | return v or 42 331 | end 332 | 333 | local data, err = cache:get("key", nil, cb) 334 | assert(data == "override", err or "invalid data value: " .. data) 335 | 336 | -- drop L1 cache value 337 | cache.lru:delete("key") 338 | 339 | -- advance 0.2 second in the future, and simulate another :get() 340 | -- call; the L2 shm entry will still be alive (as its clock is 341 | -- not faked), but mlcache will compute a remaining_ttl of 0; 342 | -- In such cases, we should _not_ cache the value indefinitely in 343 | -- the L1 LRU cache. 344 | forced_now = forced_now + 0.2 345 | 346 | local data, err, hit_lvl = cache:get("key", nil, cb) 347 | assert(data == "override", err or "invalid data value: " .. data) 348 | 349 | ngx.say("+0.200s hit_lvl: ", hit_lvl) 350 | 351 | -- the value is not cached in LRU (too short ttl anyway) 352 | 353 | data, err, hit_lvl = cache:get("key", nil, cb) 354 | assert(data == "override", err or "invalid data value: " .. data) 355 | 356 | ngx.say("+0.200s hit_lvl: ", hit_lvl) 357 | 358 | -- make it expire in shm (real wait) 359 | ngx.sleep(0.201) 360 | 361 | data, err, hit_lvl = cache:get("key", nil, cb, 91) 362 | assert(data == "override", err or "invalid data value: " .. data) 363 | 364 | ngx.say("+0.201s hit_lvl: ", hit_lvl) 365 | } 366 | } 367 | --- response_body 368 | +0.200s hit_lvl: 2 369 | +0.200s hit_lvl: 2 370 | +0.201s hit_lvl: 3 371 | --- no_error_log 372 | [error] 373 | 374 | 375 | 376 | === TEST 9: l1_serializer is called in protected mode (L2 miss) 377 | --- config 378 | location /t { 379 | content_by_lua_block { 380 | local mlcache = require "resty.mlcache" 381 | 382 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 383 | l1_serializer = function(s) 384 | error("cannot transform") 385 | end, 386 | }) 387 | if not cache then 388 | ngx.log(ngx.ERR, err) 389 | return 390 | end 391 | 392 | local data, err = cache:get("key", nil, function() return "foo" end) 393 | if not data then 394 | ngx.say(err) 395 | end 396 | 397 | ngx.say(data) 398 | } 399 | } 400 | --- response_body_like 401 | l1_serializer threw an error: .*?: cannot transform 402 | --- no_error_log 403 | [error] 404 | 405 | 406 | 407 | === TEST 10: l1_serializer is called in protected mode (L2 hit) 408 | --- config 409 | location /t { 410 | content_by_lua_block { 411 | local mlcache = require "resty.mlcache" 412 | 413 | local called = false 414 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 415 | l1_serializer = function(s) 416 | if called then error("cannot transform") end 417 | called = true 418 | return string.format("transform(%q)", s) 419 | end, 420 | }) 421 | if not cache then 422 | ngx.log(ngx.ERR, err) 423 | return 424 | end 425 | 426 | assert(cache:get("key", nil, function() return "foo" end)) 427 | cache.lru:delete("key") 428 | 429 | local data, err = cache:get("key", nil, function() return "foo" end) 430 | if not data then 431 | ngx.say(err) 432 | end 433 | 434 | ngx.say(data) 435 | } 436 | } 437 | --- response_body_like 438 | l1_serializer threw an error: .*?: cannot transform 439 | --- no_error_log 440 | [error] 441 | 442 | 443 | 444 | === TEST 11: l1_serializer is not called for L2+L3 misses (no record) 445 | --- config 446 | location /t { 447 | content_by_lua_block { 448 | local mlcache = require "resty.mlcache" 449 | 450 | local called = false 451 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 452 | l1_serializer = function(s) 453 | called = true 454 | return string.format("transform(%s)", s) 455 | end, 456 | }) 457 | if not cache then 458 | ngx.log(ngx.ERR, err) 459 | return 460 | end 461 | 462 | local data, err = cache:get("key", nil, function() return nil end) 463 | if data ~= nil then 464 | ngx.log(ngx.ERR, "got a value for a L3 miss: ", tostring(data)) 465 | return 466 | elseif err ~= nil then 467 | ngx.log(ngx.ERR, "got an error for a L3 miss: ", tostring(err)) 468 | return 469 | end 470 | 471 | -- our L3 returned nil, we do not call the l1_serializer and 472 | -- we store the LRU nil sentinel value 473 | 474 | ngx.say("l1_serializer called for L3 miss: ", called) 475 | 476 | -- delete from LRU, and try from L2 again 477 | 478 | cache.lru:delete("key") 479 | 480 | local data, err = cache:get("key", nil, function() error("not supposed to call") end) 481 | if data ~= nil then 482 | ngx.log(ngx.ERR, "got a value for a L3 miss: ", tostring(data)) 483 | return 484 | elseif err ~= nil then 485 | ngx.log(ngx.ERR, "got an error for a L3 miss: ", tostring(err)) 486 | return 487 | end 488 | 489 | ngx.say("l1_serializer called for L2 negative hit: ", called) 490 | } 491 | } 492 | --- response_body 493 | l1_serializer called for L3 miss: false 494 | l1_serializer called for L2 negative hit: false 495 | --- no_error_log 496 | [error] 497 | 498 | 499 | 500 | === TEST 12: l1_serializer is not supposed to return a nil value 501 | --- config 502 | location /t { 503 | content_by_lua_block { 504 | local mlcache = require "resty.mlcache" 505 | 506 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 507 | l1_serializer = function(s) 508 | return nil 509 | end, 510 | }) 511 | if not cache then 512 | ngx.log(ngx.ERR, err) 513 | return 514 | end 515 | 516 | local ok, err = cache:get("key", nil, function() return "foo" end) 517 | assert(not ok, "get() should not return successfully") 518 | ngx.say(err) 519 | } 520 | } 521 | --- response_body_like 522 | l1_serializer returned a nil value 523 | --- no_error_log 524 | [error] 525 | 526 | 527 | 528 | === TEST 13: l1_serializer can return nil + error 529 | --- config 530 | location /t { 531 | content_by_lua_block { 532 | local mlcache = require "resty.mlcache" 533 | 534 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 535 | l1_serializer = function(s) 536 | return nil, "l1_serializer: cannot transform" 537 | end, 538 | }) 539 | if not cache then 540 | ngx.log(ngx.ERR, err) 541 | return 542 | end 543 | 544 | local data, err = cache:get("key", nil, function() return "foo" end) 545 | if not data then 546 | ngx.say(err) 547 | end 548 | 549 | ngx.say("data: ", data) 550 | } 551 | } 552 | --- response_body 553 | l1_serializer: cannot transform 554 | data: nil 555 | --- no_error_log 556 | [error] 557 | 558 | 559 | 560 | === TEST 14: l1_serializer can be given as a get() argument 561 | --- config 562 | location /t { 563 | content_by_lua_block { 564 | local mlcache = require "resty.mlcache" 565 | 566 | local cache, err = mlcache.new("my_mlcache", "cache_shm") 567 | if not cache then 568 | ngx.log(ngx.ERR, err) 569 | return 570 | end 571 | 572 | local data, err = cache:get("key", { 573 | l1_serializer = function(s) 574 | return string.format("transform(%q)", s) 575 | end 576 | }, function() return "foo" end) 577 | if not data then 578 | ngx.log(ngx.ERR, err) 579 | return 580 | end 581 | 582 | ngx.say(data) 583 | } 584 | } 585 | --- response_body 586 | transform("foo") 587 | --- no_error_log 588 | [error] 589 | 590 | 591 | 592 | === TEST 15: l1_serializer as get() argument has precedence over the constructor one 593 | --- config 594 | location /t { 595 | content_by_lua_block { 596 | local mlcache = require "resty.mlcache" 597 | 598 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 599 | l1_serializer = function(s) 600 | return string.format("constructor(%q)", s) 601 | end 602 | }) 603 | 604 | if not cache then 605 | ngx.log(ngx.ERR, err) 606 | return 607 | end 608 | 609 | local data, err = cache:get("key1", { 610 | l1_serializer = function(s) 611 | return string.format("get_argument(%q)", s) 612 | end 613 | }, function() return "foo" end) 614 | if not data then 615 | ngx.log(ngx.ERR, err) 616 | return 617 | end 618 | 619 | ngx.say(data) 620 | 621 | local data, err = cache:get("key2", nil, function() return "bar" end) 622 | if not data then 623 | ngx.log(ngx.ERR, err) 624 | return 625 | end 626 | 627 | ngx.say(data) 628 | } 629 | } 630 | --- response_body 631 | get_argument("foo") 632 | constructor("bar") 633 | --- no_error_log 634 | [error] 635 | 636 | 637 | 638 | === TEST 16: get() validates l1_serializer is a function 639 | --- config 640 | location /t { 641 | content_by_lua_block { 642 | local mlcache = require "resty.mlcache" 643 | 644 | local cache, err = mlcache.new("my_mlcache", "cache_shm") 645 | if not cache then 646 | ngx.log(ngx.ERR, err) 647 | return 648 | end 649 | 650 | local ok, err = pcall(cache.get, cache, "key", { 651 | l1_serializer = false, 652 | }, function() return "foo" end) 653 | if not data then 654 | ngx.say(err) 655 | end 656 | } 657 | } 658 | --- response_body 659 | opts.l1_serializer must be a function 660 | --- no_error_log 661 | [error] 662 | 663 | 664 | 665 | === TEST 17: set() calls l1_serializer 666 | --- config 667 | location /t { 668 | content_by_lua_block { 669 | local mlcache = require "resty.mlcache" 670 | 671 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 672 | ipc_shm = "ipc_shm", 673 | l1_serializer = function(s) 674 | return string.format("transform(%q)", s) 675 | end 676 | }) 677 | if not cache then 678 | ngx.log(ngx.ERR, err) 679 | return 680 | end 681 | 682 | local ok, err = cache:set("key", nil, "value") 683 | if not ok then 684 | ngx.log(ngx.ERR, err) 685 | return 686 | end 687 | 688 | local value, err = cache:get("key", nil, error) 689 | if not value then 690 | ngx.log(ngx.ERR, err) 691 | return 692 | end 693 | 694 | ngx.say(value) 695 | } 696 | } 697 | --- response_body 698 | transform("value") 699 | --- no_error_log 700 | [error] 701 | 702 | 703 | 704 | === TEST 18: set() calls l1_serializer for boolean false values 705 | --- config 706 | location /t { 707 | content_by_lua_block { 708 | local mlcache = require "resty.mlcache" 709 | 710 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 711 | ipc_shm = "ipc_shm", 712 | l1_serializer = function(s) 713 | return string.format("transform_boolean(%q)", s) 714 | end 715 | }) 716 | if not cache then 717 | ngx.log(ngx.ERR, err) 718 | return 719 | end 720 | 721 | local ok, err = cache:set("key", nil, false) 722 | if not ok then 723 | ngx.log(ngx.ERR, err) 724 | return 725 | end 726 | 727 | local value, err = cache:get("key", nil, error) 728 | if not value then 729 | ngx.log(ngx.ERR, err) 730 | return 731 | end 732 | 733 | ngx.say(value) 734 | } 735 | } 736 | --- response_body 737 | transform_boolean("false") 738 | --- no_error_log 739 | [error] 740 | 741 | 742 | 743 | === TEST 19: l1_serializer as set() argument has precedence over the constructor one 744 | --- config 745 | location /t { 746 | content_by_lua_block { 747 | local mlcache = require "resty.mlcache" 748 | 749 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 750 | ipc_shm = "ipc_shm", 751 | l1_serializer = function(s) 752 | return string.format("constructor(%q)", s) 753 | end 754 | }) 755 | if not cache then 756 | ngx.log(ngx.ERR, err) 757 | return 758 | end 759 | 760 | local ok, err = cache:set("key", { 761 | l1_serializer = function(s) 762 | return string.format("set_argument(%q)", s) 763 | end 764 | }, "value") 765 | if not ok then 766 | ngx.log(ngx.ERR, err) 767 | return 768 | end 769 | 770 | local value, err = cache:get("key", nil, error) 771 | if not value then 772 | ngx.log(ngx.ERR, err) 773 | return 774 | end 775 | 776 | ngx.say(value) 777 | } 778 | } 779 | --- response_body 780 | set_argument("value") 781 | --- no_error_log 782 | [error] 783 | 784 | 785 | 786 | === TEST 20: set() validates l1_serializer is a function 787 | --- config 788 | location /t { 789 | content_by_lua_block { 790 | local mlcache = require "resty.mlcache" 791 | 792 | local cache, err = mlcache.new("my_mlcache", "cache_shm", { 793 | ipc_shm = "ipc_shm", 794 | }) 795 | if not cache then 796 | ngx.log(ngx.ERR, err) 797 | return 798 | end 799 | 800 | local ok, err = pcall(cache.set, cache, "key", { 801 | l1_serializer = true 802 | }, "value") 803 | if not data then 804 | ngx.say(err) 805 | end 806 | } 807 | } 808 | --- response_body 809 | opts.l1_serializer must be a function 810 | --- no_error_log 811 | [error] 812 | -------------------------------------------------------------------------------- /t/08-purge.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | #repeat_each(2); 8 | 9 | plan tests => repeat_each() * blocks() * 3; 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: purge() errors if no ipc 16 | --- config 17 | location /t { 18 | content_by_lua_block { 19 | local mlcache = require "resty.mlcache" 20 | 21 | local cache = assert(mlcache.new("my_mlcache", "cache_shm")) 22 | 23 | local ok, err = pcall(cache.purge, cache) 24 | ngx.say(err) 25 | } 26 | } 27 | --- response_body 28 | no ipc to propagate purge, specify opts.ipc_shm or opts.ipc 29 | --- no_error_log 30 | [error] 31 | 32 | 33 | 34 | === TEST 2: purge() deletes all items from L1 + L2 (sanity 1/2) 35 | --- config 36 | location /t { 37 | content_by_lua_block { 38 | local mlcache = require "resty.mlcache" 39 | 40 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 41 | ipc_shm = "ipc_shm", 42 | })) 43 | 44 | -- populate mlcache 45 | 46 | for i = 1, 100 do 47 | assert(cache:get(tostring(i), nil, function() return i end)) 48 | end 49 | 50 | -- purge 51 | 52 | assert(cache:purge()) 53 | 54 | for i = 1, 100 do 55 | local value, err = cache:get(tostring(i), nil, function() return nil end) 56 | if err then 57 | ngx.log(ngx.ERR, err) 58 | return 59 | end 60 | 61 | if value ~= nil then 62 | ngx.say("key ", i, " had: ", value) 63 | end 64 | end 65 | 66 | ngx.say("ok") 67 | } 68 | } 69 | --- response_body 70 | ok 71 | --- no_error_log 72 | [error] 73 | 74 | 75 | 76 | === TEST 3: purge() deletes all items from L1 (sanity 2/2) 77 | --- config 78 | location /t { 79 | content_by_lua_block { 80 | local mlcache = require "resty.mlcache" 81 | 82 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 83 | ipc_shm = "ipc_shm", 84 | })) 85 | 86 | -- populate mlcache 87 | 88 | for i = 1, 100 do 89 | assert(cache:get(tostring(i), nil, function() return i end)) 90 | end 91 | 92 | -- purge 93 | 94 | assert(cache:purge()) 95 | 96 | for i = 1, 100 do 97 | local value = cache.lru:get(tostring(i)) 98 | 99 | if value ~= nil then 100 | ngx.say("key ", i, " had: ", value) 101 | end 102 | end 103 | 104 | ngx.say("ok") 105 | } 106 | } 107 | --- response_body 108 | ok 109 | --- no_error_log 110 | [error] 111 | 112 | 113 | 114 | === TEST 4: purge() deletes all items from L1 with a custom LRU 115 | --- skip_eval: 3: t::TestMLCache::skip_openresty('<', '1.13.6.2') 116 | --- config 117 | location /t { 118 | content_by_lua_block { 119 | local mlcache = require "resty.mlcache" 120 | local lrucache = require "resty.lrucache" 121 | 122 | local lru = lrucache.new(100) 123 | 124 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 125 | ipc_shm = "ipc_shm", 126 | lru = lru, 127 | })) 128 | 129 | -- populate mlcache 130 | 131 | for i = 1, 100 do 132 | assert(cache:get(tostring(i), nil, function() return i end)) 133 | end 134 | 135 | -- purge 136 | 137 | assert(cache:purge()) 138 | 139 | for i = 1, 100 do 140 | local value = cache.lru:get(tostring(i)) 141 | 142 | if value ~= nil then 143 | ngx.say("key ", i, " had: ", value) 144 | end 145 | end 146 | 147 | ngx.say("ok") 148 | ngx.say("lru instance is the same one: ", lru == cache.lru) 149 | } 150 | } 151 | --- response_body 152 | ok 153 | lru instance is the same one: true 154 | --- no_error_log 155 | [error] 156 | 157 | 158 | 159 | === TEST 5: purge() is prevented if custom LRU does not support flush_all() 160 | --- skip_eval: 3: t::TestMLCache::skip_openresty('>', '1.13.6.1') 161 | --- config 162 | location /t { 163 | content_by_lua_block { 164 | local mlcache = require "resty.mlcache" 165 | local lrucache = require "resty.lrucache" 166 | 167 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 168 | ipc_shm = "ipc_shm", 169 | lru = lrucache.new(10), 170 | })) 171 | 172 | local pok, perr = pcall(cache.purge, cache) 173 | if not pok then 174 | ngx.say(perr) 175 | return 176 | end 177 | 178 | ngx.say("ok") 179 | } 180 | } 181 | --- response_body 182 | cannot purge when using custom LRU cache with OpenResty < 1.13.6.2 183 | --- no_error_log 184 | [error] 185 | 186 | 187 | 188 | === TEST 6: purge() deletes all items from shm_miss is specified 189 | --- config 190 | location /t { 191 | content_by_lua_block { 192 | local mlcache = require "resty.mlcache" 193 | 194 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 195 | ipc_shm = "ipc_shm", 196 | shm_miss = "cache_shm_miss", 197 | })) 198 | 199 | -- populate mlcache 200 | 201 | for i = 1, 100 do 202 | local _, err = cache:get(tostring(i), nil, function() return nil end) 203 | if err then 204 | ngx.log(ngx.ERR, err) 205 | return 206 | end 207 | end 208 | 209 | -- purge 210 | 211 | assert(cache:purge()) 212 | 213 | local called = 0 214 | 215 | for i = 1, 100 do 216 | local value, err = cache:get(tostring(i), nil, function() return i end) 217 | 218 | if value ~= i then 219 | ngx.say("key ", i, " had: ", value) 220 | end 221 | end 222 | 223 | ngx.say("ok") 224 | } 225 | } 226 | --- response_body 227 | ok 228 | --- no_error_log 229 | [error] 230 | 231 | 232 | 233 | === TEST 7: purge() does not call shm:flush_expired() by default 234 | --- config 235 | location /t { 236 | content_by_lua_block { 237 | do 238 | local cache_shm = ngx.shared.cache_shm 239 | local mt = getmetatable(cache_shm) 240 | local orig_cache_shm_flush_expired = mt.flush_expired 241 | 242 | mt.flush_expired = function(self, ...) 243 | ngx.say("flush_expired called with 'max_count'") 244 | 245 | return orig_cache_shm_flush_expired(self, ...) 246 | end 247 | end 248 | 249 | local mlcache = require "resty.mlcache" 250 | 251 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 252 | ipc_shm = "ipc_shm", 253 | })) 254 | 255 | assert(cache:purge()) 256 | } 257 | } 258 | --- response_body_unlike 259 | flush_expired called with 'max_count' 260 | --- no_error_log 261 | [error] 262 | 263 | 264 | 265 | === TEST 8: purge() calls shm:flush_expired() if argument specified 266 | --- config 267 | location /t { 268 | content_by_lua_block { 269 | do 270 | local cache_shm = ngx.shared.cache_shm 271 | local mt = getmetatable(cache_shm) 272 | local orig_cache_shm_flush_expired = mt.flush_expired 273 | 274 | mt.flush_expired = function(self, ...) 275 | local arg = { ... } 276 | local n = arg[1] 277 | ngx.say("flush_expired called with 'max_count': ", n) 278 | 279 | return orig_cache_shm_flush_expired(self, ...) 280 | end 281 | end 282 | 283 | local mlcache = require "resty.mlcache" 284 | 285 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 286 | ipc_shm = "ipc_shm", 287 | })) 288 | 289 | assert(cache:purge(true)) 290 | } 291 | } 292 | --- response_body 293 | flush_expired called with 'max_count': nil 294 | --- no_error_log 295 | [error] 296 | 297 | 298 | 299 | === TEST 9: purge() calls shm:flush_expired() if shm_miss is specified 300 | --- config 301 | location /t { 302 | content_by_lua_block { 303 | do 304 | local cache_shm = ngx.shared.cache_shm 305 | local mt = getmetatable(cache_shm) 306 | local orig_cache_shm_flush_expired = mt.flush_expired 307 | 308 | mt.flush_expired = function(self, ...) 309 | local arg = { ... } 310 | local n = arg[1] 311 | ngx.say("flush_expired called with 'max_count': ", n) 312 | 313 | return orig_cache_shm_flush_expired(self, ...) 314 | end 315 | end 316 | 317 | local mlcache = require "resty.mlcache" 318 | 319 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 320 | ipc_shm = "ipc_shm", 321 | shm_miss = "cache_shm_miss", 322 | })) 323 | 324 | assert(cache:purge(true)) 325 | } 326 | } 327 | --- response_body 328 | flush_expired called with 'max_count': nil 329 | flush_expired called with 'max_count': nil 330 | --- no_error_log 331 | [error] 332 | 333 | 334 | 335 | === TEST 10: purge() calls broadcast() on purge channel 336 | --- config 337 | location /t { 338 | content_by_lua_block { 339 | local mlcache = require "resty.mlcache" 340 | 341 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 342 | ipc = { 343 | register_listeners = function() end, 344 | broadcast = function(channel, data, ...) 345 | ngx.say("channel: ", channel) 346 | ngx.say("data:", data) 347 | ngx.say("other args:", ...) 348 | return true 349 | end, 350 | poll = function() end, 351 | } 352 | })) 353 | 354 | assert(cache:purge()) 355 | } 356 | } 357 | --- response_body 358 | channel: mlcache:purge:my_mlcache 359 | data: 360 | other args: 361 | --- no_error_log 362 | [error] 363 | -------------------------------------------------------------------------------- /t/09-isolation.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | repeat_each(2); 8 | 9 | plan tests => repeat_each() * blocks() * 3; 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: multiple instances with the same name have same lua-resty-lru instance 16 | --- config 17 | location /t { 18 | content_by_lua_block { 19 | local mlcache = require "resty.mlcache" 20 | 21 | local cache_1 = assert(mlcache.new("my_mlcache", "cache_shm")) 22 | local cache_2 = assert(mlcache.new("my_mlcache", "cache_shm")) 23 | 24 | ngx.say("lua-resty-lru instances are the same: ", 25 | cache_1.lru == cache_2.lru) 26 | } 27 | } 28 | --- response_body 29 | lua-resty-lru instances are the same: true 30 | --- no_error_log 31 | [error] 32 | 33 | 34 | 35 | === TEST 2: multiple instances with different names have different lua-resty-lru instances 36 | --- config 37 | location /t { 38 | content_by_lua_block { 39 | local mlcache = require "resty.mlcache" 40 | 41 | local cache_1 = assert(mlcache.new("my_mlcache_1", "cache_shm")) 42 | local cache_2 = assert(mlcache.new("my_mlcache_2", "cache_shm")) 43 | 44 | ngx.say("lua-resty-lru instances are the same: ", 45 | cache_1.lru == cache_2.lru) 46 | } 47 | } 48 | --- response_body 49 | lua-resty-lru instances are the same: false 50 | --- no_error_log 51 | [error] 52 | 53 | 54 | 55 | === TEST 3: garbage-collected instances also GC their lru instance 56 | --- config 57 | location /t { 58 | content_by_lua_block { 59 | local mlcache = require "resty.mlcache" 60 | 61 | collectgarbage("collect") 62 | local cache_1 = assert(mlcache.new("my_mlcache", "cache_shm")) 63 | local cache_2 = assert(mlcache.new("my_mlcache", "cache_shm")) 64 | 65 | -- cache something in cache_1's LRU 66 | 67 | cache_1.lru:set("key", 123) 68 | 69 | -- GC cache_1 (the LRU should survive because it is shared with cache_2) 70 | 71 | cache_1 = nil 72 | collectgarbage("collect") 73 | 74 | -- prove LRU survived 75 | 76 | ngx.say((cache_2.lru:get("key"))) 77 | 78 | -- GC cache_2 (and the LRU this time, since no more references) 79 | 80 | cache_2 = nil 81 | collectgarbage("collect") 82 | 83 | -- re-create the caches and a new LRU 84 | 85 | cache_1 = assert(mlcache.new("my_mlcache", "cache_shm")) 86 | cache_2 = assert(mlcache.new("my_mlcache", "cache_shm")) 87 | 88 | -- this is a new LRU, it has nothing in it 89 | 90 | ngx.say((cache_2.lru:get("key"))) 91 | } 92 | } 93 | --- response_body 94 | 123 95 | nil 96 | --- no_error_log 97 | [error] 98 | 99 | 100 | 101 | === TEST 4: multiple instances with different names get() of the same key are isolated 102 | --- config 103 | location /t { 104 | content_by_lua_block { 105 | local mlcache = require "resty.mlcache" 106 | 107 | -- create 2 mlcache 108 | 109 | local cache_1 = assert(mlcache.new("my_mlcache_1", "cache_shm")) 110 | local cache_2 = assert(mlcache.new("my_mlcache_2", "cache_shm")) 111 | 112 | -- set a value in both mlcaches 113 | 114 | local data_1 = assert(cache_1:get("my_key", nil, function() return "value A" end)) 115 | local data_2 = assert(cache_2:get("my_key", nil, function() return "value B" end)) 116 | 117 | -- get values from LRU 118 | 119 | local lru_1_value = cache_1.lru:get("my_key") 120 | local lru_2_value = cache_2.lru:get("my_key") 121 | 122 | ngx.say("cache_1 lru has: ", lru_1_value) 123 | ngx.say("cache_2 lru has: ", lru_2_value) 124 | 125 | -- delete values from LRU 126 | 127 | cache_1.lru:delete("my_key") 128 | cache_2.lru:delete("my_key") 129 | 130 | -- get values from shm 131 | 132 | local shm_1_value = assert(cache_1:get("my_key", nil, function() end)) 133 | local shm_2_value = assert(cache_2:get("my_key", nil, function() end)) 134 | 135 | ngx.say("cache_1 shm has: ", shm_1_value) 136 | ngx.say("cache_2 shm has: ", shm_2_value) 137 | } 138 | } 139 | --- response_body 140 | cache_1 lru has: value A 141 | cache_2 lru has: value B 142 | cache_1 shm has: value A 143 | cache_2 shm has: value B 144 | --- no_error_log 145 | [error] 146 | 147 | 148 | 149 | === TEST 5: multiple instances with different names delete() of the same key are isolated 150 | --- config 151 | location /t { 152 | content_by_lua_block { 153 | local mlcache = require "resty.mlcache" 154 | 155 | -- create 2 mlcache 156 | 157 | local cache_1 = assert(mlcache.new("my_mlcache_1", "cache_shm", { 158 | ipc_shm = "ipc_shm", 159 | })) 160 | local cache_2 = assert(mlcache.new("my_mlcache_2", "cache_shm", { 161 | ipc_shm = "ipc_shm", 162 | })) 163 | 164 | -- set 2 values in both mlcaches 165 | 166 | local data_1 = assert(cache_1:get("my_key", nil, function() return "value A" end)) 167 | local data_2 = assert(cache_2:get("my_key", nil, function() return "value B" end)) 168 | 169 | -- test if value is set from shm (safer to check due to the key) 170 | 171 | local shm_v = ngx.shared.cache_shm:get(cache_1.name .. "my_key") 172 | ngx.say("cache_1 shm has a value: ", shm_v ~= nil) 173 | 174 | -- delete value from mlcache 1 175 | 176 | ngx.say("delete from cache_1") 177 | assert(cache_1:delete("my_key")) 178 | 179 | -- ensure cache 1 key is deleted from LRU 180 | 181 | local lru_v = cache_1.lru:get("my_key") 182 | ngx.say("cache_1 lru has: ", lru_v) 183 | 184 | -- ensure cache 1 key is deleted from shm 185 | 186 | local shm_v = ngx.shared.cache_shm:get(cache_1.name .. "my_key") 187 | ngx.say("cache_1 shm has: ", shm_v) 188 | 189 | -- ensure cache 2 still has its value 190 | 191 | local shm_v_2 = ngx.shared.cache_shm:get(cache_2.name .. "my_key") 192 | ngx.say("cache_2 shm has a value: ", shm_v_2 ~= nil) 193 | 194 | local lru_v_2 = cache_2.lru:get("my_key") 195 | ngx.say("cache_2 lru has: ", lru_v_2) 196 | } 197 | } 198 | --- response_body 199 | cache_1 shm has a value: true 200 | delete from cache_1 201 | cache_1 lru has: nil 202 | cache_1 shm has: nil 203 | cache_2 shm has a value: true 204 | cache_2 lru has: value B 205 | --- no_error_log 206 | [error] 207 | 208 | 209 | 210 | === TEST 6: multiple instances with different names peek() of the same key are isolated 211 | --- config 212 | location /t { 213 | content_by_lua_block { 214 | -- must reset the shm so that when repeated, this tests doesn't 215 | -- return unpredictible TTLs (0.9xxxs) 216 | ngx.shared.cache_shm:flush_all() 217 | ngx.shared.cache_shm:flush_expired() 218 | 219 | local mlcache = require "resty.mlcache" 220 | 221 | -- create 2 mlcaches 222 | 223 | local cache_1 = assert(mlcache.new("my_mlcache_1", "cache_shm", { 224 | ipc_shm = "ipc_shm", 225 | })) 226 | local cache_2 = assert(mlcache.new("my_mlcache_2", "cache_shm", { 227 | ipc_shm = "ipc_shm", 228 | })) 229 | 230 | -- reset LRUs so repeated tests allow the below get() to set the 231 | -- value in the shm 232 | 233 | cache_1.lru:delete("my_key") 234 | cache_2.lru:delete("my_key") 235 | 236 | -- set a value in both mlcaches 237 | 238 | local data_1 = assert(cache_1:get("my_key", { ttl = 1 }, function() return "value A" end)) 239 | local data_2 = assert(cache_2:get("my_key", { ttl = 2 }, function() return "value B" end)) 240 | 241 | -- peek cache 1 242 | 243 | local ttl, err, val = assert(cache_1:peek("my_key")) 244 | 245 | ngx.say("cache_1 ttl: ", ttl) 246 | ngx.say("cache_1 value: ", val) 247 | 248 | -- peek cache 2 249 | 250 | local ttl, err, val = assert(cache_2:peek("my_key")) 251 | 252 | ngx.say("cache_2 ttl: ", ttl) 253 | ngx.say("cache_2 value: ", val) 254 | } 255 | } 256 | --- response_body 257 | cache_1 ttl: 1 258 | cache_1 value: value A 259 | cache_2 ttl: 2 260 | cache_2 value: value B 261 | --- no_error_log 262 | [error] 263 | 264 | 265 | 266 | === TEST 7: non-namespaced instances use different delete() broadcast channel 267 | --- config 268 | location /t { 269 | content_by_lua_block { 270 | local mlcache = require "resty.mlcache" 271 | 272 | -- create 2 mlcaches 273 | 274 | local cache_1 = assert(mlcache.new("my_mlcache_1", "cache_shm", { 275 | ipc = { 276 | register_listeners = function() end, 277 | broadcast = function(channel) 278 | ngx.say("cache_1 channel: ", channel) 279 | return true 280 | end, 281 | poll = function() end, 282 | } 283 | })) 284 | local cache_2 = assert(mlcache.new("my_mlcache_2", "cache_shm", { 285 | ipc = { 286 | register_listeners = function() end, 287 | broadcast = function(channel) 288 | ngx.say("cache_2 channel: ", channel) 289 | return true 290 | end, 291 | poll = function() end, 292 | } 293 | })) 294 | 295 | assert(cache_1:delete("my_key")) 296 | assert(cache_2:delete("my_key")) 297 | } 298 | } 299 | --- response_body 300 | cache_1 channel: mlcache:invalidations:my_mlcache_1 301 | cache_2 channel: mlcache:invalidations:my_mlcache_2 302 | --- no_error_log 303 | [error] 304 | 305 | 306 | 307 | === TEST 8: non-namespaced instances use different purge() broadcast channel 308 | --- config 309 | location /t { 310 | content_by_lua_block { 311 | local mlcache = require "resty.mlcache" 312 | 313 | -- create 2 mlcaches 314 | 315 | local cache_1 = assert(mlcache.new("my_mlcache_1", "cache_shm", { 316 | ipc = { 317 | register_listeners = function() end, 318 | broadcast = function(channel) 319 | ngx.say("cache_1 channel: ", channel) 320 | return true 321 | end, 322 | poll = function() end, 323 | } 324 | })) 325 | local cache_2 = assert(mlcache.new("my_mlcache_2", "cache_shm", { 326 | ipc = { 327 | register_listeners = function() end, 328 | broadcast = function(channel) 329 | ngx.say("cache_2 channel: ", channel) 330 | return true 331 | end, 332 | poll = function() end, 333 | } 334 | })) 335 | 336 | assert(cache_1:purge()) 337 | assert(cache_2:purge()) 338 | } 339 | } 340 | --- response_body 341 | cache_1 channel: mlcache:purge:my_mlcache_1 342 | cache_2 channel: mlcache:purge:my_mlcache_2 343 | --- no_error_log 344 | [error] 345 | -------------------------------------------------------------------------------- /t/10-ipc_shm.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | workers(2); 8 | #repeat_each(2); 9 | 10 | plan tests => repeat_each() * blocks() * 4; 11 | 12 | run_tests(); 13 | 14 | __DATA__ 15 | 16 | === TEST 1: update() with ipc_shm catches up with invalidation events 17 | --- config 18 | location /t { 19 | content_by_lua_block { 20 | local mlcache = require "resty.mlcache" 21 | 22 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 23 | ipc_shm = "ipc_shm", 24 | debug = true -- allows same worker to receive its own published events 25 | })) 26 | 27 | cache.ipc:subscribe(cache.events.invalidation.channel, function(data) 28 | ngx.log(ngx.NOTICE, "received event from invalidations: ", data) 29 | end) 30 | 31 | assert(cache:delete("my_key")) 32 | assert(cache:update()) 33 | } 34 | } 35 | --- ignore_response_body 36 | --- error_log 37 | received event from invalidations: my_key 38 | --- no_error_log 39 | [error] 40 | [crit] 41 | 42 | 43 | 44 | === TEST 2: update() with ipc_shm timeouts when waiting for too long 45 | --- config 46 | location /t { 47 | content_by_lua_block { 48 | local mlcache = require "resty.mlcache" 49 | 50 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 51 | ipc_shm = "ipc_shm", 52 | debug = true -- allows same worker to receive its own published events 53 | })) 54 | 55 | cache.ipc:subscribe(cache.events.invalidation.channel, function(data) 56 | ngx.log(ngx.NOTICE, "received event from invalidations: ", data) 57 | end) 58 | 59 | assert(cache:delete("my_key")) 60 | assert(cache:delete("my_other_key")) 61 | ngx.shared.ipc_shm:delete(2) 62 | 63 | local ok, err = cache:update(0.1) 64 | if not ok then 65 | ngx.say(err) 66 | end 67 | } 68 | } 69 | --- response_body 70 | could not poll ipc events: timeout 71 | --- error_log 72 | received event from invalidations: my_key 73 | --- no_error_log 74 | [error] 75 | received event from invalidations: my_other 76 | 77 | 78 | 79 | === TEST 3: update() with ipc_shm JITs when no events to catch up 80 | --- config 81 | location /t { 82 | content_by_lua_block { 83 | local mlcache = require "resty.mlcache" 84 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 85 | ipc_shm = "ipc_shm", 86 | debug = true -- allows same worker to receive its own published events 87 | })) 88 | for i = 1, 10e3 do 89 | assert(cache:update()) 90 | end 91 | } 92 | } 93 | --- ignore_response_body 94 | --- error_log eval 95 | qr/\[TRACE\s+\d+ content_by_lua\(nginx\.conf:\d+\):7 loop\]/ 96 | --- no_error_log 97 | [error] 98 | [crit] 99 | 100 | 101 | 102 | === TEST 4: set() with ipc_shm invalidates other workers' LRU cache 103 | --- config 104 | location /t { 105 | content_by_lua_block { 106 | local mlcache = require "resty.mlcache" 107 | 108 | local opts = { 109 | ipc_shm = "ipc_shm", 110 | debug = true -- allows same worker to receive its own published events 111 | } 112 | 113 | local cache = assert(mlcache.new("namespace", "cache_shm", opts)) 114 | local cache_clone = assert(mlcache.new("namespace", "cache_shm", opts)) 115 | 116 | do 117 | local lru_delete = cache.lru.delete 118 | cache.lru.delete = function(self, key) 119 | ngx.say("called lru:delete() with key: ", key) 120 | return lru_delete(self, key) 121 | end 122 | end 123 | 124 | assert(cache:set("my_key", nil, nil)) 125 | 126 | ngx.say("calling update on cache") 127 | assert(cache:update()) 128 | 129 | ngx.say("calling update on cache_clone") 130 | assert(cache_clone:update()) 131 | } 132 | } 133 | --- response_body 134 | calling update on cache 135 | called lru:delete() with key: my_key 136 | calling update on cache_clone 137 | called lru:delete() with key: my_key 138 | --- no_error_log 139 | [error] 140 | [crit] 141 | 142 | 143 | 144 | === TEST 5: delete() with ipc_shm invalidates other workers' LRU cache 145 | --- config 146 | location /t { 147 | content_by_lua_block { 148 | local mlcache = require "resty.mlcache" 149 | 150 | local opts = { 151 | ipc_shm = "ipc_shm", 152 | debug = true -- allows same worker to receive its own published events 153 | } 154 | 155 | local cache = assert(mlcache.new("namespace", "cache_shm", opts)) 156 | local cache_clone = assert(mlcache.new("namespace", "cache_shm", opts)) 157 | 158 | do 159 | local lru_delete = cache.lru.delete 160 | cache.lru.delete = function(self, key) 161 | ngx.say("called lru:delete() with key: ", key) 162 | return lru_delete(self, key) 163 | end 164 | end 165 | 166 | assert(cache:delete("my_key")) 167 | 168 | ngx.say("calling update on cache") 169 | assert(cache:update()) 170 | 171 | ngx.say("calling update on cache_clone") 172 | assert(cache_clone:update()) 173 | } 174 | } 175 | --- response_body 176 | called lru:delete() with key: my_key 177 | calling update on cache 178 | called lru:delete() with key: my_key 179 | calling update on cache_clone 180 | called lru:delete() with key: my_key 181 | --- no_error_log 182 | [error] 183 | [crit] 184 | 185 | 186 | 187 | === TEST 6: purge() with mlcache_shm invalidates other workers' LRU cache (OpenResty < 1.13.6.2) 188 | --- skip_eval: 3: t::TestMLCache::skip_openresty('>=', '1.13.6.2') 189 | --- config 190 | location /t { 191 | content_by_lua_block { 192 | local mlcache = require "resty.mlcache" 193 | 194 | local opts = { 195 | ipc_shm = "ipc_shm", 196 | debug = true -- allows same worker to receive its own published events 197 | } 198 | 199 | local cache = assert(mlcache.new("namespace", "cache_shm", opts)) 200 | local cache_clone = assert(mlcache.new("namespace", "cache_shm", opts)) 201 | 202 | local lru = cache.lru 203 | local lru_clone = cache_clone.lru 204 | 205 | assert(cache:purge()) 206 | 207 | -- cache.lru should be different now 208 | ngx.say("cache has new lru: ", cache.lru ~= lru) 209 | 210 | ngx.say("cache_clone still has same lru: ", cache_clone.lru == lru_clone) 211 | 212 | ngx.say("calling update on cache_clone") 213 | assert(cache_clone:update()) 214 | 215 | -- cache.lru should be different now 216 | ngx.say("cache_clone has new lru: ", cache_clone.lru ~= lru_clone) 217 | } 218 | } 219 | --- response_body 220 | cache has new lru: true 221 | cache_clone still has same lru: true 222 | calling update on cache_clone 223 | cache_clone has new lru: true 224 | --- no_error_log 225 | [error] 226 | [crit] 227 | 228 | 229 | 230 | === TEST 7: purge() with mlcache_shm invalidates other workers' LRU cache (OpenResty >= 1.13.6.2) 231 | --- skip_eval: 3: t::TestMLCache::skip_openresty('<', '1.13.6.2') 232 | --- config 233 | location /t { 234 | content_by_lua_block { 235 | local mlcache = require "resty.mlcache" 236 | 237 | local opts = { 238 | ipc_shm = "ipc_shm", 239 | debug = true -- allows same worker to receive its own published events 240 | } 241 | 242 | local cache = assert(mlcache.new("namespace", "cache_shm", opts)) 243 | local cache_clone = assert(mlcache.new("namespace", "cache_shm", opts)) 244 | 245 | local lru = cache.lru 246 | 247 | ngx.say("both instances use the same lru: ", cache.lru == cache_clone.lru) 248 | 249 | do 250 | local lru_flush_all = lru.flush_all 251 | cache.lru.flush_all = function(self) 252 | ngx.say("called lru:flush_all()") 253 | return lru_flush_all(self) 254 | end 255 | end 256 | 257 | assert(cache:purge()) 258 | 259 | ngx.say("calling update on cache_clone") 260 | assert(cache_clone:update()) 261 | 262 | ngx.say("both instances use the same lru: ", cache.lru == cache_clone.lru) 263 | ngx.say("lru didn't change after purge: ", cache.lru == lru) 264 | } 265 | } 266 | --- response_body 267 | both instances use the same lru: true 268 | called lru:flush_all() 269 | calling update on cache_clone 270 | called lru:flush_all() 271 | both instances use the same lru: true 272 | lru didn't change after purge: true 273 | --- no_error_log 274 | [error] 275 | [crit] 276 | -------------------------------------------------------------------------------- /t/11-locks_shm.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | plan tests => repeat_each() * blocks() * 3; 8 | 9 | run_tests(); 10 | 11 | __DATA__ 12 | 13 | === TEST 1: new() validates opts.shm_locks 14 | --- config 15 | location /t { 16 | content_by_lua_block { 17 | local mlcache = require "resty.mlcache" 18 | 19 | local ok, err = pcall(mlcache.new, "name", "cache_shm", { 20 | shm_locks = false, 21 | }) 22 | if not ok then 23 | ngx.say(err) 24 | end 25 | } 26 | } 27 | --- response_body 28 | opts.shm_locks must be a string 29 | --- no_error_log 30 | [error] 31 | 32 | 33 | 34 | === TEST 2: new() ensures opts.shm_locks exists 35 | --- config 36 | location /t { 37 | content_by_lua_block { 38 | local mlcache = require "resty.mlcache" 39 | 40 | local ok, err = mlcache.new("name", "cache_shm", { 41 | shm_locks = "foo", 42 | }) 43 | if not ok then 44 | ngx.say(err) 45 | end 46 | } 47 | } 48 | --- response_body 49 | no such lua_shared_dict for opts.shm_locks: foo 50 | --- no_error_log 51 | [error] 52 | 53 | 54 | 55 | === TEST 3: get() stores resty-locks in opts.shm_locks if specified 56 | --- config 57 | location /t { 58 | content_by_lua_block { 59 | local mlcache = require "resty.mlcache" 60 | 61 | local cache = assert(mlcache.new("name", "cache_shm", { 62 | shm_locks = "locks_shm", 63 | })) 64 | 65 | local function cb() 66 | local keys = ngx.shared.locks_shm:get_keys() 67 | for i, key in ipairs(keys) do 68 | ngx.say(i, ": ", key) 69 | end 70 | 71 | return 123 72 | end 73 | 74 | cache:get("key", nil, cb) 75 | } 76 | } 77 | --- response_body 78 | 1: lua-resty-mlcache:lock:namekey 79 | --- no_error_log 80 | [error] 81 | -------------------------------------------------------------------------------- /t/12-resurrect-stale.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | log_level('warn'); 8 | 9 | plan tests => repeat_each() * blocks() * 4; 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: new() validates 'opts.resurrect_ttl' (number && >= 0) 16 | --- config 17 | location /t { 18 | content_by_lua_block { 19 | local mlcache = require "resty.mlcache" 20 | 21 | local pok, perr = pcall(mlcache.new, "my_mlcache", "cache_shm", { 22 | resurrect_ttl = "", 23 | }) 24 | if not pok then 25 | ngx.say(perr) 26 | end 27 | 28 | local pok, perr = pcall(mlcache.new, "my_mlcache", "cache_shm", { 29 | resurrect_ttl = -1, 30 | }) 31 | if not pok then 32 | ngx.say(perr) 33 | end 34 | } 35 | } 36 | --- response_body 37 | opts.resurrect_ttl must be a number 38 | opts.resurrect_ttl must be >= 0 39 | --- no_error_log 40 | [error] 41 | [crit] 42 | 43 | 44 | 45 | === TEST 2: get() validates 'opts.resurrect_ttl' (number && >= 0) 46 | --- config 47 | location /t { 48 | content_by_lua_block { 49 | local mlcache = require "resty.mlcache" 50 | local cache = assert(mlcache.new("my_mlcache", "cache_shm")) 51 | 52 | local function cb() 53 | -- nop 54 | end 55 | 56 | local pok, perr = pcall(cache.get, cache, "key", { 57 | resurrect_ttl = "", 58 | }, cb) 59 | if not pok then 60 | ngx.say(perr) 61 | end 62 | 63 | local pok, perr = pcall(cache.get, cache, "key", { 64 | resurrect_ttl = -1, 65 | }, cb) 66 | if not pok then 67 | ngx.say(perr) 68 | end 69 | } 70 | } 71 | --- response_body 72 | opts.resurrect_ttl must be a number 73 | opts.resurrect_ttl must be >= 0 74 | --- no_error_log 75 | [error] 76 | [crit] 77 | 78 | 79 | 80 | === TEST 3: get() resurrects a stale value upon callback soft error for 'resurrect_ttl' instance option 81 | --- config 82 | location /t { 83 | content_by_lua_block { 84 | local mlcache = require "resty.mlcache" 85 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 86 | ttl = 0.3, 87 | resurrect_ttl = 0.2, 88 | })) 89 | 90 | local cb_called = 0 91 | 92 | local function cb() 93 | cb_called = cb_called + 1 94 | 95 | if cb_called == 1 then 96 | return 123 97 | 98 | elseif cb_called == 2 then 99 | return nil, "some error" 100 | 101 | elseif cb_called == 3 then 102 | return 456 103 | end 104 | end 105 | 106 | ngx.say("-> 1st get()") 107 | local data, err = cache:get("key", nil, cb) 108 | if err then 109 | ngx.log(ngx.ERR, err) 110 | return 111 | end 112 | ngx.say("data: ", data) 113 | 114 | ngx.say() 115 | ngx.say("sleeping for 0.3s...") 116 | ngx.sleep(0.3) 117 | ngx.say() 118 | 119 | ngx.say("-> stale get()") 120 | data, err, hit_lvl = cache:get("key", nil, cb) 121 | ngx.say("data: ", data) 122 | ngx.say("err: ", err) 123 | ngx.say("hit_lvl: ", hit_lvl) 124 | 125 | ngx.say() 126 | ngx.say("-> subsequent get() from LRU") 127 | data, err, hit_lvl = cache:get("key", nil, cb) 128 | ngx.say("data: ", data) 129 | ngx.say("err: ", err) 130 | ngx.say("hit_lvl: ", hit_lvl) 131 | 132 | ngx.say() 133 | ngx.say("-> subsequent get() from shm") 134 | cache.lru:delete("key") 135 | data, err, hit_lvl = cache:get("key", nil, cb) 136 | ngx.say("data: ", data) 137 | ngx.say("err: ", err) 138 | ngx.say("hit_lvl: ", hit_lvl) 139 | 140 | ngx.say() 141 | ngx.say("sleeping for 0.2s...") 142 | ngx.sleep(0.21) 143 | ngx.say() 144 | 145 | ngx.say("-> successfull callback get()") 146 | data, err, hit_lvl = cache:get("key", nil, cb) 147 | ngx.say("data: ", data) 148 | ngx.say("err: ", err) 149 | ngx.say("hit_lvl: ", hit_lvl) 150 | } 151 | } 152 | --- response_body 153 | -> 1st get() 154 | data: 123 155 | 156 | sleeping for 0.3s... 157 | 158 | -> stale get() 159 | data: 123 160 | err: nil 161 | hit_lvl: 4 162 | 163 | -> subsequent get() from LRU 164 | data: 123 165 | err: nil 166 | hit_lvl: 1 167 | 168 | -> subsequent get() from shm 169 | data: 123 170 | err: nil 171 | hit_lvl: 4 172 | 173 | sleeping for 0.2s... 174 | 175 | -> successfull callback get() 176 | data: 456 177 | err: nil 178 | hit_lvl: 3 179 | --- no_error_log 180 | [error] 181 | [crit] 182 | 183 | 184 | 185 | === TEST 4: get() logs soft callback error with warn level when resurrecting 186 | --- config 187 | location /t { 188 | content_by_lua_block { 189 | local mlcache = require "resty.mlcache" 190 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 191 | ttl = 0.3, 192 | resurrect_ttl = 0.2, 193 | })) 194 | 195 | local cb_called = 0 196 | 197 | local function cb() 198 | cb_called = cb_called + 1 199 | 200 | if cb_called == 1 then 201 | return 123 202 | 203 | elseif cb_called == 2 then 204 | return nil, "some error" 205 | 206 | elseif cb_called == 3 then 207 | return 456 208 | end 209 | end 210 | 211 | ngx.say("-> 1st get()") 212 | local data, err = cache:get("key", nil, cb) 213 | if err then 214 | ngx.log(ngx.ERR, err) 215 | return 216 | end 217 | ngx.say("data: ", data) 218 | 219 | ngx.say() 220 | ngx.say("sleeping for 0.3s...") 221 | ngx.sleep(0.3) 222 | ngx.say() 223 | 224 | ngx.say("-> stale get()") 225 | data, err, hit_lvl = cache:get("key", nil, cb) 226 | ngx.say("data: ", data) 227 | ngx.say("err: ", err) 228 | ngx.say("hit_lvl: ", hit_lvl) 229 | } 230 | } 231 | --- response_body 232 | -> 1st get() 233 | data: 123 234 | 235 | sleeping for 0.3s... 236 | 237 | -> stale get() 238 | data: 123 239 | err: nil 240 | hit_lvl: 4 241 | --- error_log eval 242 | qr/\[warn\] .*? callback returned an error \(some error\) but stale value found/ 243 | --- no_error_log 244 | [error] 245 | 246 | 247 | 248 | === TEST 5: get() accepts 'opts.resurrect_ttl' option to override instance option 249 | --- config 250 | location /t { 251 | content_by_lua_block { 252 | local mlcache = require "resty.mlcache" 253 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 254 | ttl = 0.3, 255 | resurrect_ttl = 0.8, 256 | })) 257 | 258 | local cb_called = 0 259 | 260 | local function cb() 261 | cb_called = cb_called + 1 262 | 263 | if cb_called == 1 then 264 | return 123 265 | 266 | else 267 | return nil, "some error" 268 | end 269 | end 270 | 271 | ngx.say("-> 1st get()") 272 | local data, err = cache:get("key", nil, cb) 273 | if err then 274 | ngx.log(ngx.ERR, err) 275 | return 276 | end 277 | ngx.say("data: ", data) 278 | 279 | ngx.say() 280 | ngx.say("sleeping for 0.3s...") 281 | ngx.sleep(0.3) 282 | ngx.say() 283 | 284 | ngx.say("-> stale get()") 285 | data, err, hit_lvl = cache:get("key", { 286 | resurrect_ttl = 0.2 287 | }, cb) 288 | ngx.say("data: ", data) 289 | ngx.say("err: ", err) 290 | ngx.say("hit_lvl: ", hit_lvl) 291 | 292 | ngx.say() 293 | ngx.say("sleeping for 0.2s...") 294 | ngx.sleep(0.21) 295 | ngx.say() 296 | 297 | ngx.say("-> subsequent stale get()") 298 | data, err, hit_lvl = cache:get("key", nil, cb) 299 | ngx.say("data: ", data) 300 | ngx.say("err: ", err) 301 | ngx.say("hit_lvl: ", hit_lvl) 302 | } 303 | } 304 | --- response_body 305 | -> 1st get() 306 | data: 123 307 | 308 | sleeping for 0.3s... 309 | 310 | -> stale get() 311 | data: 123 312 | err: nil 313 | hit_lvl: 4 314 | 315 | sleeping for 0.2s... 316 | 317 | -> subsequent stale get() 318 | data: 123 319 | err: nil 320 | hit_lvl: 4 321 | --- no_error_log 322 | [error] 323 | [crit] 324 | 325 | 326 | 327 | === TEST 6: get() resurrects a nil stale value (negative cache) 328 | --- config 329 | location /t { 330 | content_by_lua_block { 331 | local mlcache = require "resty.mlcache" 332 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 333 | neg_ttl = 0.3, 334 | resurrect_ttl = 0.2, 335 | })) 336 | 337 | local cb_called = 0 338 | 339 | local function cb() 340 | cb_called = cb_called + 1 341 | 342 | if cb_called == 1 then 343 | return nil 344 | 345 | elseif cb_called == 2 then 346 | return nil, "some error" 347 | 348 | elseif cb_called == 3 then 349 | return 456 350 | end 351 | end 352 | 353 | ngx.say("-> 1st get()") 354 | local data, err = cache:get("key", nil, cb) 355 | if err then 356 | ngx.log(ngx.ERR, err) 357 | return 358 | end 359 | ngx.say("data: ", data) 360 | 361 | ngx.say() 362 | ngx.say("sleeping for 0.3s...") 363 | ngx.sleep(0.3) 364 | ngx.say() 365 | 366 | ngx.say("-> stale get()") 367 | data, err, hit_lvl = cache:get("key", nil, cb) 368 | ngx.say("data: ", data) 369 | ngx.say("err: ", err) 370 | ngx.say("hit_lvl: ", hit_lvl) 371 | 372 | ngx.say() 373 | ngx.say("-> subsequent get()") 374 | data, err, hit_lvl = cache:get("key", nil, cb) 375 | ngx.say("data: ", data) 376 | ngx.say("err: ", err) 377 | ngx.say("hit_lvl: ", hit_lvl) 378 | 379 | ngx.say() 380 | ngx.say("sleeping for 0.2s...") 381 | ngx.sleep(0.21) 382 | ngx.say() 383 | 384 | ngx.say("-> successfull callback get()") 385 | data, err, hit_lvl = cache:get("key", nil, cb) 386 | ngx.say("data: ", data) 387 | ngx.say("err: ", err) 388 | ngx.say("hit_lvl: ", hit_lvl) 389 | } 390 | } 391 | --- response_body 392 | -> 1st get() 393 | data: nil 394 | 395 | sleeping for 0.3s... 396 | 397 | -> stale get() 398 | data: nil 399 | err: nil 400 | hit_lvl: 4 401 | 402 | -> subsequent get() 403 | data: nil 404 | err: nil 405 | hit_lvl: 1 406 | 407 | sleeping for 0.2s... 408 | 409 | -> successfull callback get() 410 | data: 456 411 | err: nil 412 | hit_lvl: 3 413 | --- no_error_log 414 | [error] 415 | [crit] 416 | 417 | 418 | 419 | === TEST 7: get() resurrects a nil stale value (negative cache) in 'opts.shm_miss' 420 | --- config 421 | location /t { 422 | content_by_lua_block { 423 | local mlcache = require "resty.mlcache" 424 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 425 | neg_ttl = 0.3, 426 | resurrect_ttl = 0.2, 427 | shm_miss = "cache_shm_miss" 428 | })) 429 | 430 | local cb_called = 0 431 | 432 | local function cb() 433 | cb_called = cb_called + 1 434 | 435 | if cb_called == 1 then 436 | return nil 437 | 438 | elseif cb_called == 2 then 439 | return nil, "some error" 440 | 441 | elseif cb_called == 3 then 442 | return 456 443 | end 444 | end 445 | 446 | ngx.say("-> 1st get()") 447 | local data, err = cache:get("key", nil, cb) 448 | if err then 449 | ngx.log(ngx.ERR, err) 450 | return 451 | end 452 | ngx.say("data: ", data) 453 | 454 | ngx.say() 455 | ngx.say("sleeping for 0.3s...") 456 | ngx.sleep(0.3) 457 | ngx.say() 458 | 459 | ngx.say("-> stale get()") 460 | data, err, hit_lvl = cache:get("key", nil, cb) 461 | ngx.say("data: ", data) 462 | ngx.say("err: ", err) 463 | ngx.say("hit_lvl: ", hit_lvl) 464 | 465 | ngx.say() 466 | ngx.say("-> subsequent get()") 467 | data, err, hit_lvl = cache:get("key", nil, cb) 468 | ngx.say("data: ", data) 469 | ngx.say("err: ", err) 470 | ngx.say("hit_lvl: ", hit_lvl) 471 | 472 | ngx.say() 473 | ngx.say("sleeping for 0.2s...") 474 | ngx.sleep(0.21) 475 | ngx.say() 476 | 477 | ngx.say("-> successfull callback get()") 478 | data, err, hit_lvl = cache:get("key", nil, cb) 479 | ngx.say("data: ", data) 480 | ngx.say("err: ", err) 481 | ngx.say("hit_lvl: ", hit_lvl) 482 | } 483 | } 484 | --- response_body 485 | -> 1st get() 486 | data: nil 487 | 488 | sleeping for 0.3s... 489 | 490 | -> stale get() 491 | data: nil 492 | err: nil 493 | hit_lvl: 4 494 | 495 | -> subsequent get() 496 | data: nil 497 | err: nil 498 | hit_lvl: 1 499 | 500 | sleeping for 0.2s... 501 | 502 | -> successfull callback get() 503 | data: 456 504 | err: nil 505 | hit_lvl: 3 506 | --- no_error_log 507 | [error] 508 | [crit] 509 | 510 | 511 | 512 | === TEST 8: get() ignores cb return values upon stale value resurrection 513 | --- config 514 | location /t { 515 | content_by_lua_block { 516 | local mlcache = require "resty.mlcache" 517 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 518 | ttl = 0.3, 519 | resurrect_ttl = 0.2, 520 | })) 521 | 522 | local cb_called = 0 523 | 524 | local function cb() 525 | cb_called = cb_called + 1 526 | 527 | if cb_called == 2 then 528 | -- ignore ret values 1 and 3 529 | return 456, "some error", 10 530 | 531 | else 532 | return 123 533 | end 534 | end 535 | 536 | ngx.say("-> 1st get()") 537 | local data, err = cache:get("key", nil, cb) 538 | if err then 539 | ngx.log(ngx.ERR, err) 540 | return 541 | end 542 | ngx.say("data: ", data) 543 | 544 | ngx.say() 545 | ngx.say("sleeping for 0.3s...") 546 | ngx.sleep(0.3) 547 | ngx.say() 548 | 549 | ngx.say("-> stale get()") 550 | data, err, hit_lvl = cache:get("key", nil, cb) 551 | ngx.say("data: ", data) 552 | ngx.say("err: ", err) 553 | ngx.say("hit_lvl: ", hit_lvl) 554 | 555 | ngx.say() 556 | ngx.say("-> subsequent get()") 557 | data, err, hit_lvl = cache:get("key", nil, cb) 558 | ngx.say("data: ", data) 559 | ngx.say("err: ", err) 560 | ngx.say("hit_lvl: ", hit_lvl) 561 | 562 | ngx.say() 563 | ngx.say("sleeping for 0.2s...") 564 | ngx.sleep(0.21) 565 | ngx.say() 566 | 567 | ngx.say("-> successfull callback get()") 568 | data, err, hit_lvl = cache:get("key", nil, cb) 569 | ngx.say("data: ", data) 570 | ngx.say("err: ", err) 571 | ngx.say("hit_lvl: ", hit_lvl) 572 | } 573 | } 574 | --- response_body 575 | -> 1st get() 576 | data: 123 577 | 578 | sleeping for 0.3s... 579 | 580 | -> stale get() 581 | data: 123 582 | err: nil 583 | hit_lvl: 4 584 | 585 | -> subsequent get() 586 | data: 123 587 | err: nil 588 | hit_lvl: 1 589 | 590 | sleeping for 0.2s... 591 | 592 | -> successfull callback get() 593 | data: 123 594 | err: nil 595 | hit_lvl: 3 596 | --- no_error_log 597 | [error] 598 | [crit] 599 | 600 | 601 | 602 | === TEST 9: get() does not resurrect a stale value when callback throws error 603 | --- config 604 | location /t { 605 | content_by_lua_block { 606 | local mlcache = require "resty.mlcache" 607 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 608 | ttl = 0.3, 609 | resurrect_ttl = 0.2, 610 | })) 611 | 612 | local cb_called = 0 613 | 614 | local function cb() 615 | cb_called = cb_called + 1 616 | 617 | if cb_called == 1 then 618 | return 123 619 | 620 | elseif cb_called == 2 then 621 | error("thrown error") 622 | 623 | elseif cb_called == 3 then 624 | return 123 625 | end 626 | end 627 | 628 | ngx.say("-> 1st get()") 629 | local data, err = cache:get("key", nil, cb) 630 | if err then 631 | ngx.log(ngx.ERR, err) 632 | return 633 | end 634 | ngx.say("data: ", data) 635 | 636 | ngx.say() 637 | ngx.say("sleeping for 0.3s...") 638 | ngx.sleep(0.3) 639 | ngx.say() 640 | 641 | ngx.say("-> stale get()") 642 | data, err, hit_lvl = cache:get("key", nil, cb) 643 | ngx.say("data: ", data) 644 | ngx.say("err: ", string.match(err, "callback threw an error:"), " ", 645 | string.match(err, "thrown error")) 646 | ngx.say("hit_lvl: ", hit_lvl) 647 | 648 | ngx.say() 649 | ngx.say("-> subsequent get()") 650 | data, err, hit_lvl = cache:get("key", nil, cb) 651 | ngx.say("data: ", data) 652 | ngx.say("err: ", err) 653 | ngx.say("hit_lvl: ", hit_lvl) 654 | } 655 | } 656 | --- response_body 657 | -> 1st get() 658 | data: 123 659 | 660 | sleeping for 0.3s... 661 | 662 | -> stale get() 663 | data: nil 664 | err: callback threw an error: thrown error 665 | hit_lvl: nil 666 | 667 | -> subsequent get() 668 | data: 123 669 | err: nil 670 | hit_lvl: 3 671 | --- no_error_log 672 | [error] 673 | [crit] 674 | 675 | 676 | 677 | === TEST 10: get() returns error and data on lock timeout but does not resurrect 678 | --- config 679 | location /t { 680 | content_by_lua_block { 681 | -- insert 2 dummy values to ensure that lock acquisition (which 682 | -- uses shm:set) will _not_ evict out stale cached value 683 | ngx.shared.cache_shm:set(1, true, 0.2) 684 | ngx.shared.cache_shm:set(2, true, 0.2) 685 | 686 | local mlcache = require "resty.mlcache" 687 | local cache_1 = assert(mlcache.new("my_mlcache", "cache_shm", { 688 | ttl = 0.3, 689 | resurrect_ttl = 0.3 690 | })) 691 | local cache_2 = assert(mlcache.new("my_mlcache", "cache_shm", { 692 | ttl = 0.3, 693 | resurrect_ttl = 0.3, 694 | resty_lock_opts = { 695 | timeout = 0.2 696 | } 697 | })) 698 | 699 | local function cb(delay, return_val) 700 | if delay then 701 | ngx.sleep(delay) 702 | end 703 | 704 | return return_val or 123 705 | end 706 | 707 | -- cache in shm 708 | 709 | local data, err, hit_lvl = cache_1:get("my_key", nil, cb) 710 | assert(data == 123) 711 | assert(err == nil) 712 | assert(hit_lvl == 3) 713 | 714 | -- make shm + LRU expire 715 | 716 | ngx.sleep(0.3) 717 | 718 | local t1 = ngx.thread.spawn(function() 719 | -- trigger L3 callback again, but slow to return this time 720 | 721 | cache_1:get("my_key", nil, cb, 0.3, 456) 722 | end) 723 | 724 | local t2 = ngx.thread.spawn(function() 725 | -- make this mlcache wait on other's callback, and timeout 726 | 727 | local data, err, hit_lvl = cache_2:get("my_key", nil, cb) 728 | ngx.say("data: ", data) 729 | ngx.say("err: ", err) 730 | ngx.say("hit_lvl: ", hit_lvl) 731 | end) 732 | 733 | assert(ngx.thread.wait(t1)) 734 | assert(ngx.thread.wait(t2)) 735 | 736 | ngx.say() 737 | ngx.say("-> subsequent get()") 738 | data, err, hit_lvl = cache_2:get("my_key", nil, cb, nil, 123) 739 | ngx.say("data: ", data) 740 | ngx.say("err: ", err) 741 | ngx.say("hit_lvl: ", hit_lvl) -- should be 1 since LRU instances are shared by mlcache namespace, and t1 finished 742 | } 743 | } 744 | --- response_body 745 | data: 123 746 | err: nil 747 | hit_lvl: 4 748 | 749 | -> subsequent get() 750 | data: 456 751 | err: nil 752 | hit_lvl: 1 753 | --- error_log eval 754 | qr/\[warn\] .*? could not acquire callback lock: timeout/ 755 | --- no_error_log 756 | [error] 757 | 758 | 759 | 760 | === TEST 11: get() returns nil cached item on callback lock timeout 761 | --- config 762 | location /t { 763 | content_by_lua_block { 764 | -- insert 2 dummy values to ensure that lock acquisition (which 765 | -- uses shm:set) will _not_ evict out stale cached value 766 | ngx.shared.cache_shm:set(1, true, 0.2) 767 | ngx.shared.cache_shm:set(2, true, 0.2) 768 | 769 | local mlcache = require "resty.mlcache" 770 | local cache_1 = assert(mlcache.new("my_mlcache", "cache_shm", { 771 | neg_ttl = 0.3, 772 | resurrect_ttl = 0.3 773 | })) 774 | local cache_2 = assert(mlcache.new("my_mlcache", "cache_shm", { 775 | neg_ttl = 0.3, 776 | resurrect_ttl = 0.3, 777 | resty_lock_opts = { 778 | timeout = 0.2 779 | } 780 | })) 781 | 782 | local function cb(delay) 783 | if delay then 784 | ngx.sleep(delay) 785 | end 786 | 787 | return nil 788 | end 789 | 790 | -- cache in shm 791 | 792 | local data, err, hit_lvl = cache_1:get("my_key", nil, cb) 793 | assert(data == nil) 794 | assert(err == nil) 795 | assert(hit_lvl == 3) 796 | 797 | -- make shm + LRU expire 798 | 799 | ngx.sleep(0.3) 800 | 801 | local t1 = ngx.thread.spawn(function() 802 | -- trigger L3 callback again, but slow to return this time 803 | 804 | cache_1:get("my_key", nil, cb, 0.3) 805 | end) 806 | 807 | local t2 = ngx.thread.spawn(function() 808 | -- make this mlcache wait on other's callback, and timeout 809 | 810 | local data, err, hit_lvl = cache_2:get("my_key", nil, cb) 811 | ngx.say("data: ", data) 812 | ngx.say("err: ", err) 813 | ngx.say("hit_lvl: ", hit_lvl) 814 | end) 815 | 816 | assert(ngx.thread.wait(t1)) 817 | assert(ngx.thread.wait(t2)) 818 | 819 | ngx.say() 820 | ngx.say("-> subsequent get()") 821 | data, err, hit_lvl = cache_2:get("my_key", nil, cb) 822 | ngx.say("data: ", data) 823 | ngx.say("err: ", err) 824 | ngx.say("hit_lvl: ", hit_lvl) -- should be 1 since LRU instances are shared by mlcache namespace, and t1 finished 825 | } 826 | } 827 | --- response_body 828 | data: nil 829 | err: nil 830 | hit_lvl: 4 831 | 832 | -> subsequent get() 833 | data: nil 834 | err: nil 835 | hit_lvl: 1 836 | --- error_log eval 837 | qr/\[warn\] .*? could not acquire callback lock: timeout/ 838 | --- no_error_log 839 | [error] 840 | 841 | 842 | 843 | === TEST 12: get() does not resurrect a stale value if no 'resurrect_ttl' is set on the instance 844 | --- config 845 | location /t { 846 | content_by_lua_block { 847 | local mlcache = require "resty.mlcache" 848 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 849 | ttl = 0.3, 850 | })) 851 | 852 | local cb_called = 0 853 | 854 | local function cb() 855 | cb_called = cb_called + 1 856 | 857 | if cb_called == 1 then 858 | return 123 859 | end 860 | 861 | return nil, "some error" 862 | end 863 | 864 | ngx.say("-> 1st get()") 865 | local data, err = cache:get("key", nil, cb) 866 | if err then 867 | ngx.log(ngx.ERR, err) 868 | return 869 | end 870 | ngx.say("data: ", data) 871 | 872 | ngx.say() 873 | ngx.say("sleeping for 0.3s...") 874 | ngx.sleep(0.3) 875 | ngx.say() 876 | 877 | ngx.say("-> stale get()") 878 | data, err, hit_lvl = cache:get("key", nil, cb) 879 | ngx.say("data: ", data) 880 | ngx.say("err: ", err) 881 | ngx.say("hit_lvl: ", hit_lvl) 882 | 883 | ngx.say() 884 | ngx.say("-> subsequent get()") 885 | data, err, hit_lvl = cache:get("key", nil, cb) 886 | ngx.say("data: ", data) 887 | ngx.say("err: ", err) 888 | ngx.say("hit_lvl: ", hit_lvl) 889 | } 890 | } 891 | --- response_body 892 | -> 1st get() 893 | data: 123 894 | 895 | sleeping for 0.3s... 896 | 897 | -> stale get() 898 | data: nil 899 | err: some error 900 | hit_lvl: nil 901 | 902 | -> subsequent get() 903 | data: nil 904 | err: some error 905 | hit_lvl: nil 906 | --- no_error_log 907 | [error] 908 | [crit] 909 | 910 | 911 | 912 | === TEST 13: get() callback can return nil + err (non-string) safely with opts.resurrect_ttl 913 | --- config 914 | location /t { 915 | content_by_lua_block { 916 | local mlcache = require "resty.mlcache" 917 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 918 | ttl = 0.3, 919 | resurrect_ttl = 1, 920 | })) 921 | 922 | local data, err = cache:get("1", nil, function() return 123 end) 923 | if err then 924 | ngx.log(ngx.ERR, err) 925 | return 926 | end 927 | 928 | ngx.sleep(0.3) 929 | 930 | local data, err = cache:get("1", nil, function() return nil, {} end) 931 | if err then 932 | ngx.log(ngx.ERR, err) 933 | return 934 | end 935 | 936 | ngx.say("cb return values: ", data, " ", err) 937 | } 938 | } 939 | --- response_body 940 | cb return values: 123 nil 941 | --- error_log eval 942 | qr/\[warn\] .*? callback returned an error \(table: 0x[[:xdigit:]]+\)/ 943 | --- no_error_log 944 | [error] 945 | 946 | 947 | 948 | === TEST 14: get() returns stale hit_lvl when retrieved from shm on last ms (see GH PR #58) 949 | --- config 950 | location /t { 951 | content_by_lua_block { 952 | local forced_now = ngx.now() 953 | ngx.now = function() 954 | return forced_now 955 | end 956 | 957 | local mlcache = require "resty.mlcache" 958 | 959 | local cache = assert(mlcache.new("my_mlcache", "cache_shm", { 960 | ttl = 0.2, 961 | resurrect_ttl = 0.2, 962 | })) 963 | 964 | local cb_called = 0 965 | 966 | local function cb() 967 | cb_called = cb_called + 1 968 | 969 | if cb_called == 1 then 970 | return 42 971 | end 972 | 973 | return nil, "some error causing a resurrect" 974 | end 975 | 976 | local data, err = cache:get("key", nil, cb) 977 | assert(data == 42, err or "invalid data value: " .. data) 978 | 979 | -- cause a resurrect in L2 shm 980 | ngx.sleep(0.201) 981 | forced_now = forced_now + 0.201 982 | 983 | local data, err, hit_lvl = cache:get("key", nil, cb) 984 | assert(data == 42, err or "invalid data value: " .. data) 985 | assert(hit_lvl == 4, "hit_lvl should be 4 (resurrected data), got: " .. hit_lvl) 986 | 987 | -- value is now resurrected 988 | 989 | -- drop L1 cache value 990 | cache.lru:delete("key") 991 | 992 | -- advance 0.2 second in the future, and simulate another :get() 993 | -- call; the L2 shm entry will still be alive (as its clock is 994 | -- not faked), but mlcache will compute a remaining_ttl of 0; 995 | -- in such cases we should still see the stale flag returned 996 | -- as hit_lvl 997 | forced_now = forced_now + 0.2 998 | 999 | local data, err, hit_lvl = cache:get("key", nil, cb) 1000 | assert(data == 42, err or "invalid data value: " .. data) 1001 | 1002 | ngx.say("+0.200s after resurrect hit_lvl: ", hit_lvl) 1003 | } 1004 | } 1005 | --- response_body 1006 | +0.200s after resurrect hit_lvl: 4 1007 | --- no_error_log 1008 | [error] 1009 | [crit] 1010 | -------------------------------------------------------------------------------- /t/14-bulk-and-res.t: -------------------------------------------------------------------------------- 1 | # vim:set ts=4 sts=4 sw=4 et ft=: 2 | 3 | use strict; 4 | use lib '.'; 5 | use t::TestMLCache; 6 | 7 | workers(2); 8 | #repeat_each(2); 9 | 10 | plan tests => repeat_each() * blocks() * 3; 11 | 12 | run_tests(); 13 | 14 | __DATA__ 15 | 16 | === TEST 1: new_bulk() creates a bulk 17 | --- config 18 | location /t { 19 | content_by_lua_block { 20 | local mlcache = require "resty.mlcache" 21 | 22 | local bulk = mlcache.new_bulk() 23 | 24 | ngx.say("type: ", type(bulk)) 25 | ngx.say("size: ", #bulk) 26 | ngx.say("bulk.n: ", bulk.n) 27 | } 28 | } 29 | --- response_body 30 | type: table 31 | size: 0 32 | bulk.n: 0 33 | --- no_error_log 34 | [error] 35 | 36 | 37 | 38 | === TEST 2: new_bulk() creates a bulk with narr in arg #1 39 | --- config 40 | location /t { 41 | content_by_lua_block { 42 | local mlcache = require "resty.mlcache" 43 | 44 | local bulk = mlcache.new_bulk(3) 45 | 46 | ngx.say("type: ", type(bulk)) 47 | ngx.say("size: ", #bulk) 48 | ngx.say("bulk.n: ", bulk.n) 49 | } 50 | } 51 | --- response_body 52 | type: table 53 | size: 0 54 | bulk.n: 0 55 | --- no_error_log 56 | [error] 57 | 58 | 59 | 60 | === TEST 3: bulk:add() adds bulk operations 61 | --- config 62 | location /t { 63 | content_by_lua_block { 64 | local mlcache = require "resty.mlcache" 65 | 66 | local function cb() end 67 | 68 | local bulk = mlcache.new_bulk(3) 69 | 70 | for i = 1, 3 do 71 | bulk:add("key_" .. i, nil, cb, i) 72 | end 73 | 74 | for i = 1, 3*4, 4 do 75 | ngx.say(tostring(bulk[i]), " ", 76 | tostring(bulk[i + 1]), " ", 77 | tostring(bulk[i + 2]), " ", 78 | tostring(bulk[i + 3])) 79 | end 80 | 81 | ngx.say("bulk.n: ", bulk.n) 82 | } 83 | } 84 | --- response_body_like 85 | key_1 nil function: 0x[0-9a-fA-F]+ 1 86 | key_2 nil function: 0x[0-9a-fA-F]+ 2 87 | key_3 nil function: 0x[0-9a-fA-F]+ 3 88 | bulk\.n: 3 89 | --- no_error_log 90 | [error] 91 | 92 | 93 | 94 | === TEST 4: bulk:add() can be given to get_bulk() 95 | --- config 96 | location /t { 97 | content_by_lua_block { 98 | local mlcache = require "resty.mlcache" 99 | local cache = assert(mlcache.new("my_mlcache", "cache_shm")) 100 | 101 | local function cb(i) return i end 102 | 103 | local bulk = mlcache.new_bulk(3) 104 | 105 | for i = 1, 3 do 106 | bulk:add("key_" .. i, nil, cb, i) 107 | end 108 | 109 | local res, err = cache:get_bulk(bulk) 110 | if not res then 111 | ngx.log(ngx.ERR, err) 112 | return 113 | end 114 | 115 | for i = 1, res.n, 3 do 116 | ngx.say(tostring(res[i]), " ", 117 | tostring(res[i + 1]), " ", 118 | tostring(res[i + 2])) 119 | end 120 | } 121 | } 122 | --- response_body 123 | 1 nil 3 124 | 2 nil 3 125 | 3 nil 3 126 | --- no_error_log 127 | [error] 128 | 129 | 130 | 131 | === TEST 5: each_bulk_res() iterates over get_bulk() results 132 | --- config 133 | location /t { 134 | content_by_lua_block { 135 | local mlcache = require "resty.mlcache" 136 | local cache = assert(mlcache.new("my_mlcache", "cache_shm")) 137 | 138 | local res, err = cache:get_bulk { 139 | "key_a", nil, function() return 1 end, nil, 140 | "key_b", nil, function() return 2 end, nil, 141 | "key_c", nil, function() return 3 end, nil, 142 | n = 3, 143 | } 144 | if not res then 145 | ngx.log(ngx.ERR, err) 146 | return 147 | end 148 | 149 | for i, data, err, hit_lvl in mlcache.each_bulk_res(res) do 150 | ngx.say(i, " ", data, " ", err, " ", hit_lvl) 151 | end 152 | } 153 | } 154 | --- response_body 155 | 1 1 nil 3 156 | 2 2 nil 3 157 | 3 3 nil 3 158 | --- no_error_log 159 | [error] 160 | 161 | 162 | 163 | === TEST 6: each_bulk_res() throws an error on unrocognized res 164 | --- config 165 | location /t { 166 | content_by_lua_block { 167 | local mlcache = require "resty.mlcache" 168 | 169 | local pok, perr = pcall(mlcache.each_bulk_res, {}) 170 | if not pok then 171 | ngx.say(perr) 172 | end 173 | } 174 | } 175 | --- response_body 176 | res must have res.n field; is this a get_bulk() result? 177 | --- no_error_log 178 | [error] 179 | -------------------------------------------------------------------------------- /t/TestMLCache.pm: -------------------------------------------------------------------------------- 1 | package t::TestMLCache; 2 | 3 | use strict; 4 | use Test::Nginx::Socket::Lua -Base; 5 | use Cwd qw(cwd); 6 | 7 | our $pwd = cwd(); 8 | 9 | our @EXPORT = qw( 10 | $pwd 11 | skip_openresty 12 | ); 13 | 14 | my $PackagePath = qq{lua_package_path "$pwd/lib/?.lua;;";}; 15 | 16 | my $HttpConfig = qq{ 17 | lua_shared_dict cache_shm 1m; 18 | lua_shared_dict cache_shm_miss 1m; 19 | lua_shared_dict locks_shm 1m; 20 | lua_shared_dict ipc_shm 1m; 21 | 22 | init_by_lua_block { 23 | -- local verbose = true 24 | local verbose = false 25 | local outfile = "$Test::Nginx::Util::ErrLogFile" 26 | -- local outfile = "/tmp/v.log" 27 | if verbose then 28 | local dump = require "jit.dump" 29 | dump.on(nil, outfile) 30 | else 31 | local v = require "jit.v" 32 | v.on(outfile) 33 | end 34 | 35 | require "resty.core" 36 | -- jit.opt.start("hotloop=1") 37 | -- jit.opt.start("loopunroll=1000000") 38 | -- jit.off() 39 | } 40 | }; 41 | 42 | add_block_preprocessor(sub { 43 | my $block = shift; 44 | 45 | if (!defined $block->request) { 46 | $block->set_value("request", "GET /t"); 47 | } 48 | 49 | my $http_config = $block->http_config || ''; 50 | $http_config .= $PackagePath; 51 | 52 | if ($http_config !~ m/init_by_lua_block/) { 53 | $http_config .= $HttpConfig; 54 | } 55 | 56 | $block->set_value("http_config", $http_config); 57 | }); 58 | 59 | sub get_openresty_canon_version (@) { 60 | sprintf "%d.%03d%03d%03d", $_[0], $_[1], $_[2], $_[3]; 61 | } 62 | 63 | sub get_openresty_version () { 64 | my $NginxBinary = $ENV{TEST_NGINX_BINARY} || 'nginx'; 65 | my $out = `$NginxBinary -V 2>&1`; 66 | 67 | if (!defined $out || $? != 0) { 68 | bail_out("Failed to get the version of the OpenResty in PATH"); 69 | die; 70 | } 71 | 72 | if ($out =~ m{openresty[^/]*/(\d+)\.(\d+)\.(\d+)\.(\d+)}s) { 73 | return get_openresty_canon_version($1, $2, $3, $4); 74 | } 75 | 76 | if ($out =~ m{nginx[^/]*/(\d+)\.(\d+)\.(\d+)}s) { 77 | return; 78 | } 79 | 80 | bail_out("Failed to parse the output of \"nginx -V\": $out\n"); 81 | die; 82 | } 83 | 84 | sub skip_openresty ($$) { 85 | my ($op, $ver) = @_; 86 | my $OpenrestyVersion = get_openresty_version(); 87 | 88 | if ($ver =~ m{(\d+)\.(\d+)\.(\d+)\.(\d+)}s) { 89 | $ver = get_openresty_canon_version($1, $2, $3, $4); 90 | 91 | } else { 92 | bail_out("Invalid skip_openresty() arg: $ver"); 93 | die; 94 | } 95 | 96 | if (defined $OpenrestyVersion and eval "$OpenrestyVersion $op $ver") { 97 | return 1; 98 | } 99 | } 100 | 101 | no_long_string(); 102 | 103 | 1; 104 | --------------------------------------------------------------------------------