Skip to content

Commit 2a5186b

Browse files
committed
driver: fix duplicate id error with mvvc on
Taking the maximum of the index is an implicit transactions, so it is always done with 'read-confirmed' mvcc isolation level. It can lead to errors when trying to make parallel 'put' calls with mvcc enabled. It is hapenning because 'max' for several puts in parallel will be the same since read confirmed isolation level makes visible all transactions that finished the commit. To fix it we wrap it with box.begin/commit and set right isolation level. Closes #207
1 parent 16e3b74 commit 2a5186b

File tree

5 files changed

+180
-4
lines changed

5 files changed

+180
-4
lines changed

queue/abstract/driver/fifo.lua

+33-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ local state = require('queue.abstract.state')
22

33
local num_type = require('queue.compat').num_type
44
local str_type = require('queue.compat').str_type
5+
local check_version = require('queue.compat').check_version
56

67
local tube = {}
78
local method = {}
@@ -66,9 +67,40 @@ function method.normalize_task(self, task)
6667
return task
6768
end
6869

70+
-- check if mvcc is enabled
71+
-- returns true if mvcc is enabled
72+
local function check_mvcc_state()
73+
if not check_version({2, 6, 1}) then
74+
return false
75+
end
76+
77+
if box.cfg.memtx_use_mvcc_engine then
78+
return true
79+
end
80+
81+
return false
82+
end
83+
6984
-- put task in space
7085
function method.put(self, data, opts)
71-
local max = self.space.index.task_id:max()
86+
local max
87+
88+
-- taking the maximum of the index is an implicit transactions, so it is
89+
-- always done with 'read-confirmed' mvcc isolation level.
90+
-- It can lead to errors when trying to make parallel 'put' calls with mvcc enabled.
91+
-- It is hapenning because 'max' for several puts in parallel will be the same since
92+
-- read confirmed isolation level makes visible all transactions that finished the commit.
93+
-- To fix it we wrap it with box.begin/commit and set right isolation level.
94+
-- See https://github.com/tarantool/queue/issues/207
95+
-- See https://www.tarantool.io/ru/doc/latest/concepts/atomic/txn_mode_mvcc/
96+
if check_mvcc_state() then
97+
box.begin({txn_isolation = 'read-committed'})
98+
max = self.space.index.task_id:max()
99+
box.commit()
100+
else
101+
max = self.space.index.task_id:max()
102+
end
103+
72104
local id = max and max[1] + 1 or 0
73105
local task = self.space:insert{id, state.READY, data}
74106
self.on_task_change(task, 'put')

queue/abstract/driver/fifottl.lua

+28-1
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,36 @@ function method.normalize_task(self, task)
207207
return task and task:transform(3, 5)
208208
end
209209

210+
-- check if mvcc is enabled
211+
local function check_mvcc_state()
212+
local cfg = box.cfg
213+
if cfg['memtx_use_mvcc_engine'] == nil or cfg['memtx_use_mvcc_engine'] == false then
214+
return false
215+
end
216+
217+
return true
218+
end
219+
210220
-- put task in space
211221
function method.put(self, data, opts)
212-
local max = self.space.index.task_id:max()
222+
local max
223+
224+
-- taking the maximum of the index is an implicit transactions, so it is
225+
-- always done with 'read-confirmed' mvcc isolation level.
226+
-- It can lead to errors when trying to make parallel 'put' calls with mvcc enabled.
227+
-- It is hapenning because 'max' for several puts in parallel will be the same since
228+
-- read confirmed isolation level makes visible all transactions that finished the commit.
229+
-- To fix it we wrap it with box.begin/commit and set right isolation level.
230+
-- See https://github.com/tarantool/queue/issues/207
231+
-- See https://www.tarantool.io/ru/doc/latest/concepts/atomic/txn_mode_mvcc/
232+
if check_mvcc_state() then
233+
box.begin({txn_isolation = 'read-committed'})
234+
max = self.space.index.task_id:max()
235+
box.commit()
236+
else
237+
max = self.space.index.task_id:max()
238+
end
239+
213240
local id = max and max[i_id] + 1 or 0
214241

215242
local status

queue/abstract/driver/utube.lua

+28-1
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,36 @@ function method.normalize_task(self, task)
7373
return task and task:transform(3, 1)
7474
end
7575

