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

Commit 52e393e

Browse files
committed
Handle unique index constraint violation in shard
Fixes #188.
1 parent 428e8ec commit 52e393e

File tree

5 files changed

+291
-63
lines changed

5 files changed

+291
-63
lines changed

graphql/accessor_shard.lua

Lines changed: 77 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,13 @@ local index_info_cache = {}
2223

2324
local function shard_check_error(func_name, result, err)
2425
if result ~= nil then return end
26+
27+
-- show an error in the same format as accessor_space does
28+
if type(err) == 'table' and type(err.error) == 'string' and
29+
err.error:find(':') then
30+
error(err.error:gsub('^[^:]+: *', ''))
31+
end
32+
2533
error(('%s: %s'):format(func_name, json.encode(err)))
2634
end
2735

@@ -215,6 +223,12 @@ local function space_operation(collection_name, nodes, operation, ...)
215223
return master_result
216224
end
217225

226+
local function get_shard_key_hash(key)
227+
local shards_n = #shard.shards
228+
local num = type(key) == 'number' and key or digest.crc32(key)
229+
return 1 + digest.guava(num, shards_n)
230+
end
231+
218232
-- }}}
219233

220234
--- Check whether a collection (it is sharded space for that accessor) exists.
@@ -395,6 +409,40 @@ end
395409

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

421469
shard_check_status(func_name)
422470

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-
433471
-- We follow tarantool convention and disallow update of primary key parts.
434472
local primary_index_info = get_index_info(collection_name, 0)
435473
for _, statement in ipairs(statements) do
@@ -443,14 +481,37 @@ local function update_tuple(self, collection_name, key, statements, opts)
443481
end
444482
end
445483

484+
local is_shard_key_to_be_updated = false
485+
local new_shard_key_value
486+
for _, statement in ipairs(statements) do
487+
-- statement is {operator, field_no, value}
488+
local field_no = statement[2]
489+
if field_no == SHARD_KEY_FIELD_NO then
490+
is_shard_key_to_be_updated = true
491+
new_shard_key_value = statement[3]
492+
break
493+
end
494+
end
495+
496+
local tuple = opts.tuple or get_tuple(self, collection_name, key)
497+
498+
local is_storage_to_be_changed = false
446499
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)
500+
local old_shard_key_value = tuple[1]
501+
local old_shard_key_hash = get_shard_key_hash(old_shard_key_value)
502+
local new_shard_key_hash = get_shard_key_hash(new_shard_key_value)
503+
is_storage_to_be_changed = old_shard_key_hash ~= new_shard_key_hash
504+
end
505+
506+
if is_storage_to_be_changed then
507+
-- different storages case
508+
local old_tuple = opts.tuple or get_tuple(self, collection_name, key)
509+
local new_tuple = old_tuple:update(statements)
510+
self.funcs.insert_tuple(self, collection_name, new_tuple)
511+
self.funcs.delete_tuple(self, collection_name, key, {tuple = old_tuple})
512+
return new_tuple
451513
else
452-
local tuple = opts.tuple or get_tuple(self, collection_name,
453-
key)
514+
-- one storage case
454515
local nodes = shard.shard(tuple[SHARD_KEY_FIELD_NO])
455516
local tuple = space_operation(collection_name, nodes, 'update', key,
456517
statements)

0 commit comments

Comments
 (0)