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

Commit 742c93f

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

File tree

5 files changed

+297
-63
lines changed

5 files changed

+297
-63
lines changed

graphql/accessor_shard.lua

Lines changed: 83 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,46 @@ 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+
--- a unique index constraints check within the storage, but not on the overall
422+
--- cluster. In case when the tuple changes its storage the insert operation
423+
--- 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 delete' 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 in the following ways:
440+
---
441+
--- * as update (because tarantool forbids update of a primary key),
442+
--- * 'insert, then delete' way (because insert can report a unique index
443+
--- constraint violation due to values in the old version of the tuple),
444+
--- * 'tuple:update(), then replace' (at least because old tuple resides in the
445+
--- storage and because an other tuple can be silently rewritten).
446+
---
447+
--- To support primary key update for **one storage** case one can use 'delete,
448+
--- then insert' way and perform the rollback action (insert old tuple) in case
449+
--- when insert of the new tuple reports an error. There are other ways, e.g.
450+
--- manual unique constraints check.
451+
---
398452
--- @tparam table self accessor_general instance
399453
---
400454
--- @tparam string collection_name
@@ -420,16 +474,6 @@ local function update_tuple(self, collection_name, key, statements, opts)
420474

421475
shard_check_status(func_name)
422476

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-
433477
-- We follow tarantool convention and disallow update of primary key parts.
434478
local primary_index_info = get_index_info(collection_name, 0)
435479
for _, statement in ipairs(statements) do
@@ -443,14 +487,37 @@ local function update_tuple(self, collection_name, key, statements, opts)
443487
end
444488
end
445489

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

0 commit comments

Comments
 (0)