# vim:set ft= ts=4 sw=4 et:

use Test::Nginx::Socket::Lua;
use Cwd qw(cwd);

repeat_each(2);

plan tests => repeat_each() * (blocks() * 3);

my $pwd = cwd();

our $HttpConfig = qq{
    lua_package_path "../lua-resty-core/lib/?.lua;lib/?.lua;;";
    lua_package_cpath "/usr/local/openresty-debug/lualib/?.so;/usr/local/openresty/lualib/?.so;;";
    lua_shared_dict cache_locks 100k;
};

$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8';
$ENV{TEST_NGINX_REDIS_PORT} ||= 6379;

no_long_string();
#no_diff();

run_tests();

__DATA__

=== TEST 1: lock is subject to garbage collection
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local lock = require "resty.lock"
            for i = 1, 2 do
                collectgarbage("collect")
                local lock = lock:new("cache_locks")
                local elapsed, err = lock:lock("foo")
                ngx.say("lock: ", elapsed, ", ", err)
            end
            collectgarbage("collect")
        }
    }
--- request
GET /t
--- response_body
lock: 0, nil
lock: 0, nil

--- no_error_log
[error]



=== TEST 2: serial lock and unlock
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local lock = require "resty.lock"
            for i = 1, 2 do
                local lock = lock:new("cache_locks")
                local elapsed, err = lock:lock("foo")
                ngx.say("lock: ", elapsed, ", ", err)
                local ok, err = lock:unlock()
                if not ok then
                    ngx.say("failed to unlock: ", err)
                end
                ngx.say("unlock: ", ok)
            end
        }
    }
--- request
GET /t
--- response_body
lock: 0, nil
unlock: 1
lock: 0, nil
unlock: 1

--- no_error_log
[error]



=== TEST 3: timed out locks
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local lock = require "resty.lock"
            for i = 1, 2 do
                local lock1 = lock:new("cache_locks", { timeout = 0.01 })
                local lock2 = lock:new("cache_locks", { timeout = 0.01 })

                local elapsed, err = lock1:lock("foo")
                ngx.say("lock 1: lock: ", elapsed, ", ", err)

                local elapsed, err = lock2:lock("foo")
                ngx.say("lock 2: lock: ", elapsed, ", ", err)

                local ok, err = lock1:unlock()
                ngx.say("lock 1: unlock: ", ok, ", ", err)

                local ok, err = lock2:unlock()
                ngx.say("lock 2: unlock: ", ok, ", ", err)
            end
        }
    }
--- request
GET /t
--- response_body
lock 1: lock: 0, nil
lock 2: lock: nil, timeout
lock 1: unlock: 1, nil
lock 2: unlock: nil, unlocked
lock 1: lock: 0, nil
lock 2: lock: nil, timeout
lock 1: unlock: 1, nil
lock 2: unlock: nil, unlocked

--- no_error_log
[error]



=== TEST 4: waited locks
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local resty_lock = require "resty.lock"
            local key = "blah"
            local t, err = ngx.thread.spawn(function ()
                local lock = resty_lock:new("cache_locks")
                local elapsed, err = lock:lock(key)
                ngx.say("sub thread: lock: ", elapsed, " ", err)
                ngx.sleep(0.1)
                ngx.say("sub thread: unlock: ", lock:unlock(key))
            end)

            local lock = resty_lock:new("cache_locks")
            local elapsed, err = lock:lock(key)
            ngx.say("main thread: lock: ", elapsed, " ", err)
            ngx.say("main thread: unlock: ", lock:unlock())
        }
    }
--- request
GET /t
--- response_body_like chop
^sub thread: lock: 0 nil
sub thread: unlock: 1
main thread: lock: 0.12[6-9] nil
main thread: unlock: 1
$
--- no_error_log
[error]



=== TEST 5: waited locks (custom step)
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local resty_lock = require "resty.lock"
            local key = "blah"
            local t, err = ngx.thread.spawn(function ()
                local lock = resty_lock:new("cache_locks")
                local elapsed, err = lock:lock(key)
                ngx.say("sub thread: lock: ", elapsed, " ", err)
                ngx.sleep(0.1)
                ngx.say("sub thread: unlock: ", lock:unlock(key))
            end)

            local lock = resty_lock:new("cache_locks", { step = 0.01 })
            local elapsed, err = lock:lock(key)
            ngx.say("main thread: lock: ", elapsed, " ", err)
            ngx.say("main thread: unlock: ", lock:unlock())
        }
    }
