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

Commit 087502a

Browse files
authored
Merge pull request #193 from tarantool/gh-188-handle-unique-index-contraint-violation
Handle unique index constraint violation in shard
2 parents 342c997 + b5ea0d2 commit 087502a

File tree

6 files changed

+308
-65
lines changed

6 files changed

+308
-65
lines changed

graphql/accessor_general.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ local DEF_TIMEOUT_MS = 1000
3535
-- save start time at start, calculate current time on each iteration and
3636
-- substract the start time from it, compare with the timeout. With such
3737
-- approch we don't add the timeout in nanoseconds to a start time and can
38-
-- remove the divide by two below.
39-
local TIMEOUT_INFINITY = 18446744073709551615ULL / (2 * 10^6) -- microseconds
38+
-- remove the divide by two below. The value is roughly equal to 292 years.
39+
local TIMEOUT_INFINITY = 18446744073709551615ULL / (2 * 10^6) -- milliseconds
4040

4141
accessor_general.TIMEOUT_INFINITY = TIMEOUT_INFINITY
4242

graphql/accessor_shard.lua

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
local json = require('json')
66
local yaml = require('yaml')
7+
local digest = require('digest')
78
local utils = require('graphql.utils')
89
local shard = utils.optional_require('shard')
910
local accessor_general = require('graphql.accessor_general')
@@ -22,6 +23,18 @@ local index_info_cache = {}
2223

2324
local function shard_check_error(func_name, result, err)
2425
if result ~= nil then return end
26+
27+
-- avoid json encoding of an error message (when the error is in the known
28+
-- format)
29+
if type(err) == 'table' and type(err.error) == 'string' then
30+
error({
31+
message = err.error,
32+
extensions = {
33+
shard_error = err,
34+
}
35+
})
36+
end
37+
2538
error(('%s: %s'):format(func_name, json.encode(err)))
2639
end
2740

@@ -215,6 +228,12 @@ local function space_operation(collection_name, nodes, operation, ...)
215228
return master_result
216229
end
217230

231+
local function get_shard_key_hash(key)
232+
local shards_n = #shard.shards
233+
local num = type(key) == 'number' and key or digest.crc32(key)
234+
return 1 + digest.guava(num, shards_n)
235+
end
236+
218237
-- }}}
219238

220239
--- Check whether a collection (it is sharded space for that accessor) exists.
@@ -395,6 +414,46 @@ end
395414

396415
--- Update a tuple with an update statements.
397416
---
417+
--- In case when the update should change the storage where the tuple stored
418+
--- we perform insert to the new storage and delete from the old one. The
419+
--- order must be 'first insert, then delete', because insert can report an
420+
--- error in case of unique index constraints violation and we must not
421+
--- perform delete in the case.
422+
---
423+
--- This function emulates (more or less preciselly, see below) behaviour of
424+
--- update as if it would be performed on a local tarantool instance. In case
425+
--- when the tuple resides on the same storage the update operation performs
426+
--- a unique index constraints check within the storage, but not on the overall
427+
--- cluster. In case when the tuple changes its storage the insert operation
428+
--- performs the check within the target storage.
429+
---
430+
--- We can consider this as relaxing of the constraints: the function can
431+
--- silently violate cluster-wide uniqueness constraints or report a
432+
--- violation that was introduced by some previous operation, but cannot
433+
--- report a violation when a local tarantool space would not.
434+
---
435+
--- 'Insert, then delete' approach is applicable and do not lead to a false
436+
--- positive unique index constraint violation when storage nodes are different
437+
--- and do not contain same tuples. We check the first condition in the
438+
--- function and the second is guaranteed by the shard module.
439+
---
440+
--- Note: if one want to use this function as basis for a similar one, but
441+
--- allowing update of a primary key the following details should be noticed. A
442+
--- primary key update that **changes a storage** where the tuple saved can be
443+
--- performed with the 'insert, then delete' approach. An update **within one
444+
--- storage** cannot be performed in the following ways:
445+
---
446+
--- * as update (because tarantool forbids update of a primary key),
447+
--- * 'insert, then delete' way (because insert can report a unique index
448+
--- constraint violation due to values in the old version of the tuple),
449+
--- * 'tuple:update(), then replace' (at least because old tuple resides in the
450+
--- storage and because an other tuple can be silently rewritten).
451+
---
452+
--- To support primary key update for **one storage** case one can use 'delete,
453+
--- then insert' way and perform the rollback action (insert old tuple) in case
454+
--- when insert of the new tuple reports an error. There are other ways, e.g.
455+
--- manual unique constraints check.
456+
---
398457
--- @tparam table self accessor_general instance
399458
---
400459
--- @tparam string collection_name
@@ -420,16 +479,6 @@ local function update_tuple(self, collection_name, key, statements, opts)
420479

421480
shard_check_status(func_name)
422481

423-
local is_shard_key_to_be_updated = false
424-
for _, statement in ipairs(statements) do
425-
-- statement is {operator, field_no, value}
426-
local field_no = statement[2]
427-
if field_no == SHARD_KEY_FIELD_NO then
428-
is_shard_key_to_be_updated = true
429-
break
430-
end
431-
end
432-
433482
-- We follow tarantool convention and disallow update of primary key parts.
434483
local primary_index_info = get_index_info(collection_name, 0)
435484
for _, statement in ipairs(statements) do
@@ -443,14 +492,37 @@ local function update_tuple(self, collection_name, key, statements, opts)
443492
end
444493
end
445494

495+
local is_shard_key_to_be_updated = false
496+
local new_shard_key_value
497+
for _, statement in ipairs(statements) do
498+
-- statement is {operator, field_no, value}
499+
local field_no = statement[2]
500+
if field_no == SHARD_KEY_FIELD_NO then
501+
is_shard_key_to_be_updated = true
502+
new_shard_key_value = statement[3]
503+
break
504+
end
505+
end
506+
507+
local tuple = opts.tuple or get_tuple(self, collection_name, key)
508+
509+
local is_storage_to_be_changed = false
446510
if is_shard_key_to_be_updated then
447-
local tuple = self.funcs.delete_tuple(self, collection_name, key,
448-
{tuple = opts.tuple})
449-
tuple = tuple:update(statements)
450-
return self.funcs.insert_tuple(self, collection_name, tuple)
511+
local old_shard_key_value = tuple[1]
512+
local old_shard_key_hash = get_shard_key_hash(old_shard_key_value)
513+
local new_shard_key_hash = get_shard_key_hash(new_shard_key_value)
514+
is_storage_to_be_changed = old_shard_key_hash ~= new_shard_key_hash
515+
end
516+
517+
if is_storage_to_be_changed then
518+
-- different storages case
519+
local old_tuple = opts.tuple or get_tuple(self, collection_name, key)
520+
local new_tuple = old_tuple:update(statements)
521+
self.funcs.insert_tuple(self, collection_name, new_tuple)
522+
self.funcs.delete_tuple(self, collection_name, key, {tuple = old_tuple})
523+
return new_tuple
451524
else
452-
local tuple = opts.tuple or get_tuple(self, collection_name,
453-
key)
525+
-- one storage case
454526
local nodes = shard.shard(tuple[SHARD_KEY_FIELD_NO])
455527
local tuple = space_operation(collection_name, nodes, 'update', key,
456528
statements)

0 commit comments

Comments
 (0)