[Tarantool-patches] [PATCH] vinyl: add NULL check of xrow_upsert_execute() retval

Nikita Pettik korablev at tarantool.org
Wed Jul 8 15:53:27 MSK 2020


On 29 May 23:24, Vladislav Shpilevoy wrote:
> Hi! Thanks for the patch!
> 
> While the patch is obviously correct (we need to check NULL
> for sure), it solves the problem only partially, and creates
> another.

Okay, I suggest following. Let's push patch with minor test
changes as is (in scope of 1.10.7 release), but leave issue open.
As a result, we will get rid of crash, and postpone a bit
reconsideration of upsert application till 1.10.8.
We are going to rework upsert (according to the plan defined in #5107
https://github.com/tarantool/tarantool/issues/5107). Here's changed
test (https://github.com/tarantool/tarantool/tree/np/gh-4957-master):

diff --git a/test/vinyl/gh-4957-too-many-upserts.test.lua b/test/vinyl/gh-4957-too-many-upserts.test.lua
new file mode 100644
index 000000000..e5adfe41c
--- /dev/null
+++ b/test/vinyl/gh-4957-too-many-upserts.test.lua
@@ -0,0 +1,38 @@
+s = box.schema.create_space('test', {engine = 'vinyl'})
+pk = s:create_index('pk')
+s:insert{1, 1}
+box.snapshot()
+
+-- Let's test number of upserts in one transaction that exceeds
+-- the limit of operations allowed in one update.
+--
+ups_cnt = 5000
+box.begin()
+for i = 1, ups_cnt do s:upsert({1}, {{'&', 2, 1}}) end
+box.commit()
+-- Upserts are not able to squash, so scheduler will get stuck.
+-- So let's not waste much time here, just check that no crash
+-- takes place.
+--
+box.snapshot()
+
+fiber = require('fiber')
+fiber.sleep(0.01)
+
+s:drop()
+
+s = box.schema.create_space('test', {engine = 'vinyl'})
+pk = s:create_index('pk')
+
+tuple = {}
+for i = 1, ups_cnt do tuple[i] = i end
+_ = s:insert(tuple)
+box.snapshot()
+
+box.begin()
+for k = 1, ups_cnt do s:upsert({1}, {{'+', k, 1}}) end
+box.commit()
+box.snapshot()
+fiber.sleep(0.01)
+

Are you guys okay with this suggestion?
 
> We discussed that verbally, and here is a short resume of what
> is happening in the patch, and where we have a tricky problem:
> if there are 2 perfectly valid upserts, each with 2.5k operations,
> and they are merged into one, both of them are skipped, because
> after merge they become too fat - opcount > 4k.
> 
> It looks at first that this can only happen when field count > 4k,
> because otherwise all the operations would be squashed into something
> smaller or equal than field count, but it is not. There are a few
> cases, when even after squash total operation count will be bigger
> than field count:
> 
> 1) operations are complex - ':', '&', '|', '^', '#', '!'. The last
> two operations are actually used by people. These operations are not
> squashed. The last one - '!' - can't be squashed even in theory.
> 
> 2) operations have negative field number. For example, {'=', -1, ...} -
> assign a value to the last field in the tuple. But honestly I don't
> remember. Perhaps they are merged, if in both squashed upserts the
> field number is the same. But imagine this: {'=', -1, 100}, and
> {'=', 5, 100}. They look different, but if the tuple has only 5 fields,
> they operate on the same field.
> 
> That means it is not safe to drop any upsert having more than 4k
> operations. Because it can consist of many small valid upserts.
> 
> I don't know how to fix it in a simple way. The only thing I could
> come up with is probably don't squash such fat upserts. Just keep
> them all on the disk, until they eventually meet bottom of their key,
> or a terminal statement like REPLACE/INSERT/DELETE.
> 
> This is not only about disk, btw. 2 fat upserts could be inserted into
> the memory level, turn into an invalid upsert, and that will be skipped.
> 
> Here is a test. Create a tuple, and dump it on disk so as it would
> disappear from the memory level and from the cache:
> 
> 	box.cfg{}
> 	s = box.schema.create_space('test', {engine = 'vinyl'})
> 	pk = s:create_index('pk')
> 	s:insert({1, 1})
> 	box.snapshot()
> 
> Then restart (to ensure the cache is clear), and create 2 upserts:
> 
> 	box.cfg{}
> 	s = box.space.test
> 	ops = {}
> 	op = {'=', 2, 100}
> 	for i = 1, 2500 do table.insert(ops, op) end
> 	s:upsert({1}, ops)
> 	op = {'=', -1, 200}
> 	ops = {}
> 	for i = 1, 2500 do table.insert(ops, op) end
> 	s:upsert({1}, ops)
> 
> Now if I do select, I get
> 
> 	tarantool> s:select{}
> 	---
> 	- - [1, 200]
> 	...
> 
> But if I do dump + select, I get:
> 
> 	tarantool> box.snapshot()
> 	---
> 	- ok
> 	...
> 
> 	tarantool> s:select{}
> 	---
> 	- - [1, 100]
> 	...
> 
> During dump the second upsert was skipped even though it was valid.


More information about the Tarantool-patches mailing list