├── .gitattributes ├── .gitignore ├── dist.ini ├── Makefile ├── .travis.yml ├── util └── lua-releng ├── README.markdown ├── t └── sanity.t └── lib └── resty └── sync.lua /.gitattributes: -------------------------------------------------------------------------------- 1 | *.t linguist-language=Text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | t/servroot/* 2 | go-test 3 | .* 4 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name = lua-resty-sync 2 | abstract = Synchronizing data based on version changes 3 | version = 0.06 4 | author = Alex Zhang(张超) zchao1995@gmail.com, UPYUN Inc. 5 | is_original = yes 6 | license = 2bsd 7 | lib_dir = lib 8 | repo_link = https://github.com/upyun/lua-resty-sync 9 | requires = openresty/lua-resty-lock 10 | -------------------------------------------------------------------------------- /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 9 | 10 | all: ; 11 | 12 | install: all 13 | $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/resty/ 14 | $(INSTALL) lib/resty/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/resty/ 15 | 16 | test: all 17 | util/lua-releng 18 | sudo cp lib/resty/*.lua $(OPENRESTY_PREFIX)/lualib/resty 19 | PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r -v t/ 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: perl 2 | perl: 3 | - 5.10 4 | cache: 5 | - apt 6 | - ccache 7 | services: 8 | - redis-server 9 | env: 10 | - V_OPENRESTY=1.9.15.1 11 | install: 12 | - cpanm -v --notest Test::Nginx 13 | before_script: 14 | - sudo apt-get update -q 15 | - sudo apt-get install libreadline-dev libncurses5-dev libpcre3-dev libssl-dev -y 16 | - sudo apt-get install make build-essential lua5.1 -y 17 | - wget https://openresty.org/download/openresty-$V_OPENRESTY.tar.gz 18 | - tar xzf openresty-$V_OPENRESTY.tar.gz 19 | - cd openresty-$V_OPENRESTY && ./configure && make && sudo make install && cd .. 20 | script: 21 | - make test 22 | -------------------------------------------------------------------------------- /util/lua-releng: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | sub file_contains ($$); 7 | 8 | my $version; 9 | for my $file (map glob, qw{ *.lua lib/*.lua lib/*/*.lua lib/*/*/*.lua lib/*/*/*/*.lua lib/*/*/*/*/*.lua }) { 10 | # Check the sanity of each .lua file 11 | open my $in, $file or 12 | die "ERROR: Can't open $file for reading: $!\n"; 13 | my $found_ver; 14 | while (<$in>) { 15 | my ($ver, $skipping); 16 | if (/(?x) (?:_VERSION) \s* = .*? ([\d\.]*\d+) (.*? SKIP)?/) { 17 | my $orig_ver = $ver = $1; 18 | $found_ver = 1; 19 | # $skipping = $2; 20 | $ver =~ s{^(\d+)\.(\d{3})(\d{3})$}{join '.', int($1), int($2), int($3)}e; 21 | warn "$file: $orig_ver ($ver)\n"; 22 | } elsif (/(?x) (?:_VERSION) \s* = \s* ([a-zA-Z_]\S*)/) { 23 | warn "$file: $1\n"; 24 | $found_ver = 1; 25 | last; 26 | } 27 | if ($ver and $version and !$skipping) { 28 | if ($version ne $ver) { 29 | # die "$file: $ver != $version\n"; 30 | } 31 | } elsif ($ver and !$version) { 32 | $version = $ver; 33 | } 34 | } 35 | if (!$found_ver) { 36 | warn "WARNING: No \"_VERSION\" or \"version\" field found in `$file`.\n"; 37 | } 38 | close $in; 39 | print "Checking use of Lua global variables in file $file ...\n"; 40 | my $output = `luac -p -l $file | grep ETGLOBAL | grep -vE 'require|type|tostring|error|ngx\$|ndk|jit|setmetatable|getmetatable|string|table|io|os|print|tonumber|math|pcall|xpcall|unpack|pairs|ipairs|assert|module|package|coroutine|[gs]etfenv|next|select|rawset|rawget|debug'`; 41 | if ($output) { 42 | print $output; 43 | exit 1; 44 | } 45 | #file_contains($file, "attempt to write to undeclared variable"); 46 | system("grep -H -n -E --color '.{120}' $file"); 47 | } 48 | 49 | sub file_contains ($$) { 50 | my ($file, $regex) = @_; 51 | open my $in, $file 52 | or die "Cannot open $file fo reading: $!\n"; 53 | my $content = do { local $/; <$in> }; 54 | close $in; 55 | #print "$content"; 56 | return scalar ($content =~ /$regex/); 57 | } 58 | 59 | if (-d 't') { 60 | for my $file (map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }) { 61 | system(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $file}); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | lua-resty-sync - synchronizing data based on version changes 5 | 6 | Table of Contents 7 | ================= 8 | 9 | * [Name](#name) 10 | * [Synopsis](#synopsis) 11 | * [Description](#description) 12 | * [Status](#status) 13 | * [Methods](#methods) 14 | + [new](#new) 15 | + [register](#register) 16 | + [start](#start) 17 | + [get_version](#get_version) 18 | + [get_data](#get_data) 19 | + [get_last_modified_time](#get_last_modified_time) 20 | * [TODO](#todo) 21 | * [Author](#author) 22 | * [Copyright and License](#copyright-and-license) 23 | 24 | Synopsis 25 | ======= 26 | 27 | ```nginx 28 | 29 | http { 30 | lua_shared_dict sync 5m; 31 | lua_shared_dict locks 1m; 32 | 33 | init_worker_by_lua_block { 34 | local sync = require "resty.sync" 35 | 36 | local syncer, err = sync.new(5, "sync") 37 | if not syncer then 38 | ngx.log(ngx.WARN, "failed to create sync object: ", err) 39 | return 40 | end 41 | 42 | local callback = function(mode) 43 | if mode == sync.ACTION_DATA then 44 | -- GET DATA 45 | return "data " .. math.random(100) -- just some fake data 46 | else 47 | -- GET VERSION 48 | return "version " .. math.random(100) 49 | end 50 | end 51 | 52 | -- register some tasks 53 | syncer:register(callback, "ex1") 54 | 55 | -- start to run 56 | syncer:start() 57 | 58 | -- save it 59 | SYNCER = syncer 60 | } 61 | 62 | server { 63 | server_name _; 64 | listen *:9080; 65 | 66 | location = /t { 67 | content_by_lua_block { 68 | local sync = require "resty.sync" 69 | 70 | local syncer = SYNCER 71 | 72 | local version, err = syncer:get_version("ex1") 73 | if not version then 74 | ngx.log(ngx.WARN, "failed to fetch version: ", err) 75 | return 76 | end 77 | 78 | local data, err = syncer:get_data("ex1") 79 | 80 | if not data then 81 | ngx.log(ngx.WARN, "failed to fetch data: ", err) 82 | return 83 | end 84 | 85 | ngx.say("task ex1, data: ", data, " and version: ", version) 86 | 87 | ngx.sleep(5) 88 | 89 | -- after 5s 90 | local version2, err = syncer:get_version("ex1") 91 | if not version2 then 92 | ngx.log(ngx.WARN, "failed to fetch version: ", err) 93 | return 94 | end 95 | 96 | local data2, err = syncer:get_data("ex1") 97 | 98 | if not data2 then 99 | ngx.log(ngx.WARN, "failed to fetch data: ", err) 100 | return 101 | end 102 | 103 | ngx.say("after 5s, task ex1, data: ", data2, " and version: ", version2) 104 | } 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | Description 111 | =========== 112 | 113 | This lua-resty library help you to synchronize data(from redis, mysql, 114 | memcached and so on) based on the version changes. 115 | 116 | It will check the freshness by comparing the version cached by itself(stored in shared memory) and the one from your external suits, 117 | data will be updated when the cached one is stale or for the first time. 118 | See the [Synopsis](#synopsis) and [Methods](#methods) for learning how to use this library. 119 | 120 | Note this lua module relies on [lua-resty-lock](https://github.com/openresty/lua-resty-lock). 121 | 122 | 123 | Status 124 | ====== 125 | 126 | Probably production ready in most cases, though not yet proven in the wild. 127 | Please check the issues list and let me know if you have any problems / 128 | questions. 129 | 130 | 131 | Methods 132 | ======= 133 | 134 | new 135 | --- 136 | 137 | **syntax:** *local syncer, err = sync.new(interval, shm)* 138 | 139 | **phase:** *init_worker* 140 | 141 | 142 | Create and return an instance of the sync. 143 | 144 | The first argument, `interval`, indicates the interval of two successive 145 | operations(in seconds), which shall be greater than 0. 146 | The second argument `shm`, holds a Lua string, represents a shared 147 | memory. 148 | 149 | In the case of failure, `nil` and a Lua string described the corresponding error will be given. 150 | 151 | register 152 | ------- 153 | 154 | **syntax:** *local ok, err = syncer:register(callback, tag)* 155 | 156 | **phase:** *init_worker* 157 | 158 | 159 | Register a task to the instance `syncer` which created by [new](#new). 160 | 161 | The first argument `callback`, can be any Lua function which will be invoked later in a background "light thread". 162 | The callback function not only used for capturing data, but also used for fetching version. 163 | 164 | Only one argument `mode` can be passed to this function and the value always is: 165 | 166 | * sync.ACTION_DATA - capturing data this time. 167 | * sync.ACTION_VERSION - fetching version this time. 168 | 169 | 170 | The second argument `tag` is a Lua string which is used for distinguishing different tasks, 171 | so it can't be duplicate with one task registered previously. 172 | 173 | In the case of failure, `nil` and a Lua string described the corresponding error will be given. 174 | 175 | start 176 | ===== 177 | 178 | **syntax:** *local ok, err = syncer:start()* 179 | 180 | **phase:** *init_worker* 181 | 182 | Let the instance `syncer` starts to work. Note there will be only one timer created among all workers. 183 | The uniqueness is kept throughout your service's lifetime even the timer owner worker is crash or nginx reload happens. 184 | 185 | Callback in this instance will be run orderly(accroding the order of register). 186 | 187 | In the case of failure, `nil` and a Lua string described the corresponding error will be given. 188 | 189 | get_version 190 | ----------- 191 | 192 | **syntax:** *local version, err = syncer:get_version(tag)* 193 | 194 | **phase**: *set_by_lua, rewrite_by_lua, access_by_lua, content_by_lua, header_filter_by_lua, body_filter_by_lua, log_by_lua,* 195 | *ngx.timer.\*, balancer_by_lua, ssl_certificate_by_lua, ssl_session_fetch_by_lua, ssl_session_store_by_lua* 196 | 197 | Get the current version of one task(specified by `tag`). 198 | 199 | In the case of failure, `nil` and a Lua string described the corresponding error will be given. 200 | 201 | In particually, `nil` and `"no data"` will be given when there is no data. 202 | 203 | get_data 204 | -------- 205 | 206 | **syntax:** *local data, err = syncer:get_data(tag)* 207 | 208 | **phase**: *set_by_lua, rewrite_by_lua, access_by_lua, content_by_lua, header_filter_by_lua, body_filter_by_lua, log_by_lua,* 209 | *ngx.timer.\*, balancer_by_lua, ssl_certificate_by_lua, ssl_session_fetch_by_lua, ssl_session_store_by_lua* 210 | 211 | Get the current data of one task(specified by `tag`). 212 | 213 | In the case of failure, `nil` and a Lua string described the corresponding error will be given. 214 | 215 | In particually, `nil` and `"no data"` will be given when there is no data. 216 | 217 | get_last_modified_time 218 | ---------------------- 219 | 220 | **syntax:** *local timestamp, err = syncer:get_last_modified_time(tag)* 221 | 222 | **phase**: *set_by_lua, rewrite_by_lua, access_by_lua, content_by_lua, header_filter_by_lua, body_filter_by_lua, log_by_lua,* 223 | *ngx.timer.\*, balancer_by_lua, ssl_certificate_by_lua, ssl_session_fetch_by_lua, ssl_session_store_by_lua* 224 | 225 | Get the last update time(unix timestamp) of one task(specified by `tag`). 226 | 227 | In the case of failure, `nil` and a Lua string described the corresponding error will be given. 228 | 229 | In particually, `nil` and `"no data"` will be given when there is no data. 230 | 231 | 232 | TODO 233 | ==== 234 | 235 | * Do the updates cocurrently in one sync instance. 236 | 237 | 238 | Author 239 | ====== 240 | 241 | Alex Zhang(张超) zchao1995@gmail.com, UPYUN Inc. 242 | 243 | 244 | Copyright and License 245 | ===================== 246 | 247 | The bundle itself is licensed under the 2-clause BSD license. 248 | 249 | Copyright (c) 2017, UPYUN(又拍云) Inc. 250 | 251 | This module is licensed under the terms of the BSD license. 252 | 253 | Redistribution and use in source and binary forms, with or without 254 | modification, are permitted provided that the following conditions are 255 | met: 256 | 257 | * Redistributions of source code must retain the above copyright notice, this 258 | list of conditions and the following disclaimer. 259 | * Redistributions in binary form must reproduce the above copyright notice, this 260 | list of conditions and the following disclaimer in the documentation and/or 261 | other materials provided with the distribution. 262 | 263 | 264 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 265 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 266 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 267 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 268 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 269 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 270 | -------------------------------------------------------------------------------- /t/sanity.t: -------------------------------------------------------------------------------- 1 | use lib 'lib'; 2 | use Cwd qw(cwd); 3 | use Test::Nginx::Socket 'no_plan'; 4 | 5 | my $pwd = cwd(); 6 | 7 | our $HttpConfig1 = qq{ 8 | lua_shared_dict sync 5m; 9 | lua_shared_dict locks 1m; 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | 12 | init_worker_by_lua_block { 13 | local sync = require "resty.sync" 14 | 15 | local syncer, err = sync.new(2, "sync") 16 | if not syncer then 17 | ngx.log(ngx.ERR, "failed to create sync object: ", err) 18 | return 19 | end 20 | 21 | local count = 0 22 | 23 | local callback = function(mode) 24 | count = count + 1 25 | if mode == sync.ACTION_DATA then 26 | -- GET DATA 27 | return "data " .. count 28 | else 29 | return "version " .. count 30 | end 31 | end 32 | 33 | syncer:register(callback, "ex1") 34 | 35 | syncer:start() 36 | 37 | SYNCER = syncer 38 | } 39 | }; 40 | 41 | our $HttpConfig2 = qq{ 42 | lua_shared_dict sync 5m; 43 | lua_shared_dict locks 1m; 44 | lua_package_path "$pwd/lib/?.lua;;"; 45 | 46 | init_worker_by_lua_block { 47 | local syncer = require "resty.sync" 48 | 49 | local sync, err = syncer.new(2, "sync") 50 | if not sync then 51 | ngx.log(ngx.ERR, "failed to create sync: ", err) 52 | return 53 | end 54 | 55 | local data, version = 0, 0 56 | local callback_task1 = function(mode) 57 | if mode == syncer.ACTION_DATA then 58 | data = data + 1 59 | return data 60 | else 61 | version = version + 1 62 | return version 63 | end 64 | end 65 | 66 | local data, version = 0, 0 67 | local callback_task2 = function(mode) 68 | if mode == syncer.ACTION_DATA then 69 | data = data + 2 70 | return data 71 | else 72 | version = version + 1 73 | return version 74 | end 75 | end 76 | 77 | local ok, err = sync.register(sync, callback_task1, "task1") 78 | if not ok then 79 | ngx.log(ngx.ERR, "failed to register task1: ", err) 80 | return 81 | end 82 | 83 | local ok, err = sync.register(sync, callback_task2, "task2") 84 | if not ok then 85 | ngx.log(ngx.ERR, "failed to register task2: ", err) 86 | return 87 | end 88 | 89 | sync.start(sync) 90 | 91 | SYNC = sync 92 | } 93 | }; 94 | 95 | our $HttpConfig3 = qq{ 96 | lua_shared_dict sync 5m; 97 | lua_shared_dict locks 1m; 98 | lua_package_path "$pwd/lib/?.lua;;"; 99 | 100 | init_worker_by_lua_block { 101 | local syncer = require "resty.sync" 102 | 103 | local sync1, err = syncer.new(1, "sync") 104 | if not sync1 then 105 | ngx.log(ngx.ERR, "failed to create sync: ", err) 106 | return 107 | end 108 | 109 | local sync2, err = syncer.new(2, "sync") 110 | if not sync2 then 111 | ngx.log(ngx.ERR, "failed to create sync: ", err) 112 | return 113 | end 114 | 115 | local data, version = 0, 0 116 | local callback_task1 = function(mode) 117 | if mode == syncer.ACTION_DATA then 118 | data = data + 0.1 119 | return data 120 | else 121 | version = version + 1 122 | return version 123 | end 124 | end 125 | 126 | local data, version = 0, 0 127 | local callback_task2 = function(mode) 128 | if mode == syncer.ACTION_DATA then 129 | data = data + 1 130 | return data 131 | else 132 | version = version + 1 133 | return version 134 | end 135 | end 136 | 137 | local ok, err = sync1.register(sync1, callback_task1, "task1") 138 | if not ok then 139 | ngx.log(ngx.ERR, "failed to register task1: ", err) 140 | return 141 | end 142 | 143 | local ok, err = sync2.register(sync2, callback_task2, "task2") 144 | if not ok then 145 | ngx.log(ngx.ERR, "failed to register task2: ", err) 146 | return 147 | end 148 | 149 | sync1.start(sync1) 150 | sync2.start(sync2) 151 | 152 | SYNC1 = sync1 153 | SYNC2 = sync2 154 | } 155 | }; 156 | 157 | master_on(); 158 | workers(4); 159 | log_level("error"); 160 | no_long_string(); 161 | run_tests(); 162 | 163 | __DATA__ 164 | 165 | === TEST 1: running normally 166 | 167 | --- http_config eval: $::HttpConfig1 168 | 169 | --- config 170 | location = /t { 171 | content_by_lua_block { 172 | local sync = require "resty.sync" 173 | 174 | local syncer = SYNCER 175 | local version, err = syncer:get_version("ex1") 176 | if not version then 177 | ngx.log(ngx.ERR, "failed to fetch version: ", err) 178 | ngx.say("failed") 179 | return 180 | end 181 | 182 | local data, err = syncer:get_data("ex1") 183 | 184 | if not data then 185 | ngx.log(ngx.ERR, "failed to fetch data: ", err) 186 | ngx.say("failed") 187 | return 188 | end 189 | 190 | ngx.say("first time, task ex1, data: ", data, " and version: ", version) 191 | ngx.log(ngx.WARN, data, " ", version) 192 | 193 | ngx.sleep(2.1) 194 | 195 | -- after 1s 196 | local version2, err = syncer:get_version("ex1") 197 | if not version2 then 198 | ngx.log(ngx.ERR, "failed to fetch version: ", err) 199 | return 200 | end 201 | 202 | local data2, err = syncer:get_data("ex1") 203 | 204 | if not data2 then 205 | ngx.log(ngx.ERR, "failed to fetch data: ", err) 206 | ngx.say("failed") 207 | return 208 | end 209 | 210 | ngx.say("second time, task ex1, data: ", data2, " and version: ", version2) 211 | 212 | ngx.sleep(0.1) 213 | local version3, err = syncer:get_version("ex1") 214 | if not version3 then 215 | ngx.log(ngx.ERR, "failed to fetch version: ", err) 216 | return 217 | end 218 | 219 | local data3, err = syncer:get_data("ex1") 220 | 221 | if not data3 then 222 | ngx.log(ngx.ERR, "failed to fetch data: ", err) 223 | ngx.say("failed") 224 | return 225 | end 226 | 227 | ngx.say("third time, task ex1, data: ", data3, " and version: ", version3) 228 | } 229 | } 230 | 231 | --- request 232 | GET /t 233 | 234 | --- response_body 235 | first time, task ex1, data: data 2 and version: version 1 236 | second time, task ex1, data: data 4 and version: version 3 237 | third time, task ex1, data: data 4 and version: version 3 238 | 239 | --- no_error_log 240 | [error] 241 | 242 | 243 | === TEST 2: mutil tasks register on one sync 244 | --- http_config eval: $::HttpConfig2 245 | --- config 246 | location = /t { 247 | content_by_lua_block { 248 | local syncer = require "resty.sync" 249 | local sync = SYNC 250 | 251 | local task1_tag = "task1" 252 | local task2_tag = "task2" 253 | 254 | 255 | local data1, err = sync.get_data(sync, task1_tag) 256 | if not data1 then 257 | ngx.log(ngx.ERR, "failed to get data: ", err) 258 | ngx.say("failed") 259 | end 260 | 261 | ngx.say("data from task1: ", data1) 262 | 263 | local data2, err = sync.get_data(sync, task2_tag) 264 | if not data2 then 265 | ngx.log(ngx.ERR, "failed to get data: ", err) 266 | ngx.say("failed") 267 | end 268 | 269 | ngx.say("data from task2: ", data2) 270 | 271 | ngx.flush(true) 272 | 273 | ngx.sleep(0.1) 274 | 275 | local data, err = sync.get_data(sync, task1_tag) 276 | if not data then 277 | ngx.log(ngx.ERR, "failed to get data: ", err) 278 | ngx.say("failed") 279 | end 280 | ngx.say("data from task1: ", data) 281 | 282 | local data, err = sync.get_data(sync, task2_tag) 283 | if not data then 284 | ngx.log(ngx.ERR, "failed to get data: ", err) 285 | ngx.say("failed") 286 | end 287 | ngx.say("data from task2: ", data) 288 | 289 | ngx.flush(true) 290 | 291 | ngx.sleep(2) 292 | 293 | local data, err = sync.get_data(sync, task1_tag) 294 | if not data then 295 | ngx.log(ngx.ERR, "failed to get data: ", err) 296 | ngx.say("failed") 297 | end 298 | 299 | ngx.say("data from task1: ", data) 300 | 301 | local data, err = sync.get_data(sync, task2_tag) 302 | if not data then 303 | ngx.log(ngx.ERR, "failed to get data: ", err) 304 | ngx.say("failed") 305 | end 306 | 307 | ngx.say("data from task2: ", data) 308 | ngx.flush(true) 309 | } 310 | } 311 | 312 | --- request 313 | GET /t 314 | 315 | --- response_body 316 | data from task1: 1 317 | data from task2: 2 318 | data from task1: 1 319 | data from task2: 2 320 | data from task1: 2 321 | data from task2: 4 322 | 323 | 324 | --- no_error_log 325 | [error] 326 | 327 | 328 | === TEST 3: mutil tasks register on mutil sync 329 | --- http_config eval: $::HttpConfig3 330 | 331 | --- config 332 | location = /t { 333 | content_by_lua_block { 334 | local syncer = require "resty.sync" 335 | local sync1 = SYNC1 336 | local sync2 = SYNC2 337 | 338 | local task1_tag = "task1" 339 | local task2_tag = "task2" 340 | 341 | local data1, err = sync1.get_data(sync1, task1_tag) 342 | if not data1 then 343 | ngx.log(ngx.ERR, "failed to get data: ", err) 344 | ngx.say("failed") 345 | end 346 | 347 | ngx.say("data from task1: ", data1) 348 | 349 | local data2, err = sync2.get_data(sync2, task2_tag) 350 | if not data2 then 351 | ngx.log(ngx.ERR, "failed to get data: ", err) 352 | ngx.say("failed") 353 | end 354 | 355 | ngx.say("data from task2: ", data2) 356 | 357 | ngx.flush(true) 358 | 359 | ngx.sleep(0.1) 360 | 361 | local data, err = sync1.get_data(sync1, task1_tag) 362 | if not data then 363 | ngx.log(ngx.ERR, "failed to get data: ", err) 364 | ngx.say("failed") 365 | end 366 | 367 | ngx.say("data from task1: ", data) 368 | 369 | local data, err = sync2.get_data(sync2, task2_tag) 370 | if not data then 371 | ngx.log(ngx.ERR, "failed to get data: ", err) 372 | ngx.say("failed") 373 | end 374 | 375 | ngx.say("data from task2: ", data) 376 | 377 | ngx.flush(true) 378 | 379 | ngx.sleep(2) 380 | 381 | local data, err = sync1.get_data(sync1, task1_tag) 382 | if not data then 383 | ngx.log(ngx.ERR, "failed to get data: ", err) 384 | ngx.say("failed") 385 | end 386 | 387 | ngx.say("data from task1: ", data) 388 | 389 | local data, err = sync2.get_data(sync2, task2_tag) 390 | if not data then 391 | ngx.log(ngx.ERR, "failed to get data: ", err) 392 | ngx.say("failed") 393 | end 394 | 395 | ngx.say("data from task2: ", data) 396 | ngx.flush(true) 397 | } 398 | } 399 | 400 | --- request 401 | GET /t 402 | 403 | --- response_body 404 | data from task1: 0.1 405 | data from task2: 1 406 | data from task1: 0.1 407 | data from task2: 1 408 | data from task1: 0.3 409 | data from task2: 2 410 | 411 | --- no_error_log 412 | [error] 413 | -------------------------------------------------------------------------------- /lib/resty/sync.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) UPYUN, Inc. 2 | 3 | 4 | local lock = require "resty.lock" 5 | 6 | local ngx = ngx 7 | local ngx_lua_ver = ngx.config.ngx_lua_version 8 | local ngx_timer_at = ngx.timer.at 9 | local ngx_timer_every = ngx.timer.every 10 | local ngx_time = ngx.time 11 | local ngx_null = ngx.null 12 | local ngx_log = ngx.log 13 | local ngx_shared = ngx.shared 14 | local ngx_now = ngx.now 15 | local worker_id = ngx.worker.id 16 | local worker_pid = ngx.worker.pid 17 | local get_phase = ngx.get_phase 18 | local WARN = ngx.WARN 19 | local NOTICE = ngx.NOTICE 20 | 21 | local table_insert = table.insert 22 | local str_format = string.format 23 | local os_time = os.time 24 | local type = type 25 | local setmetatable = setmetatable 26 | 27 | local UNINITIALIZED = 0 28 | local INITIALIZED = 1 29 | local TIMER_RUNNING = 2 30 | 31 | local INIT_WORKER = "init_worker" 32 | 33 | 34 | local _M = { 35 | _VERSION = "0.06", 36 | state = UNINITIALIZED, 37 | 38 | ACTION_DATA = 0, 39 | ACTION_VERSION = 1, 40 | } 41 | 42 | local mt = { __index = _M } 43 | 44 | local STATE_MAP = { 45 | [UNINITIALIZED] = "uninitalized", 46 | [INITIALIZED] = "no task", 47 | [TIMER_RUNNING] = "timer is running", 48 | } 49 | 50 | 51 | local function is_tab(obj) return type(obj) == "table" end 52 | local function is_null(obj) return obj == ngx_null or obj == nil end 53 | local function is_num(obj) return type(obj) == "number" end 54 | local function is_str(obj) return type(obj) == "string" end 55 | local function is_func(obj) return type(obj) == "function" end 56 | 57 | 58 | local function warn(...) 59 | ngx_log(WARN, "sync: ", ...) 60 | end 61 | 62 | 63 | local function notice(...) 64 | ngx_log(NOTICE, "sync: ", ...) 65 | end 66 | 67 | 68 | local function get_lock(key) 69 | local phase = get_phase() 70 | local timeout = (phase == INIT_WORKER and 0 or nil) 71 | 72 | -- ngx.sleep API(called by lock:lock) is disabled in phase init_worker, so 73 | -- just set wait timeout to 0(return immediately) 74 | local lock = lock:new("locks", {timeout=timeout}) 75 | 76 | local elapsed, err = lock:lock(key) 77 | 78 | if not elapsed then 79 | warn("failed to acquire lock: ", key, err) 80 | return 81 | end 82 | 83 | return lock 84 | end 85 | 86 | 87 | local function release_lock(lock) 88 | local ok, err = lock:unlock() 89 | if not ok then 90 | warn("failed to unlock: ", err) 91 | end 92 | end 93 | 94 | 95 | local function set_to_shm(self, task, data, version, last_modified) 96 | local shm = ngx_shared[self.shm] 97 | 98 | local succ, err = shm:set(task.shm_time_key, last_modified) 99 | if not succ then 100 | return nil, "failed to set last_modified to shm " 101 | .. self.shm .. ": " .. err 102 | end 103 | 104 | succ, err = shm:set(task.shm_data_key, data) 105 | if not succ then 106 | return nil, "failed to set data to shm " .. self.shm .. ": " .. err 107 | end 108 | 109 | succ, err = shm:set(task.shm_version_key, version) 110 | if not succ then 111 | return nil, "failed to set version to shm " .. self.shm .. ": " .. err 112 | end 113 | 114 | return true 115 | end 116 | 117 | 118 | local function work(self, task) 119 | local version, err = task.callback(_M.ACTION_VERSION) 120 | 121 | if is_null(version) then 122 | warn("failed to get version of ", task.tag, ": ", err) 123 | return 124 | end 125 | 126 | if version ~= task.version then 127 | local data, err = task.callback(_M.ACTION_DATA) 128 | if is_null(data) then 129 | warn("failed to get data of ", task.tag, ": ", err) 130 | return 131 | end 132 | local now = ngx_time() 133 | 134 | local ok, err = set_to_shm(self, task, data, version, now) 135 | if not ok then 136 | warn(err) 137 | return 138 | end 139 | 140 | task.version = version 141 | task.data = data 142 | task.last_modified = now 143 | 144 | notice("update successfully for task ", task.tag, ", version: ", 145 | version, ", last_modified: ", now) 146 | end 147 | end 148 | 149 | 150 | local function run(self) 151 | notice("sync timer running, interval: ", self.interval, 152 | " total tasks: ", #self.tasks, ", owner worker id: ", worker_id(), 153 | ", owner worker pid: ", worker_pid()) 154 | 155 | local tasks = self.tasks 156 | for _, v in ipairs(tasks) do 157 | work(self, v) 158 | end 159 | end 160 | 161 | 162 | function _M.get_version(self, tag) 163 | if self.state ~= TIMER_RUNNING then 164 | return nil, STATE_MAP[self.state] 165 | end 166 | 167 | if not is_str(tag) or not self.tags[tag] then 168 | return nil, "invalid tag" 169 | end 170 | 171 | local id = self.tags[tag] 172 | local task = self.tasks[id] 173 | 174 | local shm = ngx_shared[self.shm] 175 | local shm_version, err = shm:get(task.shm_version_key) 176 | if err then 177 | return nil, err 178 | end 179 | 180 | if is_null(shm_version) then 181 | return nil, "no version" 182 | end 183 | 184 | if shm_version == task.version then 185 | return task.version 186 | end 187 | 188 | -- update data 189 | local data, err = shm:get(task.shm_data_key) 190 | if err then 191 | return nil, err 192 | end 193 | if is_null(data) then 194 | return nil, "no data" 195 | end 196 | 197 | task.version = shm_version 198 | task.data = data 199 | 200 | return task.version 201 | end 202 | 203 | 204 | function _M.get_data(self, tag) 205 | if self.state ~= TIMER_RUNNING then 206 | return nil, STATE_MAP[self.state] 207 | end 208 | 209 | if not is_str(tag) or not self.tags[tag] then 210 | return nil, "invalid tag" 211 | end 212 | 213 | local id = self.tags[tag] 214 | local task = self.tasks[id] 215 | 216 | local shm = ngx_shared[self.shm] 217 | local shm_version, err = shm:get(task.shm_version_key) 218 | if err then 219 | return nil, err 220 | end 221 | 222 | if is_null(shm_version) then 223 | return nil, "no version" 224 | end 225 | 226 | -- equal version, no need fetch from shdict 227 | if shm_version == task.version then 228 | return task.data 229 | end 230 | 231 | local data, err = shm:get(task.shm_data_key) 232 | if err then 233 | return nil, err 234 | end 235 | 236 | if is_null(data) then 237 | return nil, "no data" 238 | end 239 | 240 | task.version = shm_version 241 | task.data = data 242 | 243 | return data 244 | 245 | end 246 | 247 | 248 | function _M.get_last_modified_time(self, tag) 249 | if self.state ~= TIMER_RUNNING then 250 | return nil, STATE_MAP[self.state] 251 | end 252 | 253 | if not is_str(tag) or not self.tags[tag] then 254 | return nil, "invalid tag: " .. tag 255 | end 256 | 257 | local id = self.tags[tag] 258 | 259 | if self.owner == true then 260 | if is_null(self.tasks[id].last_modified) then 261 | return nil, "no data" 262 | end 263 | 264 | return self.tasks[id].last_modified 265 | end 266 | 267 | local shm = ngx_shared[self.shm] 268 | local time, err = shm:get(self.tasks[id].shm_time_key) 269 | if err then 270 | return nil, err 271 | end 272 | 273 | if is_null(time) then 274 | return nil, "no data" 275 | end 276 | 277 | return time 278 | end 279 | 280 | 281 | function _M.start(self) 282 | if self.state ~= INITIALIZED then 283 | return nil, STATE_MAP[self.state] 284 | end 285 | 286 | if #self.tasks == 0 then 287 | return nil, "no task registered" 288 | end 289 | 290 | self.state = TIMER_RUNNING 291 | 292 | local LOCK_KEY = self.LOCK_KEY 293 | local LOCK_TIMER_KEY = self.LOCK_TIMER_KEY 294 | local id = worker_id() 295 | if not is_num(id) then 296 | -- cache loader process and privileged_agent also will run 297 | -- the "init_worker_by_lua*" hooks, so let's just reject them. 298 | return true 299 | end 300 | 301 | local function wrapper_event(premature) 302 | if premature then 303 | return 304 | end 305 | 306 | run(self) 307 | 308 | local shm = ngx_shared[self.shm] 309 | local ok, err = shm:set(LOCK_TIMER_KEY, id, self.interval + 10) 310 | if not ok then 311 | warn("failed to set shm ", self.shm, ": ", err) 312 | end 313 | 314 | if ngx_lua_ver < 10009 then 315 | local interval = self.interval 316 | local ok, err = ngx_timer_at(interval, wrapper_event) 317 | if not ok then 318 | warn("failed to create timer: ", err) 319 | end 320 | end 321 | end 322 | 323 | local shm = ngx.shared[self.shm] 324 | local val = shm:get(LOCK_TIMER_KEY) 325 | 326 | if val and val ~= id then 327 | -- timer existed 328 | return true 329 | end 330 | 331 | -- timer can be created in these cases: 332 | -- * Nginx start 333 | -- * Nginx reload 334 | -- * the timer owner worker crash 335 | 336 | local lock = get_lock(LOCK_KEY) 337 | if not lock then 338 | return 339 | end 340 | 341 | val = shm:get(LOCK_TIMER_KEY) 342 | 343 | if val and val ~= id then 344 | if lock then 345 | release_lock(lock) 346 | end 347 | 348 | return true 349 | end 350 | 351 | self.owner = true 352 | 353 | run(self) 354 | 355 | local timer_method 356 | -- ngx.timer.every was first introduced in the v0.10.9 release. 357 | if ngx_lua_ver < 10009 then 358 | timer_method = ngx_timer_at 359 | else 360 | timer_method = ngx_timer_every 361 | end 362 | 363 | local ok, err = timer_method(self.interval, wrapper_event) 364 | if not ok then 365 | warn("failed to create timer: ", err) 366 | release_lock(lock) 367 | return nil, err 368 | end 369 | 370 | ok, err = shm:set(LOCK_TIMER_KEY, id, self.interval + 10) 371 | if not ok then 372 | warn("failed to set shm ",self.shm, ": ", err) 373 | end 374 | 375 | release_lock(lock) 376 | 377 | return true 378 | end 379 | 380 | 381 | -- register one task for the specific task group 382 | function _M.register(self, callback, tag) 383 | if self.state ~= INITIALIZED then 384 | return nil, STATE_MAP[self.state] 385 | end 386 | 387 | if not is_func(callback) then 388 | return nil, "type of callback is function but seen " .. type(callback) 389 | end 390 | 391 | if not is_str(tag) then 392 | return nil, "type of tag is string but seen " .. type(tag) 393 | end 394 | 395 | if self.tags[tag] then 396 | return nil, "tag is existed" 397 | end 398 | 399 | local id = #self.tasks + 1 400 | self.tags[tag] = id 401 | 402 | table_insert(self.tasks, { 403 | tag = tag, 404 | callback = callback, 405 | shm_data_key = "_data_" .. tag, 406 | shm_version_key = "_version_" .. tag, 407 | shm_time_key = "_time_" .. tag, 408 | }) 409 | 410 | notice("register a task successfully, tag: ", tag) 411 | 412 | return true 413 | end 414 | 415 | 416 | -- construct a task group 417 | function _M.new(interval, shm) 418 | if not is_num(interval) then 419 | return nil, "type of interval is number but get " .. type(interval) 420 | end 421 | 422 | if not is_str(shm) then 423 | return nil, "type of shm name is string but get " .. type(shm) 424 | end 425 | 426 | if not ngx_shared[shm] then 427 | return nil, "no such shm: " .. shm 428 | end 429 | 430 | if interval <= 0 then 431 | return nil, "interval must be larger than 0" 432 | end 433 | 434 | local now = os_time() 435 | 436 | local instance = { 437 | interval = interval, 438 | owner = false, 439 | shm = shm, 440 | state = INITIALIZED, 441 | tags = {}, 442 | tasks = {}, 443 | 444 | -- key of each instance mustn't be duplicate 445 | LOCK_KEY = str_format("sync_%d", now), 446 | LOCK_TIMER_KEY = str_format("sync_timer_%d", now) 447 | } 448 | 449 | return setmetatable(instance, mt) 450 | end 451 | 452 | 453 | return _M 454 | --------------------------------------------------------------------------------