Skip to content

Commit 379a252

Browse files
committed
Implementation of batch replace
Batch upsert is mostly used for operation with one bucket / one Tarantool node in a transaction. In this case batch replace is more efficient then replacing tuple-by-tuple. Right now CRUD cannot provide batch replace with full consistency. CRUD offers batch upsert with partial consistency. That means that full consistency can be provided only on single replicaset using `box` transactions. Part of #193
1 parent 3074100 commit 379a252

File tree

6 files changed

+2135
-140
lines changed

6 files changed

+2135
-140
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1111
* Batch insert/upsert operation
1212
`crud.insert_many()`/`crud.insert_object_many()`/
1313
`crud.upsert_many()`/`crud.upsert_object_many()`
14+
`crud.replace_many()`/`crud.replace_object_many()`
1415
with partial consistency
1516

1617
### Changed

README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,95 @@ crud.replace_object('customers', {
409409
...
410410
```
411411

412+
### Replace many
413+
414+
```lua
415+
-- Batch replace tuples
416+
local result, err = crud.replace_many(space_name, tuples, opts)
417+
-- Batch replace objects
418+
local result, err = crud.replace_object_many(space_name, objects, opts)
419+
```
420+
421+
where:
422+
423+
* `space_name` (`string`) - name of the space to insert/replace an object
424+
* `tuples` / `objects` (`table`) - array of tuples/objects to insert
425+
* `opts`:
426+
* `timeout` (`?number`) - `vshard.call` timeout (in seconds)
427+
* `fields` (`?table`) - field names for getting only a subset of fields
428+
* `stop_on_error` (`?boolean`) - stop on a first error and report errors
429+
regarding the failed operation and all not performed ones., default is
430+
`false`
431+
* `rollback_on_error` (`?boolean`) - any failed operation will lead to
432+
rollback on a storage, where the operation is failed, default is
433+
`false`
434+
435+
Returns metadata and array contains inserted rows, array of errors
436+
(one error corresponds to one replicaset for which the error occurred).
437+
Error object can contain `tuple` field. This field contains the tuple
438+
for which the error occurred.
439+
440+
Right now CRUD cannot provide batch replace with full consistency.
441+
CRUD offers batch replace with partial consistency. That means
442+
that full consistency can be provided only on single replicaset
443+
using `box` transactions.
444+
445+
**Example:**
446+
447+
```lua
448+
crud.replace_many('developers', {
449+
{1, box.NULL, 'Elizabeth', 'lizaaa'},
450+
{2, box.NULL, 'Anastasia', 'iamnewdeveloper'},
451+
})
452+
---
453+
- metadata:
454+
- {'name': 'id', 'type': 'unsigned'}
455+
- {'name': 'bucket_id', 'type': 'unsigned'}
456+
- {'name': 'name', 'type': 'string'}
457+
- {'name': 'login', 'type': 'string'}
458+
rows:
459+
- [1, 477, 'Elizabeth', 'lizaaa']
460+
- [2, 401, 'Anastasia', 'iamnewdeveloper']
461+
...
462+
crud.replace_object_many('developers', {
463+
{id = 1, name = 'Inga', login = 'mylogin'},
464+
{id = 10, name = 'Anastasia', login = 'qwerty'},
465+
})
466+
---
467+
- metadata:
468+
- {'name': 'id', 'type': 'unsigned'}
469+
- {'name': 'bucket_id', 'type': 'unsigned'}
470+
- {'name': 'name', 'type': 'string'}
471+
- {'name': 'age', 'type': 'number'}
472+
rows:
473+
- [1, 477, 'Inga', 'mylogin']
474+
- [10, 569, 'Anastasia', 'qwerty']
475+
476+
-- Partial success
477+
-- Let's say login has unique secondary index
478+
local res, errs = crud.replace_object_many('developers', {
479+
{id = 22, name = 'Alex', login = 'pushkinn'},
480+
{id = 3, name = 'Anastasia', login = 'qwerty'},
481+
{id = 5, name = 'Sergey', login = 'crudisthebest'},
482+
})
483+
---
484+
res
485+
- metadata:
486+
- {'name': 'id', 'type': 'unsigned'}
487+
- {'name': 'bucket_id', 'type': 'unsigned'}
488+
- {'name': 'name', 'type': 'string'}
489+
- {'name': 'age', 'type': 'number'}
490+
rows:
491+
- [5, 1172, 'Sergey', 'crudisthebest'],
492+
- [22, 655, 'Alex', 'pushkinn'],
493+
494+
#errs -- 1
495+
errs[1].class_name -- BatchReplaceError
496+
errs[1].err -- 'Duplicate key exists <...>'
497+
errs[1].tuple -- {3, 2804, 'Anastasia', 'qwerty'}
498+
...
499+
```
500+
412501
### Upsert
413502

414503
```lua

crud.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ local cfg = require('crud.cfg')
66
local insert = require('crud.insert')
77
local batch_insert = require('crud.batch_insert')
88
local replace = require('crud.replace')
9+
local batch_replace = require('crud.batch_replace')
910
local get = require('crud.get')
1011
local update = require('crud.update')
1112
local upsert = require('crud.upsert')
@@ -53,6 +54,14 @@ crud.replace = stats.wrap(replace.tuple, stats.op.REPLACE)
5354
-- @function replace_object
5455
crud.replace_object = stats.wrap(replace.object, stats.op.REPLACE)
5556

57+
-- @refer batch_replace.tuples_batch
58+
-- @function replace_many
59+
crud.replace_many = batch_replace.tuples_batch
60+
61+
-- @refer batch_replace.objects_batch
62+
-- @function replace_object_many
63+
crud.replace_object_many = batch_replace.objects_batch
64+
5665
-- @refer update.call
5766
-- @function update
5867
crud.update = stats.wrap(update.call, stats.op.UPDATE)
@@ -145,6 +154,7 @@ function crud.init_storage()
145154
batch_insert.init()
146155
get.init()
147156
replace.init()
157+
batch_replace.init()
148158
update.init()
149159
upsert.init()
150160
batch_upsert.init()

crud/batch_replace.lua

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
local checks = require('checks')
2+
local errors = require('errors')
3+
local vshard = require('vshard')
4+
5+
local call = require('crud.common.call')
6+
local const = require('crud.common.const')
7+
local utils = require('crud.common.utils')
8+
local sharding = require('crud.common.sharding')
9+
local dev_checks = require('crud.common.dev_checks')
10+
local schema = require('crud.common.schema')
11+
12+
local BatchInsertIterator = require('crud.common.map_call_cases.batch_insert_iter')
13+
local BatchPostprocessor = require('crud.common.map_call_cases.batch_postprocessor')
14+
15+
local BatchReplaceError = errors.new_class('BatchReplaceError', {capture_stack = false})
16+
17+
local batch_replace = {}
18+
19+
local BATCH_REPLACE_FUNC_NAME = '_crud.batch_replace_on_storage'
20+
21+
local function batch_replace_on_storage(space_name, tuples, opts)
22+
dev_checks('string', 'table', {
23+
add_space_schema_hash = '?boolean',
24+
fields = '?table',
25+
stop_on_error = '?boolean',
26+
rollback_on_error = '?boolean',
27+
sharding_key_hash = '?number',
28+
sharding_func_hash = '?number',
29+
skip_sharding_hash_check = '?boolean',
30+
})
31+
32+
opts = opts or {}
33+
34+
local space = box.space[space_name]
35+
if space == nil then
36+
return nil, {BatchReplaceError:new("Space %q doesn't exist", space_name)}
37+
end
38+
39+
local _, err = sharding.check_sharding_hash(space_name,
40+
opts.sharding_func_hash,
41+
opts.sharding_key_hash,
42+
opts.skip_sharding_hash_check)
43+
44+
if err ~= nil then
45+
return nil, {err}
46+
end
47+
48+
local inserted_tuples = {}
49+
local errs = {}
50+
51+
box.begin()
52+
for _, tuple in ipairs(tuples) do
53+
-- add_space_schema_hash is true only in case of replace_object_many
54+
-- the only one case when reloading schema can avoid replace error
55+
-- is flattening object on router
56+
local insert_result = schema.wrap_box_space_func_result(space, 'replace', {tuple}, {
57+
add_space_schema_hash = opts.add_space_schema_hash,
58+
field_names = opts.fields,
59+
})
60+
61+
if insert_result.err ~= nil then
62+
local err = {
63+
err = insert_result.err,
64+
tuple = tuple,
65+
}
66+
67+
if opts.stop_on_error == true then
68+
if opts.rollback_on_error == true then
69+
box.rollback()
70+
return nil, {err}
71+
end
72+
73+
box.commit()
74+
75+
return inserted_tuples, {err}
76+
end
77+
78+
table.insert(errs, err)
79+
end
80+
81+
table.insert(inserted_tuples, insert_result.res)
82+
end
83+
84+
if next(errs) ~= nil then
85+
if opts.rollback_on_error == true then
86+
box.rollback()
87+
return nil, errs
88+
end
89+
90+
box.commit()
91+
92+
return inserted_tuples, errs
93+
end
94+
95+
box.commit()
96+
97+
return inserted_tuples
98+
end
99+
100+
function batch_replace.init()
101+
_G._crud.batch_replace_on_storage = batch_replace_on_storage
102+
end
103+
104+
-- returns result, err, need_reload
105+
-- need_reload indicates if reloading schema could help
106+
-- see crud.common.schema.wrap_func_reload()
107+
local function call_batch_replace_on_router(space_name, original_tuples, opts)
108+
dev_checks('string', 'table', {
109+
timeout = '?number',
110+
fields = '?table',
111+
add_space_schema_hash = '?boolean',
112+
stop_on_error = '?boolean',
113+
rollback_on_error = '?boolean',
114+
})
115+
116+
opts = opts or {}
117+
118+
local space = utils.get_space(space_name, vshard.router.routeall())
119+
if space == nil then
120+
return nil, {BatchReplaceError:new("Space %q doesn't exist", space_name)}, true
121+
end
122+
123+
local tuples = table.deepcopy(original_tuples)
124+
125+
local batch_replace_on_storage_opts = {
126+
add_space_schema_hash = opts.add_space_schema_hash,
127+
fields = opts.fields,
128+
stop_on_error = opts.stop_on_error,
129+
rollback_on_error = opts.rollback_on_error,
130+
}
131+
132+
local iter, err = BatchInsertIterator:new({
133+
tuples = tuples,
134+
space = space,
135+
execute_on_storage_opts = batch_replace_on_storage_opts,
136+
})
137+
if err ~= nil then
138+
return nil, {err}, const.NEED_SCHEMA_RELOAD
139+
end
140+
141+
local postprocessor = BatchPostprocessor:new()
142+
143+
local rows, errs = call.map(BATCH_REPLACE_FUNC_NAME, nil, {
144+
timeout = opts.timeout,
145+
mode = 'write',
146+
iter = iter,
147+
postprocessor = postprocessor,
148+
})
149+
150+
if next(rows) == nil then
151+
return nil, errs
152+
end
153+
154+
local res, err = utils.format_result(rows, space, opts.fields)
155+
if err ~= nil then
156+
return nil, {err}
157+
end
158+
159+
return res, errs
160+
end
161+
162+
--- Batch replace tuples to the specified space
163+
--
164+
-- @function tuples_batch
165+
--
166+
-- @param string space_name
167+
-- A space name
168+
--
169+
-- @param table tuples
170+
-- Tuples
171+
--
172+
-- @tparam ?table opts
173+
-- Options of batch_replace.tuples_batch
174+
--
175+
-- @return[1] tuples
176+
-- @treturn[2] nil
177+
-- @treturn[2] table of tables Error description
178+
179+
function batch_replace.tuples_batch(space_name, tuples, opts)
180+
checks('string', 'table', {
181+
timeout = '?number',
182+
fields = '?table',
183+
add_space_schema_hash = '?boolean',
184+
stop_on_error = '?boolean',
185+
rollback_on_error = '?boolean',
186+
})
187+
188+
return schema.wrap_func_reload(call_batch_replace_on_router, space_name, tuples, opts)
189+
end
190+
191+
--- Batch replace objects to the specified space
192+
--
193+
-- @function objects_batch
194+
--
195+
-- @param string space_name
196+
-- A space name
197+
--
198+
-- @param table objs
199+
-- Objects
200+
--
201+
-- @tparam ?table opts
202+
-- Options of batch_insert.tuples_batch
203+
--
204+
-- @return[1] objects
205+
-- @treturn[2] nil
206+
-- @treturn[2] table of tables Error description
207+
208+
function batch_replace.objects_batch(space_name, objs, opts)
209+
checks('string', 'table', {
210+
timeout = '?number',
211+
fields = '?table',
212+
stop_on_error = '?boolean',
213+
rollback_on_error = '?boolean',
214+
})
215+
216+
-- insert can fail if router uses outdated schema to flatten object
217+
opts = utils.merge_options(opts, {add_space_schema_hash = true})
218+
219+
local tuples = {}
220+
local errs = {}
221+
222+
for _, obj in ipairs(objs) do
223+
224+
local tuple, err = utils.flatten_obj_reload(space_name, obj)
225+
if err ~= nil then
226+
local err_obj = BatchReplaceError:new("Failed to flatten object: %s", err)
227+
err_obj.tuple = obj
228+
229+
if opts.stop_on_error == true then
230+
return nil, {err_obj}
231+
end
232+
233+
table.insert(errs, err_obj)
234+
end
235+
236+
table.insert(tuples, tuple)
237+
end
238+
239+
if next(errs) ~= nil then
240+
return nil, errs
241+
end
242+
243+
return batch_replace.tuples_batch(space_name, tuples, opts)
244+
end
245+
246+
return batch_replace

0 commit comments

Comments
 (0)