From: Vladislav Shpilevoy <v.shpilevoy@tarantool.org> To: Nikita Pettik <korablev@tarantool.org>, tarantool-patches@dev.tarantool.org Subject: Re: [Tarantool-patches] [PATCH] vinyl: check upsert doesn't modify PK right after execution Date: Sat, 20 Jun 2020 19:32:27 +0200 [thread overview] Message-ID: <80cedca6-159a-6c6a-6970-9891cdfa0c9c@tarantool.org> (raw) In-Reply-To: <7c98fabb09bef52546b592ce5603e7e69b3ffca1.1592480615.git.korablev@tarantool.org> 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.
prev parent reply other threads:[~2020-06-20 17:32 UTC|newest] Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top 2020-06-18 12:10 Nikita Pettik 2020-06-20 17:32 ` Vladislav Shpilevoy [this message]
Reply instructions: You may reply publicly to this message via plain-text email using any one of the following methods: * Save the following mbox file, import it into your mail client, and reply-to-all from there: mbox Avoid top-posting and favor interleaved quoting: https://en.wikipedia.org/wiki/Posting_style#Interleaved_style * Reply using the --to, --cc, and --in-reply-to switches of git-send-email(1): git send-email \ --in-reply-to=80cedca6-159a-6c6a-6970-9891cdfa0c9c@tarantool.org \ --to=v.shpilevoy@tarantool.org \ --cc=korablev@tarantool.org \ --cc=tarantool-patches@dev.tarantool.org \ --subject='Re: [Tarantool-patches] [PATCH] vinyl: check upsert doesn'\''t modify PK right after execution' \ /path/to/YOUR_REPLY https://kernel.org/pub/software/scm/git/docs/git-send-email.html * If your mail client supports setting the In-Reply-To header via mailto: links, try the mailto: link
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox