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

Commit bce843e

Browse files
committed
Limit result size (resulting_object_cnt_max)
Changes: - add two statistics to accessor_general: - returning_object_cnt - fetched_object_cnt - check `returning_object_cnt < returning_object_cnt_max` and `fetched_object_cnt < fetched_object_cnt_mx` after each tuple processed - add test for all accessors on new parameters Closes #34
1 parent 39dd8cb commit bce843e

File tree

4 files changed

+154
-14
lines changed

4 files changed

+154
-14
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ default:
44
.PHONY: lint
55
lint:
66
luacheck graphql/*.lua test/local/*.lua test/testdata/*.lua \
7-
test/common/lua/*.lua --no-redefined --no-unused-args
7+
test/common/*test.lua test/common/lua/*.lua \
8+
--no-redefined --no-unused-args
89

910
.PHONY: test
1011
test: lint

graphql/accessor_general.lua

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ local avro_schema = require('avro_schema')
99
local utils = require('graphql.utils')
1010

1111
local accessor_general = {}
12+
local DEF_RESULTING_OBJECT_CNT_MAX = 10000
13+
local DEF_FETCHED_OBJECT_CNT_MAX = 10000
1214

1315
--- Validate and compile set of avro schemas (with respect to service fields).
1416
--- @tparam table schemas map where keys are string names and values are
@@ -550,7 +552,8 @@ end
550552
---
551553
--- * `count` (number),
552554
--- * `objs` (table, list of objects),
553-
--- * `pivot_found` (boolean).
555+
--- * `pivot_found` (boolean),
556+
--- * `statistics` (table, per-query statistics).
554557
---
555558
--- @tparam cdata tuple flatten representation of an object to process
556559
---
@@ -562,7 +565,9 @@ end
562565
--- * `do_filter` (boolean, whether we need to filter out non-matching
563566
--- objects),
564567
--- * `pivot_filter` (table, set of fields to match the objected pointed by
565-
--- `offset` arqument of the GraphQL query)
568+
--- `offset` arqument of the GraphQL query),
569+
--- * `resulting_object_cnt_max` (number),
570+
--- * `fetched_object_cnt_max` (number).
566571
---
567572
--- @return nil
568573
---
@@ -580,6 +585,14 @@ local function process_tuple(state, tuple, opts)
580585
local filter = opts.filter
581586
local do_filter = opts.do_filter
582587
local pivot_filter = opts.pivot_filter
588+
local qstats = state.statistics
589+
local resulting_object_cnt_max = opts.resulting_object_cnt_max
590+
local fetched_object_cnt_max = opts.fetched_object_cnt_max
591+
qstats.fetched_object_cnt = qstats.fetched_object_cnt + 1
592+
assert(qstats.fetched_object_cnt <= fetched_object_cnt_max,
593+
('fetched object count[%d] exceeds limit[%d] ' ..
594+
'(`fetched_object_cnt_max` in accessor)'):format(
595+
qstats.fetched_object_cnt, fetched_object_cnt_max))
583596

584597
-- skip all items before pivot (the item pointed by offset)
585598
local obj = nil
@@ -604,10 +617,12 @@ local function process_tuple(state, tuple, opts)
604617
-- add the matching object, update count and check limit
605618
state.objs[#state.objs + 1] = obj
606619
state.count = state.count + 1
620+
qstats.resulting_object_cnt = qstats.resulting_object_cnt + 1
621+
assert(qstats.resulting_object_cnt <= resulting_object_cnt_max,
622+
('returning object count[%d] exceeds limit[%d] ' ..
623+
'(`resulting_object_cnt_max` in accessor)'):format(
624+
qstats.resulting_object_cnt, resulting_object_cnt_max))
607625
if limit ~= nil and state.count >= limit then
608-
assert(limit == nil or state.count <= limit,
609-
('count[%d] exceeds limit[%s] (in for)'):format(
610-
state.count, limit))
611626
return false
612627
end
613628
return true
@@ -631,8 +646,11 @@ end
631646
--- @tparam table args table of arguments passed within the query except ones
632647
--- that forms the `filter` parameter
633648
---
649+
--- @tparam table extra table which contains extra information related to
650+
--- current select and the whole query
651+
---
634652
--- @treturn table list of matching objects
635-
local function select_internal(self, collection_name, from, filter, args)
653+
local function select_internal(self, collection_name, from, filter, args, extra)
636654
assert(type(self) == 'table',
637655
'self must be a table, got ' .. type(self))
638656
assert(type(collection_name) == 'string',
@@ -694,6 +712,7 @@ local function select_internal(self, collection_name, from, filter, args)
694712
count = 0,
695713
objs = {},
696714
pivot_found = false,
715+
statistics = extra.qcontext.statistics
697716
}
698717

699718
-- read only process_tuple options
@@ -703,6 +722,8 @@ local function select_internal(self, collection_name, from, filter, args)
703722
filter = filter,
704723
do_filter = not full_match,
705724
pivot_filter = nil, -- filled later if needed
725+
resulting_object_cnt_max = self.settings.resulting_object_cnt_max,
726+
fetched_object_cnt_max = self.settings.fetched_object_cnt_max
706727
}
707728

708729
if index == nil then
@@ -791,7 +812,10 @@ end
791812
---
792813
--- @tparam table opts `schemas`, `collections`, `service_fields` and `indexes`
793814
--- to give the data accessor all needed meta-information re data; the format is
794-
--- shown below
815+
--- shown below; additional attributes `resulting_object_cnt_max` and
816+
--- `fetched_object_cnt_max` are optional positive numbers which help to control
817+
--- query behaviour in case it requires more resources than expected _(default
818+
--- value is 10,000)_
795819
---
796820
--- @tparam table funcs set of functions (`is_collection_exists`, `get_index`,
797821
--- `get_primary_index`) allows this abstract data accessor behaves in the
@@ -837,6 +861,10 @@ function accessor_general.new(opts, funcs)
837861
local collections = opts.collections
838862
local service_fields = opts.service_fields
839863
local indexes = opts.indexes
864+
local resulting_object_cnt_max = opts.resulting_object_cnt_max or
865+
DEF_RESULTING_OBJECT_CNT_MAX
866+
local fetched_object_cnt_max = opts.fetched_object_cnt_max or
867+
DEF_FETCHED_OBJECT_CNT_MAX
840868

841869
assert(type(schemas) == 'table',
842870
'schemas must be a table, got ' .. type(schemas))
@@ -846,6 +874,12 @@ function accessor_general.new(opts, funcs)
846874
'service_fields must be a table, got ' .. type(service_fields))
847875
assert(type(indexes) == 'table',
848876
'indexes must be a table, got ' .. type(indexes))
877+
assert(type(resulting_object_cnt_max) == 'number' and
878+
resulting_object_cnt_max > 0,
879+
'resulting_object_cnt_max must be natural number')
880+
assert(type(fetched_object_cnt_max) == 'number' and
881+
fetched_object_cnt_max > 0,
882+
'fetched_object_cnt_max must be natural number')
849883

850884
local models = compile_schemas(schemas, service_fields)
851885
validate_collections(collections, schemas, indexes)
@@ -861,10 +895,14 @@ function accessor_general.new(opts, funcs)
861895
models = models,
862896
index_cache = index_cache,
863897
funcs = funcs,
898+
settings = {
899+
resulting_object_cnt_max = resulting_object_cnt_max,
900+
fetched_object_cnt_max = fetched_object_cnt_max
901+
}
864902
}, {
865903
__index = {
866904
select = function(self, parent, collection_name, from,
867-
filter, args)
905+
filter, args, extra)
868906
assert(type(parent) == 'table',
869907
'parent must be a table, got ' .. type(parent))
870908
assert(from == nil or type(from) == 'table',
@@ -877,8 +915,13 @@ function accessor_general.new(opts, funcs)
877915
'from must be nil or from.connection_name ' ..
878916
'must be a string, got ' ..
879917
type((from or {}).connection_name))
918+
-- use `extra.qcontext` to store per-query variables
919+
extra.qcontext.statistics = extra.qcontext.statistics or {
920+
resulting_object_cnt = 0,
921+
fetched_object_cnt = 0
922+
}
880923
return select_internal(self, collection_name, from, filter,
881-
args)
924+
args, extra)
882925
end,
883926
list_args = function(self, collection_name)
884927
-- get name of field of primary key

graphql/tarantool_graphql.lua

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,9 @@ gql_type = function(state, avro_schema, collection, collection_name)
286286
destination_args_names = destination_args_names,
287287
destination_args_values = destination_args_values,
288288
}
289-
289+
local extra = {
290+
qcontext = info.qcontext
291+
}
290292
local object_args_instance = {} -- passed to 'filter'
291293
local list_args_instance = {} -- passed to 'args'
292294
for k, v in pairs(args_instance) do
@@ -302,7 +304,7 @@ gql_type = function(state, avro_schema, collection, collection_name)
302304
end
303305
local objs = accessor:select(parent,
304306
c.destination_collection, from,
305-
object_args_instance, list_args_instance)
307+
object_args_instance, list_args_instance, extra)
306308
assert(type(objs) == 'table',
307309
'objs list received from an accessor ' ..
308310
'must be a table, got ' .. type(objs))
@@ -397,8 +399,11 @@ local function parse_cfg(cfg)
397399
end
398400
end
399401
local from = nil
402+
local extra = {
403+
qcontext = info.qcontext
404+
}
400405
return accessor:select(rootValue, name, from,
401-
object_args_instance, list_args_instance)
406+
object_args_instance, list_args_instance, extra)
402407
end,
403408
}
404409
end
@@ -507,7 +512,7 @@ end
507512
--- accessor = setmetatable({}, {
508513
--- __index = {
509514
--- select = function(self, parent, collection_name, from,
510-
--- object_args_instance, list_args_instance)
515+
--- object_args_instance, list_args_instance, extra)
511516
--- -- from is nil for a top-level object, otherwise it is
512517
--- --
513518
--- -- {
@@ -517,6 +522,11 @@ end
517522
--- -- destination_args_values = <...>,
518523
--- -- }
519524
--- --
525+
--- -- extra is a table which contains additional data for the
526+
--- -- query; by now it consists of a single qcontext table,
527+
--- -- which can be used by accessor to store any query-related
528+
--- -- data
529+
--- --
520530
--- return ...
521531
--- end,
522532
--- list_args = function(self, collection_name)

test/common/limit_result.test.lua

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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(6)
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+
resulting_object_cnt_max = 3,
35+
fetched_object_cnt_max = 5
36+
})
37+
38+
local gql_wrapper = graphql.new({
39+
schemas = data.meta.schemas,
40+
collections = data.meta.collections,
41+
accessor = accessor,
42+
})
43+
data.fill_test_data(virtbox)
44+
local query = [[
45+
query object_result_max($user_id: Int, $description: String) {
46+
user_collection(id: $user_id) {
47+
id
48+
last_name
49+
first_name
50+
order_connection(description: $description){
51+
id
52+
user_id
53+
description
54+
}
55+
}
56+
}
57+
]]
58+
59+
local gql_query = gql_wrapper:compile(query)
60+
local variables = {
61+
user_id = 5,
62+
}
63+
local ok, result = pcall(gql_query.execute, gql_query, variables)
64+
assert(ok == false, "this test should fail")
65+
test:like(result,
66+
'count%[4%] exceeds limit%[3%] %(`resulting_object_cnt_max`',
67+
'resulting_object_cnt_max test')
68+
variables = {
69+
user_id = 5,
70+
description = "no such description"
71+
}
72+
ok, result = pcall(gql_query.execute, gql_query, variables)
73+
assert(ok == false, "this test should fail")
74+
test:like(result,
75+
'count%[6%] exceeds limit%[5%] %(`fetched_object_cnt_max`',
76+
'resulting_object_cnt_max test')
77+
78+
79+
end
80+
81+
multirunner.run(test_run,
82+
data.init_spaces,
83+
data.drop_spaces,
84+
run)
85+
86+
os.exit(test:check() == true and 0 or 1)

0 commit comments

Comments
 (0)