Skip to content

Commit 282a529

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 282a529

11 files changed

+221
-32
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

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ It can be used to convert received tuples to objects via `crud.unflatten_rows` f
2525
For example:
2626

2727
```lua
28-
res, err = crud.select('customers')
28+
res, err = crud.select('customers', nil, {fullscan = true})
2929
res
3030
---
3131
- metadata:
@@ -395,6 +395,8 @@ where:
395395
if full primary key equal condition is specified
396396
* `timeout` (`?number`) - `vshard.call` timeout (in seconds)
397397
* `fields` (`?table`) - field names for getting only a subset of fields
398+
* `fullscan` (`?boolean`) - if `true` then critical log entry will be skipped
399+
on empty or nil `select`
398400
* `mode` (`?string`, `read` or `write`) - if `write` is specified then `select` is
399401
performed on master
400402
* `prefer_replica` (`?boolean`) - if `true` then the preferred target is one of
@@ -578,7 +580,7 @@ Returns number or nil with error.
578580
Using `memtx`:
579581

580582
```lua
581-
#crud.select('customers')
583+
#crud.select('customers', nil, {fullscan = true})
582584
---
583585
- 5
584586
...

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 (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/integration/read_calls_strategies_test.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ pgroup.test_select = function(g)
7878
local _, err = g.cluster.main_server.net_box:call('crud.select', {'customers', nil, {
7979
mode = g.params.mode,
8080
balance = g.params.balance,
81-
prefer_replica = g.params.prefer_replica
81+
prefer_replica = g.params.prefer_replica,
82+
fullscan = true,
8283
}})
8384
t.assert_equals(err, nil)
8485
local vshard_calls = g.get_vshard_calls('call_impl')

test/integration/reload_test.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function g.test_router()
9393

9494
g.highload_fiber:cancel()
9595

96-
local result, err = g.router.net_box:call('crud.select', {'customers'})
96+
local result, err = g.router.net_box:call('crud.select', {'customers', nil, {fullscan = true}})
9797
t.assert_equals(err, nil)
9898
t.assert_items_include(result.rows, g.insertions_passed)
9999
end
@@ -122,7 +122,7 @@ function g.test_storage()
122122

123123
g.highload_fiber:cancel()
124124

125-
local result, err = g.router.net_box:call('crud.select', {'customers'})
125+
local result, err = g.router.net_box:call('crud.select', {'customers', nil, {fullscan = true}})
126126
t.assert_equals(err, nil)
127127
t.assert_items_include(result.rows, g.insertions_passed)
128128
end

0 commit comments

Comments
 (0)