76+
-- check if mvcc is enabled
77+
local function check_mvcc_state()
78+
local cfg = box.cfg
79+
if cfg['memtx_use_mvcc_engine'] == nil or cfg['memtx_use_mvcc_engine'] == false then
80+
return false
81+
end
82+
83+
return true
84+
end
85+
7686
-- put task in space
7787
function method.put(self, data, opts)
78-
local max = self.space.index.task_id:max()
88+
local max
89+
90+
-- taking the maximum of the index is an implicit transactions, so it is
91+
-- always done with 'read-confirmed' mvcc isolation level.
92+
-- It can lead to errors when trying to make parallel 'put' calls with mvcc enabled.
93+
-- It is hapenning because 'max' for several puts in parallel will be the same since
94+
-- read confirmed isolation level makes visible all transactions that finished the commit.
95+
-- To fix it we wrap it with box.begin/commit and set right isolation level.
96+
-- See https://github.com/tarantool/queue/issues/207
97+
-- See https://www.tarantool.io/ru/doc/latest/concepts/atomic/txn_mode_mvcc/
98+
if check_mvcc_state() then
99+
box.begin({txn_isolation = 'read-committed'})
100+
max = self.space.index.task_id:max()
101+
box.commit()
102+
else
103+
max = self.space.index.task_id:max()
104+
end
105+
79106
local id = max and max[1] + 1 or 0
80107
local task = self.space:insert{id, state.READY, tostring(opts.utube), data}
81108
self.on_task_change(task, 'put')

queue/abstract/driver/utubettl.lua

+28-1
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,36 @@ function method.normalize_task(self, task)
215215
return task and task:transform(i_next_event, i_data - i_next_event)
216216
end
217217

218+
-- check if mvcc is enabled
219+
local function check_mvcc_state()
220+
local cfg = box.cfg
221+
if cfg['memtx_use_mvcc_engine'] == nil or cfg['memtx_use_mvcc_engine'] == false then
222+
return false
223+
end
224+
225+
return true
226+
end
227+
218228
-- put task in space
219229
function method.put(self, data, opts)
220-
local max = self.space.index.task_id:max()
230+
local max
231+
232+
-- taking the maximum of the index is an implicit transactions, so it is
233+
-- always done with 'read-confirmed' mvcc isolation level.
234+
-- It can lead to errors when trying to make parallel 'put' calls with mvcc enabled.
235+
-- It is hapenning because 'max' for several puts in parallel will be the same since
236+
-- read confirmed isolation level makes visible all transactions that finished the commit.
237+
-- To fix it we wrap it with box.begin/commit and set right isolation level.
238+
-- See https://github.com/tarantool/queue/issues/207
239+
-- See https://www.tarantool.io/ru/doc/latest/concepts/atomic/txn_mode_mvcc/
240+
if check_mvcc_state() then
241+
box.begin({txn_isolation = 'read-committed'})
242+
max = self.space.index.task_id:max()
243+
box.commit()
244+
else
245+
max = self.space.index.task_id:max()
246+
end
247+
221248
local id = max and max[i_id] + 1 or 0
222249

223250
local status

t/220-mvcc.t

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env tarantool
2+
local qc = require('queue.compat')
3+
local log = require('log')
4+
if not qc.check_version({2, 6, 1}) then
5+
log.info('Tests skipped, tarantool version < 2.6.1')
6+
return
7+
end
8+
local yaml = require('yaml')
9+
local fiber = require('fiber')
10+
11+
local test = require('tap').test()
12+
test:plan(6)
13+
14+
local queue = require('queue')
15+
local state = require('queue.abstract.state')
16+
17+
local tnt = require('t.tnt')
18+
tnt.cfg{memtx_use_mvcc_engine = true}
19+
20+
local engine = 'memtx'
21+
22+
test:ok(rawget(box, 'space'), 'box started')
23+
test:ok(queue, 'queue is loaded')
24+
25+
local tube = queue.create_tube('test', 'fifo', { engine = engine })
26+
test:ok(tube, 'test tube created')
27+
test:is(tube.name, 'test', 'tube.name')
28+
test:is(tube.type, 'fifo', 'tube.type')
29+
30+
test:test('concurent take', function(test)
31+
test:plan(151)
32+
33+
local channel = fiber.channel(1000)
34+
test:ok(channel, 'channel created')
35+
36+
local res = {}
37+
for i = 1, 50 do
38+
fiber.create(function(i)
39+
local taken = tube:take(1)
40+
test:ok(taken, 'Task was taken ' .. i)
41+
table.insert(res, { taken })
42+
channel:put(true)
43+
end, i)
44+
end
45+
46+
fiber.sleep(.1)
47+
test:ok(tube:put(1), 'task 1 was put')
48+
49+
for i = 2, 50 do
50+
fiber.create(function(i)
51+
test:ok(tube:put(i), 'task ' .. i .. ' was put')
52+
end, i)
53+
end
54+
fiber.sleep(.1)
55+
for i = 1, 50 do
56+
test:ok(channel:get(1 / i), 'take was done ' .. i)
57+
end
58+
end)
59+
60+
61+
tnt.finish()
62+
os.exit(test:check() and 0 or 1)
63+
-- vim: set ft=lua:

0 commit comments

Comments
 (0)