--- request
GET /t
--- response_body_like chop
^sub thread: lock: 0 nil
sub thread: unlock: 1
main thread: lock: 0.1[4-5]\d* nil
main thread: unlock: 1
$
--- no_error_log
[error]



=== TEST 6: waited locks (custom ratio)
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local resty_lock = require "resty.lock"
            local key = "blah"
            local t, err = ngx.thread.spawn(function ()
                local lock = resty_lock:new("cache_locks")
                local elapsed, err = lock:lock(key)
                ngx.say("sub thread: lock: ", elapsed, " ", err)
                ngx.sleep(0.1)
                ngx.say("sub thread: unlock: ", lock:unlock(key))
            end)

            local lock = resty_lock:new("cache_locks", { ratio = 3 })
            local elapsed, err = lock:lock(key)
            ngx.say("main thread: lock: ", elapsed, " ", err)
            ngx.say("main thread: unlock: ", lock:unlock())
        }
    }
--- request
GET /t
--- response_body_like chop
^sub thread: lock: 0 nil
sub thread: unlock: 1
main thread: lock: 0.1[2]\d* nil
main thread: unlock: 1
$
--- no_error_log
[error]



=== TEST 7: waited locks (custom max step)
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local resty_lock = require "resty.lock"
            local key = "blah"
            local t, err = ngx.thread.spawn(function ()
                local lock = resty_lock:new("cache_locks")
                local elapsed, err = lock:lock(key)
                ngx.say("sub thread: lock: ", elapsed, " ", err)
                ngx.sleep(0.1)
                ngx.say("sub thread: unlock: ", lock:unlock(key))
            end)

            local lock = resty_lock:new("cache_locks", { max_step = 0.05 })
            local elapsed, err = lock:lock(key)
            ngx.say("main thread: lock: ", elapsed, " ", err)
            ngx.say("main thread: unlock: ", lock:unlock())
        }
    }
--- request
GET /t
--- response_body_like chop
^sub thread: lock: 0 nil
sub thread: unlock: 1
main thread: lock: 0.11[2-4]\d* nil
main thread: unlock: 1
$
--- no_error_log
[error]



=== TEST 8: lock expired by itself
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local resty_lock = require "resty.lock"
            local key = "blah"
            local t, err = ngx.thread.spawn(function ()
                local lock = resty_lock:new("cache_locks", { exptime = 0.1 })
                local elapsed, err = lock:lock(key)
                ngx.say("sub thread: lock: ", elapsed, " ", err)
                ngx.sleep(0.1)
                -- ngx.say("sub thread: unlock: ", lock:unlock(key))
            end)

            local lock = resty_lock:new("cache_locks", { max_step = 0.05 })
            local elapsed, err = lock:lock(key)
            ngx.say("main thread: lock: ", elapsed, " ", err)
            ngx.say("main thread: unlock: ", lock:unlock())
        }
    }
--- request
GET /t
--- response_body_like chop
^sub thread: lock: 0 nil
main thread: lock: 0.11[2-4]\d* nil
main thread: unlock: 1
$
--- no_error_log
[error]



