Skip to content

Commit 8b14921

Browse files
committed
utube: fix slow take on busy utubes
If some of the utube for tasks at the top of the queue were busy most of the time, `take` would slow down for every other task. This problem is fixed by creating a new space `space_ready`. It contains first task with `READY` status from each utube. This solution shows great results for the stated problem, with the cost of slowing the `put` method (it is ~3 times slower). Thus, this workaround is disabled by default. To enable it, user should set the `v2 = true` as an option while creating the tube. As example: ```lua local test_queue = queue.create_tube('test_queue', 'utube', {temporary = true, v2 = true}) ``` Part of #228
1 parent aa7c092 commit 8b14921

File tree

5 files changed

+325
-34
lines changed

5 files changed

+325
-34
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
### Added
11+
- `v2` boolean option for creating a `utube` tube (#228). It enables the
12+
workaround for slow takes while working with busy tubes.
13+
1014
### Fixed
1115

1216
- Stuck in `INIT` state if an instance failed to enter the `running` mode
1317
in time (#226). This fix works only for Tarantool versions >= 2.10.0.
18+
- Slow takes on busy `utube` tubes (#228). The workaround could be enabled by
19+
passing the `v2 = true` option while creating the tube.
1420

1521
## [1.3.3] - 2023-09-13
1622

queue/abstract/driver/utube.lua

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,35 @@ function tube.create_space(space_name, opts)
5353
type = 'tree',
5454
parts = {2, str_type(), 3, str_type(), 1, num_type()}
5555
})
56+
space.v2 = opts.v2
5657
return space
5758
end
5859

5960
-- start tube on space
6061
function tube.new(space, on_task_change)
6162
validate_space(space)
6263

64+
local space_ready_name = space.name .. "_utube_ready"
65+
local space_ready = box.space[space_ready_name]
66+
if space.v2 and not space_ready then
67+
-- Create a space for first ready tasks from each utube.
68+
space_ready = box.schema.create_space(space_ready_name, space_opts)
69+
space_ready:create_index('task_id', {
70+
type = 'tree',
71+
parts = {1, num_type()}
72+
})
73+
space_ready:create_index('utube', {
74+
type = 'tree',
75+
parts = {2, str_type()}
76+
})
77+
end
78+
6379
on_task_change = on_task_change or (function() end)
6480
local self = setmetatable({
6581
space = space,
82+
space_ready = space_ready,
6683
on_task_change = on_task_change,
84+
v2 = space.v2 or false,
6785
}, { __index = method })
6886
return self
6987
end
@@ -73,6 +91,20 @@ function method.normalize_task(self, task)
7391
return task and task:transform(3, 1)
7492
end
7593

94+
local function put_ready_task(self, id, utube)
95+
local added = self.space_ready.index.utube:get{utube}
96+
if added == nil then
97+
self.space_ready:insert{id, utube}
98+
end
99+
end
100+
101+
local function put_next_ready(self, utube)
102+
local next_task = self.space.index.utube:min{state.READY, utube}
103+
if next_task ~= nil then
104+
put_ready_task(self, next_task[1], utube)
105+
end
106+
end
107+
76108
-- put task in space
77109
function method.put(self, data, opts)
78110
local max
@@ -98,12 +130,62 @@ function method.put(self, data, opts)
98130

99131
local id = max and max[1] + 1 or 0
100132
local task = self.space:insert{id, state.READY, tostring(opts.utube), data}
133+
if self.v2 then
134+
put_ready_task(self, id, task[3])
135+
end
101136
self.on_task_change(task, 'put')
102137
return task
103138
end
104139

140+
local function delete_ready(self, id, utube)
141+
self.space_ready:delete(id)
142+
put_next_ready(self, utube)
143+
end
144+
145+
local function take_ready(self)
146+
for s, task_ready in self.space_ready:pairs({}, { iterator = 'GE' }) do
147+
local id = task_ready[1]
148+
local commit_requirements = box.cfg.memtx_use_mvcc_engine and
149+
(not box.is_in_txn())
150+
local task
151+
152+
if commit_requirements then
153+
box.begin({txn_isolation = 'read-committed'})
154+
task = self.space:get(id)
155+
box.commit()
156+
else
157+
task = self.space:get(id)
158+
end
159+
160+
if task[2] == state.READY then
161+
local taken
162+
163+
if commit_requirements then
164+
box.begin({txn_isolation = 'read-committed'})
165+
taken = self.space.index.utube:min{state.TAKEN, task[3]}
166+
box.commit()
167+
else
168+
taken = self.space.index.utube:min{state.TAKEN, task[3]}
169+
end
170+
171+
if taken == nil or taken[2] ~= state.TAKEN then
172+
task = self.space:update(id, { { '=', 2, state.TAKEN } })
173+
174+
delete_ready(self, id, task[3])
175+
176+
self.on_task_change(task, 'take')
177+
return task
178+
end
179+
end
180+
end
181+
end
182+
105183
-- take task
106184
function method.take(self)
185+
if self.v2 then
186+
return take_ready(self)
187+
end
188+
107189
for s, task in self.space.index.status:pairs(state.READY,
108190
{ iterator = 'GE' }) do
109191
if task[2] ~= state.READY then
@@ -146,6 +228,10 @@ function method.delete(self, id)
146228
local task = self.space:get(id)
147229
self.space:delete(id)
148230
if task ~= nil then
231+
if self.v2 then
232+
delete_ready(self, id, task[3])
233+
end
234+
149235
task = task:transform(2, 1, state.DONE)
150236

151237
local neighbour = self.space.index.utube:min{state.READY, task[3]}
@@ -157,10 +243,22 @@ function method.delete(self, id)
157243
return task
158244
end
159245

246+
local function on_ready_status_change(self, utube)
247+
local prev_task = self.space_ready.index.utube:get{utube}
248+
if prev_task ~= nil then
249+
delete_ready(self, prev_task[1], utube)
250+
else
251+
put_next_ready(self, utube)
252+
end
253+
end
254+
160255
-- release task
161256
function method.release(self, id, opts)
162257
local task = self.space:update(id, {{ '=', 2, state.READY }})
163258
if task ~= nil then
259+
if self.v2 then
260+
on_ready_status_change(self, task[3])
261+
end
164262
self.on_task_change(task, 'release')
165263
end
166264
return task
@@ -193,6 +291,9 @@ function method.kick(self, count)
193291
end
194292

195293
task = self.space:update(task[1], {{ '=', 2, state.READY }})
294+
if self.v2 then
295+
on_ready_status_change(self, task[3])
296+
end
196297
self.on_task_change(task, 'kick')
197298
end
198299
return count

t/030-utube.t

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,45 +18,49 @@ test:ok(queue, 'queue is loaded')
1818

1919
local tube = queue.create_tube('test', 'utube', { engine = engine })
2020
local tube2 = queue.create_tube('test_stat', 'utube', { engine = engine })
21+
local tubev2 = queue.create_tube('test_stat_v2', 'utube',
22+
{ engine = engine, v2 = true })
2123
test:ok(tube, 'test tube created')
2224
test:is(tube.name, 'test', 'tube.name')
2325
test:is(tube.type, 'utube', 'tube.type')
2426

2527
test:test('Utube statistics', function(test)
26-
test:plan(13)
27-
tube2:put('stat_0')
28-
tube2:put('stat_1')
29-
tube2:put('stat_2')
30-
tube2:put('stat_3')
31-
tube2:put('stat_4')
32-
tube2:delete(4)
33-
tube2:take(.001)
34-
tube2:release(0)
35-
tube2:take(.001)
36-
tube2:ack(0)
37-
tube2:bury(1)
38-
tube2:bury(2)
39-
tube2:kick(1)
40-
tube2:take(.001)
41-
42-
local stats = queue.statistics('test_stat')
43-
44-
-- check tasks statistics
45-
test:is(stats.tasks.taken, 1, 'tasks.taken')
46-
test:is(stats.tasks.buried, 1, 'tasks.buried')
47-
test:is(stats.tasks.ready, 1, 'tasks.ready')
48-
test:is(stats.tasks.done, 2, 'tasks.done')
49-
test:is(stats.tasks.delayed, 0, 'tasks.delayed')
50-
test:is(stats.tasks.total, 3, 'tasks.total')
51-
52-
-- check function call statistics
53-
test:is(stats.calls.delete, 1, 'calls.delete')
54-
test:is(stats.calls.ack, 1, 'calls.ack')
55-
test:is(stats.calls.take, 3, 'calls.take')
56-
test:is(stats.calls.kick, 1, 'calls.kick')
57-
test:is(stats.calls.bury, 2, 'calls.bury')
58-
test:is(stats.calls.put, 5, 'calls.put')
59-
test:is(stats.calls.release, 1, 'calls.release')
28+
test:plan(26)
29+
for _, tube_stat in ipairs({tube2, tubev2}) do
30+
tube_stat:put('stat_0')
31+
tube_stat:put('stat_1')
32+
tube_stat:put('stat_2')
33+
tube_stat:put('stat_3')
34+
tube_stat:put('stat_4')
35+
tube_stat:delete(4)
36+
tube_stat:take(.001)
37+
tube_stat:release(0)
38+
tube_stat:take(.001)
39+
tube_stat:ack(0)
40+
tube_stat:bury(1)
41+
tube_stat:bury(2)
42+
tube_stat:kick(1)
43+
tube_stat:take(.001)
44+
45+
local stats = queue.statistics('test_stat')
46+
47+
-- check tasks statistics
48+
test:is(stats.tasks.taken, 1, 'tasks.taken')
49+
test:is(stats.tasks.buried, 1, 'tasks.buried')
50+
test:is(stats.tasks.ready, 1, 'tasks.ready')
51+
test:is(stats.tasks.done, 2, 'tasks.done')
52+
test:is(stats.tasks.delayed, 0, 'tasks.delayed')
53+
test:is(stats.tasks.total, 3, 'tasks.total')
54+
55+
-- check function call statistics
56+
test:is(stats.calls.delete, 1, 'calls.delete')
57+
test:is(stats.calls.ack, 1, 'calls.ack')
58+
test:is(stats.calls.take, 3, 'calls.take')
59+
test:is(stats.calls.kick, 1, 'calls.kick')
60+
test:is(stats.calls.bury, 2, 'calls.bury')
61+
test:is(stats.calls.put, 5, 'calls.put')
62+
test:is(stats.calls.release, 1, 'calls.release')
63+
end
6064
end)
6165

6266

t/benchmark/busy_utubes.lua

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/usr/bin/env tarantool
2+
3+
local clock = require('clock')
4+
local os = require('os')
5+
local fiber = require('fiber')
6+
local queue = require('queue')
7+
8+
-- Set the number of consumers.
9+
local consumers_count = 10
10+
-- Set the number of tasks processed by one consumer per iteration.
11+
local batch_size = 150000
12+
13+
local barrier = fiber.cond()
14+
local wait_count = 0
15+
16+
box.cfg()
17+
18+
local test_queue = queue.create_tube('test_queue', 'utube',
19+
{temporary = true, v2 = true})
20+
21+
local function prepare_tasks()
22+
local test_data = 'test data'
23+
24+
for i = 1, consumers_count do
25+
for _ = 1, batch_size do
26+
test_queue:put(test_data, {utube = tostring(i)})
27+
end
28+
end
29+
end
30+
31+
local function prepare_consumers()
32+
local consumers = {}
33+
34+
-- Make half the utubes busy.
35+
for _ = 1, consumers_count / 2 do
36+
test_queue:take()
37+
end
38+
39+
for i = 1, consumers_count / 2 do
40+
consumers[i] = fiber.create(function()
41+
wait_count = wait_count + 1
42+
-- Wait for all consumers to start.
43+
barrier:wait()
44+
45+
-- Ack the tasks.
46+
for _ = 1, batch_size do
47+
local task = test_queue:take()
48+
test_queue:ack(task[1])
49+
end
50+
51+
wait_count = wait_count + 1
52+
end)
53+
end
54+
55+
return consumers
56+
end
57+
58+
local function multi_consumer_bench()
59+
--- Wait for all consumer fibers.
60+
local wait_all = function()
61+
while (wait_count ~= consumers_count / 2) do
62+
fiber.yield()
63+
end
64+
wait_count = 0
65+
end
66+
67+
fiber.set_max_slice(100)
68+
69+
prepare_tasks()
70+
71+
-- Wait for all consumers to start.
72+
local consumers = prepare_consumers()
73+
wait_all()
74+
75+
-- Start timing of task confirmation.
76+
local start_ack_time = clock.proc64()
77+
barrier:broadcast()
78+
-- Wait for all tasks to be acked.
79+
wait_all()
80+
-- Complete the timing of task confirmation.
81+
local complete_time = clock.proc64()
82+
83+
-- Print the result in milliseconds.
84+
print(string.format("Time it takes to confirm the tasks: %i",
85+
tonumber((complete_time - start_ack_time) / 10^6)))
86+
end
87+
88+
-- Start benchmark.
89+
multi_consumer_bench()
90+
91+
-- Cleanup.
92+
test_queue:drop()
93+
94+
os.exit(0)

0 commit comments

Comments
 (0)