Skip to content

Commit 885fc56

Browse files
committed
Log empty or nil select calls
Empty or nil select calls on user spaces tend to be dangerous. To mitigate this, a critical log entry containing the current stack traceback is created upon such function calls — a user can explicitly request a full scan though by passing fullscan = true to select's options table argument in which a case a log entry will not be created. Closes #276
1 parent edbe9ad commit 885fc56

File tree

6 files changed

+197
-5
lines changed

6 files changed

+197
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010
### Added
1111

1212
### Changed
13+
* Behaviour of empty or nil select calls: a critical log entry containing the
14+
current stack traceback is created upon such function calls — a user can
15+
explicitly request a full scan though by passing fullscan=true to select's
16+
options table argument in which a case a log entry will not be created (#276).
1317

1418
### Fixed
1519

crud/ratelimit.lua

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
-- ratelimit.lua
2+
-- copy-paste from tarantool/src/lua/log.lua
3+
--
4+
local ffi = require('ffi')
5+
6+
local S_CRIT = ffi.C.S_CRIT
7+
local S_WARN = ffi.C.S_WARN
8+
9+
local function say(level, fmt, ...)
10+
if ffi.C.log_level < level then
11+
-- don't waste cycles on debug.getinfo()
12+
return
13+
end
14+
local type_fmt = type(fmt)
15+
local format = "%s"
16+
if select('#', ...) ~= 0 then
17+
local stat
18+
stat, fmt = pcall(string.format, fmt, ...)
19+
if not stat then
20+
error(fmt, 3)
21+
end
22+
elseif type_fmt ~= 'string' then
23+
fmt = tostring(fmt)
24+
else
25+
return
26+
end
27+
28+
local debug = require('debug')
29+
local frame = debug.getinfo(3, "Sl")
30+
local line, file = 0, 'eval'
31+
if type(frame) == 'table' then
32+
line = frame.currentline or 0
33+
file = frame.short_src or frame.src or 'eval'
34+
end
35+
36+
ffi.C._say(level, file, line, nil, format, fmt)
37+
end
38+
39+
local Ratelimit = {
40+
interval = 60,
41+
burst = 10,
42+
emitted = 0,
43+
suppressed = 0,
44+
start = 0,
45+
}
46+
47+
function Ratelimit:new(object)
48+
object = object or {}
49+
setmetatable(object, self)
50+
self.__index = self
51+
return object
52+
end
53+
54+
function Ratelimit:check()
55+
local clock = require('clock')
56+
57+
local now = clock.monotonic()
58+
local saved_suppressed = 0
59+
if now > self.start + self.interval then
60+
saved_suppressed = self.suppressed
61+
self.suppressed = 0
62+
self.emitted = 0
63+
self.start = now
64+
end
65+
66+
if self.emitted < self.burst then
67+
self.emitted = self.emitted + 1
68+
return saved_suppressed, true
69+
end
70+
self.suppressed = self.suppressed + 1
71+
return saved_suppressed, false
72+
end
73+
74+
function Ratelimit:log_check(lvl)
75+
local suppressed, ok = self:check()
76+
if lvl >= S_WARN and suppressed > 0 then
77+
say(S_WARN, '%d messages suppressed due to rate limiting', suppressed)
78+
end
79+
return ok
80+
end
81+
82+
function Ratelimit:log(lvl, fmt, ...)
83+
if self:log_check(lvl) then
84+
say(lvl, fmt, ...)
85+
end
86+
end
87+
88+
local function log_ratelimited_closure(lvl)
89+
return function(self, fmt, ...)
90+
self:log(lvl, fmt, ...)
91+
end
92+
end
93+
94+
Ratelimit.log_crit = log_ratelimited_closure(S_CRIT)
95+
96+
return Ratelimit
97+

crud/select/compat/common.lua

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
1+
local log = require'log'
2+
3+
local ratelimit
4+
if log.internal ~= nil and log.internal.ratelimit ~= nil then
5+
ratelimit = log.internal.ratelimit
6+
else
7+
ratelimit = require'crud.ratelimit'
8+
end
9+
local check_select_args_rl = ratelimit:new({interval = 1, burst = 20})
10+
111
local common = {}
212

313
common.SELECT_FUNC_NAME = '_crud.select_on_storage'
414
common.DEFAULT_BATCH_SIZE = 100
515

16+
common.check_select_args = function(space_name, user_conditions, opts)
17+
local rl = check_select_args_rl
18+
local uc = user_conditions
19+
20+
if (type(uc) == 'nil' or (type(uc) == 'table' and next(uc) == nil)) and
21+
(opts == nil or not opts.fullscan) then
22+
rl:log_crit("empty or nil `select` call on user space=%s\n %s",
23+
space_name, debug.traceback())
24+
end
25+
end
26+
627
return common

crud/select/compat/select.lua

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,14 @@ function select_module.pairs(space_name, user_conditions, opts)
193193
bucket_id = '?number|cdata',
194194
force_map_call = '?boolean',
195195
fields = '?table',
196+
fullscan = '?boolean',
196197

197198
mode = '?vshard_call_mode',
198199
prefer_replica = '?boolean',
199200
balance = '?boolean',
200201
timeout = '?number',
201202
})
203+
common.check_select_args(space_name, user_conditions, opts)
202204

