Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit 8094acc

Browse files
committed
Support query execution timeout
key points: - use clock.monotonic64() because it is fast and it changes without yields - add option `timeout_ms` to accessor.new() - after #59 the option should be moved to `tgql.compile()`; see #63 Closes #25
1 parent b046ce8 commit 8094acc

File tree

2 files changed

+106
-10
lines changed

2 files changed

+106
-10
lines changed

graphql/accessor_general.lua

+34-10
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
local json = require('json')
88
local avro_schema = require('avro_schema')
99
local utils = require('graphql.utils')
10+
local clock = require('clock')
1011

1112
local accessor_general = {}
1213
local DEF_RESULTING_OBJECT_CNT_MAX = 10000
1314
local DEF_FETCHED_OBJECT_CNT_MAX = 10000
15+
local DEF_TIMEOUT_MS = 1000
1416

1517
--- Validate and compile set of avro schemas (with respect to service fields).
1618
--- @tparam table schemas map where keys are string names and values are
@@ -553,7 +555,7 @@ end
553555
--- * `count` (number),
554556
--- * `objs` (table, list of objects),
555557
--- * `pivot_found` (boolean),
556-
--- * `statistics` (table, per-query statistics).
558+
--- * `qcontext` (table, per-query local storage).
557559
---
558560
--- @tparam cdata tuple flatten representation of an object to process
559561
---
@@ -578,14 +580,17 @@ local function process_tuple(state, tuple, opts)
578580
local filter = opts.filter
579581
local do_filter = opts.do_filter
580582
local pivot_filter = opts.pivot_filter
581-
local qstats = state.statistics
583+
local qcontext = state.qcontext
584+
local qstats = qcontext.statistics
582585
local resulting_object_cnt_max = opts.resulting_object_cnt_max
583586
local fetched_object_cnt_max = opts.fetched_object_cnt_max
584587
qstats.fetched_object_cnt = qstats.fetched_object_cnt + 1
585588
assert(qstats.fetched_object_cnt <= fetched_object_cnt_max,
586589
('fetched object count[%d] exceeds limit[%d] ' ..
587590
'(`fetched_object_cnt_max` in accessor)'):format(
588591
qstats.fetched_object_cnt, fetched_object_cnt_max))
592+
assert(qcontext.deadline_clock > clock.monotonic64(),
593+
'query execution timeout exceeded, use `timeout_ms` to increase it')
589594

590595
-- convert tuple -> object
591596
local obj = opts.unflatten_tuple(opts.collection_name, tuple,
@@ -706,7 +711,7 @@ local function select_internal(self, collection_name, from, filter, args, extra)
706711
count = 0,
707712
objs = {},
708713
pivot_found = false,
709-
statistics = extra.qcontext.statistics
714+
qcontext = extra.qcontext
710715
}
711716

712717
-- read only process_tuple options
@@ -804,6 +809,22 @@ local function validate_funcs(funcs)
804809
type(funcs.unflatten_tuple))
805810
end
806811

812+
--- This function is called on first select related to a query. Its purpose is
813+
--- to initialize qcontext table.
814+
--- @tparam table accessor
815+
--- @tparam table qcontext per-query table which stores query internal state;
816+
--- all neccessary initialization of this parameter should be performed by this
817+
-- function
818+
local function init_qcontext(accessor, qcontext)
819+
local settings = accessor.settings
820+
qcontext.statistics = {
821+
resulting_object_cnt = 0,
822+
fetched_object_cnt = 0
823+
}
824+
qcontext.deadline_clock = clock.monotonic64() +
825+
settings.timeout_ms * 1000 * 1000
826+
end
827+
807828
--- Create a new data accessor.
808829
---
809830
--- Provided `funcs` argument determines certain functions for retrieving
@@ -814,7 +835,7 @@ end
814835
--- shown below; additional attributes `resulting_object_cnt_max` and
815836
--- `fetched_object_cnt_max` are optional positive numbers which help to control
816837
--- query behaviour in case it requires more resources than expected _(default
817-
--- value is 10,000)_
838+
--- value is 10,000)_; `timeout_ms` _(default is 1000)_
818839
---
819840
--- @tparam table funcs set of functions (`is_collection_exists`, `get_index`,
820841
--- `get_primary_index`, `unflatten_tuple`) allows this abstract data accessor
@@ -864,6 +885,8 @@ function accessor_general.new(opts, funcs)
864885
DEF_RESULTING_OBJECT_CNT_MAX
865886
local fetched_object_cnt_max = opts.fetched_object_cnt_max or
866887
DEF_FETCHED_OBJECT_CNT_MAX
888+
-- todo: move this setting to `tgql.compile` after #59
889+
local timeout_ms = opts.timeout_ms or DEF_TIMEOUT_MS
867890

