├── .gitattributes ├── .gitignore ├── .travis.yml ├── Makefile ├── README.markdown ├── dist.ini ├── lib └── resty │ ├── lrucache.lua │ └── lrucache │ └── pureffi.lua ├── t ├── 001-sanity.t ├── 002-should-store-false.t ├── 003-init-by-lua.t ├── 004-flush-all.t ├── 005-capacity.t ├── 006-count.t ├── 007-get-keys.t ├── 008-user-flags.t ├── 100-pureffi │ ├── 001-sanity.t │ ├── 002-should-store-false.t │ ├── 003-init-by-lua.t │ ├── 004-flush-all.t │ ├── 005-capacity.t │ ├── 006-count.t │ ├── 007-get-keys.t │ └── 008-user-flags.t ├── 101-mixed.t └── TestLRUCache.pm └── valgrind.suppress /.gitattributes: -------------------------------------------------------------------------------- 1 | *.t linguist-language=Text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *~ 4 | go 5 | t/servroot/ 6 | reindex 7 | nginx 8 | ctags 9 | tags 10 | a.lua 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # vim: ts=2 sw=2 et: 2 | 3 | os: linux 4 | dist: bionic 5 | 6 | branches: 7 | only: 8 | - "master" 9 | 10 | sudo: false 11 | 12 | language: c 13 | compiler: gcc 14 | 15 | env: 16 | global: 17 | - JOBS=3 18 | - NGX_BUILD_JOBS=$JOBS 19 | - LUAJIT_PREFIX=$TRAVIS_BUILD_DIR/luajit 20 | - LUAJIT_LIB=$LUAJIT_PREFIX/lib 21 | - LUAJIT_INC=$LUAJIT_PREFIX/include/luajit-2.1 22 | - LD_LIBRARY_PATH=$LUAJIT_LIB:$LD_LIBRARY_PATH 23 | - TEST_NGINX_SLEEP=0.006 24 | - TEST_NGINX_RANDOMIZE=1 25 | matrix: 26 | - NGINX_VERSION=1.27.1 27 | 28 | install: 29 | - export NGX_BUILD_CC=$CC 30 | - export PATH=$PWD/work/nginx/sbin:$PWD/nginx-devel-utils:$PATH 31 | - sudo apt-get install -qq -y axel 32 | - cpanm --sudo --notest Test::Nginx > build.log 2>&1 || (cat build.log && exit 1) 33 | - git clone https://github.com/openresty/openresty.git ../openresty 34 | - git clone https://github.com/openresty/nginx-devel-utils.git 35 | - git clone https://github.com/simpl/ngx_devel_kit.git ../ndk-nginx-module 36 | - git clone https://github.com/openresty/lua-nginx-module.git ../lua-nginx-module 37 | - git clone https://github.com/openresty/lua-resty-core.git ../lua-resty-core 38 | - git clone https://github.com/openresty/no-pool-nginx.git ../no-pool-nginx 39 | - git clone -b v2.1-agentzh https://github.com/openresty/luajit2.git 40 | - pushd luajit2/ 41 | - make -j$JOBS CCDEBUG=-g Q= PREFIX=$LUAJIT_PREFIX CC=$CC XCFLAGS='-DLUA_USE_APICHECK -DLUA_USE_ASSERT' > build.log 2>&1 || (cat build.log && exit 1) 42 | - make install PREFIX=$LUAJIT_PREFIX > build.log 2>&1 || (cat build.log && exit 1) 43 | - popd 44 | - ngx-build $NGINX_VERSION --add-module=../ndk-nginx-module --add-module=../lua-nginx-module --with-debug > build.log 2>&1 || (cat build.log && exit 1) 45 | - nginx -V 46 | - ldd `which nginx`|grep -E 'luajit|ssl|pcre' 47 | 48 | script: 49 | - make lint 50 | - prove -j$JOBS -I. -r t/ 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OPENRESTY_PREFIX=/usr/local/openresty 2 | 3 | PREFIX ?= /usr/local 4 | LUA_INCLUDE_DIR ?= $(PREFIX)/include 5 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 6 | INSTALL ?= install 7 | 8 | .PHONY: all test install lint 9 | 10 | all: ; 11 | 12 | install: all 13 | $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/resty/lrucache 14 | $(INSTALL) lib/resty/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/resty/ 15 | $(INSTALL) lib/resty/lrucache/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/resty/lrucache/ 16 | ifeq ($(LUA_LIB_DIR),/usr/local/lib/lua/) 17 | @echo 18 | @printf "\033[33mPLEASE NOTE: \033[0m\n" 19 | @printf "\033[33mThe necessary lua_package_path directive needs to be added to nginx.conf\033[0m\n" 20 | @printf "\033[33min the http context, because \"/usr/local/lib/lua/\" is not in LuaJIT’s default search path.\033[0m\n" 21 | @printf "\033[33mRefer to the Installation section of README.markdown.\033[0m\n" 22 | endif 23 | 24 | test: all lint 25 | PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r t 26 | 27 | lint: 28 | @! grep -P -n --color -- 'require.*?resty\.lrucache[^.]' t/*pureffi*/*.t || (echo "ERROR: Found pureffi tests requiring 'resty.lrucache'." > /dev/stderr; exit 1) 29 | @! grep -R -P -n --color --exclude-dir=pureffi --exclude=*mixed.t -- 'require.*?resty\.lrucache\.pureffi' t/*.t || (echo "ERROR: Found pure Lua tests requiring 'resty.lrucache.pureffi'." > /dev/stderr; exit 1) 30 | 31 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | lua-resty-lrucache - Lua-land LRU cache based on the LuaJIT FFI. 5 | 6 | Table of Contents 7 | ================= 8 | 9 | * [Name](#name) 10 | * [Status](#status) 11 | * [Synopsis](#synopsis) 12 | * [Description](#description) 13 | * [Methods](#methods) 14 | * [new](#new) 15 | * [set](#set) 16 | * [get](#get) 17 | * [delete](#delete) 18 | * [count](#count) 19 | * [capacity](#capacity) 20 | * [get_keys](#get_keys) 21 | * [flush_all](#flush_all) 22 | * [Prerequisites](#prerequisites) 23 | * [Installation](#installation) 24 | * [Community](#community) 25 | * [English Mailing List](#english-mailing-list) 26 | * [Chinese Mailing List](#chinese-mailing-list) 27 | * [Bugs and Patches](#bugs-and-patches) 28 | * [Author](#author) 29 | * [Copyright and License](#copyright-and-license) 30 | * [See Also](#see-also) 31 | 32 | Status 33 | ====== 34 | 35 | This library is considered production ready. 36 | 37 | Synopsis 38 | ======== 39 | 40 | ```lua 41 | -- file myapp.lua: example "myapp" module 42 | 43 | local _M = {} 44 | 45 | -- alternatively: local lrucache = require "resty.lrucache.pureffi" 46 | local lrucache = require "resty.lrucache" 47 | 48 | -- we need to initialize the cache on the lua module level so that 49 | -- it can be shared by all the requests served by each nginx worker process: 50 | local c, err = lrucache.new(200) -- allow up to 200 items in the cache 51 | if not c then 52 | error("failed to create the cache: " .. (err or "unknown")) 53 | end 54 | 55 | function _M.go() 56 | c:set("dog", 32) 57 | c:set("cat", 56) 58 | ngx.say("dog: ", c:get("dog")) 59 | ngx.say("cat: ", c:get("cat")) 60 | 61 | c:set("dog", { age = 10 }, 0.1) -- expire in 0.1 sec 62 | c:delete("dog") 63 | 64 | c:flush_all() -- flush all the cached data 65 | end 66 | 67 | return _M 68 | ``` 69 | 70 | ```nginx 71 | # nginx.conf 72 | 73 | http { 74 | # only if not using an official OpenResty release 75 | lua_package_path "/path/to/lua-resty-lrucache/lib/?.lua;;"; 76 | 77 | server { 78 | listen 8080; 79 | 80 | location = /t { 81 | content_by_lua_block { 82 | require("myapp").go() 83 | } 84 | } 85 | } 86 | } 87 | ``` 88 | 89 | Description 90 | =========== 91 | 92 | This library implements a simple LRU cache for 93 | [OpenResty](https://openresty.org) and the 94 | [ngx_lua](https://github.com/openresty/lua-nginx-module) module. 95 | 96 | This cache also supports expiration time. 97 | 98 | The LRU cache resides completely in the Lua VM and is subject to Lua GC. As 99 | such, do not expect it to get shared across the OS process boundary. The upside 100 | is that you can cache arbitrary complex Lua values (such as deep nested Lua 101 | tables) without the overhead of serialization (as with `ngx_lua`'s [shared 102 | dictionary 103 | API](https://github.com/openresty/lua-nginx-module#lua_shared_dict)). 104 | The downside is that your cache is always limited to the current OS process 105 | (i.e. the current Nginx worker process). It does not really make much sense to 106 | use this library in the context of 107 | [init_by_lua](https://github.com/openresty/lua-nginx-module#lua_shared_dict) 108 | because the cache will not get shared by any of the worker processes (unless 109 | you just want to "warm up" the cache with predefined items which will get 110 | inherited by the workers via `fork()`). 111 | 112 | This library offers two different implementations in the form of two classes: 113 | `resty.lrucache` and `resty.lrucache.pureffi`. Both implement the same API. 114 | The only difference is that the latter is a pure FFI implementation that also 115 | implements an FFI-based hash table for the cache lookup, while the former uses 116 | native Lua tables. 117 | 118 | If the cache hit rate is relatively high, you should use the `resty.lrucache` 119 | class which is faster than `resty.lrucache.pureffi`. 120 | 121 | However, if the cache hit rate is relatively low and there can be a *lot* of 122 | variations of keys inserted into and removed from the cache, then you should 123 | use the `resty.lrucache.pureffi` instead, because Lua tables are not good at 124 | removing keys frequently. You would likely see the `resizetab` function call in 125 | the LuaJIT runtime being very hot in [on-CPU flame 126 | graphs](https://github.com/openresty/stapxx#lj-lua-stacks) if you use the 127 | `resty.lrucache` class instead of `resty.lrucache.pureffi` in such a use case. 128 | 129 | [Back to TOC](#table-of-contents) 130 | 131 | Methods 132 | ======= 133 | 134 | To load this library, 135 | 136 | 1. use an official [OpenResty release](https://openresty.org) or follow the 137 | [Installation](#installation) instructions. 138 | 2. use `require` to load the library into a local Lua variable: 139 | 140 | ```lua 141 | local lrucache = require "resty.lrucache" 142 | ``` 143 | 144 | or 145 | 146 | ```lua 147 | local lrucache = require "resty.lrucache.pureffi" 148 | ``` 149 | 150 | [Back to TOC](#table-of-contents) 151 | 152 | new 153 | --- 154 | `syntax: cache, err = lrucache.new(max_items [, load_factor])` 155 | 156 | Creates a new cache instance. Upon failure, returns `nil` and a string 157 | describing the error. 158 | 159 | The `max_items` argument specifies the maximal number of items this cache can 160 | hold. 161 | 162 | The `load-factor` argument designates the "load factor" of the FFI-based 163 | hash-table used internally by `resty.lrucache.pureffi`; the default value is 164 | 0.5 (i.e. 50%); if the load factor is specified, it will be clamped to the 165 | range of `[0.1, 1]` (i.e. if load factor is greater than 1, it will be 166 | saturated to 1; likewise, if load-factor is smaller than `0.1`, it will be 167 | clamped to `0.1`). This argument is only meaningful for 168 | `resty.lrucache.pureffi`. 169 | 170 | [Back to TOC](#table-of-contents) 171 | 172 | set 173 | --- 174 | `syntax: cache:set(key, value, ttl?, flags?)` 175 | 176 | Sets a key with a value and an expiration time. 177 | 178 | When the cache is full, the cache will automatically evict the least recently 179 | used item. 180 | 181 | The optional `ttl` argument specifies the expiration time. The time value is in 182 | seconds, but you can also specify the fraction number part (e.g. `0.25`). A nil 183 | `ttl` argument means the value would never expire (which is the default). 184 | 185 | The optional `flags` argument specifies a user flags value associated with the 186 | item to be stored. It can be retrieved later with the item. The user flags are 187 | stored as an unsigned 32-bit integer internally, and thus must be specified as 188 | a Lua number. If not specified, flags will have a default value of `0`. This 189 | argument was added in the `v0.10` release. 190 | 191 | [Back to TOC](#table-of-contents) 192 | 193 | get 194 | --- 195 | `syntax: data, stale_data, flags = cache:get(key)` 196 | 197 | Fetches a value with the key. If the key does not exist in the cache or has 198 | already expired, `nil` will be returned. 199 | 200 | Starting from `v0.03`, the stale data is also returned as the second return 201 | value if available. 202 | 203 | Starting from `v0.10`, the user flags value associated with the stored item is 204 | also returned as the third return value. If no user flags were given to an 205 | item, its default flags will be `0`. 206 | 207 | [Back to TOC](#table-of-contents) 208 | 209 | delete 210 | ------ 211 | `syntax: cache:delete(key)` 212 | 213 | Removes an item specified by the key from the cache. 214 | 215 | [Back to TOC](#table-of-contents) 216 | 217 | count 218 | ----- 219 | `syntax: count = cache:count()` 220 | 221 | Returns the number of items currently stored in the cache **including** 222 | expired items if any. 223 | 224 | The returned `count` value will always be greater or equal to 0 and smaller 225 | than or equal to the `size` argument given to [`cache:new`](#new). 226 | 227 | This method was added in the `v0.10` release. 228 | 229 | [Back to TOC](#table-of-contents) 230 | 231 | capacity 232 | -------- 233 | `syntax: size = cache:capacity()` 234 | 235 | Returns the maximum number of items the cache can hold. The return value is the 236 | same as the `size` argument given to [`cache:new`](#new) when the cache was 237 | created. 238 | 239 | This method was added in the `v0.10` release. 240 | 241 | [Back to TOC](#table-of-contents) 242 | 243 | get_keys 244 | -------- 245 | `syntax: keys = cache:get_keys(max_count?, res?)` 246 | 247 | Fetch the list of keys currently inside the cache up to `max_count`. The keys 248 | will be ordered in MRU fashion (Most-Recently-Used keys first). 249 | 250 | This function returns a Lua (array) table (with integer keys) containing the 251 | keys. 252 | 253 | When `max_count` is `nil` or `0`, all keys (if any) will be returned. 254 | 255 | When provided with a `res` table argument, this function will not allocate a 256 | table and will instead insert the keys in `res`, along with a trailing `nil` 257 | value. 258 | 259 | This method was added in the `v0.10` release. 260 | 261 | [Back to TOC](#table-of-contents) 262 | 263 | flush_all 264 | --------- 265 | `syntax: cache:flush_all()` 266 | 267 | Flushes all the existing data (if any) in the current cache instance. This is 268 | an `O(1)` operation and should be much faster than creating a brand new cache 269 | instance. 270 | 271 | Note however that the `flush_all()` method of `resty.lrucache.pureffi` is an 272 | `O(n)` operation. 273 | 274 | [Back to TOC](#table-of-contents) 275 | 276 | Prerequisites 277 | ============= 278 | 279 | * [LuaJIT](http://luajit.org) 2.0+ 280 | * [ngx_lua](https://github.com/openresty/lua-nginx-module) 0.8.10+ 281 | 282 | [Back to TOC](#table-of-contents) 283 | 284 | Installation 285 | ============ 286 | 287 | It is recommended to use the latest [OpenResty release](https://openresty.org). 288 | At least OpenResty 1.4.2.9 is required. Recent versions of OpenResty only 289 | support LuaJIT, but if you are using an older version, make sure to enable 290 | LuaJIT when building OpenResty by passing the `--with-luajit` option to its 291 | `./configure` script. No extra Nginx configuration is required. 292 | 293 | If you want to use this library with your own Nginx build (with ngx_lua), then 294 | you need to ensure you are using ngx_lua 0.8.10 or greater. 295 | 296 | By default, ngx_lua will search Lua files in /usr/local/share/lua/5.1/. 297 | But `make install` will install this module to /usr/local/lib/lua. 298 | So you may find the error like this: 299 | 300 | ```text 301 | nginx: [alert] failed to load the 'resty.lrucache' module 302 | ``` 303 | 304 | You can install this module with the following command to resolve the above problem. 305 | 306 | ```bash 307 | cd lua-resty-lrucache 308 | sudo make install LUA_LIB_DIR=/usr/local/share/lua/5.1 309 | ``` 310 | 311 | You can also change the installation directory to any other directory you like with the LUA_LIB_DIR argument. 312 | 313 | ```bash 314 | cd lua-resty-lrucache 315 | sudo make install LUA_LIB_DIR=/opt/nginx/lualib 316 | ``` 317 | 318 | When not installed in /usr/local/share/lua/5.1, you also need to configure the 319 | [lua_package_path](https://github.com/openresty/lua-nginx-module#lua_package_path) 320 | directive to add the path to your lua-resty-lrucache source tree to ngx_lua's 321 | Lua module search path, as in: 322 | 323 | ```nginx 324 | # nginx.conf 325 | 326 | http { 327 | lua_package_path "/opt/nginx/lualib/?.lua;;"; 328 | ... 329 | } 330 | ``` 331 | 332 | and then load the library in Lua: 333 | 334 | ```lua 335 | local lrucache = require "resty.lrucache" 336 | ``` 337 | 338 | [Back to TOC](#table-of-contents) 339 | 340 | Community 341 | ========= 342 | 343 | [Back to TOC](#table-of-contents) 344 | 345 | English Mailing List 346 | -------------------- 347 | 348 | The [openresty-en](https://groups.google.com/group/openresty-en) mailing list 349 | is for English speakers. 350 | 351 | [Back to TOC](#table-of-contents) 352 | 353 | Chinese Mailing List 354 | -------------------- 355 | 356 | The [openresty](https://groups.google.com/group/openresty) mailing list is for 357 | Chinese speakers. 358 | 359 | [Back to TOC](#table-of-contents) 360 | 361 | Bugs and Patches 362 | ================ 363 | 364 | Please report bugs or submit patches by 365 | 366 | 1. creating a ticket on the [GitHub Issue 367 | Tracker](https://github.com/openresty/lua-resty-lrucache/issues), 368 | 1. or posting to the [OpenResty community](#community). 369 | 370 | [Back to TOC](#table-of-contents) 371 | 372 | Author 373 | ====== 374 | 375 | Yichun "agentzh" Zhang (章亦春) , OpenResty Inc. 376 | 377 | Shuxin Yang. 378 | 379 | [Back to TOC](#table-of-contents) 380 | 381 | Copyright and License 382 | ===================== 383 | 384 | This module is licensed under the BSD license. 385 | 386 | Copyright (C) 2014-2019, by Yichun "agentzh" Zhang, OpenResty Inc. 387 | 388 | Copyright (C) 2014-2017, by Shuxin Yang. 389 | 390 | All rights reserved. 391 | 392 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 393 | 394 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 395 | 396 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 397 | 398 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 399 | 400 | [Back to TOC](#table-of-contents) 401 | 402 | See Also 403 | ======== 404 | 405 | * OpenResty: https://openresty.org 406 | * the ngx_http_lua module: https://github.com/openresty/lua-nginx-module 407 | * the ngx_stream_lua module: https://github.com/openresty/stream-lua-nginx-module 408 | * the lua-resty-core library: https://github.com/openresty/lua-resty-core 409 | 410 | [Back to TOC](#table-of-contents) 411 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name=lua-resty-lrucache 2 | abstract=Lua-land LRU Cache based on LuaJIT FFI 3 | author=Yichun Zhang (agentzh) 4 | is_original=yes 5 | license=2bsd 6 | lib_dir=lib 7 | doc_dir=lib 8 | repo_link=https://github.com/openresty/lua-resty-lrucache 9 | main_module=lib/resty/lrucache.lua 10 | requires = luajit 11 | -------------------------------------------------------------------------------- /lib/resty/lrucache.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Yichun Zhang (agentzh) 2 | 3 | 4 | local ffi = require "ffi" 5 | local ffi_new = ffi.new 6 | local ffi_sizeof = ffi.sizeof 7 | local ffi_cast = ffi.cast 8 | local ffi_fill = ffi.fill 9 | local ngx_now = ngx.now 10 | local uintptr_t = ffi.typeof("uintptr_t") 11 | local setmetatable = setmetatable 12 | local tonumber = tonumber 13 | local type = type 14 | local new_tab 15 | do 16 | local ok 17 | ok, new_tab = pcall(require, "table.new") 18 | if not ok then 19 | new_tab = function(narr, nrec) return {} end 20 | end 21 | end 22 | 23 | 24 | if string.find(jit.version, " 2.0", 1, true) then 25 | ngx.log(ngx.ALERT, "use of lua-resty-lrucache with LuaJIT 2.0 is ", 26 | "not recommended; use LuaJIT 2.1+ instead") 27 | end 28 | 29 | 30 | local ok, tb_clear = pcall(require, "table.clear") 31 | if not ok then 32 | local pairs = pairs 33 | tb_clear = function (tab) 34 | for k, _ in pairs(tab) do 35 | tab[k] = nil 36 | end 37 | end 38 | end 39 | 40 | 41 | -- queue data types 42 | -- 43 | -- this queue is a double-ended queue and the first node 44 | -- is reserved for the queue itself. 45 | -- the implementation is mostly borrowed from nginx's ngx_queue_t data 46 | -- structure. 47 | 48 | ffi.cdef[[ 49 | typedef struct lrucache_queue_s lrucache_queue_t; 50 | struct lrucache_queue_s { 51 | double expire; /* in seconds */ 52 | lrucache_queue_t *prev; 53 | lrucache_queue_t *next; 54 | uint32_t user_flags; 55 | }; 56 | ]] 57 | 58 | local queue_arr_type = ffi.typeof("lrucache_queue_t[?]") 59 | local queue_type = ffi.typeof("lrucache_queue_t") 60 | local NULL = ffi.null 61 | 62 | 63 | -- queue utility functions 64 | 65 | local function queue_insert_tail(h, x) 66 | local last = h[0].prev 67 | x.prev = last 68 | last.next = x 69 | x.next = h 70 | h[0].prev = x 71 | end 72 | 73 | 74 | local function queue_init(size) 75 | if not size then 76 | size = 0 77 | end 78 | local q = ffi_new(queue_arr_type, size + 1) 79 | ffi_fill(q, ffi_sizeof(queue_type, size + 1), 0) 80 | 81 | if size == 0 then 82 | q[0].prev = q 83 | q[0].next = q 84 | 85 | else 86 | local prev = q[0] 87 | for i = 1, size do 88 | local e = q + i 89 | e.user_flags = 0 90 | prev.next = e 91 | e.prev = prev 92 | prev = e 93 | end 94 | 95 | local last = q[size] 96 | last.next = q 97 | q[0].prev = last 98 | end 99 | 100 | return q 101 | end 102 | 103 | 104 | local function queue_is_empty(q) 105 | -- print("q: ", tostring(q), "q.prev: ", tostring(q), ": ", q == q.prev) 106 | return q == q[0].prev 107 | end 108 | 109 | 110 | local function queue_remove(x) 111 | local prev = x.prev 112 | local next = x.next 113 | 114 | next.prev = prev 115 | prev.next = next 116 | 117 | -- for debugging purpose only: 118 | x.prev = NULL 119 | x.next = NULL 120 | end 121 | 122 | 123 | local function queue_insert_head(h, x) 124 | x.next = h[0].next 125 | x.next.prev = x 126 | x.prev = h 127 | h[0].next = x 128 | end 129 | 130 | 131 | local function queue_last(h) 132 | return h[0].prev 133 | end 134 | 135 | 136 | local function queue_head(h) 137 | return h[0].next 138 | end 139 | 140 | 141 | -- true module stuffs 142 | 143 | local _M = { 144 | _VERSION = '0.14' 145 | } 146 | local mt = { __index = _M } 147 | 148 | 149 | local function ptr2num(ptr) 150 | local v = tonumber(ffi_cast(uintptr_t, ptr)) 151 | return v 152 | end 153 | 154 | 155 | function _M.new(size) 156 | if size < 1 then 157 | return nil, "size too small" 158 | end 159 | 160 | local self = { 161 | hasht = {}, 162 | free_queue = queue_init(size), 163 | cache_queue = queue_init(), 164 | key2node = {}, 165 | node2key = {}, 166 | num_items = 0, 167 | max_items = size, 168 | } 169 | setmetatable(self, mt) 170 | return self 171 | end 172 | 173 | 174 | function _M.count(self) 175 | return self.num_items 176 | end 177 | 178 | 179 | function _M.capacity(self) 180 | return self.max_items 181 | end 182 | 183 | 184 | function _M.get(self, key) 185 | local hasht = self.hasht 186 | local val = hasht[key] 187 | if val == nil then 188 | return nil 189 | end 190 | 191 | local node = self.key2node[key] 192 | 193 | -- print(key, ": moving node ", tostring(node), " to cache queue head") 194 | local cache_queue = self.cache_queue 195 | queue_remove(node) 196 | queue_insert_head(cache_queue, node) 197 | 198 | if node.expire >= 0 and node.expire < ngx_now() then 199 | -- print("expired: ", node.expire, " > ", ngx_now()) 200 | return nil, val, node.user_flags 201 | end 202 | 203 | return val, nil, node.user_flags 204 | end 205 | 206 | 207 | function _M.delete(self, key) 208 | self.hasht[key] = nil 209 | 210 | local key2node = self.key2node 211 | local node = key2node[key] 212 | 213 | if not node then 214 | return false 215 | end 216 | 217 | key2node[key] = nil 218 | self.node2key[ptr2num(node)] = nil 219 | 220 | queue_remove(node) 221 | queue_insert_tail(self.free_queue, node) 222 | self.num_items = self.num_items - 1 223 | return true 224 | end 225 | 226 | 227 | function _M.set(self, key, value, ttl, flags) 228 | local hasht = self.hasht 229 | hasht[key] = value 230 | 231 | local key2node = self.key2node 232 | local node = key2node[key] 233 | if not node then 234 | local free_queue = self.free_queue 235 | local node2key = self.node2key 236 | 237 | if queue_is_empty(free_queue) then 238 | -- evict the least recently used key 239 | -- assert(not queue_is_empty(self.cache_queue)) 240 | node = queue_last(self.cache_queue) 241 | 242 | local oldkey = node2key[ptr2num(node)] 243 | -- print(key, ": evicting oldkey: ", oldkey, ", oldnode: ", 244 | -- tostring(node)) 245 | if oldkey then 246 | hasht[oldkey] = nil 247 | key2node[oldkey] = nil 248 | end 249 | 250 | else 251 | -- take a free queue node 252 | node = queue_head(free_queue) 253 | -- only add count if we are not evicting 254 | self.num_items = self.num_items + 1 255 | -- print(key, ": get a new free node: ", tostring(node)) 256 | end 257 | 258 | node2key[ptr2num(node)] = key 259 | key2node[key] = node 260 | end 261 | 262 | queue_remove(node) 263 | queue_insert_head(self.cache_queue, node) 264 | 265 | if ttl then 266 | node.expire = ngx_now() + ttl 267 | else 268 | node.expire = -1 269 | end 270 | 271 | if type(flags) == "number" and flags >= 0 then 272 | node.user_flags = flags 273 | 274 | else 275 | node.user_flags = 0 276 | end 277 | end 278 | 279 | 280 | function _M.get_keys(self, max_count, res) 281 | if not max_count or max_count == 0 then 282 | max_count = self.num_items 283 | end 284 | 285 | if not res then 286 | res = new_tab(max_count + 1, 0) -- + 1 for trailing hole 287 | end 288 | 289 | local cache_queue = self.cache_queue 290 | local node2key = self.node2key 291 | 292 | local i = 0 293 | local node = queue_head(cache_queue) 294 | 295 | while node ~= cache_queue do 296 | if i >= max_count then 297 | break 298 | end 299 | 300 | i = i + 1 301 | res[i] = node2key[ptr2num(node)] 302 | node = node.next 303 | end 304 | 305 | res[i + 1] = nil 306 | 307 | return res 308 | end 309 | 310 | 311 | function _M.flush_all(self) 312 | tb_clear(self.hasht) 313 | tb_clear(self.node2key) 314 | tb_clear(self.key2node) 315 | 316 | self.num_items = 0 317 | 318 | local cache_queue = self.cache_queue 319 | local free_queue = self.free_queue 320 | 321 | -- splice the cache_queue into free_queue 322 | if not queue_is_empty(cache_queue) then 323 | local free_head = free_queue[0] 324 | local free_last = free_head.prev 325 | 326 | local cache_head = cache_queue[0] 327 | local cache_first = cache_head.next 328 | local cache_last = cache_head.prev 329 | 330 | free_last.next = cache_first 331 | cache_first.prev = free_last 332 | 333 | cache_last.next = free_head 334 | free_head.prev = cache_last 335 | 336 | cache_head.next = cache_queue 337 | cache_head.prev = cache_queue 338 | end 339 | end 340 | 341 | 342 | return _M 343 | -------------------------------------------------------------------------------- /lib/resty/lrucache/pureffi.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Yichun Zhang (agentzh) 2 | -- Copyright (C) Shuxin Yang 3 | 4 | --[[ 5 | This module implements a key/value cache store. We adopt LRU as our 6 | replace/evict policy. Each key/value pair is tagged with a Time-to-Live (TTL); 7 | from user's perspective, stale pairs are automatically removed from the cache. 8 | 9 | Why FFI 10 | ------- 11 | In Lua, expression "table[key] = nil" does not *PHYSICALLY* remove the value 12 | associated with the key; it just set the value to be nil! So the table will 13 | keep growing with large number of the key/nil pairs which will be purged until 14 | resize() operator is called. 15 | 16 | This "feature" is terribly ill-suited to what we need. Therefore we have to 17 | rely on FFI to build a hash-table where any entry can be physically deleted 18 | immediately. 19 | 20 | Under the hood: 21 | -------------- 22 | In concept, we introduce three data structures to implement the cache store: 23 | 1. key/value vector for storing keys and values. 24 | 2. a queue to mimic the LRU. 25 | 3. hash-table for looking up the value for a given key. 26 | 27 | Unfortunately, efficiency and clarity usually come at each other cost. The 28 | data strucutres we are using are slightly more complicated than what we 29 | described above. 30 | 31 | o. Lua does not have efficient way to store a vector of pair. So, we use 32 | two vectors for key/value pair: one for keys and the other for values 33 | (_M.key_v and _M.val_v, respectively), and i-th key corresponds to 34 | i-th value. 35 | 36 | A key/value pair is identified by the "id" field in a "node" (we shall 37 | discuss node later) 38 | 39 | o. The queue is nothing more than a doubly-linked list of "node" linked via 40 | lrucache_pureffi_queue_s::{next|prev} fields. 41 | 42 | o. The hash-table has two parts: 43 | - the _M.bucket_v[] a vector of bucket, indiced by hash-value, and 44 | - a bucket is a singly-linked list of "node" via the 45 | lrucache_pureffi_queue_s::conflict field. 46 | 47 | A key must be a string, and the hash value of a key is evaluated by: 48 | crc32(key-cast-to-pointer) % size(_M.bucket_v). 49 | We mandate size(_M.bucket_v) being a power-of-two in order to avoid 50 | expensive modulo operation. 51 | 52 | At the heart of the module is an array of "node" (of type 53 | lrucache_pureffi_queue_s). A node: 54 | - keeps the meta-data of its corresponding key/value pair 55 | (embodied by the "id", and "expire" field); 56 | - is a part of LRU queue (embodied by "prev" and "next" fields); 57 | - is a part of hash-table (embodied by the "conflict" field). 58 | ]] 59 | 60 | local ffi = require "ffi" 61 | local bit = require "bit" 62 | 63 | 64 | local ffi_new = ffi.new 65 | local ffi_sizeof = ffi.sizeof 66 | local ffi_cast = ffi.cast 67 | local ffi_fill = ffi.fill 68 | local ngx_now = ngx.now 69 | local uintptr_t = ffi.typeof("uintptr_t") 70 | local c_str_t = ffi.typeof("const char*") 71 | local int_t = ffi.typeof("int") 72 | local int_array_t = ffi.typeof("int[?]") 73 | 74 | 75 | local crc_tab = ffi.new("const unsigned int[256]", { 76 | 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 77 | 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 78 | 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 79 | 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 80 | 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 81 | 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 82 | 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 83 | 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 84 | 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 85 | 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 86 | 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, 87 | 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, 88 | 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 89 | 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 90 | 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 91 | 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 92 | 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 93 | 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 94 | 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 95 | 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 96 | 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 97 | 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 98 | 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 99 | 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 100 | 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 101 | 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 102 | 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 103 | 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 104 | 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 105 | 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 106 | 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 107 | 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 108 | 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 109 | 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 110 | 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 111 | 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 112 | 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 113 | 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 114 | 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 115 | 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 116 | 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 117 | 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 118 | 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D }); 119 | 120 | local setmetatable = setmetatable 121 | local tonumber = tonumber 122 | local tostring = tostring 123 | local type = type 124 | 125 | local brshift = bit.rshift 126 | local bxor = bit.bxor 127 | local band = bit.band 128 | 129 | local new_tab 130 | do 131 | local ok 132 | ok, new_tab = pcall(require, "table.new") 133 | if not ok then 134 | new_tab = function(narr, nrec) return {} end 135 | end 136 | end 137 | 138 | -- queue data types 139 | -- 140 | -- this queue is a double-ended queue and the first node 141 | -- is reserved for the queue itself. 142 | -- the implementation is mostly borrowed from nginx's ngx_queue_t data 143 | -- structure. 144 | 145 | ffi.cdef[[ 146 | /* A lrucache_pureffi_queue_s node hook together three data structures: 147 | * o. the key/value store as embodied by the "id" (which is in essence the 148 | * indentifier of key/pair pair) and the "expire" (which is a metadata 149 | * of the corresponding key/pair pair). 150 | * o. The LRU queue via the prev/next fields. 151 | * o. The hash-tabble as embodied by the "conflict" field. 152 | */ 153 | typedef struct lrucache_pureffi_queue_s lrucache_pureffi_queue_t; 154 | struct lrucache_pureffi_queue_s { 155 | /* Each node is assigned a unique ID at construction time, and the 156 | * ID remain immutatble, regardless the node is in active-list or 157 | * free-list. The queue header is assigned ID 0. Since queue-header 158 | * is a sentinel node, 0 denodes "invalid ID". 159 | * 160 | * Intuitively, we can view the "id" as the identifier of key/value 161 | * pair. 162 | */ 163 | int id; 164 | 165 | /* The bucket of the hash-table is implemented as a singly-linked list. 166 | * The "conflict" refers to the ID of the next node in the bucket. 167 | */ 168 | int conflict; 169 | 170 | uint32_t user_flags; 171 | 172 | double expire; /* in seconds */ 173 | 174 | lrucache_pureffi_queue_t *prev; 175 | lrucache_pureffi_queue_t *next; 176 | }; 177 | ]] 178 | 179 | local queue_arr_type = ffi.typeof("lrucache_pureffi_queue_t[?]") 180 | --local queue_ptr_type = ffi.typeof("lrucache_pureffi_queue_t*") 181 | local queue_type = ffi.typeof("lrucache_pureffi_queue_t") 182 | local NULL = ffi.null 183 | 184 | 185 | --======================================================================== 186 | -- 187 | -- Queue utility functions 188 | -- 189 | --======================================================================== 190 | 191 | -- Append the element "x" to the given queue "h". 192 | local function queue_insert_tail(h, x) 193 | local last = h[0].prev 194 | x.prev = last 195 | last.next = x 196 | x.next = h 197 | h[0].prev = x 198 | end 199 | 200 | 201 | --[[ 202 | Allocate a queue with size + 1 elements. Elements are linked together in a 203 | circular way, i.e. the last element's "next" points to the first element, 204 | while the first element's "prev" element points to the last element. 205 | ]] 206 | local function queue_init(size) 207 | if not size then 208 | size = 0 209 | end 210 | local q = ffi_new(queue_arr_type, size + 1) 211 | ffi_fill(q, ffi_sizeof(queue_type, size + 1), 0) 212 | 213 | if size == 0 then 214 | q[0].prev = q 215 | q[0].next = q 216 | 217 | else 218 | local prev = q[0] 219 | for i = 1, size do 220 | local e = q[i] 221 | e.id = i 222 | e.user_flags = 0 223 | prev.next = e 224 | e.prev = prev 225 | prev = e 226 | end 227 | 228 | local last = q[size] 229 | last.next = q 230 | q[0].prev = last 231 | end 232 | 233 | return q 234 | end 235 | 236 | 237 | local function queue_is_empty(q) 238 | -- print("q: ", tostring(q), "q.prev: ", tostring(q), ": ", q == q.prev) 239 | return q == q[0].prev 240 | end 241 | 242 | 243 | local function queue_remove(x) 244 | local prev = x.prev 245 | local next = x.next 246 | 247 | next.prev = prev 248 | prev.next = next 249 | 250 | -- for debugging purpose only: 251 | x.prev = NULL 252 | x.next = NULL 253 | end 254 | 255 | 256 | -- Insert the element "x" the to the given queue "h" 257 | local function queue_insert_head(h, x) 258 | x.next = h[0].next 259 | x.next.prev = x 260 | x.prev = h 261 | h[0].next = x 262 | end 263 | 264 | 265 | local function queue_last(h) 266 | return h[0].prev 267 | end 268 | 269 | 270 | local function queue_head(h) 271 | return h[0].next 272 | end 273 | 274 | 275 | --======================================================================== 276 | -- 277 | -- Miscellaneous Utility Functions 278 | -- 279 | --======================================================================== 280 | 281 | local function ptr2num(ptr) 282 | return tonumber(ffi_cast(uintptr_t, ptr)) 283 | end 284 | 285 | 286 | local function crc32_ptr(ptr) 287 | local p = brshift(ptr2num(ptr), 3) 288 | local b = band(p, 255) 289 | local crc32 = crc_tab[b] 290 | 291 | b = band(brshift(p, 8), 255) 292 | crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) 293 | 294 | b = band(brshift(p, 16), 255) 295 | crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) 296 | 297 | --b = band(brshift(p, 24), 255) 298 | --crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) 299 | return crc32 300 | end 301 | 302 | 303 | --======================================================================== 304 | -- 305 | -- Implementation of "export" functions 306 | -- 307 | --======================================================================== 308 | 309 | local _M = { 310 | _VERSION = '0.13' 311 | } 312 | local mt = { __index = _M } 313 | 314 | 315 | -- "size" specifies the maximum number of entries in the LRU queue, and the 316 | -- "load_factor" designates the 'load factor' of the hash-table we are using 317 | -- internally. The default value of load-factor is 0.5 (i.e. 50%); if the 318 | -- load-factor is specified, it will be clamped to the range of [0.1, 1](i.e. 319 | -- if load-factor is greater than 1, it will be saturated to 1, likewise, 320 | -- if load-factor is smaller than 0.1, it will be clamped to 0.1). 321 | function _M.new(size, load_factor) 322 | if size < 1 then 323 | return nil, "size too small" 324 | end 325 | 326 | -- Determine bucket size, which must be power of two. 327 | local load_f = load_factor 328 | if not load_factor then 329 | load_f = 0.5 330 | elseif load_factor > 1 then 331 | load_f = 1 332 | elseif load_factor < 0.1 then 333 | load_f = 0.1 334 | end 335 | 336 | local bs_min = size / load_f 337 | -- The bucket_sz *MUST* be a power-of-two. See the hash_string(). 338 | local bucket_sz = 1 339 | repeat 340 | bucket_sz = bucket_sz * 2 341 | until bucket_sz >= bs_min 342 | 343 | local self = { 344 | size = size, 345 | bucket_sz = bucket_sz, 346 | free_queue = queue_init(size), 347 | cache_queue = queue_init(0), 348 | node_v = nil, 349 | key_v = new_tab(size, 0), 350 | val_v = new_tab(size, 0), 351 | bucket_v = ffi_new(int_array_t, bucket_sz), 352 | num_items = 0, 353 | } 354 | -- "node_v" is an array of all the nodes used in the LRU queue. Exprpession 355 | -- node_v[i] evaluates to the element of ID "i". 356 | self.node_v = self.free_queue 357 | 358 | -- Allocate the array-part of the key_v, val_v, bucket_v. 359 | --local key_v = self.key_v 360 | --local val_v = self.val_v 361 | --local bucket_v = self.bucket_v 362 | ffi_fill(self.bucket_v, ffi_sizeof(int_t, bucket_sz), 0) 363 | 364 | return setmetatable(self, mt) 365 | end 366 | 367 | 368 | function _M.count(self) 369 | return self.num_items 370 | end 371 | 372 | 373 | function _M.capacity(self) 374 | return self.size 375 | end 376 | 377 | 378 | local function hash_string(self, str) 379 | local c_str = ffi_cast(c_str_t, str) 380 | 381 | local hv = crc32_ptr(c_str) 382 | hv = band(hv, self.bucket_sz - 1) 383 | -- Hint: bucket is 0-based 384 | return hv 385 | end 386 | 387 | 388 | -- Search the node associated with the key in the bucket, if found returns 389 | -- the the id of the node, and the id of its previous node in the conflict list. 390 | -- The "bucket_hdr_id" is the ID of the first node in the bucket 391 | local function _find_node_in_bucket(key, key_v, node_v, bucket_hdr_id) 392 | if bucket_hdr_id ~= 0 then 393 | local prev = 0 394 | local cur = bucket_hdr_id 395 | 396 | while cur ~= 0 and key_v[cur] ~= key do 397 | prev = cur 398 | cur = node_v[cur].conflict 399 | end 400 | 401 | if cur ~= 0 then 402 | return cur, prev 403 | end 404 | end 405 | end 406 | 407 | 408 | -- Return the node corresponding to the key/val. 409 | local function find_key(self, key) 410 | local key_hash = hash_string(self, key) 411 | return _find_node_in_bucket(key, self.key_v, self.node_v, 412 | self.bucket_v[key_hash]) 413 | end 414 | 415 | 416 | --[[ This function tries to 417 | 1. Remove the given key and the associated value from the key/value store, 418 | 2. Remove the entry associated with the key from the hash-table. 419 | 420 | NOTE: all queues remain intact. 421 | 422 | If there was a node bound to the key/val, return that node; otherwise, 423 | nil is returned. 424 | ]] 425 | local function remove_key(self, key) 426 | local key_v = self.key_v 427 | local val_v = self.val_v 428 | local node_v = self.node_v 429 | local bucket_v = self.bucket_v 430 | 431 | local key_hash = hash_string(self, key) 432 | local cur, prev = 433 | _find_node_in_bucket(key, key_v, node_v, bucket_v[key_hash]) 434 | 435 | if cur then 436 | -- In an attempt to make key and val dead. 437 | key_v[cur] = nil 438 | val_v[cur] = nil 439 | self.num_items = self.num_items - 1 440 | 441 | -- Remove the node from the hash table 442 | local next_node = node_v[cur].conflict 443 | if prev ~= 0 then 444 | node_v[prev].conflict = next_node 445 | else 446 | bucket_v[key_hash] = next_node 447 | end 448 | node_v[cur].conflict = 0 449 | 450 | return cur 451 | end 452 | end 453 | 454 | 455 | --[[ Bind the key/val with the given node, and insert the node into the Hashtab. 456 | NOTE: this function does not touch any queue 457 | ]] 458 | local function insert_key(self, key, val, node) 459 | -- Bind the key/val with the node 460 | local node_id = node.id 461 | self.key_v[node_id] = key 462 | self.val_v[node_id] = val 463 | 464 | -- Insert the node into the hash-table 465 | local key_hash = hash_string(self, key) 466 | local bucket_v = self.bucket_v 467 | node.conflict = bucket_v[key_hash] 468 | bucket_v[key_hash] = node_id 469 | self.num_items = self.num_items + 1 470 | end 471 | 472 | 473 | function _M.get(self, key) 474 | if type(key) ~= "string" then 475 | key = tostring(key) 476 | end 477 | 478 | local node_id = find_key(self, key) 479 | if not node_id then 480 | return nil 481 | end 482 | 483 | -- print(key, ": moving node ", tostring(node), " to cache queue head") 484 | local cache_queue = self.cache_queue 485 | local node = self.node_v + node_id 486 | queue_remove(node) 487 | queue_insert_head(cache_queue, node) 488 | 489 | local expire = node.expire 490 | if expire >= 0 and expire < ngx_now() then 491 | -- print("expired: ", node.expire, " > ", ngx_now()) 492 | return nil, self.val_v[node_id], node.user_flags 493 | end 494 | 495 | return self.val_v[node_id], nil, node.user_flags 496 | end 497 | 498 | 499 | function _M.delete(self, key) 500 | if type(key) ~= "string" then 501 | key = tostring(key) 502 | end 503 | 504 | local node_id = remove_key(self, key); 505 | if not node_id then 506 | return false 507 | end 508 | 509 | local node = self.node_v + node_id 510 | queue_remove(node) 511 | queue_insert_tail(self.free_queue, node) 512 | return true 513 | end 514 | 515 | 516 | function _M.set(self, key, value, ttl, flags) 517 | if type(key) ~= "string" then 518 | key = tostring(key) 519 | end 520 | 521 | local node_id = find_key(self, key) 522 | local node 523 | if not node_id then 524 | local free_queue = self.free_queue 525 | if queue_is_empty(free_queue) then 526 | -- evict the least recently used key 527 | -- assert(not queue_is_empty(self.cache_queue)) 528 | node = queue_last(self.cache_queue) 529 | remove_key(self, self.key_v[node.id]) 530 | else 531 | -- take a free queue node 532 | node = queue_head(free_queue) 533 | -- print(key, ": get a new free node: ", tostring(node)) 534 | end 535 | 536 | -- insert the key 537 | insert_key(self, key, value, node) 538 | else 539 | node = self.node_v + node_id 540 | self.val_v[node_id] = value 541 | end 542 | 543 | queue_remove(node) 544 | queue_insert_head(self.cache_queue, node) 545 | 546 | if ttl then 547 | node.expire = ngx_now() + ttl 548 | else 549 | node.expire = -1 550 | end 551 | 552 | if type(flags) == "number" and flags >= 0 then 553 | node.user_flags = flags 554 | 555 | else 556 | node.user_flags = 0 557 | end 558 | end 559 | 560 | 561 | function _M.get_keys(self, max_count, res) 562 | if not max_count or max_count == 0 then 563 | max_count = self.num_items 564 | end 565 | 566 | if not res then 567 | res = new_tab(max_count + 1, 0) -- + 1 for trailing hole 568 | end 569 | 570 | local cache_queue = self.cache_queue 571 | local key_v = self.key_v 572 | 573 | local i = 0 574 | local node = queue_head(cache_queue) 575 | 576 | while node ~= cache_queue do 577 | if i >= max_count then 578 | break 579 | end 580 | 581 | i = i + 1 582 | res[i] = key_v[node.id] 583 | node = node.next 584 | end 585 | 586 | res[i + 1] = nil 587 | 588 | return res 589 | end 590 | 591 | 592 | function _M.flush_all(self) 593 | local cache_queue = self.cache_queue 594 | local key_v = self.key_v 595 | 596 | local node = queue_head(cache_queue) 597 | 598 | while node ~= cache_queue do 599 | local key = key_v[node.id] 600 | node = node.next 601 | _M.delete(self, key) 602 | end 603 | end 604 | 605 | 606 | return _M 607 | -------------------------------------------------------------------------------- /t/001-sanity.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: sanity 15 | --- config 16 | location = /t { 17 | content_by_lua ' 18 | local lrucache = require "resty.lrucache" 19 | local c = lrucache.new(2) 20 | 21 | collectgarbage() 22 | 23 | c:set("dog", 32) 24 | c:set("cat", 56) 25 | ngx.say("dog: ", (c:get("dog"))) 26 | ngx.say("cat: ", (c:get("cat"))) 27 | 28 | c:set("dog", 32) 29 | c:set("cat", 56) 30 | ngx.say("dog: ", (c:get("dog"))) 31 | ngx.say("cat: ", (c:get("cat"))) 32 | 33 | c:delete("dog") 34 | c:delete("cat") 35 | ngx.say("dog: ", (c:get("dog"))) 36 | ngx.say("cat: ", (c:get("cat"))) 37 | '; 38 | } 39 | --- response_body 40 | dog: 32 41 | cat: 56 42 | dog: 32 43 | cat: 56 44 | dog: nil 45 | cat: nil 46 | 47 | 48 | 49 | === TEST 2: evict existing items 50 | --- config 51 | location = /t { 52 | content_by_lua ' 53 | local lrucache = require "resty.lrucache" 54 | local c = lrucache.new(2) 55 | if not c then 56 | ngx.say("failed to init lrucace: ", err) 57 | return 58 | end 59 | 60 | c:set("dog", 32) 61 | c:set("cat", 56) 62 | ngx.say("dog: ", (c:get("dog"))) 63 | ngx.say("cat: ", (c:get("cat"))) 64 | 65 | c:set("bird", 76) 66 | ngx.say("dog: ", (c:get("dog"))) 67 | ngx.say("cat: ", (c:get("cat"))) 68 | ngx.say("bird: ", (c:get("bird"))) 69 | '; 70 | } 71 | --- response_body 72 | dog: 32 73 | cat: 56 74 | dog: nil 75 | cat: 56 76 | bird: 76 77 | 78 | 79 | 80 | === TEST 3: evict existing items (reordered, get should also count) 81 | --- config 82 | location = /t { 83 | content_by_lua ' 84 | local lrucache = require "resty.lrucache" 85 | local c = lrucache.new(2) 86 | if not c then 87 | ngx.say("failed to init lrucace: ", err) 88 | return 89 | end 90 | 91 | c:set("cat", 56) 92 | c:set("dog", 32) 93 | ngx.say("dog: ", (c:get("dog"))) 94 | ngx.say("cat: ", (c:get("cat"))) 95 | 96 | c:set("bird", 76) 97 | ngx.say("dog: ", (c:get("dog"))) 98 | ngx.say("cat: ", (c:get("cat"))) 99 | ngx.say("bird: ", (c:get("bird"))) 100 | '; 101 | } 102 | --- response_body 103 | dog: 32 104 | cat: 56 105 | dog: nil 106 | cat: 56 107 | bird: 76 108 | 109 | 110 | 111 | === TEST 4: ttl 112 | --- config 113 | location = /t { 114 | content_by_lua ' 115 | local lrucache = require "resty.lrucache" 116 | local c = lrucache.new(1) 117 | 118 | c:set("dog", 32, 0.5) 119 | ngx.say("dog: ", (c:get("dog"))) 120 | 121 | ngx.sleep(0.25) 122 | ngx.say("dog: ", (c:get("dog"))) 123 | 124 | ngx.sleep(0.26) 125 | local v, err = c:get("dog") 126 | ngx.say("dog: ", v, " ", err) 127 | '; 128 | } 129 | --- response_body 130 | dog: 32 131 | dog: 32 132 | dog: nil 32 133 | 134 | 135 | 136 | === TEST 5: ttl 137 | --- config 138 | location = /t { 139 | content_by_lua ' 140 | local lrucache = require "resty.lrucache" 141 | local lim = 5 142 | local c = lrucache.new(lim) 143 | local n = 1000 144 | 145 | for i = 1, n do 146 | c:set("dog" .. i, i) 147 | c:delete("dog" .. i) 148 | c:set("dog" .. i, i) 149 | local cnt = 0 150 | for k, v in pairs(c.hasht) do 151 | cnt = cnt + 1 152 | end 153 | assert(cnt <= lim) 154 | end 155 | 156 | for i = 1, n do 157 | local key = "dog" .. math.random(1, n) 158 | c:get(key) 159 | end 160 | 161 | for i = 1, n do 162 | local key = "dog" .. math.random(1, n) 163 | c:get(key) 164 | c:set("dog" .. i, i) 165 | 166 | local cnt = 0 167 | for k, v in pairs(c.hasht) do 168 | cnt = cnt + 1 169 | end 170 | assert(cnt <= lim) 171 | end 172 | 173 | ngx.say("ok") 174 | '; 175 | } 176 | --- response_body 177 | ok 178 | --- timeout: 20 179 | 180 | 181 | 182 | === TEST 6: replace value 183 | --- config 184 | location = /t { 185 | content_by_lua ' 186 | local lrucache = require "resty.lrucache" 187 | local c = lrucache.new(1) 188 | 189 | c:set("dog", 32) 190 | ngx.say("dog: ", (c:get("dog"))) 191 | 192 | c:set("dog", 33) 193 | ngx.say("dog: ", (c:get("dog"))) 194 | '; 195 | } 196 | --- response_body 197 | dog: 32 198 | dog: 33 199 | -------------------------------------------------------------------------------- /t/002-should-store-false.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: should-store-false 15 | --- config 16 | location = /t { 17 | content_by_lua ' 18 | local lrucache = require "resty.lrucache" 19 | local c = lrucache.new(2) 20 | 21 | collectgarbage() 22 | 23 | c:set("false-value", false) 24 | ngx.say("false-value: ", (c:get("false-value"))) 25 | 26 | c:delete("false-value") 27 | ngx.say("false-value: ", (c:get("false-value"))) 28 | '; 29 | } 30 | --- response_body 31 | false-value: false 32 | false-value: nil 33 | -------------------------------------------------------------------------------- /t/003-init-by-lua.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(1); 6 | 7 | plan tests => repeat_each() * (blocks() * 2); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: sanity 15 | --- http_config eval 16 | "$t::TestLRUCache::HttpConfig" 17 | . qq! 18 | init_by_lua ' 19 | local function log(...) 20 | print("[cache] ", ...) 21 | end 22 | 23 | local lrucache = require "resty.lrucache" 24 | local c = lrucache.new(2) 25 | 26 | collectgarbage() 27 | 28 | c:set("dog", 32) 29 | c:set("cat", 56) 30 | log("dog: ", (c:get("dog"))) 31 | log("cat: ", (c:get("cat"))) 32 | 33 | c:set("dog", 32) 34 | c:set("cat", 56) 35 | log("dog: ", (c:get("dog"))) 36 | log("cat: ", (c:get("cat"))) 37 | 38 | c:delete("dog") 39 | c:delete("cat") 40 | log("dog: ", (c:get("dog"))) 41 | log("cat: ", (c:get("cat"))) 42 | '; 43 | ! 44 | --- config 45 | location = /t { 46 | return 200; 47 | } 48 | --- ignore_response 49 | --- error_log 50 | --- grep_error_log eval: qr/\[cache\] .*? (?:\d+|nil)/ 51 | --- grep_error_log_out 52 | [cache] dog: 32 53 | [cache] cat: 56 54 | [cache] dog: 32 55 | [cache] cat: 56 56 | [cache] dog: nil 57 | [cache] cat: nil 58 | 59 | 60 | 61 | === TEST 2: sanity 62 | --- http_config eval 63 | "$t::TestLRUCache::HttpConfig" 64 | . qq! 65 | init_by_lua ' 66 | lrucache = require "resty.lrucache" 67 | flv_index, err = lrucache.new(200) 68 | if not flv_index then 69 | ngx.log(ngx.ERR, "failed to create the cache: ", err) 70 | return 71 | end 72 | 73 | flv_meta, err = lrucache.new(200) 74 | if not flv_meta then 75 | ngx.log(ngx.ERR, "failed to create the cache: ", err) 76 | return 77 | end 78 | 79 | flv_channel, err = lrucache.new(200) 80 | if not flv_channel then 81 | ngx.log(ngx.ERR, "failed to create the cache: ", err) 82 | return 83 | end 84 | 85 | print("3 lrucache initialized.") 86 | '; 87 | ! 88 | --- config 89 | location = /t { 90 | return 200; 91 | } 92 | --- ignore_response 93 | --- error_log 94 | 3 lrucache initialized. 95 | -------------------------------------------------------------------------------- /t/004-flush-all.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: flush_all() deletes all keys (cache partial occupied) 15 | --- config 16 | location = /t { 17 | content_by_lua_block { 18 | local lrucache = require "resty.lrucache" 19 | 20 | local N = 4 21 | 22 | local c = lrucache.new(N) 23 | 24 | for i = 1, N / 2 do 25 | c:set("key " .. i, i) 26 | end 27 | 28 | c:flush_all() 29 | 30 | for i = 1, N do 31 | local key = "key " .. i 32 | local v = c:get(key) 33 | ngx.say(i, ": ", v) 34 | end 35 | 36 | ngx.say("++") 37 | 38 | for i = 1, N + 1 do 39 | c:set("key " .. i, i) 40 | end 41 | 42 | for i = 1, N + 1 do 43 | ngx.say(i, ": ", (c:get("key " .. i))) 44 | end 45 | } 46 | } 47 | --- response_body 48 | 1: nil 49 | 2: nil 50 | 3: nil 51 | 4: nil 52 | ++ 53 | 1: nil 54 | 2: 2 55 | 3: 3 56 | 4: 4 57 | 5: 5 58 | 59 | 60 | 61 | === TEST 2: flush_all() deletes all keys (cache fully occupied) 62 | --- config 63 | location = /t { 64 | content_by_lua_block { 65 | local lrucache = require "resty.lrucache" 66 | 67 | local N = 4 68 | 69 | local c = lrucache.new(N) 70 | 71 | for i = 1, N + 1 do 72 | c:set("key " .. i, i) 73 | end 74 | 75 | ngx.say(c:count()) 76 | c:flush_all() 77 | ngx.say(c:count()) 78 | 79 | for i = 1, N + 1 do 80 | local key = "key " .. i 81 | local v = c:get(key) 82 | ngx.say(i, ": ", v) 83 | end 84 | 85 | ngx.say("++") 86 | 87 | for i = 1, N + 1 do 88 | c:set("key " .. i, i) 89 | end 90 | 91 | for i = 1, N + 1 do 92 | ngx.say(i, ": ", (c:get("key " .. i))) 93 | end 94 | } 95 | } 96 | --- response_body 97 | 4 98 | 0 99 | 1: nil 100 | 2: nil 101 | 3: nil 102 | 4: nil 103 | 5: nil 104 | ++ 105 | 1: nil 106 | 2: 2 107 | 3: 3 108 | 4: 4 109 | 5: 5 110 | 111 | 112 | 113 | === TEST 3: flush_all() flush empty cache store 114 | --- config 115 | location = /t { 116 | content_by_lua_block { 117 | local lrucache = require "resty.lrucache" 118 | 119 | local N = 4 120 | 121 | local c = lrucache.new(4) 122 | 123 | c:flush_all() 124 | 125 | for i = 1, N do 126 | local key = "key " .. i 127 | local v = c:get(key) 128 | ngx.say(i, ": ", v) 129 | end 130 | 131 | ngx.say("++") 132 | 133 | for i = 1, N + 1 do 134 | c:set("key " .. i, i) 135 | end 136 | 137 | for i = 1, N + 1 do 138 | ngx.say(i, ": ", (c:get("key " .. i))) 139 | end 140 | } 141 | } 142 | --- response_body 143 | 1: nil 144 | 2: nil 145 | 3: nil 146 | 4: nil 147 | ++ 148 | 1: nil 149 | 2: 2 150 | 3: 3 151 | 4: 4 152 | 5: 5 153 | -------------------------------------------------------------------------------- /t/005-capacity.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: capacity() returns total cache capacity 15 | --- config 16 | location = /t { 17 | content_by_lua_block { 18 | local lrucache = require "resty.lrucache" 19 | local c = lrucache.new(2) 20 | 21 | ngx.say("capacity: ", c:capacity()) 22 | } 23 | } 24 | --- response_body 25 | capacity: 2 26 | -------------------------------------------------------------------------------- /t/006-count.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: count() returns current cache size 15 | --- config 16 | location = /t { 17 | content_by_lua_block { 18 | local lrucache = require "resty.lrucache" 19 | local c = lrucache.new(2) 20 | 21 | ngx.say("count: ", c:count()) 22 | 23 | c:set("dog", 32) 24 | ngx.say("count: ", c:count()) 25 | c:set("dog", 33) 26 | 27 | ngx.say("count: ", c:count()) 28 | c:set("cat", 33) 29 | 30 | ngx.say("count: ", c:count()) 31 | c:set("pig", 33) 32 | 33 | ngx.say("count: ", c:count()) 34 | c:delete("dog") 35 | 36 | ngx.say("count: ", c:count()) 37 | c:delete("pig") 38 | 39 | ngx.say("count: ", c:count()) 40 | c:delete("cat") 41 | 42 | ngx.say("count: ", c:count()) 43 | } 44 | } 45 | --- response_body 46 | count: 0 47 | count: 1 48 | count: 1 49 | count: 2 50 | count: 2 51 | count: 2 52 | count: 1 53 | count: 0 54 | -------------------------------------------------------------------------------- /t/007-get-keys.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: get_keys() with some keys 15 | --- config 16 | location = /t { 17 | content_by_lua_block { 18 | local lrucache = require "resty.lrucache" 19 | local c = lrucache.new(100) 20 | 21 | c:set("hello", true) 22 | c:set("world", false) 23 | 24 | local keys = c:get_keys() 25 | 26 | ngx.say("size: ", #keys) 27 | 28 | for i = 1, #keys do 29 | ngx.say(keys[i]) 30 | end 31 | } 32 | } 33 | --- response_body 34 | size: 2 35 | world 36 | hello 37 | 38 | 39 | 40 | === TEST 2: get_keys() with no keys 41 | --- config 42 | location = /t { 43 | content_by_lua_block { 44 | local lrucache = require "resty.lrucache" 45 | local c = lrucache.new(100) 46 | 47 | local keys = c:get_keys() 48 | 49 | ngx.say("size: ", #keys) 50 | 51 | for i = 1, #keys do 52 | ngx.say(keys[i]) 53 | end 54 | } 55 | } 56 | --- response_body 57 | size: 0 58 | 59 | 60 | 61 | === TEST 3: get_keys() with full cache 62 | --- config 63 | location = /t { 64 | content_by_lua_block { 65 | local lrucache = require "resty.lrucache" 66 | local c = lrucache.new(100) 67 | 68 | for i = 1, 100 do 69 | c:set("key-" .. i, true) 70 | end 71 | 72 | c:set("extra-key", true) 73 | 74 | local keys = c:get_keys() 75 | 76 | ngx.say("size: ", #keys) 77 | ngx.say("MRU: ", keys[1]) 78 | ngx.say("LRU: ", keys[#keys]) 79 | } 80 | } 81 | --- response_body 82 | size: 100 83 | MRU: extra-key 84 | LRU: key-2 85 | 86 | 87 | 88 | === TEST 4: get_keys() max_count = 5 89 | --- config 90 | location = /t { 91 | content_by_lua_block { 92 | local lrucache = require "resty.lrucache" 93 | local c = lrucache.new(100) 94 | 95 | for i = 1, 100 do 96 | c:set("key-" .. i, true) 97 | end 98 | 99 | local keys = c:get_keys(5) 100 | 101 | ngx.say("size: ", #keys) 102 | ngx.say("MRU: ", keys[1]) 103 | ngx.say("latest: ", keys[#keys]) 104 | } 105 | } 106 | --- response_body 107 | size: 5 108 | MRU: key-100 109 | latest: key-96 110 | 111 | 112 | 113 | === TEST 5: get_keys() max_count = 0 disables max returns 114 | --- config 115 | location = /t { 116 | content_by_lua_block { 117 | local lrucache = require "resty.lrucache" 118 | local c = lrucache.new(100) 119 | 120 | for i = 1, 100 do 121 | c:set("key-" .. i, true) 122 | end 123 | 124 | local keys = c:get_keys(0) 125 | 126 | ngx.say("size: ", #keys) 127 | ngx.say("MRU: ", keys[1]) 128 | ngx.say("LRU: ", keys[#keys]) 129 | } 130 | } 131 | --- response_body 132 | size: 100 133 | MRU: key-100 134 | LRU: key-1 135 | 136 | 137 | 138 | === TEST 6: get_keys() user-fed res table 139 | --- config 140 | location = /t { 141 | content_by_lua_block { 142 | local lrucache = require "resty.lrucache" 143 | local c1 = lrucache.new(3) 144 | local c2 = lrucache.new(2) 145 | 146 | for i = 1, 3 do 147 | c1:set("c1-" .. i, true) 148 | end 149 | 150 | for i = 1, 2 do 151 | c2:set("c2-" .. i, true) 152 | end 153 | 154 | local res = {} 155 | 156 | local keys_1 = c1:get_keys(0, res) 157 | ngx.say("res is user-fed: ", keys_1 == res) 158 | 159 | for _, k in ipairs(keys_1) do 160 | ngx.say(k) 161 | end 162 | 163 | res = {} 164 | 165 | local keys_2 = c2:get_keys(0, res) 166 | 167 | for _, k in ipairs(keys_2) do 168 | ngx.say(k) 169 | end 170 | } 171 | } 172 | --- response_body 173 | res is user-fed: true 174 | c1-3 175 | c1-2 176 | c1-1 177 | c2-2 178 | c2-1 179 | 180 | 181 | 182 | === TEST 7: get_keys() user-fed res table + max_count 183 | --- config 184 | location = /t { 185 | content_by_lua_block { 186 | local lrucache = require "resty.lrucache" 187 | local c1 = lrucache.new(3) 188 | 189 | for i = 1, 3 do 190 | c1:set("key-" .. i, true) 191 | end 192 | 193 | local res = {} 194 | 195 | local keys = c1:get_keys(2, res) 196 | 197 | for _, k in ipairs(keys) do 198 | ngx.say(k) 199 | end 200 | } 201 | } 202 | --- response_body 203 | key-3 204 | key-2 205 | 206 | 207 | 208 | === TEST 8: get_keys() user-fed res table gets a trailing hole 209 | --- config 210 | location = /t { 211 | content_by_lua_block { 212 | local lrucache = require "resty.lrucache" 213 | local c1 = lrucache.new(3) 214 | 215 | for i = 1, 3 do 216 | c1:set("key-" .. i, true) 217 | end 218 | 219 | local res = {} 220 | 221 | for i = 1, 10 do 222 | res[i] = true 223 | end 224 | 225 | local keys = c1:get_keys(2, res) 226 | 227 | for _, k in ipairs(keys) do 228 | ngx.say(k) 229 | end 230 | } 231 | } 232 | --- response_body 233 | key-3 234 | key-2 235 | -------------------------------------------------------------------------------- /t/008-user-flags.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: no user flags by default 15 | --- config 16 | location = /t { 17 | content_by_lua_block { 18 | local lrucache = require "resty.lrucache" 19 | local c = lrucache.new(2) 20 | 21 | c:set("dog", 32) 22 | c:set("cat", 56) 23 | 24 | local v, err, flags = c:get("dog") 25 | ngx.say("dog: ", v, " ", err, " ", flags) 26 | 27 | local v, err, flags = c:get("cat") 28 | ngx.say("cat: ", v, " ", err, " ", flags) 29 | } 30 | } 31 | --- response_body 32 | dog: 32 nil 0 33 | cat: 56 nil 0 34 | 35 | 36 | 37 | === TEST 2: stores user flags if specified 38 | --- config 39 | location = /t { 40 | content_by_lua_block { 41 | local lrucache = require "resty.lrucache" 42 | local c = lrucache.new(2) 43 | 44 | c:set("dog", 32, nil, 0x01) 45 | c:set("cat", 56, nil, 0x02) 46 | 47 | local v, err, flags = c:get("dog") 48 | ngx.say("dog: ", v, " ", err, " ", flags) 49 | 50 | local v, err, flags = c:get("cat") 51 | ngx.say("cat: ", v, " ", err, " ", flags) 52 | } 53 | } 54 | --- response_body 55 | dog: 32 nil 1 56 | cat: 56 nil 2 57 | 58 | 59 | 60 | === TEST 3: user flags cannot be negative 61 | --- config 62 | location = /t { 63 | content_by_lua_block { 64 | local lrucache = require "resty.lrucache" 65 | local c = lrucache.new(3) 66 | 67 | c:set("dog", 32, nil, 0) 68 | c:set("cat", 56, nil, -1) 69 | 70 | local v, err, flags = c:get("dog") 71 | ngx.say("dog: ", v, " ", err, " ", flags) 72 | 73 | local v, err, flags = c:get("cat") 74 | ngx.say("cat: ", v, " ", err, " ", flags) 75 | } 76 | } 77 | --- response_body 78 | dog: 32 nil 0 79 | cat: 56 nil 0 80 | 81 | 82 | 83 | === TEST 4: user flags not number is ignored 84 | --- config 85 | location = /t { 86 | content_by_lua_block { 87 | local lrucache = require "resty.lrucache" 88 | local c = lrucache.new(2) 89 | 90 | c:set("dog", 32, nil, "") 91 | 92 | local v, err, flags = c:get("dog") 93 | ngx.say(v, " ", err, " ", flags) 94 | } 95 | } 96 | --- response_body 97 | 32 nil 0 98 | 99 | 100 | 101 | === TEST 5: all nodes from double-ended queue have flags 102 | --- config 103 | location = /t { 104 | content_by_lua_block { 105 | local len = 10 106 | 107 | local lrucache = require "resty.lrucache" 108 | local c = lrucache.new(len) 109 | 110 | for i = 1, len do 111 | c:set(i, 32, nil, 1) 112 | end 113 | 114 | for i = 1, len do 115 | local v, _, flags = c:get(i) 116 | if not flags then 117 | ngx.say("item ", i, " does not have flags") 118 | return 119 | end 120 | end 121 | 122 | ngx.say("ok") 123 | } 124 | } 125 | --- response_body 126 | ok 127 | 128 | 129 | 130 | === TEST 6: user flags are preserved when item is stale 131 | --- config 132 | location = /t { 133 | content_by_lua_block { 134 | local lrucache = require "resty.lrucache" 135 | local c = lrucache.new(1) 136 | 137 | c:set("dogs", 32, 0.2, 3) 138 | ngx.sleep(0.21) 139 | 140 | local v, err, flags = c:get("dogs") 141 | ngx.say(v, " ", err, " ", flags) 142 | } 143 | } 144 | --- response_body 145 | nil 32 3 146 | 147 | 148 | 149 | === TEST 7: user flags are not preserved upon eviction 150 | --- config 151 | location = /t { 152 | content_by_lua_block { 153 | local lrucache = require "resty.lrucache" 154 | local c = lrucache.new(1) 155 | 156 | for i = 1, 10 do 157 | local flags = i % 2 == 0 and i 158 | c:set(i, true, nil, flags) 159 | 160 | local v, err, flags = c:get(i) 161 | ngx.say(v, " ", err, " ", flags) 162 | end 163 | } 164 | } 165 | --- response_body 166 | true nil 0 167 | true nil 2 168 | true nil 0 169 | true nil 4 170 | true nil 0 171 | true nil 6 172 | true nil 0 173 | true nil 8 174 | true nil 0 175 | true nil 10 176 | -------------------------------------------------------------------------------- /t/100-pureffi/001-sanity.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: sanity 15 | --- config 16 | location = /t { 17 | content_by_lua ' 18 | local lrucache = require "resty.lrucache.pureffi" 19 | local c = lrucache.new(2) 20 | 21 | collectgarbage() 22 | 23 | c:set("dog", 32) 24 | c:set("cat", 56) 25 | ngx.say("dog: ", (c:get("dog"))) 26 | ngx.say("cat: ", (c:get("cat"))) 27 | 28 | c:set("dog", 32) 29 | c:set("cat", 56) 30 | ngx.say("dog: ", (c:get("dog"))) 31 | ngx.say("cat: ", (c:get("cat"))) 32 | 33 | c:delete("dog") 34 | c:delete("cat") 35 | ngx.say("dog: ", (c:get("dog"))) 36 | ngx.say("cat: ", (c:get("cat"))) 37 | '; 38 | } 39 | --- response_body 40 | dog: 32 41 | cat: 56 42 | dog: 32 43 | cat: 56 44 | dog: nil 45 | cat: nil 46 | 47 | 48 | 49 | === TEST 2: evict existing items 50 | --- config 51 | location = /t { 52 | content_by_lua ' 53 | local lrucache = require "resty.lrucache.pureffi" 54 | local c = lrucache.new(2) 55 | if not c then 56 | ngx.say("failed to init lrucace: ", err) 57 | return 58 | end 59 | 60 | c:set("dog", 32) 61 | c:set("cat", 56) 62 | ngx.say("dog: ", (c:get("dog"))) 63 | ngx.say("cat: ", (c:get("cat"))) 64 | 65 | c:set("bird", 76) 66 | ngx.say("dog: ", (c:get("dog"))) 67 | ngx.say("cat: ", (c:get("cat"))) 68 | ngx.say("bird: ", (c:get("bird"))) 69 | '; 70 | } 71 | --- response_body 72 | dog: 32 73 | cat: 56 74 | dog: nil 75 | cat: 56 76 | bird: 76 77 | 78 | 79 | 80 | === TEST 3: evict existing items (reordered, get should also count) 81 | --- config 82 | location = /t { 83 | content_by_lua ' 84 | local lrucache = require "resty.lrucache.pureffi" 85 | local c = lrucache.new(2) 86 | if not c then 87 | ngx.say("failed to init lrucace: ", err) 88 | return 89 | end 90 | 91 | c:set("cat", 56) 92 | c:set("dog", 32) 93 | ngx.say("dog: ", (c:get("dog"))) 94 | ngx.say("cat: ", (c:get("cat"))) 95 | 96 | c:set("bird", 76) 97 | ngx.say("dog: ", (c:get("dog"))) 98 | ngx.say("cat: ", (c:get("cat"))) 99 | ngx.say("bird: ", (c:get("bird"))) 100 | '; 101 | } 102 | --- response_body 103 | dog: 32 104 | cat: 56 105 | dog: nil 106 | cat: 56 107 | bird: 76 108 | 109 | 110 | 111 | === TEST 4: ttl 112 | --- config 113 | location = /t { 114 | content_by_lua ' 115 | local lrucache = require "resty.lrucache.pureffi" 116 | local c = lrucache.new(1) 117 | 118 | c:set("dog", 32, 0.6) 119 | ngx.say("dog: ", (c:get("dog"))) 120 | 121 | ngx.sleep(0.3) 122 | ngx.say("dog: ", (c:get("dog"))) 123 | 124 | ngx.sleep(0.31) 125 | local v, err = c:get("dog") 126 | ngx.say("dog: ", v, " ", err) 127 | '; 128 | } 129 | --- response_body 130 | dog: 32 131 | dog: 32 132 | dog: nil 32 133 | 134 | 135 | 136 | === TEST 5: load factor 137 | --- config 138 | location = /t { 139 | content_by_lua ' 140 | local lrucache = require "resty.lrucache.pureffi" 141 | local c = lrucache.new(1, 0.25) 142 | 143 | ngx.say(c.bucket_sz) 144 | '; 145 | } 146 | --- response_body 147 | 4 148 | 149 | 150 | 151 | === TEST 6: load factor clamped to 0.1 152 | --- config 153 | location = /t { 154 | content_by_lua ' 155 | local lrucache = require "resty.lrucache.pureffi" 156 | local c = lrucache.new(3, 0.05) 157 | 158 | ngx.say(c.bucket_sz) 159 | '; 160 | } 161 | --- response_body 162 | 32 163 | 164 | 165 | 166 | === TEST 7: load factor saturated to 1 167 | --- config 168 | location = /t { 169 | content_by_lua ' 170 | local lrucache = require "resty.lrucache.pureffi" 171 | local c = lrucache.new(3, 2.1) 172 | 173 | ngx.say(c.bucket_sz) 174 | '; 175 | } 176 | --- response_body 177 | 4 178 | 179 | 180 | 181 | === TEST 8: non-string keys 182 | --- config 183 | location = /t { 184 | content_by_lua ' 185 | local function log(...) 186 | ngx.say(...) 187 | end 188 | 189 | local lrucache = require "resty.lrucache.pureffi" 190 | local c = lrucache.new(2) 191 | 192 | collectgarbage() 193 | 194 | local tab1 = {1, 2} 195 | local tab2 = {3, 4} 196 | 197 | c:set(tab1, 32) 198 | c:set(tab2, 56) 199 | log("tab1: ", (c:get(tab1))) 200 | log("tab2: ", (c:get(tab2))) 201 | 202 | c:set(tab1, 32) 203 | c:set(tab2, 56) 204 | log("tab1: ", (c:get(tab1))) 205 | log("tab2: ", (c:get(tab2))) 206 | 207 | c:delete(tab1) 208 | c:delete(tab2) 209 | log("tab1: ", (c:get(tab1))) 210 | log("tab2: ", (c:get(tab2))) 211 | '; 212 | } 213 | --- response_body 214 | tab1: 32 215 | tab2: 56 216 | tab1: 32 217 | tab2: 56 218 | tab1: nil 219 | tab2: nil 220 | 221 | 222 | 223 | === TEST 9: replace value 224 | --- config 225 | location = /t { 226 | content_by_lua ' 227 | local lrucache = require "resty.lrucache.pureffi" 228 | local c = lrucache.new(1) 229 | 230 | c:set("dog", 32) 231 | ngx.say("dog: ", (c:get("dog"))) 232 | 233 | c:set("dog", 33) 234 | ngx.say("dog: ", (c:get("dog"))) 235 | '; 236 | } 237 | --- response_body 238 | dog: 32 239 | dog: 33 240 | 241 | 242 | 243 | === TEST 10: replace value 2 244 | --- config 245 | location = /t { 246 | content_by_lua ' 247 | local lrucache = require "resty.lrucache.pureffi" 248 | local c = lrucache.new(1) 249 | 250 | c:set("dog", 32, 1.0) 251 | ngx.say("dog: ", (c:get("dog"))) 252 | 253 | c:set("dog", 33, 0.3) 254 | ngx.say("dog: ", (c:get("dog"))) 255 | 256 | ngx.sleep(0.4) 257 | local v, err = c:get("dog") 258 | ngx.say("dog: ", v, " ", err) 259 | '; 260 | } 261 | --- response_body 262 | dog: 32 263 | dog: 33 264 | dog: nil 33 265 | 266 | 267 | 268 | === TEST 11: replace value 3 (the old value has longer expire time) 269 | --- config 270 | location = /t { 271 | content_by_lua ' 272 | local lrucache = require "resty.lrucache.pureffi" 273 | local c = lrucache.new(1) 274 | 275 | c:set("dog", 32, 1.2) 276 | c:set("dog", 33, 0.6) 277 | ngx.sleep(0.2) 278 | ngx.say("dog: ", (c:get("dog"))) 279 | 280 | ngx.sleep(0.5) 281 | local v, err = c:get("dog") 282 | ngx.say("dog: ", v, " ", err) 283 | '; 284 | } 285 | --- response_body 286 | dog: 33 287 | dog: nil 33 288 | 289 | 290 | 291 | === TEST 12: replace value 4 292 | --- config 293 | location = /t { 294 | content_by_lua ' 295 | local lrucache = require "resty.lrucache.pureffi" 296 | local c = lrucache.new(1) 297 | 298 | c:set("dog", 32, 0.1) 299 | ngx.sleep(0.2) 300 | 301 | c:set("dog", 33) 302 | ngx.sleep(0.2) 303 | ngx.say("dog: ", (c:get("dog"))) 304 | '; 305 | } 306 | --- response_body 307 | dog: 33 308 | -------------------------------------------------------------------------------- /t/100-pureffi/002-should-store-false.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: should-store-false 15 | --- config 16 | location = /t { 17 | content_by_lua ' 18 | local lrucache = require "resty.lrucache.pureffi" 19 | local c = lrucache.new(2) 20 | 21 | collectgarbage() 22 | 23 | c:set("false-value", false) 24 | ngx.say("false-value: ", (c:get("false-value"))) 25 | 26 | c:delete("false-value") 27 | ngx.say("false-value: ", (c:get("false-value"))) 28 | '; 29 | } 30 | --- response_body 31 | false-value: false 32 | false-value: nil 33 | -------------------------------------------------------------------------------- /t/100-pureffi/003-init-by-lua.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(1); 6 | 7 | plan tests => repeat_each() * (blocks() * 2); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: sanity 15 | --- http_config eval 16 | "$t::TestLRUCache::HttpConfig" 17 | . qq! 18 | init_by_lua ' 19 | local function log(...) 20 | print("[cache] ", ...) 21 | end 22 | 23 | local lrucache = require "resty.lrucache.pureffi" 24 | local c = lrucache.new(2) 25 | 26 | collectgarbage() 27 | 28 | c:set("dog", 32) 29 | c:set("cat", 56) 30 | log("dog: ", (c:get("dog"))) 31 | log("cat: ", (c:get("cat"))) 32 | 33 | c:set("dog", 32) 34 | c:set("cat", 56) 35 | log("dog: ", (c:get("dog"))) 36 | log("cat: ", (c:get("cat"))) 37 | 38 | c:delete("dog") 39 | c:delete("cat") 40 | log("dog: ", (c:get("dog"))) 41 | log("cat: ", (c:get("cat"))) 42 | '; 43 | ! 44 | --- config 45 | location = /t { 46 | return 200; 47 | } 48 | --- ignore_response 49 | --- error_log 50 | --- grep_error_log eval: qr/\[cache\] .*? (?:\d+|nil)/ 51 | --- grep_error_log_out 52 | [cache] dog: 32 53 | [cache] cat: 56 54 | [cache] dog: 32 55 | [cache] cat: 56 56 | [cache] dog: nil 57 | [cache] cat: nil 58 | 59 | 60 | 61 | === TEST 2: sanity 62 | --- http_config eval 63 | "$t::TestLRUCache::HttpConfig" 64 | . qq! 65 | init_by_lua ' 66 | lrucache = require "resty.lrucache.pureffi" 67 | flv_index, err = lrucache.new(200) 68 | if not flv_index then 69 | ngx.log(ngx.ERR, "failed to create the cache: ", err) 70 | return 71 | end 72 | 73 | flv_meta, err = lrucache.new(200) 74 | if not flv_meta then 75 | ngx.log(ngx.ERR, "failed to create the cache: ", err) 76 | return 77 | end 78 | 79 | flv_channel, err = lrucache.new(200) 80 | if not flv_channel then 81 | ngx.log(ngx.ERR, "failed to create the cache: ", err) 82 | return 83 | end 84 | 85 | print("3 lrucache initialized.") 86 | '; 87 | ! 88 | --- config 89 | location = /t { 90 | return 200; 91 | } 92 | --- ignore_response 93 | --- error_log 94 | 3 lrucache initialized. 95 | -------------------------------------------------------------------------------- /t/100-pureffi/004-flush-all.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: flush_all() deletes all keys (cache partial occupied) 15 | --- config 16 | location = /t { 17 | content_by_lua_block { 18 | local lrucache = require "resty.lrucache.pureffi" 19 | 20 | local N = 4 21 | 22 | local c = lrucache.new(N) 23 | 24 | for i = 1, N / 2 do 25 | c:set("key " .. i, i) 26 | end 27 | 28 | c:flush_all() 29 | 30 | for i = 1, N do 31 | local key = "key " .. i 32 | local v = c:get(key) 33 | ngx.say(i, ": ", v) 34 | end 35 | 36 | ngx.say("++") 37 | 38 | for i = 1, N + 1 do 39 | c:set("key " .. i, i) 40 | end 41 | 42 | for i = 1, N + 1 do 43 | ngx.say(i, ": ", (c:get("key " .. i))) 44 | end 45 | } 46 | } 47 | --- response_body 48 | 1: nil 49 | 2: nil 50 | 3: nil 51 | 4: nil 52 | ++ 53 | 1: nil 54 | 2: 2 55 | 3: 3 56 | 4: 4 57 | 5: 5 58 | 59 | 60 | 61 | === TEST 2: flush_all() deletes all keys (cache fully occupied) 62 | --- config 63 | location = /t { 64 | content_by_lua_block { 65 | local lrucache = require "resty.lrucache.pureffi" 66 | 67 | local N = 4 68 | 69 | local c = lrucache.new(N) 70 | 71 | for i = 1, N + 1 do 72 | c:set("key " .. i, i) 73 | end 74 | 75 | ngx.say(c:count()) 76 | c:flush_all() 77 | ngx.say(c:count()) 78 | 79 | for i = 1, N + 1 do 80 | local key = "key " .. i 81 | local v = c:get(key) 82 | ngx.say(i, ": ", v) 83 | end 84 | 85 | ngx.say("++") 86 | 87 | for i = 1, N + 1 do 88 | c:set("key " .. i, i) 89 | end 90 | 91 | for i = 1, N + 1 do 92 | ngx.say(i, ": ", (c:get("key " .. i))) 93 | end 94 | } 95 | } 96 | --- response_body 97 | 4 98 | 0 99 | 1: nil 100 | 2: nil 101 | 3: nil 102 | 4: nil 103 | 5: nil 104 | ++ 105 | 1: nil 106 | 2: 2 107 | 3: 3 108 | 4: 4 109 | 5: 5 110 | 111 | 112 | 113 | === TEST 3: flush_all() flush empty cache store 114 | --- config 115 | location = /t { 116 | content_by_lua_block { 117 | local lrucache = require "resty.lrucache.pureffi" 118 | 119 | local N = 4 120 | 121 | local c = lrucache.new(4) 122 | 123 | c:flush_all() 124 | 125 | for i = 1, N do 126 | local key = "key " .. i 127 | local v = c:get(key) 128 | ngx.say(i, ": ", v) 129 | end 130 | 131 | ngx.say("++") 132 | 133 | for i = 1, N + 1 do 134 | c:set("key " .. i, i) 135 | end 136 | 137 | for i = 1, N + 1 do 138 | ngx.say(i, ": ", (c:get("key " .. i))) 139 | end 140 | } 141 | } 142 | --- response_body 143 | 1: nil 144 | 2: nil 145 | 3: nil 146 | 4: nil 147 | ++ 148 | 1: nil 149 | 2: 2 150 | 3: 3 151 | 4: 4 152 | 5: 5 153 | -------------------------------------------------------------------------------- /t/100-pureffi/005-capacity.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: capacity() returns total cache capacity 15 | --- config 16 | location = /t { 17 | content_by_lua_block { 18 | local lrucache = require "resty.lrucache.pureffi" 19 | local c = lrucache.new(2) 20 | 21 | ngx.say("capacity: ", c:capacity()) 22 | } 23 | } 24 | --- response_body 25 | capacity: 2 26 | -------------------------------------------------------------------------------- /t/100-pureffi/006-count.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: count() returns current cache size 15 | --- config 16 | location = /t { 17 | content_by_lua_block { 18 | local lrucache = require "resty.lrucache.pureffi" 19 | local c = lrucache.new(2) 20 | 21 | ngx.say("count: ", c:count()) 22 | 23 | c:set("dog", 32) 24 | ngx.say("count: ", c:count()) 25 | c:set("dog", 33) 26 | 27 | ngx.say("count: ", c:count()) 28 | c:set("cat", 33) 29 | 30 | ngx.say("count: ", c:count()) 31 | c:set("pig", 33) 32 | 33 | ngx.say("count: ", c:count()) 34 | c:delete("dog") 35 | 36 | ngx.say("count: ", c:count()) 37 | c:delete("pig") 38 | 39 | ngx.say("count: ", c:count()) 40 | c:delete("cat") 41 | 42 | ngx.say("count: ", c:count()) 43 | } 44 | } 45 | --- response_body 46 | count: 0 47 | count: 1 48 | count: 1 49 | count: 2 50 | count: 2 51 | count: 2 52 | count: 1 53 | count: 0 54 | -------------------------------------------------------------------------------- /t/100-pureffi/007-get-keys.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: get_keys() with some keys 15 | --- config 16 | location = /t { 17 | content_by_lua_block { 18 | local lrucache = require "resty.lrucache.pureffi" 19 | local c = lrucache.new(100) 20 | 21 | c:set("hello", true) 22 | c:set("world", false) 23 | 24 | local keys = c:get_keys() 25 | 26 | ngx.say("size: ", #keys) 27 | 28 | for i = 1, #keys do 29 | ngx.say(keys[i]) 30 | end 31 | } 32 | } 33 | --- response_body 34 | size: 2 35 | world 36 | hello 37 | 38 | 39 | 40 | === TEST 2: get_keys() with no keys 41 | --- config 42 | location = /t { 43 | content_by_lua_block { 44 | local lrucache = require "resty.lrucache.pureffi" 45 | local c = lrucache.new(100) 46 | 47 | local keys = c:get_keys() 48 | 49 | ngx.say("size: ", #keys) 50 | 51 | for i = 1, #keys do 52 | ngx.say(keys[i]) 53 | end 54 | } 55 | } 56 | --- response_body 57 | size: 0 58 | 59 | 60 | 61 | === TEST 3: get_keys() with full cache 62 | --- config 63 | location = /t { 64 | content_by_lua_block { 65 | local lrucache = require "resty.lrucache.pureffi" 66 | local c = lrucache.new(100) 67 | 68 | for i = 1, 100 do 69 | c:set("key-" .. i, true) 70 | end 71 | 72 | c:set("extra-key", true) 73 | 74 | local keys = c:get_keys() 75 | 76 | ngx.say("size: ", #keys) 77 | ngx.say("MRU: ", keys[1]) 78 | ngx.say("LRU: ", keys[#keys]) 79 | } 80 | } 81 | --- response_body 82 | size: 100 83 | MRU: extra-key 84 | LRU: key-2 85 | 86 | 87 | 88 | === TEST 4: get_keys() max_count = 5 89 | --- config 90 | location = /t { 91 | content_by_lua_block { 92 | local lrucache = require "resty.lrucache.pureffi" 93 | local c = lrucache.new(100) 94 | 95 | for i = 1, 100 do 96 | c:set("key-" .. i, true) 97 | end 98 | 99 | local keys = c:get_keys(5) 100 | 101 | ngx.say("size: ", #keys) 102 | ngx.say("MRU: ", keys[1]) 103 | ngx.say("latest: ", keys[#keys]) 104 | } 105 | } 106 | --- response_body 107 | size: 5 108 | MRU: key-100 109 | latest: key-96 110 | 111 | 112 | 113 | === TEST 5: get_keys() max_count = 0 disables max returns 114 | --- config 115 | location = /t { 116 | content_by_lua_block { 117 | local lrucache = require "resty.lrucache.pureffi" 118 | local c = lrucache.new(100) 119 | 120 | for i = 1, 100 do 121 | c:set("key-" .. i, true) 122 | end 123 | 124 | local keys = c:get_keys(0) 125 | 126 | ngx.say("size: ", #keys) 127 | ngx.say("MRU: ", keys[1]) 128 | ngx.say("LRU: ", keys[#keys]) 129 | } 130 | } 131 | --- response_body 132 | size: 100 133 | MRU: key-100 134 | LRU: key-1 135 | 136 | 137 | 138 | === TEST 6: get_keys() user-fed res table 139 | --- config 140 | location = /t { 141 | content_by_lua_block { 142 | local lrucache = require "resty.lrucache.pureffi" 143 | local c1 = lrucache.new(3) 144 | local c2 = lrucache.new(2) 145 | 146 | for i = 1, 3 do 147 | c1:set("c1-" .. i, true) 148 | end 149 | 150 | for i = 1, 2 do 151 | c2:set("c2-" .. i, true) 152 | end 153 | 154 | local res = {} 155 | 156 | local keys_1 = c1:get_keys(0, res) 157 | ngx.say("res is user-fed: ", keys_1 == res) 158 | 159 | for _, k in ipairs(keys_1) do 160 | ngx.say(k) 161 | end 162 | 163 | res = {} 164 | 165 | local keys_2 = c2:get_keys(0, res) 166 | 167 | for _, k in ipairs(keys_2) do 168 | ngx.say(k) 169 | end 170 | } 171 | } 172 | --- response_body 173 | res is user-fed: true 174 | c1-3 175 | c1-2 176 | c1-1 177 | c2-2 178 | c2-1 179 | 180 | 181 | 182 | === TEST 7: get_keys() user-fed res table + max_count 183 | --- config 184 | location = /t { 185 | content_by_lua_block { 186 | local lrucache = require "resty.lrucache.pureffi" 187 | local c1 = lrucache.new(3) 188 | 189 | for i = 1, 3 do 190 | c1:set("key-" .. i, true) 191 | end 192 | 193 | local res = {} 194 | 195 | local keys = c1:get_keys(2, res) 196 | 197 | for _, k in ipairs(keys) do 198 | ngx.say(k) 199 | end 200 | } 201 | } 202 | --- response_body 203 | key-3 204 | key-2 205 | 206 | 207 | 208 | === TEST 8: get_keys() user-fed res table gets a trailing hole 209 | --- config 210 | location = /t { 211 | content_by_lua_block { 212 | local lrucache = require "resty.lrucache.pureffi" 213 | local c1 = lrucache.new(3) 214 | 215 | for i = 1, 3 do 216 | c1:set("key-" .. i, true) 217 | end 218 | 219 | local res = {} 220 | 221 | for i = 1, 10 do 222 | res[i] = true 223 | end 224 | 225 | local keys = c1:get_keys(2, res) 226 | 227 | for _, k in ipairs(keys) do 228 | ngx.say(k) 229 | end 230 | } 231 | } 232 | --- response_body 233 | key-3 234 | key-2 235 | -------------------------------------------------------------------------------- /t/100-pureffi/008-user-flags.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: no user flags by default 15 | --- config 16 | location = /t { 17 | content_by_lua_block { 18 | local lrucache = require "resty.lrucache.pureffi" 19 | local c = lrucache.new(2) 20 | 21 | c:set("dog", 32) 22 | c:set("cat", 56) 23 | 24 | local v, err, flags = c:get("dog") 25 | ngx.say("dog: ", v, " ", err, " ", flags) 26 | 27 | local v, err, flags = c:get("cat") 28 | ngx.say("cat: ", v, " ", err, " ", flags) 29 | } 30 | } 31 | --- response_body 32 | dog: 32 nil 0 33 | cat: 56 nil 0 34 | 35 | 36 | 37 | === TEST 2: stores user flags if specified 38 | --- config 39 | location = /t { 40 | content_by_lua_block { 41 | local lrucache = require "resty.lrucache.pureffi" 42 | local c = lrucache.new(2) 43 | 44 | c:set("dog", 32, nil, 0x01) 45 | c:set("cat", 56, nil, 0x02) 46 | 47 | local v, err, flags = c:get("dog") 48 | ngx.say("dog: ", v, " ", err, " ", flags) 49 | 50 | local v, err, flags = c:get("cat") 51 | ngx.say("cat: ", v, " ", err, " ", flags) 52 | } 53 | } 54 | --- response_body 55 | dog: 32 nil 1 56 | cat: 56 nil 2 57 | 58 | 59 | 60 | === TEST 3: user flags cannot be negative 61 | --- config 62 | location = /t { 63 | content_by_lua_block { 64 | local lrucache = require "resty.lrucache.pureffi" 65 | local c = lrucache.new(3) 66 | 67 | c:set("dog", 32, nil, 0) 68 | c:set("cat", 56, nil, -1) 69 | 70 | local v, err, flags = c:get("dog") 71 | ngx.say("dog: ", v, " ", err, " ", flags) 72 | 73 | local v, err, flags = c:get("cat") 74 | ngx.say("cat: ", v, " ", err, " ", flags) 75 | } 76 | } 77 | --- response_body 78 | dog: 32 nil 0 79 | cat: 56 nil 0 80 | 81 | 82 | 83 | === TEST 4: user flags not number is ignored 84 | --- config 85 | location = /t { 86 | content_by_lua_block { 87 | local lrucache = require "resty.lrucache.pureffi" 88 | local c = lrucache.new(2) 89 | 90 | c:set("dog", 32, nil, "") 91 | 92 | local v, err, flags = c:get("dog") 93 | ngx.say(v, " ", err, " ", flags) 94 | } 95 | } 96 | --- response_body 97 | 32 nil 0 98 | 99 | 100 | 101 | === TEST 5: all nodes from double-ended queue have flags 102 | --- config 103 | location = /t { 104 | content_by_lua_block { 105 | local len = 10 106 | 107 | local lrucache = require "resty.lrucache.pureffi" 108 | local c = lrucache.new(len) 109 | 110 | for i = 1, len do 111 | c:set(i, 32, nil, 1) 112 | end 113 | 114 | for i = 1, len do 115 | local v, _, flags = c:get(i) 116 | if not flags then 117 | ngx.say("item ", i, " does not have flags") 118 | return 119 | end 120 | end 121 | 122 | ngx.say("ok") 123 | } 124 | } 125 | --- response_body 126 | ok 127 | 128 | 129 | 130 | === TEST 6: user flags are preserved when item is stale 131 | --- config 132 | location = /t { 133 | content_by_lua_block { 134 | local lrucache = require "resty.lrucache.pureffi" 135 | local c = lrucache.new(1) 136 | 137 | c:set("dogs", 32, 0.2, 3) 138 | ngx.sleep(0.21) 139 | 140 | local v, err, flags = c:get("dogs") 141 | ngx.say(v, " ", err, " ", flags) 142 | } 143 | } 144 | --- response_body 145 | nil 32 3 146 | 147 | 148 | 149 | === TEST 7: user flags are not preserved upon eviction 150 | --- config 151 | location = /t { 152 | content_by_lua_block { 153 | local lrucache = require "resty.lrucache.pureffi" 154 | local c = lrucache.new(1) 155 | 156 | for i = 1, 10 do 157 | local flags = i % 2 == 0 and i 158 | c:set(i, true, nil, flags) 159 | 160 | local v, err, flags = c:get(i) 161 | ngx.say(v, " ", err, " ", flags) 162 | end 163 | } 164 | } 165 | --- response_body 166 | true nil 0 167 | true nil 2 168 | true nil 0 169 | true nil 4 170 | true nil 0 171 | true nil 6 172 | true nil 0 173 | true nil 8 174 | true nil 0 175 | true nil 10 176 | -------------------------------------------------------------------------------- /t/101-mixed.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | use lib '.'; 3 | use t::TestLRUCache; 4 | 5 | repeat_each(2); 6 | 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | no_long_string(); 10 | run_tests(); 11 | 12 | __DATA__ 13 | 14 | === TEST 1: sanity 15 | --- config 16 | location = /t { 17 | content_by_lua ' 18 | local lrucache = require "resty.lrucache" 19 | local c = lrucache.new(2) 20 | 21 | collectgarbage() 22 | 23 | c:set("dog", 32) 24 | c:set("cat", 56) 25 | ngx.say("dog: ", (c:get("dog"))) 26 | ngx.say("cat: ", (c:get("cat"))) 27 | 28 | local lrucache = require "resty.lrucache.pureffi" 29 | local c2 = lrucache.new(2) 30 | 31 | ngx.say("dog: ", (c2:get("dog"))) 32 | ngx.say("cat: ", (c2:get("cat"))) 33 | 34 | c2:set("dog", 9) 35 | c2:set("cat", "hi") 36 | 37 | ngx.say("dog: ", (c2:get("dog"))) 38 | ngx.say("cat: ", (c2:get("cat"))) 39 | 40 | ngx.say("dog: ", (c:get("dog"))) 41 | ngx.say("cat: ", (c:get("cat"))) 42 | '; 43 | } 44 | --- response_body 45 | dog: 32 46 | cat: 56 47 | dog: nil 48 | cat: nil 49 | dog: 9 50 | cat: hi 51 | dog: 32 52 | cat: 56 53 | -------------------------------------------------------------------------------- /t/TestLRUCache.pm: -------------------------------------------------------------------------------- 1 | package t::TestLRUCache; 2 | 3 | use Test::Nginx::Socket::Lua -Base; 4 | use Cwd qw(cwd); 5 | 6 | $ENV{TEST_NGINX_HOTLOOP} ||= 10; 7 | 8 | our $pwd = cwd(); 9 | 10 | our $lua_package_path = './lib/?.lua;;'; 11 | 12 | our $HttpConfig = <<_EOC_; 13 | lua_package_path '$lua_package_path'; 14 | _EOC_ 15 | 16 | our @EXPORT = qw( 17 | $pwd 18 | $lua_package_path 19 | $HttpConfig 20 | ); 21 | 22 | add_block_preprocessor(sub { 23 | my $block = shift; 24 | 25 | if (!defined $block->http_config) { 26 | $block->set_value("http_config", $HttpConfig); 27 | } 28 | 29 | if (!defined $block->request) { 30 | $block->set_value("request", "GET /t"); 31 | } 32 | 33 | if (!defined $block->no_error_log) { 34 | $block->set_value("no_error_log", "[error]"); 35 | } 36 | }); 37 | 38 | 1; 39 | -------------------------------------------------------------------------------- /valgrind.suppress: -------------------------------------------------------------------------------- 1 | { 2 | 3 | Memcheck:Leak 4 | fun:malloc 5 | fun:ngx_alloc 6 | fun:ngx_event_process_init 7 | } 8 | { 9 | 10 | Memcheck:Param 11 | epoll_ctl(event) 12 | fun:epoll_ctl 13 | fun:ngx_epoll_add_event 14 | } 15 | { 16 | 17 | Memcheck:Cond 18 | fun:index 19 | fun:expand_dynamic_string_token 20 | fun:_dl_map_object 21 | fun:map_doit 22 | fun:_dl_catch_error 23 | fun:do_preload 24 | fun:dl_main 25 | fun:_dl_sysdep_start 26 | fun:_dl_start 27 | } 28 | { 29 | 30 | Memcheck:Param 31 | epoll_ctl(event) 32 | fun:epoll_ctl 33 | fun:ngx_epoll_init 34 | fun:ngx_event_process_init 35 | } 36 | { 37 | 38 | Memcheck:Param 39 | epoll_ctl(event) 40 | fun:epoll_ctl 41 | fun:ngx_epoll_notify_init 42 | fun:ngx_epoll_init 43 | fun:ngx_event_process_init 44 | } 45 | { 46 | 47 | Memcheck:Param 48 | epoll_ctl(event) 49 | fun:epoll_ctl 50 | fun:ngx_epoll_test_rdhup 51 | } 52 | { 53 | 54 | Memcheck:Leak 55 | match-leak-kinds: definite 56 | fun:malloc 57 | fun:ngx_alloc 58 | fun:ngx_set_environment 59 | fun:ngx_single_process_cycle 60 | } 61 | { 62 | 63 | Memcheck:Leak 64 | match-leak-kinds: definite 65 | fun:malloc 66 | fun:ngx_alloc 67 | fun:ngx_set_environment 68 | fun:ngx_worker_process_init 69 | fun:ngx_worker_process_cycle 70 | } 71 | { 72 | 73 | Memcheck:Leak 74 | match-leak-kinds: definite 75 | fun:malloc 76 | fun:ngx_alloc 77 | fun:ngx_set_environment 78 | fun:ngx_http_lua_post_init_handler 79 | fun:ngx_cycle_post_init 80 | fun:main 81 | } 82 | --------------------------------------------------------------------------------