├── .gitignore ├── dist.ini ├── Makefile ├── t ├── init.t ├── options.t ├── dynamic.t ├── load_script.t ├── version.t └── load.t ├── util └── lua-releng ├── README.md └── lib └── resty └── load.lua /.gitignore: -------------------------------------------------------------------------------- 1 | t/servroot 2 | 3 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name=lua-resty-load 2 | abstract=dynamically require lua files/scripts for the ngx_lua 3 | author= huangnauh huanglibo2010@gmail.com 4 | is_original=yes 5 | license=2bsd 6 | lib_dir=lib 7 | repo_link=https://github.com/huangnauh/lua-resty-load 8 | -------------------------------------------------------------------------------- /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 | PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r t/ -------------------------------------------------------------------------------- /t/init.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket::Lua; 4 | use Cwd qw(cwd); 5 | 6 | repeat_each(2); 7 | 8 | plan tests => repeat_each() * (3 * blocks()); 9 | 10 | my $pwd = cwd(); 11 | 12 | our $HttpConfig = qq{ 13 | lua_package_path "$pwd/lib/?.lua;\$prefix/html/?.lua;;"; 14 | lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; 15 | lua_shared_dict load 1m; 16 | }; 17 | 18 | no_long_string(); 19 | #no_diff(); 20 | 21 | run_tests(); 22 | 23 | __DATA__ 24 | 25 | === TEST 1: init config 26 | --- http_config eval: $::HttpConfig 27 | --- config 28 | location = /t { 29 | content_by_lua ' 30 | local rload = require "resty.load" 31 | local config = {load_init={module_name="foo"}} 32 | rload.init(config) 33 | local test = require "modules.abc" 34 | ngx.say("version: ", test.version) 35 | '; 36 | } 37 | --- user_files 38 | >>> foo.lua 39 | local _M = {} 40 | local mt = { __index = _M } 41 | function _M.new(self, config) 42 | return setmetatable(config, mt) 43 | end 44 | 45 | function _M.lget(self, key) 46 | if key == "modules.abc" then 47 | return "local _M = {version = 0.01} return _M" 48 | else 49 | return nil, "no code here" 50 | end 51 | end 52 | 53 | function _M.lkeys(self) 54 | return {"modules.abc"} 55 | end 56 | 57 | function _M.lversion(self) 58 | return "xxxx" 59 | end 60 | 61 | return _M 62 | 63 | --- request 64 | GET /t 65 | --- response_body 66 | version: 0.01 67 | --- no_error_log 68 | [error] 69 | -------------------------------------------------------------------------------- /t/options.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket::Lua; 4 | use Cwd qw(cwd); 5 | 6 | repeat_each(2); 7 | 8 | plan tests => repeat_each() * (3 * blocks()); 9 | 10 | my $pwd = cwd(); 11 | 12 | our $HttpConfig = qq{ 13 | lua_package_path "$pwd/lib/?.lua;;"; 14 | lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; 15 | lua_shared_dict load 10m; 16 | }; 17 | 18 | no_long_string(); 19 | #no_diff(); 20 | 21 | run_tests(); 22 | 23 | __DATA__ 24 | 25 | === TEST 1: set commit version 26 | --- http_config eval: $::HttpConfig 27 | --- config 28 | location = /t { 29 | content_by_lua ' 30 | local rload = require "resty.load" 31 | rload.init() 32 | 33 | local skey = "script.abc" 34 | rload.set_code(skey, "ngx.say(\'hello world\')", "xxxx") 35 | rload.install_code(skey) 36 | local test = require "script.abc" 37 | test() 38 | local commit_version = rload.get_load_version() 39 | ngx.say("commit_version: ", commit_version) 40 | 41 | local skey = "modules.abc" 42 | rload.set_code(skey, "local _M = {version = 0.01} return _M", "yyyy") 43 | rload.install_code(skey) 44 | local test = require "modules.abc" 45 | ngx.say("version: ", test.version) 46 | local commit_version = rload.get_load_version() 47 | ngx.say("commit_version: ", commit_version) 48 | '; 49 | } 50 | --- request 51 | GET /t 52 | --- response_body 53 | hello world 54 | commit_version: xxxx 55 | version: 0.01 56 | commit_version: yyyy 57 | --- no_error_log 58 | [error] 59 | -------------------------------------------------------------------------------- /t/dynamic.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket::Lua; 4 | use Cwd qw(cwd); 5 | 6 | # repeat_each(2); 7 | 8 | plan tests => repeat_each() * (3 * blocks()); 9 | 10 | my $pwd = cwd(); 11 | 12 | our $HttpConfig = qq{ 13 | lua_package_path "$pwd/lib/?.lua;;"; 14 | lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; 15 | lua_shared_dict load 10m; 16 | }; 17 | 18 | no_long_string(); 19 | #no_diff(); 20 | 21 | run_tests(); 22 | 23 | __DATA__ 24 | 25 | === TEST 1: dynamic load need create_load_syncer 26 | --- http_config eval: $::HttpConfig 27 | --- config 28 | location = /t { 29 | content_by_lua ' 30 | local rload = require "resty.load" 31 | rload.init() 32 | rload.create_load_syncer() 33 | local skey = "script.abc" 34 | rload.set_code(skey, "ngx.say(\'hello world\')") 35 | rload.install_code(skey) 36 | local x = rload.load_script("script.abc", {env={ngx=ngx}}) 37 | x() 38 | rload.set_code(skey, "ngx.say(\'hello world2\')") 39 | rload.install_code(skey) 40 | ngx.sleep(2) 41 | local y = rload.load_script("script.abc", {env={ngx=ngx}}) 42 | y() 43 | '; 44 | } 45 | --- request 46 | GET /t 47 | --- response_body 48 | hello world 49 | hello world2 50 | --- no_error_log 51 | [error] 52 | 53 | 54 | === TEST 2: otherwise dynamic load not work 55 | --- http_config eval: $::HttpConfig 56 | --- config 57 | location = /t { 58 | content_by_lua ' 59 | local rload = require "resty.load" 60 | rload.init() 61 | local skey = "script.abc" 62 | rload.set_code(skey, "ngx.say(\'hello world\')") 63 | rload.install_code(skey) 64 | local x = rload.load_script("script.abc", {env={ngx=ngx}}) 65 | x() 66 | rload.set_code(skey, "ngx.say(\'hello world2\')") 67 | rload.install_code(skey) 68 | ngx.sleep(2) 69 | local y = rload.load_script("script.abc", {env={ngx=ngx}}) 70 | y() 71 | '; 72 | } 73 | --- request 74 | GET /t 75 | --- response_body 76 | hello world 77 | hello world 78 | --- no_error_log 79 | [error] -------------------------------------------------------------------------------- /t/load_script.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket::Lua; 4 | use Cwd qw(cwd); 5 | 6 | repeat_each(2); 7 | 8 | plan tests => repeat_each() * (3 * blocks()); 9 | 10 | my $pwd = cwd(); 11 | 12 | our $HttpConfig = qq{ 13 | lua_package_path "$pwd/lib/?.lua;;"; 14 | lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; 15 | lua_shared_dict load 10m; 16 | }; 17 | 18 | no_long_string(); 19 | #no_diff(); 20 | 21 | run_tests(); 22 | 23 | __DATA__ 24 | 25 | === TEST 1: load_script 26 | --- http_config eval: $::HttpConfig 27 | --- config 28 | location = /t { 29 | content_by_lua ' 30 | local rload = require "resty.load" 31 | rload.init() 32 | local skey = "script.abc" 33 | rload.set_code(skey, "ngx.say(\'hello world\')") 34 | rload.install_code(skey) 35 | local test = rload.load_script("script.abc") 36 | test() 37 | '; 38 | } 39 | --- request 40 | GET /t 41 | --- response_body_like: 500 Internal Server Error 42 | --- error_code: 500 43 | --- error_log 44 | attempt to index global 'ngx' (a nil value) 45 | 46 | 47 | === TEST 2: load_script env 48 | --- http_config eval: $::HttpConfig 49 | --- config 50 | location = /t { 51 | content_by_lua ' 52 | local rload = require "resty.load" 53 | rload.init() 54 | local skey = "script.abc" 55 | rload.set_code(skey, "ngx.say(\'hello world\')") 56 | rload.install_code(skey) 57 | local test = rload.load_script("script.abc", {env={ngx=ngx}}) 58 | test() 59 | '; 60 | } 61 | --- request 62 | GET /t 63 | --- response_body 64 | hello world 65 | --- no_error_log 66 | [error] 67 | 68 | 69 | 70 | === TEST 3: load_script global 71 | --- http_config eval: $::HttpConfig 72 | --- config 73 | location = /t { 74 | content_by_lua ' 75 | local rload = require "resty.load" 76 | rload.init() 77 | local skey = "script.abc" 78 | rload.set_code(skey, "ngx.say(\'hello world\')") 79 | rload.install_code(skey) 80 | local test = rload.load_script("script.abc", {global=true}) 81 | test() 82 | '; 83 | } 84 | --- request 85 | GET /t 86 | --- response_body 87 | hello world 88 | --- no_error_log 89 | [error] 90 | -------------------------------------------------------------------------------- /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 | 23 | } elsif (/(?x) (?:_VERSION) \s* = \s* ([a-zA-Z_]\S*)/) { 24 | warn "$file: $1\n"; 25 | $found_ver = 1; 26 | last; 27 | } 28 | 29 | if ($ver and $version and !$skipping) { 30 | if ($version ne $ver) { 31 | # die "$file: $ver != $version\n"; 32 | } 33 | } elsif ($ver and !$version) { 34 | $version = $ver; 35 | } 36 | } 37 | if (!$found_ver) { 38 | warn "WARNING: No \"_VERSION\" or \"version\" field found in `$file`.\n"; 39 | } 40 | close $in; 41 | 42 | print "Checking use of Lua global variables in file $file ...\n"; 43 | # use tee: use Capture::Tiny 'tee' 44 | #my $output = tee {system("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'")}; 45 | # or this simple way 46 | my $output = `luac5.1 -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|loadfile'`; 47 | print $output; 48 | 49 | if ($output =~ /(SET|GET)GLOBAL/) { 50 | exit 1; 51 | } 52 | 53 | #file_contains($file, "attempt to write to undeclared variable"); 54 | system("grep -H -n -E --color '.{120}' $file"); 55 | } 56 | 57 | sub file_contains ($$) { 58 | my ($file, $regex) = @_; 59 | open my $in, $file 60 | or die "Cannot open $file fo reading: $!\n"; 61 | my $content = do { local $/; <$in> }; 62 | close $in; 63 | #print "$content"; 64 | return scalar ($content =~ /$regex/); 65 | } 66 | 67 | if (-d 't') { 68 | for my $file (map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }) { 69 | system(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $file}); 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /t/version.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket::Lua; 4 | use Cwd qw(cwd); 5 | 6 | repeat_each(2); 7 | 8 | plan tests => repeat_each() * (3 * blocks()); 9 | 10 | my $pwd = cwd(); 11 | 12 | our $HttpConfig = qq{ 13 | lua_package_path "$pwd/lib/?.lua;;"; 14 | lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; 15 | lua_shared_dict load 10m; 16 | }; 17 | 18 | no_long_string(); 19 | #no_diff(); 20 | 21 | run_tests(); 22 | 23 | __DATA__ 24 | 25 | === TEST 1: get script version 26 | --- http_config eval: $::HttpConfig 27 | --- config 28 | location = /t { 29 | content_by_lua ' 30 | local rload = require "resty.load" 31 | rload.init() 32 | local skey = "script.abc" 33 | rload.set_code(skey, "ngx.say(\'hello world\')") 34 | rload.install_code(skey) 35 | local test = require "script.abc" 36 | test() 37 | local version = rload.get_version(skey) 38 | ngx.say("code_name: ", version.name) 39 | ngx.say("code_md5: ", version.version) 40 | '; 41 | } 42 | --- request 43 | GET /t 44 | --- response_body 45 | hello world 46 | code_name: script.abc 47 | code_md5: 8ef7fe02783fada43ef65123095166f0 48 | --- no_error_log 49 | [error] 50 | 51 | 52 | === TEST 2: get module version 53 | --- http_config eval: $::HttpConfig 54 | --- config 55 | location = /t { 56 | content_by_lua ' 57 | local rload = require "resty.load" 58 | rload.init() 59 | local skey = "modules.abc" 60 | rload.set_code(skey, "local _M = {version = 0.01} return _M") 61 | rload.install_code(skey) 62 | local test = require "modules.abc" 63 | ngx.say("version: ", test.version) 64 | local version = rload.get_version(skey) 65 | ngx.say("code_name: ", version.name) 66 | ngx.say("code_md5: ", version.version) 67 | '; 68 | } 69 | --- request 70 | GET /t 71 | --- response_body 72 | version: 0.01 73 | code_name: modules.abc 74 | code_md5: 7e55d7f4eec0b53ee9af18980f7d9082 75 | --- no_error_log 76 | [error] 77 | 78 | 79 | === TEST 3: get all version 80 | --- http_config eval: $::HttpConfig 81 | --- config 82 | location = /t { 83 | content_by_lua ' 84 | local rload = require "resty.load" 85 | rload.init() 86 | rload.set_code("script.abc", "local test = require \'modules.abc\' ngx.say(\'version: \',test.version)") 87 | rload.set_code("modules.abc", "local _M = {version = 0.01} return _M") 88 | rload.install_code() 89 | local test = require "script.abc" 90 | test() 91 | local version = rload.get_version() 92 | for _, value in ipairs(version.modules) do 93 | ngx.say("code_name: ", value.name) 94 | ngx.say("code_md5: ", value.version) 95 | end 96 | 97 | '; 98 | } 99 | --- request 100 | GET /t 101 | --- response_body 102 | version: 0.01 103 | code_name: modules.abc 104 | code_md5: 7e55d7f4eec0b53ee9af18980f7d9082 105 | code_name: script.abc 106 | code_md5: cdef2515c6916ef1415252e4715e87a6 107 | --- no_error_log 108 | [error] -------------------------------------------------------------------------------- /t/load.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket::Lua; 4 | use Cwd qw(cwd); 5 | 6 | repeat_each(2); 7 | 8 | plan tests => repeat_each() * (3 * blocks()); 9 | 10 | my $pwd = cwd(); 11 | 12 | our $HttpConfig = qq{ 13 | lua_package_path "$pwd/lib/?.lua;;"; 14 | lua_package_cpath "/usr/local/openresty/lualib/?.so;;"; 15 | lua_shared_dict load 10m; 16 | }; 17 | 18 | no_long_string(); 19 | #no_diff(); 20 | 21 | run_tests(); 22 | 23 | __DATA__ 24 | 25 | === TEST 1: load script 26 | --- http_config eval: $::HttpConfig 27 | --- config 28 | location = /t { 29 | content_by_lua ' 30 | local rload = require "resty.load" 31 | rload.init() 32 | local skey = "script.abc" 33 | rload.set_code(skey, "ngx.say(\'hello world\')") 34 | rload.install_code(skey) 35 | local test = require "script.abc" 36 | test() 37 | '; 38 | } 39 | --- request 40 | GET /t 41 | --- response_body 42 | hello world 43 | --- no_error_log 44 | [error] 45 | 46 | 47 | === TEST 2: load module 48 | --- http_config eval: $::HttpConfig 49 | --- config 50 | location = /t { 51 | content_by_lua ' 52 | local rload = require "resty.load" 53 | rload.init() 54 | local skey = "modules.abc" 55 | rload.set_code(skey, "local _M = {version = 0.01} return _M") 56 | rload.install_code(skey) 57 | local test = require "modules.abc" 58 | ngx.say("version: ", test.version) 59 | '; 60 | } 61 | --- request 62 | GET /t 63 | --- response_body 64 | version: 0.01 65 | --- no_error_log 66 | [error] 67 | 68 | 69 | === TEST 4: dependent 70 | --- http_config eval: $::HttpConfig 71 | --- config 72 | location = /t { 73 | content_by_lua ' 74 | local rload = require "resty.load" 75 | rload.init() 76 | rload.set_code("script.abc", "local test = require \'modules.abc\' ngx.say(\'version: \',test.version)") 77 | rload.set_code("modules.abc", "local _M = {version = 0.01} return _M") 78 | rload.install_code() 79 | local test = require "script.abc" 80 | test() 81 | '; 82 | } 83 | --- request 84 | GET /t 85 | --- response_body 86 | version: 0.01 87 | --- no_error_log 88 | [error] 89 | 90 | 91 | === TEST 5: dependent before use 92 | --- http_config eval: $::HttpConfig 93 | --- config 94 | location = /t { 95 | content_by_lua ' 96 | local rload = require "resty.load" 97 | rload.init() 98 | rload.set_code("script.abc", "local test = require \'modules.abc\' ngx.say(\'version: \',test.version)") 99 | rload.install_code("script.abc") 100 | rload.set_code("modules.abc", "local _M = {version = 0.01} return _M") 101 | rload.install_code("modules.abc") 102 | local test = require "script.abc" 103 | test() 104 | '; 105 | } 106 | --- request 107 | GET /t 108 | --- response_body 109 | version: 0.01 110 | --- no_error_log 111 | [error] 112 | 113 | 114 | === TEST 6: dependent after use 115 | --- http_config eval: $::HttpConfig 116 | --- config 117 | location = /t { 118 | content_by_lua ' 119 | local rload = require "resty.load" 120 | rload.init() 121 | rload.set_code("script.abc", "local test = require \'modules.abc\' ngx.say(\'version: \',test.version)") 122 | rload.install_code("script.abc") 123 | rload.set_code("modules.abc", "local _M = {version = 0.01} return _M") 124 | local test = require "script.abc" 125 | test() 126 | rload.install_code("modules.abc") 127 | '; 128 | } 129 | --- request 130 | GET /t 131 | --- response_body_like: 500 Internal Server Error 132 | --- error_code: 500 133 | --- error_log 134 | module 'modules.abc' not found 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | lua-resty-load - Dynamically require lua files/scripts for the ngx_lua 5 | 6 | Table of Contents 7 | ================= 8 | 9 | * [Name](#name) 10 | * [Status](#status) 11 | * [Description](#description) 12 | * [Synopsis](#synopsis) 13 | * [Methods](#methods) 14 | * [init](#init) 15 | * [create_load_syncer](#create_load_syncer) 16 | * [set_code](#set_code) 17 | * [install_code](#install_code) 18 | * [load_script](#load_script) 19 | * [get_load_version](#get_load_version) 20 | * [get_version](#get_version) 21 | * [Author](#author) 22 | * [Copyright and License](#copyright-and-license) 23 | 24 | Status 25 | ====== 26 | 27 | This library is already usable though still experimental. 28 | 29 | The Lua API is still in flux and may change in the near future. 30 | 31 | Description 32 | =========== 33 | 34 | This Lua library to help OpenResty/ngx_lua users to dynamically load lua files/scripts 35 | 36 | One caveat is that your dynamically loaded Lua code should not use the FFI API to define new C symbols or C types. 37 | 38 | Note that at least [ngx_lua v0.7.18](https://github.com/openresty/lua-nginx-module/tags) is required 39 | 40 | 41 | Synopsis 42 | ======== 43 | 44 | ```lua 45 | http { 46 | lua_package_path "/path/to/lua-resty-load/lib/?.lua;;"; 47 | 48 | init_by_lua ' 49 | local rload = require "resty.load" 50 | rload.init() 51 | -- if you need code to be loaded in the beginning 52 | -- please provide the module with following interfaces: 53 | -- 54 | -- 55 | -- local load_init_module = require "load_init_module_name" 56 | -- local load_init = load_init_module:new() 57 | -- local keys = load_init:lkeys() 58 | -- 59 | -- for _, key in ipairs(keys) do 60 | -- local code = load_init:lget(key) 61 | -- print("script/module name: ", key, ", code: ", code) 62 | -- end 63 | -- 64 | -- 65 | -- then just pass the name to rload.init: 66 | -- local rload = require "resty.load" 67 | -- rload.init({module_name="load_init_module_name"}) 68 | '; 69 | 70 | init_worker_by_lua ' 71 | local rload = require "resty.load" 72 | rload.create_load_syncer() 73 | '; 74 | } 75 | 76 | server { 77 | location /test { 78 | content_by_lua ' 79 | local rload = require "resty.load" 80 | rload.set_code("script.abc", "local test = require \'modules.abc\' ngx.say(\'version: \',test.version)") 81 | rload.set_code("modules.abc", "local _M = {version = 0.01} return _M") 82 | rload.install_code() 83 | local test = require "script.abc" 84 | test() 85 | -- dynamic load 86 | rload.set_code("script.abc", "ngx.say(\'hello world\')") 87 | rload.install_code("script.abc") 88 | local test = require "script.abc" 89 | test() 90 | '; 91 | } 92 | } 93 | ``` 94 | 95 | [Back to TOC](#table-of-contents) 96 | 97 | Methods 98 | ======= 99 | 100 | [Back to TOC](#table-of-contents) 101 | 102 | init 103 | ------- 104 | `syntax: ok, err = load.init(options_config?)` 105 | 106 | `context: init_by_lua*` 107 | 108 | Initialize the library. In case of failures, returns `nil` and a string describing the error. 109 | 110 | If you need to load any code in the beginning, you can do so by defining a custom load_init module. 111 | 112 | An optional Lua table `options_config` can be specified as the only argument to this method to specify load_init module config: 113 | 114 | * `module_name` 115 | 116 | Your load_init module must implement the new(), lkeys() and lget(key) methods, along with the optional method lversion(). 117 | 118 | * [new](#load_init_modulenew) 119 | * [lkeys](#load_init_modulelkeys) 120 | * [lget](#load_init_modulelget) 121 | * [lversion](#load_init_modulelversion) 122 | 123 | load_init_module:new 124 | --- 125 | Creates a load_init object. In case of failures, returns nil and a string describing the error. 126 | 127 | * `options_config` 128 | 129 | Just the parameter in `load.init` 130 | 131 | load_init_module:lkeys 132 | --- 133 | **syntax:** *keys, err = load_init_module:lkeys()* 134 | 135 | Retrieving a lua array that include all the script/module names. In case of failures, returns nil and a string describing the error. 136 | 137 | load_init_module:lget 138 | --- 139 | **syntax:** *code, err = load_init_module:lget(key)* 140 | 141 | Retrieving the code for the script/module name `key`. In case of failures, returns nil and a string describing the error. 142 | 143 | load_init_module:lversion 144 | --- 145 | **syntax:** *version, err = load_init_module:lversion()* 146 | 147 | Retrieving the version for current codes. This is optional for fallback and version checking. `version` no longer than 32 characters. 148 | 149 | In case of failures, returns nil and a string describing the error. 150 | 151 | [Back to TOC](#table-of-contents) 152 | 153 | create_load_syncer 154 | ------- 155 | 156 | `syntax: ok, err = rload.create_load_syncer()` 157 | 158 | `context: init_worker_by_lua*` 159 | 160 | Creates an Nginx timer to make dynamical loading work. In case of failures, returns nil and a string describing the error. 161 | 162 | [Back to TOC](#table-of-contents) 163 | 164 | set_code 165 | ------- 166 | `syntax: ok, err = rload.set_code(name, code)` 167 | 168 | Set the code to the module `name`, but it don't take effect yet. In case of failures, returns nil and a string describing the error. 169 | 170 | [Back to TOC](#table-of-contents) 171 | 172 | install_code 173 | ------- 174 | `syntax: ok, err = rload.install_code(name?)` 175 | 176 | By default, all the set codes will be installed. It will take effect after the load_syncer timer be called. 177 | 178 | When the `name` argument is given, only the module `name` will be installed. 179 | 180 | In case of failures, returns nil and a string describing the error. 181 | 182 | [Back to TOC](#table-of-contents) 183 | 184 | load_script 185 | ------- 186 | `syntax: fun, err = rload.load_script(name, options_table?)` 187 | 188 | Load script by the name `name`, this method returns the (successfully) script function `fun` for later use. 189 | 190 | An optional Lua table can be specified as the last argument to this method to specify the environment for the script: 191 | 192 | * `env` 193 | 194 | If this option is set to a table, then a function environment will be set. 195 | 196 | * `global` 197 | 198 | If this option is set to `true`, then the global environment will be set. 199 | 200 | In case of failures, returns nil and a string describing the error. 201 | 202 | [Back to TOC](#table-of-contents) 203 | 204 | get_load_version 205 | ------- 206 | `syntax: commit_version, err = rload.get_load_version()` 207 | 208 | This method returns the value if `load_init_module` has the `lversion` method or "0" by default. 209 | 210 | In case of failures, returns nil and a string describing the error. 211 | 212 | [Back to TOC](#table-of-contents) 213 | 214 | get_version 215 | ------- 216 | `syntax: ok, err = rload.get_version()` 217 | 218 | Returns the resulting json string, for example, 219 | 220 | ``` 221 | { 222 | "global_version": 4, 223 | "commit_version": "79630b", 224 | "modules": [ 225 | { 226 | "time": "2016-11-23 17:38:11", 227 | "version": "aed4a968ef14f8db732e3602c34dc37a", 228 | "name": "modules.abc" 229 | }, 230 | { 231 | "time": "2016-11-23 17:38:11", 232 | "version": "7a170b7731543b56722101c4167965b3", 233 | "name": "script.test" 234 | } 235 | ] 236 | } 237 | ``` 238 | 239 | * `global_version` 240 | 241 | Returns how many times the lua library loads script/module since nginx start/reload 242 | 243 | * `commit_version` 244 | 245 | Returns the exactly the same version as [get_load_version](#get_version). 246 | 247 | * `version` 248 | 249 | Returns the MD5 hash of the code. 250 | 251 | In case of failures, returns nil and a string describing the error. 252 | 253 | [Back to TOC](#table-of-contents) 254 | 255 | 256 | Author 257 | ====== 258 | 259 | UPYUN Inc. 260 | 261 | [Back to TOC](#table-of-contents) 262 | 263 | Copyright and License 264 | ===================== 265 | 266 | This module is licensed under the BSD license. 267 | 268 | Copyright (C) 2016, by UPYUN Inc. 269 | 270 | All rights reserved. 271 | 272 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 273 | 274 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 275 | 276 | * 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. 277 | 278 | 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. 279 | 280 | [Back to TOC](#table-of-contents) 281 | -------------------------------------------------------------------------------- /lib/resty/load.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2016 Libo Huang (huangnauh), UPYUN Inc. 2 | local cjson = require "cjson.safe" 3 | local setmetatable = setmetatable 4 | local getfenv = getfenv 5 | local setfenv = setfenv 6 | local require = require 7 | local loadfile = loadfile 8 | local pcall = pcall 9 | local loadstring = loadstring 10 | local next = next 11 | local pairs = pairs 12 | local ipairs = ipairs 13 | local type = type 14 | local str_find = string.find 15 | local str_format = string.format 16 | local str_sub = string.sub 17 | local tab_insert = table.insert 18 | local localtime = ngx.localtime 19 | local timer_at = ngx.timer.at 20 | local md5 = ngx.md5 21 | local log = ngx.log 22 | local ERR = ngx.ERR 23 | local INFO = ngx.INFO 24 | local WARN = ngx.WARN 25 | local load_dict = ngx.shared.load 26 | 27 | local SKEYS_KEY = "lua:skeys" 28 | local VERSION_KEY = "lua:version" 29 | local LOAD_VERSION = "load:version" 30 | local TIMER_DELAY = 1 31 | local CODE_PREFIX = "update:" 32 | local version_dict = {} 33 | local global_version 34 | 35 | 36 | local _M = {_VERSION = '0.01'} 37 | 38 | 39 | local function pre_post(str, pattern) 40 | local from, to = str_find(str, pattern) 41 | if not from then 42 | return str, nil 43 | end 44 | local prefix = str_sub(str, 1, from - 1) 45 | local postfix = str_sub(str, to + 1) 46 | return prefix, postfix 47 | end 48 | 49 | 50 | local function set_load_version(version) 51 | local ok, err = load_dict:safe_set(LOAD_VERSION, version) 52 | if not ok then 53 | err = str_format('failed to set key: %s in load, err: %s', LOAD_VERSION, err) 54 | log(ERR, err) 55 | return nil, err 56 | else 57 | return true 58 | end 59 | end 60 | 61 | 62 | function _M.get_load_version() 63 | local load_version = load_dict:get(LOAD_VERSION) 64 | return load_version 65 | end 66 | 67 | 68 | local function lua_loader(module_name) 69 | local prefix = pre_post(module_name, "%.") 70 | if prefix ~= "script" then 71 | return "\n\tnot a script" 72 | end 73 | local filename = package.searchpath(module_name, package.path) 74 | if not filename then 75 | return "\n\tscript not in the filesystem" 76 | end 77 | 78 | local file, err = loadfile(filename) 79 | if not file then 80 | log(ERR, err) 81 | return function() return true end 82 | else 83 | log(INFO, "load " .. module_name .. " from filesystem") 84 | return function() return file end 85 | end 86 | end 87 | 88 | 89 | local function load_module(module_name, code) 90 | local f, err = loadstring(code, module_name) 91 | if not f then 92 | local err_str = str_format('failed to load %s from code: %s', module_name, err) 93 | log(ERR, err_str) 94 | return nil, err_str 95 | end 96 | local prefix, _ = pre_post(module_name, "%.") 97 | if prefix == "script" then 98 | return {mod=f, version=md5(code)} 99 | end 100 | local mod = f() 101 | if not mod then 102 | local err_str = str_format('%s lua source does not return the module', module_name) 103 | log(ERR, err_str) 104 | return nil, err_str 105 | end 106 | return {mod=mod, version=md5(code)} 107 | end 108 | 109 | 110 | local function get_code_version(skey) 111 | local code = load_dict:get(CODE_PREFIX .. skey) 112 | if not code then 113 | log(INFO, str_format("%s code not found in shdict", skey)) 114 | return nil, "code not found in shdict" 115 | end 116 | return md5(code) 117 | end 118 | 119 | 120 | local function load_syncer(premature) 121 | if premature then 122 | return 123 | end 124 | 125 | local version = load_dict:get(VERSION_KEY) 126 | if version and version ~= global_version then 127 | local skeys = load_dict:get(SKEYS_KEY) 128 | if skeys then 129 | skeys = cjson.decode(skeys) 130 | else 131 | skeys = {} 132 | end 133 | 134 | for key in pairs(version_dict) do 135 | if not skeys[key] then 136 | log(INFO, key, " unload from package") 137 | version_dict[key] = nil 138 | package.loaded[key] = nil 139 | end 140 | end 141 | 142 | for skey, sh_value in pairs(skeys) do 143 | local worker_version 144 | if type(version_dict[skey]) == "table" then 145 | worker_version = version_dict[skey]["version"] 146 | end 147 | if package.loaded[skey] and worker_version ~= sh_value.version then 148 | log(INFO, skey, " version changed") 149 | version_dict[skey] = nil 150 | package.loaded[skey] = nil 151 | end 152 | end 153 | 154 | global_version = version 155 | end 156 | 157 | local ok, err = timer_at(TIMER_DELAY, load_syncer) 158 | if not ok then 159 | log(ERR, "failed to create timer: ", err) 160 | end 161 | end 162 | 163 | 164 | function _M.create_load_syncer() 165 | global_version = load_dict:get(VERSION_KEY) 166 | local ok, err = timer_at(TIMER_DELAY, load_syncer) 167 | if not ok then 168 | log(ERR, "failed to create load_lua timer: ", err) 169 | return nil, err 170 | else 171 | return true 172 | end 173 | end 174 | 175 | 176 | local function module_loader(module_name) 177 | local skeys = load_dict:get(SKEYS_KEY) 178 | local sh_version 179 | if skeys then 180 | skeys = cjson.decode(skeys) 181 | local sh_value = skeys[module_name] 182 | if sh_value then 183 | sh_version = sh_value.version 184 | end 185 | end 186 | 187 | if sh_version then 188 | local code = load_dict:get(CODE_PREFIX .. module_name) 189 | local code_version 190 | if code then 191 | code_version = md5(code) 192 | end 193 | if sh_version ~= code_version then 194 | return str_format("\n\tcode version in confusion, sh_version: %s, code_version: %s", 195 | sh_version, code_version) 196 | else 197 | local mod_tab, err = load_module(module_name, code) 198 | if mod_tab then 199 | local mod = mod_tab.mod 200 | version_dict[module_name] = { version=mod_tab.version } 201 | log(INFO, module_name, " loaded from shdict, md5:", mod_tab.version) 202 | return function() return mod end 203 | else 204 | return "\n\t" .. err 205 | end 206 | end 207 | else 208 | local prefix = pre_post(module_name, "%.") 209 | if prefix == "script" then 210 | return function() return true end 211 | else 212 | return "\n\tcode not in shdict" 213 | end 214 | end 215 | end 216 | 217 | 218 | local function pre_load(config) 219 | if not config then 220 | return true 221 | end 222 | 223 | if type(config) ~= "table" then 224 | return nil, "load config invalid" 225 | end 226 | 227 | local load_init = config.load_init or {} 228 | local config_module = load_init.module_name 229 | if type(config_module) ~= "string" then 230 | return nil, "load_init invalid" 231 | end 232 | 233 | 234 | local ok, module = pcall(require, config_module) 235 | if not ok then 236 | log(ERR, module) 237 | return nil, str_format("module_name %s not find", config_module) 238 | end 239 | 240 | local mod = module:new(config) 241 | 242 | local load_version, err 243 | if type(mod.lversion) == "function" then 244 | load_version, err = mod:lversion() 245 | if not load_version then 246 | return nil, err 247 | end 248 | 249 | if type(load_version) ~= "string" or #load_version < 1 then 250 | return nil, "load_version invalid" 251 | end 252 | end 253 | 254 | load_version = load_version or "0" 255 | local ok, err = set_load_version(load_version) 256 | if not ok then 257 | return nil, err 258 | end 259 | 260 | local script_keys, err = mod:lkeys() 261 | if err then 262 | return nil, err 263 | end 264 | 265 | if not script_keys then 266 | log(WARN, "no code to load") 267 | return true 268 | end 269 | 270 | if type(script_keys) ~= "table" then 271 | return nil, "script_keys invalid" 272 | end 273 | 274 | if not next(script_keys) then 275 | log(WARN, "no code to load") 276 | return true 277 | end 278 | 279 | local skeys = {} 280 | for _, skey in ipairs(script_keys) do 281 | local ok = pcall(require, skey) 282 | if not ok then 283 | local code = mod:lget(skey) 284 | if not code then 285 | return nil, "fail to get code from consul" 286 | end 287 | local ok, err = load_dict:safe_set(CODE_PREFIX .. skey, code) 288 | if not ok then 289 | return nil, err 290 | end 291 | skeys[skey] = { version = md5(code), time = localtime() } 292 | end 293 | end 294 | 295 | if next(skeys) then 296 | local ok, err = load_dict:safe_set(SKEYS_KEY, cjson.encode(skeys)) 297 | if not ok then 298 | err = str_format('failed to set key: %s in load, err: %s', SKEYS_KEY, err) 299 | log(ERR, err) 300 | return nil, err 301 | end 302 | end 303 | 304 | return true 305 | end 306 | 307 | 308 | function _M.init(config) 309 | tab_insert(package.loaders, 1, lua_loader) 310 | load_dict:flush_all() 311 | 312 | -- try load code from somewhere 313 | local ok, err = pre_load(config) 314 | if not ok then 315 | return nil, err 316 | end 317 | 318 | local ok, err = load_dict:safe_set(VERSION_KEY, 0) 319 | if not ok then 320 | err = str_format('failed to set key: %s in load, err: %s', VERSION_KEY, err) 321 | log(ERR, err) 322 | return nil, err 323 | end 324 | 325 | log(INFO, "load shdict finished") 326 | tab_insert(package.loaders, module_loader) 327 | return true 328 | end 329 | 330 | 331 | local function set_version(skey) 332 | local new_version, err = get_code_version(skey) 333 | if not new_version then 334 | return nil, err 335 | end 336 | 337 | local skeys = load_dict:get(SKEYS_KEY) 338 | if skeys then 339 | skeys = cjson.decode(skeys) 340 | else 341 | skeys = {} 342 | end 343 | 344 | local sh_value = skeys[skey] 345 | local sh_version 346 | if sh_value then 347 | sh_version = sh_value.version 348 | end 349 | 350 | if new_version ~= sh_version then 351 | skeys[skey] = {version=new_version, time=localtime()} 352 | local ok, err = load_dict:safe_set(SKEYS_KEY, cjson.encode(skeys)) 353 | if not ok then 354 | return nil, str_format('failed to set key: %s in load, err: %s', SKEYS_KEY, err) 355 | end 356 | else 357 | return false, "code already loaded" 358 | end 359 | 360 | log(INFO, skey, " new version setted") 361 | return true 362 | end 363 | 364 | 365 | function _M.uninstall_code(skey) 366 | local skeys = load_dict:get(SKEYS_KEY) 367 | if skeys then 368 | skeys = cjson.decode(skeys) 369 | local sh_value = skeys[skey] 370 | if sh_value then 371 | skeys[skey] = nil 372 | if next(skeys) then 373 | local ok, err = load_dict:safe_set(SKEYS_KEY, cjson.encode(skeys)) 374 | if not ok then 375 | return false, str_format('failed to set key: %s in load, err: %s', skey, err) 376 | end 377 | else 378 | load_dict:delete(SKEYS_KEY) 379 | end 380 | end 381 | end 382 | 383 | load_dict:delete(CODE_PREFIX .. skey) 384 | 385 | local _, err = load_dict:incr(VERSION_KEY, 1) 386 | if err then 387 | return false, err 388 | end 389 | 390 | log(INFO, skey, " deleted from shdict") 391 | return true 392 | end 393 | 394 | function _M.set_code(skey, body, load_version) 395 | if not body then 396 | return nil, "need code to set" 397 | end 398 | 399 | local load_version = load_version or "0" 400 | 401 | if type(load_version) ~= "string" or load_version == "" or #load_version > 32 then 402 | return nil, "load version invalid" 403 | end 404 | 405 | local old_body = load_dict:get(CODE_PREFIX .. skey) 406 | if old_body then 407 | local old_md5 = md5(old_body) 408 | local new_md5 = md5(body) 409 | if new_md5 == old_md5 then 410 | return nil, "code already in shdict" 411 | end 412 | end 413 | 414 | local ok, err = load_dict:safe_set(CODE_PREFIX .. skey, body) 415 | if not ok then 416 | return nil, str_format('failed to set key: %s in load, err: %s', skey, err) 417 | end 418 | 419 | local ok, err = set_load_version(load_version) 420 | if not ok then 421 | return nil, err 422 | end 423 | 424 | log(INFO, skey, " new code setted") 425 | return true 426 | end 427 | 428 | 429 | function _M.install_code(skey) 430 | if not skey then 431 | local keys = load_dict:get_keys() 432 | local srcipt_keys = {} 433 | 434 | -- first set module 435 | for _, key in ipairs(keys) do 436 | local prefix, postfix = pre_post(key, ":") 437 | if prefix == "update" and postfix then 438 | local pre = pre_post(postfix, "%.") 439 | if pre == "srcipt" then 440 | tab_insert(srcipt_keys, key) 441 | else 442 | local ok, err = set_version(postfix) 443 | if not ok and err ~= "code already loaded" then 444 | return nil, err 445 | end 446 | end 447 | end 448 | end 449 | 450 | -- then set script 451 | for _, key in ipairs(srcipt_keys) do 452 | local ok, err = set_version(key) 453 | if not ok and err ~= "code already loaded" then 454 | return nil, err 455 | end 456 | end 457 | else 458 | local ok, err = set_version(skey) 459 | if not ok then 460 | return nil, err 461 | end 462 | end 463 | 464 | local _, err = load_dict:incr(VERSION_KEY, 1) 465 | if err then 466 | return nil, err 467 | end 468 | return true 469 | end 470 | 471 | 472 | function _M.get_version(skey) 473 | local skeys = load_dict:get(SKEYS_KEY) 474 | if skeys then 475 | skeys = cjson.decode(skeys) 476 | else 477 | skeys = {} 478 | end 479 | 480 | if not skey then 481 | local ver = load_dict:get(VERSION_KEY) 482 | local load_ver = load_dict:get(LOAD_VERSION) 483 | local data = {global_version=ver, commit_version=load_ver, modules={}} 484 | for key, value in pairs(skeys) do 485 | tab_insert(data.modules, {version=value.version, time=value.time, name=key}) 486 | end 487 | return data 488 | else 489 | local mod_name = skey 490 | local sh_value = skeys[skey] 491 | if sh_value then 492 | return {version=sh_value.version, time=sh_value.time, name=mod_name } 493 | else 494 | return {} 495 | end 496 | end 497 | end 498 | 499 | 500 | function _M.load_script(script_name, opts) 501 | local opts = opts or {} 502 | local ok, mode = pcall(require, script_name) 503 | if not ok then 504 | log(ERR, mode) 505 | return nil 506 | end 507 | 508 | if mode and type(mode) == "function" then 509 | local env = opts.env or {} 510 | if opts.global then 511 | env = setmetatable(env, { __index = getfenv(0) }) 512 | end 513 | setfenv(mode, env) 514 | return mode 515 | end 516 | return nil 517 | end 518 | 519 | 520 | return _M 521 | --------------------------------------------------------------------------------