868891
assert(type(schemas) == 'table',
869892
'schemas must be a table, got ' .. type(schemas))
@@ -910,7 +933,8 @@ function accessor_general.new(opts, funcs)
910933
funcs = funcs,
911934
settings = {
912935
resulting_object_cnt_max = resulting_object_cnt_max,
913-
fetched_object_cnt_max = fetched_object_cnt_max
936+
fetched_object_cnt_max = fetched_object_cnt_max,
937+
timeout_ms = timeout_ms
914938
}
915939
}, {
916940
__index = {
@@ -928,11 +952,11 @@ function accessor_general.new(opts, funcs)
928952
'from must be nil or from.connection_name ' ..
929953
'must be a string, got ' ..
930954
type((from or {}).connection_name))
931-
-- use `extra.qcontext` to store per-query variables
932-
extra.qcontext.statistics = extra.qcontext.statistics or {
933-
resulting_object_cnt = 0,
934-
fetched_object_cnt = 0
935-
}
955+
--`qcontext` initialization
956+
if extra.qcontext.initialized ~= true then
957+
init_qcontext(self, extra.qcontext)
958+
extra.qcontext.initialized = true
959+
end
936960
return select_internal(self, collection_name, from, filter,
937961
args, extra)
938962
end,

test/common/query_timeout.test.lua

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env tarantool
2+
local multirunner = require('multirunner')
3+
local data = require('test_data_user_order')
4+
local test_run = require('test_run').new()
5+
local tap = require('tap')
6+
local graphql = require('graphql')
7+
8+
box.cfg({})
9+
local test = tap.test('result cnt')
10+
test:plan(3)
11+
12+
-- require in-repo version of graphql/ sources despite current working directory
13+
local fio = require('fio')
14+
package.path = fio.abspath(debug.getinfo(1).source:match("@?(.*/)")
15+
:gsub('/./', '/'):gsub('/+$', '')) .. '/../../?.lua' .. ';' .. package.path
16+
17+
local function run(setup_name, shard)
18+
print(setup_name)
19+
local accessor_class
20+
local virtbox
21+
-- SHARD
22+
if shard ~= nil then
23+
accessor_class = graphql.accessor_shard
24+
virtbox = shard
25+
else
26+
accessor_class = graphql.accessor_space
27+
virtbox = box.space
28+
end
29+
local accessor = accessor_class.new({
30+
schemas = data.meta.schemas,
31+
collections = data.meta.collections,
32+
service_fields = data.meta.service_fields,
33+
indexes = data.meta.indexes,
34+
timeout_ms = 0.001
35+
})
36+
37+
local gql_wrapper = graphql.new({
38+
schemas = data.meta.schemas,
39+
collections = data.meta.collections,
40+
accessor = accessor,
41+
})
42+
data.fill_test_data(virtbox)
43+
local query = [[
44+
query object_result_max {
45+
user_collection {
46+
id
47+
last_name
48+
first_name
49+
order_connection {
50+
id
51+
user_id
52+
description
53+
}
54+
}
55+
}
56+
]]
57+
58+
local gql_query = gql_wrapper:compile(query)
59+
local variables = {
60+
}
61+
local ok, result = pcall(gql_query.execute, gql_query, variables)
62+
assert(ok == false, 'this test should fail')
63+
test:like(result, 'query execution timeout exceeded', 'timeout test')
64+
65+
end
66+
67+
multirunner.run(test_run,
68+
data.init_spaces,
69+
data.drop_spaces,
70+
run)
71+
72+
os.exit(test:check() == true and 0 or 1)

0 commit comments

Comments
 (0)