Skip to content

Commit f38adac

Browse files
authored
feat: reduce auth requests (#100)
1 parent c576da6 commit f38adac

File tree

2 files changed

+266
-8
lines changed

2 files changed

+266
-8
lines changed

lib/resty/etcd/v3.lua

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
local typeof = require("typeof")
33
local cjson = require("cjson.safe")
44
local setmetatable = setmetatable
5+
local random = math.random
56
local clear_tab = require("table.clear")
67
local utils = require("resty.etcd.utils")
78
local tab_nkeys = require("table.nkeys")
@@ -19,6 +20,7 @@ local decode_json = cjson.decode
1920
local encode_json = cjson.encode
2021
local encode_base64 = ngx.encode_base64
2122
local decode_base64 = ngx.decode_base64
23+
local semaphore = require("ngx.semaphore")
2224
local INIT_COUNT_RESIZE = 2e8
2325

2426
local _M = {}
@@ -29,6 +31,7 @@ local mt = { __index = _M }
2931
local refresh_jwt_token
3032

3133
local function _request_uri(self, method, uri, opts, timeout, ignore_auth)
34+
utils.log_info("v3 request uri: ", uri, ", timeout: ", timeout)
3235

3336
local body
3437
if opts and opts.body and tab_nkeys(opts.body) > 0 then
@@ -44,7 +47,7 @@ local function _request_uri(self, method, uri, opts, timeout, ignore_auth)
4447
if self.is_auth then
4548
if not ignore_auth then
4649
-- authentication reqeust not need auth request
47-
local _, err = refresh_jwt_token(self)
50+
local _, err = refresh_jwt_token(self, timeout)
4851
if err then
4952
return nil, err
5053
end
@@ -162,8 +165,15 @@ function _M.new(opts)
162165
})
163166
end
164167

