From 83a6757f33028e66e0d02bb7e7877ee8e955d8c2 Mon Sep 17 00:00:00 2001 From: Shuxin Yang Date: Mon, 22 Dec 2014 15:16:47 -0800 Subject: [PATCH 1/3] add comment to pureffi.lua --- lib/resty/lrucache/pureffi.lua | 157 ++++++++++++++++++++++++++------- 1 file changed, 127 insertions(+), 30 deletions(-) diff --git a/lib/resty/lrucache/pureffi.lua b/lib/resty/lrucache/pureffi.lua index 48bcea2..8529600 100644 --- a/lib/resty/lrucache/pureffi.lua +++ b/lib/resty/lrucache/pureffi.lua @@ -1,6 +1,60 @@ -- Copyright (C) Yichun Zhang (agentzh) -- Copyright (C) Shuxin Yang +--[[ + This module implement a key/value cache store. We adopt LRU as our +replace/evict policy. Each key/value pair is tagged with a Time-to-Live (TTL); +from user's perspective, stale pairs are automatically removed from the cache. + +Why FFI +------- + In Lua, expression "table[key] = nil" does not *PHYSICALLY* remove the value +associated with the key; it just set the value to be nil! So the table will +keep growing with large number of the key/nil pairs which will be purged until +resize() operator is called. + + This "feature" is terribly ill-suited to what we need. Therefore we have to +rely on FFI to build a hash-table where any entry can be physically deleted +immediately. + +Under the hood: +-------------- + In concept, we introduce three data structures to implement the cache store: + 1. key/value vector for storing keys and values. + 2. a queue to mimic the LRU. + 3. hash-table for looking up the value for a given key. + + Unfortunately, efficiency and clarity usually come at each other cost. The +data strucutres we are using are slightly more complicated than what we +described above. + + o. Lua does not have efficient way to store a vector of pair. So, we use + two vectors for key/value pair: one for keys and the other for values + (_M.key_v and _M.val_v, respectively), and i-th key corresponds to + i-th value. + + A key/value pair is identified by the "id" field in a "node" (we shall + discuss node later) + + o. The queue is nothing more than a doubly-linked list of "node" linked via + lrucache_pureffi_queue_s::{next|prev} fields. + + o. The hash-table has two parts: + - the _M.bucket_v[] a vector of bucket, indiced by hash-value, and + - a bucket is a singly-linked list of "node" via the + lrucache_pureffi_queue_s::conflict field. + + A key must be a string, and the hash value of a key is evaluated by: + crc32(key) % size(_M.bucket_v). We mandate size(_M.bucket_v) being a + power-of-two in order to avoid expensive modulo operation. + + At the heart of the module is an array of "node" (of type + lrucache_pureffi_queue_s). A node: + - keeps the meta-data of its corresponding key/value pair + (embodied by the "id", and "expire" field); + - is a part of LRU queue (embodied by "prev" and "next" fields); + - is a part of hash-table (mbodied by the "conflict" field). +]] local ffi = require "ffi" local bit = require "bit" @@ -82,12 +136,21 @@ end -- structure. ffi.cdef[[ + /* A lrucache_pureffi_queue_s node hook together three data structures: + * o. the key/value store as embodied by the "id" (which is in essence the + * indentifier of key/pair pair) and the "expire" (which is a metadata + * of the corresponding key/pair pair). + * o. The LRU queue via the prev/next fields. + * o. The hash-tabble as embodied by the "conflict" field. + */ typedef struct lrucache_pureffi_queue_s lrucache_pureffi_queue_t; struct lrucache_pureffi_queue_s { /* Each node is assigned a unique ID at construction time, and the * ID remain immutatble, regardless the node is in active-list or * free-list. The queue header is assigned ID 0. Since queue-header * is a sentinel node, 0 denodes "invalid ID". + * + * Intuitively, we can the "id" as the identifier of key/value pair. */ int id; @@ -109,8 +172,13 @@ local queue_type = ffi.typeof("lrucache_pureffi_queue_t") local NULL = ffi.null --- queue utility functions +--======================================================================== +-- +-- Queue utility functions +-- +--======================================================================== +-- Append the element "x" to the given queue "h". local function queue_insert_tail(h, x) local last = h[0].prev x.prev = last @@ -120,6 +188,11 @@ local function queue_insert_tail(h, x) end +--[[ +Allocate a queue with size + 1 elements. Elements are linked together in a +circular way, i.e. the last element's "next" points to the first element, +while the first element's "prev" element points to the last element. +]] local function queue_init(size) if not size then size = 0 @@ -169,6 +242,7 @@ local function queue_remove(x) end +-- Insert the element "x" the to the given queue "h" local function queue_insert_head(h, x) x.next = h[0].next x.next.prev = x @@ -187,7 +261,40 @@ local function queue_head(h) end --- true module stuffs +--======================================================================== +-- +-- Miscellaneous Utility Functions +-- +--======================================================================== + +local function ptr2num(ptr) + return tonumber(ffi_cast(uintptr_t, ptr)) +end + +local function crc32_ptr(ptr) + local crc32 = 0; + + local p = brshift(ptr2num(ptr), 3) + local b = band(p, 255) + crc32 = crc_tab[b] + + b = band(brshift(p, 8), 255) + crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) + + b = band(brshift(p, 16), 255) + crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) + + --b = band(brshift(p, 24), 255) + --crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) + return crc32 +end + + +--======================================================================== +-- +-- Implementation of "export" functions +-- +--======================================================================== local _M = { _VERSION = '0.03' @@ -195,11 +302,6 @@ local _M = { local mt = { __index = _M } -local function ptr2num(ptr) - return tonumber(ffi_cast(uintptr_t, ptr)) -end - - -- "size" specifies the maximum number of entries in the LRU queue, and the -- "load_factor" designates the 'load factor' of the hash-table we are using -- internally. The default value of load-factor is 0.5 (i.e. 50%); if the @@ -222,6 +324,7 @@ function _M.new(size, load_factor) end local bs_min = size / load_f + -- the bucket_sz *MUST* be a power-of-two. See the hash_string(). local bucket_sz = 1 repeat bucket_sz = bucket_sz * 2 @@ -251,25 +354,6 @@ function _M.new(size, load_factor) end -local function crc32_ptr(ptr) - local crc32 = 0; - - local p = brshift(ptr2num(ptr), 3) - local b = band(p, 255) - crc32 = crc_tab[b] - - b = band(brshift(p, 8), 255) - crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) - - b = band(brshift(p, 16), 255) - crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) - - --b = band(brshift(p, 24), 255) - --crc32 = bxor(brshift(crc32, 8), crc_tab[band(bxor(crc32, b), 255)]) - return crc32 -end - - local function hash_string(self, str) local c_str = ffi_cast(c_str_t, str) @@ -279,6 +363,7 @@ local function hash_string(self, str) return hv end + -- Search the node associated with the key in the bucket, if found returns -- the the id of the node, and the id of its previous node in the conflict list. -- The "bucket_hdr_id" is the ID of the first node in the bucket @@ -299,6 +384,7 @@ local function _find_node_in_bucket(key, key_v, node_v, bucket_hdr_id) end +-- Return the node corresponding to the key/val. local function find_key(self, key) local key_hash = hash_string(self, key) return _find_node_in_bucket(key, self.key_v, self.node_v, @@ -306,8 +392,15 @@ local function find_key(self, key) end --- Return the index of the node associated with the key, or nil if the node --- was not found. +--[[ This function tries to + 1. Remove the given key and the associated value from the key/value store, + 2. Remove the entry associated with the key from the hash-table. + + NOTE: all queues remain intact. + + If there was a node bound to the key/val, return that node; otherwise, + nil is returned. +]] local function remove_key(self, key) local key_v = self.key_v local val_v = self.val_v @@ -323,7 +416,7 @@ local function remove_key(self, key) key_v[cur] = nil val_v[cur] = nil - -- Remote the node from the hash table + -- Remove the node from the hash table local next_node = node_v[cur].conflict if prev ~= 0 then node_v[prev].conflict = next_node @@ -337,12 +430,16 @@ local function remove_key(self, key) end +--[[ Bind the key/val with the given node, and insert the node into the Hashtab. + NOTE: this function does not touch any queue +]] local function insert_key(self, key, val, node) + -- Bind the key/val with the node local node_id = node.id self.key_v[node_id] = key self.val_v[node_id] = val - -- maintain the hash table + -- Insert the node into the hash-table local key_hash = hash_string(self, key) local bucket_v = self.bucket_v node.conflict = bucket_v[key_hash] From 6d8e2f40ce6bdd2773c70255613ec3af4c1be9e1 Mon Sep 17 00:00:00 2001 From: Shuxin Yang Date: Mon, 22 Dec 2014 16:06:37 -0800 Subject: [PATCH 2/3] fix typos --- lib/resty/lrucache/pureffi.lua | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/resty/lrucache/pureffi.lua b/lib/resty/lrucache/pureffi.lua index 8529600..aa66a8c 100644 --- a/lib/resty/lrucache/pureffi.lua +++ b/lib/resty/lrucache/pureffi.lua @@ -2,7 +2,7 @@ -- Copyright (C) Shuxin Yang --[[ - This module implement a key/value cache store. We adopt LRU as our + This module implements a key/value cache store. We adopt LRU as our replace/evict policy. Each key/value pair is tagged with a Time-to-Live (TTL); from user's perspective, stale pairs are automatically removed from the cache. @@ -45,15 +45,16 @@ described above. lrucache_pureffi_queue_s::conflict field. A key must be a string, and the hash value of a key is evaluated by: - crc32(key) % size(_M.bucket_v). We mandate size(_M.bucket_v) being a - power-of-two in order to avoid expensive modulo operation. + crc32(key-cast-to-pointer) % size(_M.bucket_v). + We mandate size(_M.bucket_v) being a power-of-two in order to avoid + expensive modulo operation. At the heart of the module is an array of "node" (of type lrucache_pureffi_queue_s). A node: - keeps the meta-data of its corresponding key/value pair (embodied by the "id", and "expire" field); - is a part of LRU queue (embodied by "prev" and "next" fields); - - is a part of hash-table (mbodied by the "conflict" field). + - is a part of hash-table (embodied by the "conflict" field). ]] local ffi = require "ffi" @@ -150,7 +151,8 @@ ffi.cdef[[ * free-list. The queue header is assigned ID 0. Since queue-header * is a sentinel node, 0 denodes "invalid ID". * - * Intuitively, we can the "id" as the identifier of key/value pair. + * Intuitively, we can view the "id" as the identifier of key/value + * pair. */ int id; @@ -271,6 +273,7 @@ local function ptr2num(ptr) return tonumber(ffi_cast(uintptr_t, ptr)) end + local function crc32_ptr(ptr) local crc32 = 0; @@ -313,7 +316,7 @@ function _M.new(size, load_factor) return nil, "size too small" end - -- determine bucket size, which must be power of two + -- Determine bucket size, which must be power of two. local load_f = load_factor if not load_factor then load_f = 0.5 @@ -324,7 +327,7 @@ function _M.new(size, load_factor) end local bs_min = size / load_f - -- the bucket_sz *MUST* be a power-of-two. See the hash_string(). + -- The bucket_sz *MUST* be a power-of-two. See the hash_string(). local bucket_sz = 1 repeat bucket_sz = bucket_sz * 2 @@ -344,7 +347,7 @@ function _M.new(size, load_factor) -- node_v[i] evaluates to the element of ID "i". self.node_v = self.free_queue - -- allocate the array-part of the key_v, val_v, bucket_v. + -- Allocate the array-part of the key_v, val_v, bucket_v. local key_v = self.key_v local val_v = self.val_v local bucket_v = self.bucket_v @@ -359,7 +362,7 @@ local function hash_string(self, str) local hv = crc32_ptr(c_str) hv = band(hv, self.bucket_sz - 1) - -- hint: bucket is 0-based + -- Hint: bucket is 0-based return hv end From 592319342bd4ba428c3f2e8fa8a555d57ca7a252 Mon Sep 17 00:00:00 2001 From: Shuxin Yang Date: Wed, 7 Jan 2015 11:15:46 -0800 Subject: [PATCH 3/3] Stress testing by comparing lru-cache and lru-cache-ffi side by side. Both caches are fed with same keys randomly generated by extract some words from plain text files. The values associated with the keys are integers which starts from 0, and increased by 1 each time a set() operator is called. The stress-test can easily expose the bug I fixed not a while ago. --- Makefile | 7 ++++- stress_test/Makefile | 56 ++++++++++++++++++++++++++++++++++++ stress_test/key.py | 30 ++++++++++++++++++++ stress_test/test.lua | 67 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 stress_test/Makefile create mode 100755 stress_test/key.py create mode 100644 stress_test/test.lua diff --git a/Makefile b/Makefile index 938949d..5d2d27f 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ LUA_INCLUDE_DIR ?= $(PREFIX)/include LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) INSTALL ?= install -.PHONY: all test install +.PHONY: all test install stress_test clean all: ; @@ -17,3 +17,8 @@ install: all test: all PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r t +stress_test: + make -C stress_test ITERATOR=8 + +clean: + make -C stress_test clean diff --git a/stress_test/Makefile b/stress_test/Makefile new file mode 100644 index 0000000..9b98f3e --- /dev/null +++ b/stress_test/Makefile @@ -0,0 +1,56 @@ +.PHONY: all clean +default : all + +TARGZ_INPUTS := http://nginx.org/download/nginx-1.7.9.tar.gz \ + https://github.com/cloudflare/lua-aho-corasick/archive/46f1a4146b8b9021b8df25a39572d1242ece899d.tar.gz + +LOCALFILES := $(foreach f, $(TARGZ_INPUTS), $(call notdir, $f)) +KEYFILES := $(patsubst %.tar.gz, %.key, $(LOCALFILES)) +TMP_FILE := a.tmp +TMP_DIR := tmp + +ITERATOR := 4 +LUAJIT := luajit + +###################################################################### +# +# Download *.tar.gz files from Internet +# +###################################################################### +# +define DOWNLOAD +$$(notdir $(1)) : + wget $(1) -O $$@ +endef +$(foreach k,$(TARGZ_INPUTS),$(eval $(call DOWNLOAD,$(k)))) + +###################################################################### +# +# generate files of keys from ther *.tar.gz downloaded from Internet +# +###################################################################### +# +define GEN_KEY +$$(patsubst %.tar.gz, %.key, $(1)): $(1) + rm -rf $(TMP_DIR); \ + mkdir $(TMP_DIR); \ + tar -C $(TMP_DIR) -zxvf $(1); \ + find $(TMP_DIR) -type f -exec cat {} \; > $(TMP_FILE); \ + ./key.py $(TMP_FILE) > $$@ + rm -rf $(TMP_DIR) $(TMP_FILE) +endef +$(foreach k,$(LOCALFILES),$(eval $(call GEN_KEY,$(k)))) + +all: + @for f in $(KEYFILES); do \ + echo "Testing with $${f}..."; \ + for i in `seq $(ITERATOR)`; do \ + echo "Iteration $${i}"; \ + rm -f $${f}; \ + $(MAKE) $${f} > /dev/null; \ + $(LUAJIT) ./test.lua $${f}; \ + done; \ + done + +clean: + rm -rf $(LOCALFILES) $(KEYFILES) $(TMP_FILE) $(TMP_DIR) diff --git a/stress_test/key.py b/stress_test/key.py new file mode 100755 index 0000000..e8913da --- /dev/null +++ b/stress_test/key.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import random +import fileinput +import sys +import os + +def print_usage(prog): + print ("%s [list of files]" % prog) + return + +if len(sys.argv) == 1: + print_usage(sys.argv[0]) + sys.exit(1) + +for f in sys.argv[1:]: + if not os.access(f, os.R_OK): + print "file '%s' is not readable" % f + sys.exit(1) + +for line in fileinput.input(): + columns = line.split() + cn = len(columns) + if cn > 0: + col_idx = int(random.random() * cn + 0.5) + if col_idx == cn: + col_idx = 0 + print columns[col_idx] + +sys.exit(0) diff --git a/stress_test/test.lua b/stress_test/test.lua new file mode 100644 index 0000000..c89450e --- /dev/null +++ b/stress_test/test.lua @@ -0,0 +1,67 @@ +local argv = {...} +if #argv ~= 1 then + print ("Usage test.lua key-file") + os.exit(1) +end + +local keyfile = io.open(argv[1]) +if not keyfile then + io.write(string.format("Fail to open key file '%s'\n", argv[1])) + os.exit(1) +end + +ngx = {} +ngx.new = function() return os.time() end + +package.path = "../lib/resty/?.lua;../lib/resty/lrucache/?.lua;" .. package.path +lruffi = require "pureffi" +lru = require "lrucache" + +local key_num = 128 +local lru_inst = lru.new(key_num) +local lruffi_inst = lruffi.new(key_num, 0.5) + +local key_vect = {} +local key_idx = 0 +local key_cnt = 0 + +local function compare() + for i = 1, key_idx do + local key = key_vect[i] + local val1 = lru_inst:get(key) + local val2 = lruffi_inst:get(key) + -- print(key, val1, val2) + if val1 ~= val2 then + io.write( + string.format("disagree on key '%s', values are %d vs %d\n", + key, val1, val2)) + os.exit(1) + end + end +end + +local function main() + for line in keyfile:lines() do + + lru_inst:set(line, key_cnt) + lruffi_inst:set(line, key_cnt) + + key_cnt = key_cnt + 1 + key_idx = key_idx + 1 + key_vect[key_idx] = line + + if key_idx == key_num then + compare() + for i = 1, key_idx do + key_vect[i] = nil + end + key_idx = 0 + end + end + + compare() +end + +main() + +os.exit(0)