[Tarantool-patches] [PATCH] vinyl: check upsert doesn't modify PK right after execution

Vladislav Shpilevoy v.shpilevoy at tarantool.org
Sat Jun 20 20:32:27 MSK 2020


Hi! Thanks for the patch!

See 2 comments below.

On 18/06/2020 14:10, Nikita Pettik wrote:
> vy_upsert_apply() consists of two parts: execution and squash.
> Assume we have two upserts passing to vy_apply_upsert():
> s1(key1, ops1) (old one) and s2(key1, ops2) (new one).
> To execute upsert s2 we fetch key1 from s1 and attempt at applying ops2
> on key1. Note that in case of memtx engine key1 is always whole tuple
> fetched from index. Meanwhile in vinyl for the sake of performance we
> don't seek tuple in index. As a result key1 can contain less fields than
> the whole tuple stored in index. So when passing -1 as field index of
> upsert operation, tuple update routine may modify key value.
> For instance:
> 
> s:insert{1, 1} -- stored on disk; pk is the first field
> s:upsert({1}, {{'+', 2, 200}}) -- s1
> s:upsert({1}, {{'=', -1, 100}}) -- s2
> 
> In this case {'=', -1, 100} operation is applied to {1} key.
> Result of operation is {100} - in other words - primary key has been
> modified. As far as upserts modifying PK are ignored, this upsert is
> skipped.
> 
> To resolve this problem let's check if PK is changed right after
> tuple execution procedure. If it is, 'rollback' effects of execution and
> continue processing upserts squash using unmodified key:
> 
> squash(s1, s2) --> s3(key1, {ops1, ops2})
> 
> The only drawback of this approach is that if one of upserts *really*
> modifies PK of tuple stored on disk, all squashed operations will be
> skipped as well. For example:
> 
> s:upsert({1}, {{'+', 2, 100}})
> s:upsert({1}, {{'=', -2, 200}})
> 
> These two upserts are squashed into one: ({1}, {{'+', 2, 100}, {'=', -2, 200}}
> The latter fails to be applied so neither of upserts will be executed
> (despite the fact that the first one is absolutely OK).
> 
> Closes #5087
> ---
> Branch: https://github.com/tarantool/tarantool/tree/np/gh-5087-upsert-negative-index
> Issue: https://github.com/tarantool/tarantool/issues/5087
> 
>  src/box/vy_upsert.c                           |  67 +++++----
>  .../gh-5087-upsert-negative-fieldno.result    | 131 ++++++++++++++++++
>  .../gh-5087-upsert-negative-fieldno.test.lua  |  48 +++++++
>  3 files changed, 221 insertions(+), 25 deletions(-)
>  create mode 100644 test/vinyl/gh-5087-upsert-negative-fieldno.result
>  create mode 100644 test/vinyl/gh-5087-upsert-negative-fieldno.test.lua
> 
> diff --git a/src/box/vy_upsert.c b/src/box/vy_upsert.c
> index 007921bb2..210b7e3a4 100644
> --- a/src/box/vy_upsert.c
> +++ b/src/box/vy_upsert.c
> @@ -118,33 +118,63 @@ vy_apply_upsert(const struct tuple *new_stmt, const struct tuple *old_stmt,
>  	/*
>  	 * Apply new operations to the old stmt
>  	 */
> -	const char *result_mp;
> +	const char *old_mp = NULL;
> +	uint32_t old_mp_size = 0;
>  	if (vy_stmt_type(old_stmt) == IPROTO_UPSERT)
> -		result_mp = vy_upsert_data_range(old_stmt, &mp_size);
> +		old_mp = vy_upsert_data_range(old_stmt, &old_mp_size);
>  	else
> -		result_mp = tuple_data_range(old_stmt, &mp_size);
> -	const char *result_mp_end = result_mp + mp_size;
> -	struct tuple *result_stmt = NULL;
> +		old_mp = tuple_data_range(old_stmt, &old_mp_size);
> +	const char *old_mp_end = old_mp + old_mp_size;
>  	struct region *region = &fiber()->gc;
>  	size_t region_svp = region_used(region);
>  	uint8_t old_type = vy_stmt_type(old_stmt);
>  	uint64_t column_mask = COLUMN_MASK_FULL;
> -	result_mp = tuple_upsert_execute(vy_update_alloc, region, new_ops,
> -					 new_ops_end, result_mp, result_mp_end,
> -					 &mp_size, 0, suppress_error,
> -					 &column_mask);
> +	uint32_t result_mp_size = 0;
> +	const char *result_mp =
> +		tuple_upsert_execute(vy_update_alloc, region, new_ops,
> +				     new_ops_end, old_mp, old_mp_end,
> +				     &result_mp_size, 0, suppress_error,
> +				     &column_mask);
>  	if (result_mp == NULL) {
>  		region_truncate(region, region_svp);
>  		return NULL;
>  	}
> -	result_mp_end = result_mp + mp_size;
> +
> +	const char *result_mp_end = result_mp + result_mp_size;
> +	/*
> +	 * tuple_upsert_execute() may modify PK field.
> +	 * So before moving to further upsert processing, let's
> +	 * check if it's true. If so, rollback *_execute effects
> +	 * and ignore this UPSERT.
> +	 */
> +	if (unlikely(!key_update_can_be_skipped(cmp_def->column_mask,
> +						column_mask))) {
> +		if (vy_tuple_compare_with_raw_key(old_stmt, result_mp,
> +						  cmp_def) != 0) {

1. You probably wanted to compare two tuples, not tuple and a key.
Because result_mp is not a key. I wrote a test on it which works
differently on memtx and vinyl, uses only valid upserts.

	box.cfg{}
	engine = 'vinyl'
	s = box.schema.create_space('test', {engine = engine})
	pk = s:create_index('pk', {parts = {{2, 'unsigned'}}})
	s:insert({1, 1, 1})
	box.snapshot()
	os.exit()

	box.cfg{}
	s = box.space.test
	s:upsert({1, 1}, {{'=', 1, 100}})
	s:upsert({2, 1}, {{'=', 2, 1}})
	box.snapshot()
	s:select()

Vinyl output:

	---
	- - [1, 1, 1]
	...

Change engine to 'memtx':

	---
	- - [100, 1, 1]
	...

In some cases it may crash, if tuple_compare_with_key_slowpath would
be used, because it validates, that part_count <= key_def->part_count
in an assertion.

> +			result_mp = old_mp;
> +			result_mp_end = old_mp_end;
> +			region_truncate(region, region_svp);
> +			if (!suppress_error) {
> +				say_error("UPSERT operation failed: "
> +					  "primary key is not allowed to be "
> +					  "modified");

2. The error is printed even when the upserts are perfectly valid, like
in the ticket. I don't think it is ok.


More information about the Tarantool-patches mailing list