168+
local sema, err = semaphore.new()
169+
if not sema then
170+
return nil, err
171+
end
172+
165173
return setmetatable({
166174
last_auth_time = now(), -- save last Authentication time
175+
last_refresh_jwt_err = nil,
176+
sema = sema,
167177
jwt_token = nil, -- last Authentication token
168178
is_auth = not not (user and password),
169179
user = user,
@@ -195,34 +205,69 @@ local function choose_endpoint(self)
195205
return endpoints[pos]
196206
end
197207

208+
209+
local function wake_up_everyone(self)
210+
local count = -self.sema:count()
211+
if count > 0 then
212+
self.sema:post(count)
213+
end
214+
end
215+
216+
198217
-- return refresh_is_ok, error
199-
function refresh_jwt_token(self)
218+
function refresh_jwt_token(self, timeout)
200219
-- token exist and not expire
201-
-- default is 5min, we use 3min
220+
-- default is 5min, we use 3min plus random seconds to smooth the refresh across workers
202221
-- https://github.com/etcd-io/etcd/issues/8287
203-
if self.jwt_token and now() - self.last_auth_time < 60 * 3 then
222+
if self.jwt_token and now() - self.last_auth_time < 60 * 3 + random(0, 60) then
204223
return true, nil
205224
end
206225

226+
if self.requesting_token then
227+
self.sema:wait(timeout)
228+
if self.jwt_token and now() - self.last_auth_time < 60 * 3 + random(0, 60) then
229+
return true, nil
230+
end
231+
232+
if self.last_refresh_jwt_err then
233+
utils.log_info("v3 refresh jwt last err: ", self.last_refresh_jwt_err)
234+
return nil, self.last_refresh_jwt_err
235+
end
236+
237+
-- something unexpected happened, try again
238+
utils.log_info("v3 try auth after waiting, timeout: ", timeout)
239+
end
240+
241+
self.last_refresh_jwt_err = nil
242+
self.requesting_token = true
243+
207244
local opts = {
208245
body = {
209246
name = self.user,
210247
password = self.password,
211248
}
212249
}
213250
local res, err = _request_uri(self, 'POST',
214-
choose_endpoint(self).full_prefix .. "/auth/authenticate",
215-
opts, 5, true) -- default authenticate timeout 5 second
251+
choose_endpoint(self).full_prefix .. "/auth/authenticate",
252+
opts, timeout, true)
253+
self.requesting_token = false
254+
216255
if err then
256+
self.last_refresh_jwt_err = err
257+
wake_up_everyone(self)
217258
return nil, err
218259
end
219260

220261
if not res or not res.body or not res.body.token then
221-
return nil, 'authenticate refresh token fail'
262+
err = 'authenticate refresh token fail'
263+
self.last_refresh_jwt_err = err
264+
wake_up_everyone(self)
265+
return nil, err
222266
end
223267

224268
self.jwt_token = res.body.token
225269
self.last_auth_time = now()
270+
wake_up_everyone(self)
226271

227272
return true, nil
228273
end
@@ -469,7 +514,7 @@ local function request_chunk(self, method, scheme, host, port, path, opts, timeo
469514
local headers = {}
470515
if self.is_auth then
471516
-- authentication reqeust not need auth request
472-
_, err = refresh_jwt_token(self)
517+
_, err = refresh_jwt_token(self, timeout)
473518
if err then
474519
return nil, err
475520
end

t/v3/auth.t

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
use Test::Nginx::Socket::Lua;
2+
3+
log_level('info');
4+
no_long_string();
5+
repeat_each(1);
6+
7+
my $etcd_version = `etcd --version`;
8+
if ($etcd_version =~ /^etcd Version: 2/ || $etcd_version =~ /^etcd Version: 3.[123]./) {
9+
plan(skip_all => "etcd is too old");
10+
} else {
11+
my $enable_tls = $ENV{ETCD_ENABLE_TLS};
12+
if ((defined $enable_tls) && $enable_tls eq "TRUE") {
13+
plan(skip_all => "skip test cases with auth when TLS is enabled");
14+
} else {
15+
plan 'no_plan';
16+
}
17+
}
18+
19+
our $HttpConfig = <<'_EOC_';
20+
lua_socket_log_errors off;
21+
lua_package_path 'lib/?.lua;/usr/local/share/lua/5.3/?.lua;/usr/share/lua/5.1/?.lua;;';
22+
init_by_lua_block {
23+
local cjson = require("cjson.safe")
24+
25+
function check_res(data, err, val, status)
26+
if err then
27+
ngx.log(ngx.ERR, "err: ", err)
28+
return
29+
end
30+
31+
if val then
32+
if data.body.kvs==nil then
33+
ngx.exit(404)
34+
end
35+
if data.body.kvs and val ~= data.body.kvs[1].value then
36+
ngx.say("failed to check value")
37+
ngx.log(ngx.ERR, "failed to check value, got: ", data.body.kvs[1].value,
38+
", expect: ", val)
39+
ngx.exit(200)
40+
else
41+
ngx.say("checked val as expect: ", val)
42+
end
43+
end
44+
45+
if status and status ~= data.status then
46+
ngx.exit(data.status)
47+
end
48+
end
49+
}
50+
_EOC_
51+
52+
run_tests();
53+
54+
__DATA__
55+
56+
=== TEST 1: share same etcd auth token
57+
--- http_config eval: $::HttpConfig
58+
--- config
59+
location /t {
60+
content_by_lua_block {
61+
local etcd, err = require "resty.etcd" .new({
62+
protocol = "v3",
63+
user = 'root',
64+
password = 'abc123',
65+
timeout = 3,
66+
http_host = {
67+
"http://127.0.0.1:12379",
68+
},
69+
})
70+
check_res(etcd, err)
71+
72+
local t = {}
73+
for i = 1, 3 do
74+
local th = assert(ngx.thread.spawn(function(i)
75+
local res, err = etcd:set("/test", { a='abc'})
76+
check_res(res, err)
77+
78+
ngx.sleep(0.1)
79+
80+
res, err = etcd:delete("/test")
81+
check_res(res, err)
82+
end))
83+
table.insert(t, th)
84+
end
85+
for i, th in ipairs(t) do
86+
ngx.thread.wait(th)
87+
end
88+
ngx.say('ok')
89+
}
90+
}
91+
--- request
92+
GET /t
93+
--- no_error_log
94+
[error]
95+
--- response_body
96+
ok
97+
--- grep_error_log eval
98+
qr/uri: .+, timeout: \d+/
99+
--- grep_error_log_out
100+
uri: http://127.0.0.1:12379/v3/kv/put, timeout: 3
101+
uri: http://127.0.0.1:12379/v3/auth/authenticate, timeout: 3
102+
uri: http://127.0.0.1:12379/v3/kv/put, timeout: 3
103+
uri: http://127.0.0.1:12379/v3/kv/put, timeout: 3
104+
uri: http://127.0.0.1:12379/v3/kv/deleterange, timeout: 3
105+
uri: http://127.0.0.1:12379/v3/kv/deleterange, timeout: 3
106+
uri: http://127.0.0.1:12379/v3/kv/deleterange, timeout: 3
107+
108+
109+
110+
=== TEST 2: share same etcd auth token, auth failed
111+
--- http_config eval: $::HttpConfig
112+
--- config
113+
location /t {
114+
content_by_lua_block {
115+
local etcd, err = require "resty.etcd" .new({
116+
protocol = "v3",
117+
user = 'root',
118+
password = '123',
119+
timeout = 3,
120+
http_host = {
121+
"http://127.0.0.1:12379",
122+
},
123+
})
124+
check_res(etcd, err)
125+
126+
local t = {}
127+
for i = 1, 3 do
128+
local th = assert(ngx.thread.spawn(function(i)
129+
local res, err = etcd:set("/test", { a='abc'})
130+
if not res then
131+
ngx.log(ngx.ERR, err)
132+
end
133+
end))
134+
table.insert(t, th)
135+
end
136+
for i, th in ipairs(t) do
137+
ngx.thread.wait(th)
138+
end
139+
ngx.say('ok')
140+
}
141+
}
142+
--- request
143+
GET /t
144+
--- response_body
145+
ok
146+
--- grep_error_log eval
147+
qr/(uri: .+, timeout: \d+|v3 refresh jwt last err: [^,]+|authenticate refresh token fail)/
148+
--- grep_error_log_out
149+
uri: http://127.0.0.1:12379/v3/kv/put, timeout: 3
150+
uri: http://127.0.0.1:12379/v3/auth/authenticate, timeout: 3
151+
uri: http://127.0.0.1:12379/v3/kv/put, timeout: 3
152+
uri: http://127.0.0.1:12379/v3/kv/put, timeout: 3
153+
authenticate refresh token fail
154+
v3 refresh jwt last err: authenticate refresh token fail
155+
authenticate refresh token fail
156+
v3 refresh jwt last err: authenticate refresh token fail
157+
authenticate refresh token fail
158+
159+
160+
161+
=== TEST 3: share same etcd auth token, failed to connect
162+
--- http_config eval: $::HttpConfig
163+
--- config
164+
location /t {
165+
content_by_lua_block {
166+
local etcd, err = require "resty.etcd" .new({
167+
protocol = "v3",
168+
user = 'root',
169+
password = '123',
170+
timeout = 3,
171+
})
172+
check_res(etcd, err)
173+
174+
-- hack to inject 'connection refused' error
175+
etcd.endpoints = {{
176+
full_prefix = "http://127.0.0.1:1997/v3",
177+
scheme = "http",
178+
host = "127.0.0.1",
179+
port = "1997",
180+
}}
181+
182+
local t = {}
183+
for i = 1, 3 do
184+
local th = assert(ngx.thread.spawn(function(i)
185+
local res, err = etcd:set("/test", { a='abc'})
186+
if not res then
187+
ngx.log(ngx.ERR, err)
188+
end
189+
end))
190+
table.insert(t, th)
191+
end
192+
for i, th in ipairs(t) do
193+
ngx.thread.wait(th)
194+
end
195+
ngx.say('ok')
196+
}
197+
}
198+
--- request
199+
GET /t
200+
--- response_body
201+
ok
202+
--- grep_error_log eval
203+
qr/(uri: .+, timeout: \d+|v3 refresh jwt last err: [^,]+|connection refused)/
204+
--- grep_error_log_out
205+
uri: http://127.0.0.1:1997/v3/kv/put, timeout: 3
206+
uri: http://127.0.0.1:1997/v3/auth/authenticate, timeout: 3
207+
uri: http://127.0.0.1:1997/v3/kv/put, timeout: 3
208+
uri: http://127.0.0.1:1997/v3/kv/put, timeout: 3
209+
connection refused
210+
v3 refresh jwt last err: connection refused
211+
connection refused
212+
v3 refresh jwt last err: connection refused
213+
connection refused

0 commit comments

Comments
 (0)