203205
opts = opts or {}
204206

@@ -249,18 +251,21 @@ function select_module.pairs(space_name, user_conditions, opts)
249251
end
250252

251253
local function select_module_call_xc(space_name, user_conditions, opts)
252-
checks('string', '?table', {
254+
checks('string', '?table', {
253255
after = '?table|cdata',
254256
first = '?number',
255-
timeout = '?number',
256257
batch_size = '?number',
257258
bucket_id = '?number|cdata',
258259
force_map_call = '?boolean',
259260
fields = '?table',
261+
fullscan = '?boolean',
262+
263+
mode = '?vshard_call_mode',
260264
prefer_replica = '?boolean',
261265
balance = '?boolean',
262-
mode = '?vshard_call_mode',
266+
timeout = '?number',
263267
})
268+
common.check_select_args(space_name, user_conditions, opts)
264269

265270
opts = opts or {}
266271

crud/select/compat/select_old.lua

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,14 @@ function select_module.pairs(space_name, user_conditions, opts)
225225
bucket_id = '?number|cdata',
226226
force_map_call = '?boolean',
227227
fields = '?table',
228+
fullscan = '?boolean',
228229

229230
mode = '?vshard_call_mode',
230231
prefer_replica = '?boolean',
231232
balance = '?boolean',
232233
timeout = '?number',
233234
})
235+
common.check_select_args(space_name, user_conditions, opts)
234236

235237
opts = opts or {}
236238

@@ -302,6 +304,7 @@ end
302304

303305
local function select_module_call_xc(space_name, user_conditions, opts)
304306
dev_checks('string', '?table', '?table')
307+
common.check_select_args(space_name, user_conditions, opts)
305308

306309
opts = opts or {}
307310

@@ -366,14 +369,16 @@ function select_module.call(space_name, user_conditions, opts)
366369
checks('string', '?table', {
367370
after = '?table',
368371
first = '?number',
369-
timeout = '?number',
370372
batch_size = '?number',
371373
bucket_id = '?number|cdata',
372374
force_map_call = '?boolean',
373375
fields = '?table',
376+
fullscan = '?boolean',
377+
378+
mode = '?vshard_call_mode',
374379
prefer_replica = '?boolean',
375380
balance = '?boolean',
376-
mode = '?vshard_call_mode',
381+
timeout = '?number',
377382
})
378383

379384
return sharding.wrap_method(select_module_call_xc, space_name, user_conditions, opts)

test/unit/select_nil_test.lua

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
local t = require('luatest')
2+
local g = t.group('select_nil')
3+
local luatest_capture = require('luatest.capture')
4+
local crud = require'crud'
5+
-- We throw a warning message when checking arguments for syntax correctness.
6+
-- It doesn't matter if a space exists or not (but a string argument must be)
7+
-- and if a select will be executed or not. We will not care about errors too.
8+
local any_space = 'non_existent_space'
9+
local expected_log = "empty or nil `select` call on user space=" .. any_space
10+
local selects = {
11+
crud.select,
12+
crud.pairs,
13+
}
14+
15+
g.test_warnings = function()
16+
local opts = {
17+
{uc = nil, opts = nil},
18+
{uc = nil, opts = {}},
19+
{uc = nil, opts = {fullscan = false}},
20+
{uc = {}, opts = nil},
21+
{uc = {}, opts = {}},
22+
{uc = {}, opts = {fullscan = false}},
23+
}
24+
25+
for _, sel in ipairs(selects) do
26+
for _, o in ipairs(opts) do
27+
local capture = luatest_capture:new()
28+
29+
capture:wrap(true, function()
30+
pcall(sel, any_space, o.uc, o.opts)
31+
end)
32+
33+
local captured = capture:flush()
34+
t.assert_str_contains(captured.stderr, expected_log)
35+
end
36+
end
37+
end
38+
39+
g.test_no_warnings = function()
40+
local opts = {
41+
{uc = nil, opts = {fullscan = true}},
42+
{uc = {}, opts = {fullscan = true}},
43+
{uc = {0}, opts = nil},
44+
{uc = {0}, opts = {}},
45+
{uc = {0}, opts = {fullscan = false}},
46+
}
47+
48+
for _, sel in ipairs(selects) do
49+
for _, o in ipairs(opts) do
50+
local capture = luatest_capture:new()
51+
52+
capture:wrap(true, function()
53+
pcall(sel, any_space, o.uc, o.opts)
54+
end)
55+
56+
local captured = capture:flush()
57+
t.assert_equals(captured.stderr, "")
58+
end
59+
end
60+
end

0 commit comments

Comments
 (0)