Skip to content

Commit 38921d5

Browse files
committed
drivers: fix concurrency with mvcc
By default idx:max() or idx:min() have read confirmed isolation level. It could lead to a task duplication or double task take when we already insert or update a task, commited, but it is not yet confirmed. See also: 1. tarantool/queue#207 2. tarantool/queue#211
1 parent 4b1aa1b commit 38921d5

File tree

3 files changed

+65
-30
lines changed

3 files changed

+65
-30
lines changed

sharded_queue/drivers/fifo.lua

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ end
7474

7575
-- put task in space
7676
function method.put(args)
77-
local task = box.atomic(function()
77+
local task = utils.atomic(function()
7878
local idx = get_index(args)
7979
local task_id = utils.pack_task_id(
8080
args.bucket_id,
@@ -96,7 +96,7 @@ end
9696

9797
-- take task
9898
function method.take(args)
99-
local task = box.atomic(function()
99+
local task = utils.atomic(function()
100100
local task = get_space(args).index.status:min { state.READY }
101101
if task == nil or task[3] ~= state.READY then
102102
return

sharded_queue/drivers/fifottl.lua

+37-28
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ function method.put(args)
233233
local ttr = args.ttr or args.options.ttr or ttl
234234
local priority = args.priority or args.options.priority or 0
235235

236-
local task = box.atomic(function()
236+
local task = utils.atomic(function()
237237
local idx = get_index(args.tube_name, args.bucket_id)
238238

239239
local next_event
@@ -301,7 +301,7 @@ end
301301

302302
function method.take(args)
303303

304-
local task = box.atomic(take, args)
304+
local task = utils.atomic(take, args)
305305
if task == nil then return end
306306

307307
if args.extra and args.extra.log_request then
@@ -314,10 +314,14 @@ function method.take(args)
314314
end
315315

316316
function method.delete(args)
317-
box.begin()
318-
local task = box.space[args.tube_name]:get(args.task_id)
319-
box.space[args.tube_name]:delete(args.task_id)
320-
box.commit()
317+
local task = utils.atomic(function()
318+
local task = box.space[args.tube_name]:get(args.task_id)
319+
if task ~= nil then
320+
box.space[args.tube_name]:delete(args.task_id)
321+
end
322+
return task
323+
end)
324+
321325
if task ~= nil then
322326
task = task:tomap()
323327
task.status = state.DONE
@@ -355,10 +359,13 @@ function method.touch(args)
355359
end
356360

357361
function method.ack(args)
358-
box.begin()
359-
local task = box.space[args.tube_name]:get(args.task_id)
360-
box.space[args.tube_name]:delete(args.task_id)
361-
box.commit()
362+
local task = utils.atomic(function()
363+
local task = box.space[args.tube_name]:get(args.task_id)
364+
if task ~= nil then
365+
box.space[args.tube_name]:delete(args.task_id)
366+
end
367+
return task
368+
end)
362369
if task ~= nil then
363370
task = task:tomap()
364371
task.status = state.DONE
@@ -385,15 +392,16 @@ function method.peek(args)
385392
end
386393

387394
function method.release(args)
388-
box.begin()
389-
local task = box.space[args.tube_name]:get(args.task_id)
390-
if task ~= nil then
391-
task = box.space[args.tube_name]:update(args.task_id, {
392-
{'=', index.status, state.READY},
393-
{'=', index.next_event, task[index.created] + task[index.ttl]},
394-
})
395-
end
396-
box.commit()
395+
local task = utils.atomic(function()
396+
local task = box.space[args.tube_name]:get(args.task_id)
397+
if task ~= nil then
398+
task = box.space[args.tube_name]:update(args.task_id, {
399+
{'=', index.status, state.READY},
400+
{'=', index.next_event, task[index.created] + task[index.ttl]},
401+
})
402+
end
403+
return task
404+
end)
397405

398406
if args.extra and args.extra.log_request then
399407
log_operation("release", task)
@@ -408,15 +416,16 @@ function method.bury(args)
408416
update_stat(args.tube_name, 'bury')
409417
wc_signal(args.tube_name)
410418

411-
box.begin()
412-
local task = box.space[args.tube_name]:get(args.task_id)
413-
if task ~= nil then
414-
task = box.space[args.tube_name]:update(args.task_id, {
415-
{'=', index.status, state.BURIED},
416-
{'=', index.next_event, task[index.created] + task[index.ttl]},
417-
})
418-
end
419-
box.commit()
419+
local task = utils.atomic(function()
420+
local task = box.space[args.tube_name]:get(args.task_id)
421+
if task ~= nil then
422+
task = box.space[args.tube_name]:update(args.task_id, {
423+
{'=', index.status, state.BURIED},
424+
{'=', index.next_event, task[index.created] + task[index.ttl]},
425+
})
426+
end
427+
return task
428+
end)
420429

421430
if args.extra and args.extra.log_request then
422431
log_operation("bury", task)

sharded_queue/utils.lua

+26
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@ local fiber = require('fiber')
22

33
local utils = {}
44

5+
local function atomic_tail(status, ...)
6+
if not status then
7+
box.rollback()
8+
error((...), 2)
9+
end
10+
box.commit()
11+
return ...
12+
end
13+
14+
-- box.atomic(opts, fun, args) does not supported for all Tarantool's versions,
15+
-- so we an analog.
16+
function utils.atomic(fun, ...)
17+
if box.cfg.memtx_use_mvcc_engine then
18+
-- max() + insert() or min() + update() do not work as expected with
19+
-- best-effort visibility: for write transactions it chooses
20+
-- read-committed, for read transactions it chooses read-confirmed.
21+
--
22+
-- So max()/min() could return the same tuple even if a concurrent
23+
-- insert()/update() has been committed, but has not confirmed yet.
24+
box.begin({txn_isolation = 'read-committed'})
25+
else
26+
box.begin()
27+
end
28+
return atomic_tail(pcall(fun, ...))
29+
end
30+
531
function utils.array_shuffle(array)
632
if not array then return nil end
733
math.randomseed(tonumber(0ULL + fiber.time64()))

0 commit comments

Comments
 (0)