=== TEST 9: ref & unref (1 at most)
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local lock = require "resty.lock"
            local memo = lock.memo
            local ref = lock.ref_obj("foo")
            ngx.say(#memo)
            lock.unref_obj(ref)
            ngx.say(#memo)
            ref = lock.ref_obj("bar")
            ngx.say(#memo)
            lock.unref_obj(ref)
            ngx.say(#memo)
        }
    }
--- request
GET /t
--- response_body
1
0
1
0

--- no_error_log
[error]
--- skip_eval: 3: system("$NginxBinary -V 2>&1 | grep -- '--with-debug'") ne 0



=== TEST 10: ref & unref (2 at most)
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local lock = require "resty.lock"
            local memo = lock.memo

            for i = 1, 2 do
                local refs = {}

                refs[1] = lock.ref_obj("foo")
                ngx.say(#memo)

                refs[2] = lock.ref_obj("bar")
                ngx.say(#memo)

                lock.unref_obj(refs[1])
                ngx.say(#memo)

                lock.unref_obj(refs[2])
                ngx.say(#memo)
            end
        }
    }
--- request
GET /t
--- response_body
1
2
2
2
2
2
1
1

--- no_error_log
[error]
--- skip_eval: 3: system("$NginxBinary -V 2>&1 | grep -- '--with-debug'") ne 0



=== TEST 11: lock on a nil key
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local lock = require "resty.lock"
            local lock = lock:new("cache_locks")
            local elapsed, err = lock:lock(nil)
            if elapsed then
                ngx.say("lock: ", elapsed, ", ", err)
                local ok, err = lock:unlock()
                if not ok then
                    ngx.say("failed to unlock: ", err)
                end
            else
                ngx.say("failed to lock: ", err)
            end
        }
    }
--- request
GET /t
--- response_body
failed to lock: nil key

--- no_error_log
[error]



=== TEST 12: same shdict, multple locks
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local lock = require "resty.lock"
            local memo = lock.memo
            local lock1 = lock:new("cache_locks", { timeout = 0.01 })
            for i = 1, 3 do
                lock1:lock("lock_key")
                lock1:unlock()
                collectgarbage("collect")
            end

            local lock2 = lock:new("cache_locks", { timeout = 0.01 })
            local lock3 = lock:new("cache_locks", { timeout = 0.01 })
            lock2:lock("lock_key")
            lock3:lock("lock_key")
            collectgarbage("collect")

            ngx.say(#memo)

            lock2:unlock()
            lock3:unlock()
            collectgarbage("collect")
        }
    }
--- request
GET /t
--- response_body
4
--- no_error_log
[error]
--- skip_eval: 3: system("$NginxBinary -V 2>&1 | grep -- '--with-debug'") ne 0



=== TEST 13: timed out locks (0 timeout)
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local lock = require "resty.lock"
            for i = 1, 2 do
                local lock1 = lock:new("cache_locks", { timeout = 0 })
                local lock2 = lock:new("cache_locks", { timeout = 0 })

                local elapsed, err = lock1:lock("foo")
                ngx.say("lock 1: lock: ", elapsed, ", ", err)

                local elapsed, err = lock2:lock("foo")
                ngx.say("lock 2: lock: ", elapsed, ", ", err)

                local ok, err = lock1:unlock()
                ngx.say("lock 1: unlock: ", ok, ", ", err)

                local ok, err = lock2:unlock()
                ngx.say("lock 2: unlock: ", ok, ", ", err)
            end
        }
    }
--- request
GET /t
--- response_body
lock 1: lock: 0, nil
lock 2: lock: nil, timeout
lock 1: unlock: 1, nil
lock 2: unlock: nil, unlocked
lock 1: lock: 0, nil
lock 2: lock: nil, timeout
lock 1: unlock: 1, nil
lock 2: unlock: nil, unlocked

--- no_error_log
[error]



=== TEST 13: expire()
--- http_config eval: $::HttpConfig
--- config
    location = /t {
        content_by_lua_block {
            local lock = require "resty.lock"
            for i = 1, 2 do
                local lock1 = lock:new("cache_locks", { timeout = 0, exptime = 0.1 })
                local lock2 = lock:new("cache_locks", { timeout = 0, exptime = 0.1 })

                local exp, err = lock1:expire()
                ngx.say("lock 1: expire: ", exp, ", ", err)

                local elapsed, err = lock1:lock("foo")
                ngx.say("lock 1: lock: ", elapsed, ", ", err)

                ngx.sleep(0.06)

                local exp, err = lock1:expire()
                ngx.say("lock 1: expire: ", exp, ", ", err)

                ngx.sleep(0.06)

                local elapsed, err = lock2:lock("foo")
                ngx.say("lock 2: lock: ", elapsed, ", ", err)

                local exp, err = lock1:expire(0.2)
                ngx.say("lock 1: expire: ", exp, ", ", err)

                ngx.sleep(0.15)

                local elapsed, err = lock2:lock("foo")
                ngx.say("lock 2: lock: ", elapsed, ", ", err)

                ngx.sleep(0.1)

                local elapsed, err = lock2:lock("foo")
                ngx.say("lock 2: lock: ", elapsed, ", ", err)

                local ok, err = lock2:unlock()
                ngx.say("lock 2: unlock: ", ok, ", ", err)

                local exp, err = lock2:expire(0.2)
                ngx.say("lock 2: expire: ", exp, ", ", err)
            end
        }
    }
--- request
GET /t
--- response_body
lock 1: expire: nil, unlocked
lock 1: lock: 0, nil
lock 1: expire: true, nil
lock 2: lock: nil, timeout
lock 1: expire: true, nil
lock 2: lock: nil, timeout
lock 2: lock: 0, nil
lock 2: unlock: 1, nil
lock 2: expire: nil, unlocked
lock 1: expire: nil, unlocked
lock 1: lock: 0, nil
lock 1: expire: true, nil
lock 2: lock: nil, timeout
lock 1: expire: true, nil
lock 2: lock: nil, timeout
lock 2: lock: 0, nil
lock 2: unlock: 1, nil
lock 2: expire: nil, unlocked

--- no_error_log
[error]
