Skip to content

Commit c2553b3

Browse files
committed
Log empty or nil select calls
Empty or nil select calls on user spaces tend to be dangerous. A critical log entry containing the current stack traceback is created upon empty or nil select 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. Tarantool has a similar implementation[1]. 1. tarantool/tarantool#7064 Closes #276
1 parent edbe9ad commit c2553b3

File tree

6 files changed

+186
-4
lines changed

6 files changed

+186
-4
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: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
-- Mostly it's a copy-paste from tarantool/src/lua/log.lua. We have three
2+
-- reasons:
3+
-- 1. Tarantool has not log.crit() (a function for logging with CRIT level).
4+
-- 2. Only new versions of Tarantool have Ratelimit type.
5+
-- 3. We want own copy of Ratelimit in case the implementation in Tarantool
6+
-- changes. Less pain between Tarantool versions.
7+
local ffi = require('ffi')
8+
9+
local S_CRIT = ffi.C.S_CRIT
10+
local S_WARN = ffi.C.S_WARN
11+
12+
local function say(level, fmt, ...)
13+
if ffi.C.log_level < level then
14+
-- don't waste cycles on debug.getinfo()
15+
return
16+
end
17+
local type_fmt = type(fmt)
18+
local format = "%s"
19+
if select('#', ...) ~= 0 then
20+
local stat
21+
stat, fmt = pcall(string.format, fmt, ...)
22+
if not stat then
23+
error(fmt, 3)
24+
end
25+
elseif type_fmt == 'table' then
26+
-- An implementation in Tarantool supports encoding a table in JSON,
27+
-- but it requires more dependencies from FFI. So we just deleted it
28+
-- because we don't need such encoding in the module.
29+
error("table not supported", 3)
30+
elseif type_fmt ~= 'string' then
31+
fmt = tostring(fmt)
32+
end
33+
34+
local debug = require('debug')
35+
local frame = debug.getinfo(3, "Sl")
36+
local line, file = 0, 'eval'
37+
if type(frame) == 'table' then
38+
line = frame.currentline or 0
39+
file = frame.short_src or frame.src or 'eval'
40+
end
41+
42+
ffi.C._say(level, file, line, nil, format, fmt)
43+
end
44+
45+
local Ratelimit = {
46+
interval = 60,
47+
burst = 10,
48+
emitted = 0,
49+
suppressed = 0,
50+
start = 0,
51+
}
52+
53+
function Ratelimit:new(object)
54+
object = object or {}
55+
setmetatable(object, self)
56+
self.__index = self
57+
return object
58+
end
59+
60+
function Ratelimit:check()
61+
local clock = require('clock')
62+
63+
local now = clock.monotonic()
64+
local saved_suppressed = 0
65+
if now > self.start + self.interval then
66+
saved_suppressed = self.suppressed
67+
self.suppressed = 0
68+
self.emitted = 0
69+
self.start = now
70+
end
71+
72+
if self.emitted < self.burst then
73+
self.emitted = self.emitted + 1
74+
return saved_suppressed, true
75+
end
76+
self.suppressed = self.suppressed + 1
77+
return saved_suppressed, false
78+
end
79+
80+
function Ratelimit:log_check(lvl)
81+
local suppressed, ok = self:check()
82+
if lvl >= S_WARN and suppressed > 0 then
83+
say(S_WARN, '%d messages suppressed due to rate limiting', suppressed)
84+
end
85+
return ok
86+
end
87+
88+
function Ratelimit:log(lvl, fmt, ...)
89+
if self:log_check(lvl) then
90+
say(lvl, fmt, ...)
91+
end
92+
end
93+
94+
local function log_ratelimited_closure(lvl)
95+
return function(self, fmt, ...)
96+
self:log(lvl, fmt, ...)
97+
end
98+
end
99+
100+
Ratelimit.log_crit = log_ratelimited_closure(S_CRIT)
101+
102+
return Ratelimit

crud/select/compat/common.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
1+
local ratelimit = require('crud.ratelimit')
2+
local check_select_args_rl = ratelimit:new()
3+
14
local common = {}
25

36
common.SELECT_FUNC_NAME = '_crud.select_on_storage'
47
common.DEFAULT_BATCH_SIZE = 100
58

9+
common.check_select_args = function(space_name, user_conditions, opts)
10+
local rl = check_select_args_rl
11+
local uc = user_conditions
12+
13+
if (type(uc) == 'nil' or (type(uc) == 'table' and next(uc) == nil)) and
14+
(opts == nil or not opts.fullscan) then
15+
rl:log_crit("empty or nil `select` call on user space=%s\n %s",
16+
space_name, debug.traceback())
17+
end
18+
end
19+
620
return common

crud/select/compat/select.lua

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ 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',
@@ -252,15 +253,18 @@ local function select_module_call_xc(space_name, user_conditions, opts)
252253
checks('string', '?table', {
253254
after = '?table|cdata',
254255
first = '?number',
255-
timeout = '?number',
256256
batch_size = '?number',
257257
bucket_id = '?number|cdata',
258258
force_map_call = '?boolean',
259259
fields = '?table',
260+
fullscan = '?boolean',
261+
262+
mode = '?vshard_call_mode',
260263
prefer_replica = '?boolean',
261264
balance = '?boolean',
262-
mode = '?vshard_call_mode',
265+
timeout = '?number',
263266
})
267+
common.check_select_args(space_name, user_conditions, opts)
264268

265269
opts = opts or {}
266270

crud/select/compat/select_old.lua

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ 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',
@@ -302,6 +303,7 @@ end
302303

303304
local function select_module_call_xc(space_name, user_conditions, opts)
304305
dev_checks('string', '?table', '?table')
306+
common.check_select_args(space_name, user_conditions, opts)
305307

306308
opts = opts or {}
307309

@@ -366,14 +368,16 @@ function select_module.call(space_name, user_conditions, opts)
366368
checks('string', '?table', {
367369
after = '?table',
368370
first = '?number',
369-
timeout = '?number',
370371
batch_size = '?number',
371372
bucket_id = '?number|cdata',
372373
force_map_call = '?boolean',
373374
fields = '?table',
375+
fullscan = '?boolean',
376+
377+
mode = '?vshard_call_mode',
374378
prefer_replica = '?boolean',
375379
balance = '?boolean',
376-
mode = '?vshard_call_mode',
380+
timeout = '?number',
377381
})
378382

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

test/unit/select_nil_test.lua

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

0 commit comments

Comments
 (0)