Skip to content

Commit 76eaca1

Browse files
gc: prevent potential fiber yield
Yielding in gc is prohibited since Tarantool 2.6.0-138-gd3f1dd720, 2.5.1-105-gc690b3337, 2.4.2-89-g83037df15, 1.10.7-47-g8099cb053. If fiber yield happens in object gc, Tarantool process exits with code 1. Gc hook of a connection from a connection pool calls `pool.queue:put(...)`. Fiber `channel:put()` may yield if the channel is full. `channel:put()`/`channel:get()` in mysql driver connection pool is balanced: * the channel capacity is equal to the connection count; * pool create fills the channel with multiple `channel:put()`s all the way through; * `pool:get()` causes single `channel:get()` and creates a connection object with gc hook; * `pool:put()` causes single `channel:put()` and removes gc hook; * gc hook causes single `channel:put()`. There are no other cases of `channel:put()`. `pool:put()` and gc hook cannot be executed both in the same time: either a connection is used inside `pool:put()` and won't be garbage collected or a connection is no longer used anywhere else (including `pool:put()`) and it will be garbage collected. So now there are no valid cases when gc hook may yield unless someone messes up with pool queue. Messing up with pool queue anyway breaks a connection pool internal logic, so we make the pool unavailable in that case. Delayed gc should not break existing logic. If `pool:get()` is called, we expect to get a connection after gc and it's not yet returned, `channel:get()` will yield and trigger all planned returns. Closes #67
1 parent e4a850d commit 76eaca1

File tree

2 files changed

+89
-2
lines changed

2 files changed

+89
-2
lines changed

mysql/init.lua

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
local fiber = require('fiber')
44
local driver = require('mysql.driver')
55
local ffi = require('ffi')
6+
local log = require('log')
67

78
local pool_mt
89
local conn_mt
@@ -25,6 +26,22 @@ local function conn_create(mysql_conn)
2526
return conn
2627
end
2728

29+
-- There is no reason to make it configurable: either everyting is alright and
30+
-- channel put is immediate or pool is doomed.
31+
local CONN_GC_HOOK_TIMEOUT = 0
32+
33+
local function conn_gc_hook(pool)
34+
local success = pool.queue:put(POOL_EMPTY_SLOT, CONN_GC_HOOK_TIMEOUT)
35+
if not success then
36+
log.error('pool %s internal queue unexpected state: there are no ' ..
37+
'empty slots. It is likely that someone had messed with ' ..
38+
'pool.queue manually. Closing the pool...')
39+
log.error(debug.traceback)
40+
41+
pool:close()
42+
end
43+
end
44+
2845
-- get connection from pool
2946
local function conn_get(pool, timeout)
3047
local mysql_conn = pool.queue:get(timeout)
@@ -48,7 +65,12 @@ local function conn_get(pool, timeout)
4865
conn.__gc_hook = ffi.gc(ffi.new('void *'),
4966
function(self)
5067
mysql_conn:close()
51-
pool.queue:put(POOL_EMPTY_SLOT)
68+
-- Fiber yields are prohibited in gc since Tarantool
69+
-- * 2.6.0-138-gd3f1dd720
70+
-- * 2.5.1-105-gc690b3337
71+
-- * 2.4.2-89-g83037df15
72+
-- * 1.10.7-47-g8099cb053
73+
fiber.new(conn_gc_hook, pool)
5274
end)
5375
-- If the connection belongs to a connection pool, it must be returned to
5476
-- the pool when calling "close" without actually closing the connection.

test/mysql.test.lua

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ local function test_gc(test, pool)
9191
-- Collect lost connections.
9292
collectgarbage('collect')
9393

94+
-- Run a fiber scheduler cycle to finish gc process.
95+
fiber.yield()
96+
9497
-- Verify that a pool is aware of collected connections.
9598
test:is(pool.queue:count(), pool.size, 'all connections are put back')
9699
end)
@@ -111,6 +114,9 @@ local function test_gc(test, pool)
111114
-- Collect the lost connection.
112115
collectgarbage('collect')
113116

117+
-- Run a fiber scheduler cycle to finish gc process.
118+
fiber.yield()
119+
114120
-- Verify that a pool is aware of collected connections.
115121
test:is(pool.queue:count(), pool.size, 'all connections are put back')
116122
end)
@@ -129,6 +135,9 @@ local function test_gc(test, pool)
129135
-- Collect the lost connection.
130136
collectgarbage('collect')
131137

138+
-- Run a fiber scheduler cycle to finish gc process.
139+
fiber.yield()
140+
132141
-- Verify that a pool is aware of collected connections.
133142
test:is(pool.queue:count(), pool.size, 'all connections are put back')
134143
end)
@@ -646,8 +655,63 @@ local function test_put_to_wrong_pool(test)
646655
format(p1, p2))
647656
end
648657

658+
-- gh-67: Check that garbage collection of a connection from a connection pool
659+
-- not yields. Yielding in gc is prohibited since Tarantool
660+
-- 2.6.0-138-gd3f1dd720, 2.5.1-105-gc690b3337, 2.4.2-89-g83037df15,
661+
-- 1.10.7-47-g8099cb053. If fiber yield happens in object gc, Tarantool process
662+
-- exits with code 1.
663+
--
664+
-- Gc hook of a connection from a connection pool calls `pool.queue:put(...)`.
665+
-- Fiber `channel:put()` may yield if the channel is full.
666+
-- `channel:put()`/`channel:get()` in mysql driver connection pool is balanced:
667+
-- * the channel capacity is equal to the connection count;
668+
-- * pool create fills the channel with multiple `channel:put()`s
669+
-- all the way through;
670+
-- * `pool:get()` causes single `channel:get()` and creates a connection object
671+
-- with gc hook;
672+
-- * `pool:put()` causes single `channel:put()` and removes gc hook;
673+
-- * gc hook causes single `channel:put()`.
674+
--
675+
-- There are no other cases of `channel:put()`. `pool:put()` and gc hook cannot
676+
-- be executed both in the same time: either a connection is used inside
677+
-- `pool:put()` and won't be garbage collected or a connection is no longer used
678+
-- anywhere else (including `pool:put()`) and it will be garbage collected.
679+
-- So now there are no valid cases when gc hook may yield unless someone messes
680+
-- up with pool queue. Messing up with pool queue anyway breaks a connection
681+
-- pool internal logic, but at least it won't be causing a process exit.
682+
local function test_conn_from_pool_gc_yield(test)
683+
test:plan(4)
684+
685+
local p = mysql.pool_create({
686+
host = host,
687+
port = port,
688+
user = user,
689+
password = password,
690+
db = db,
691+
size = 1
692+
})
693+
694+
local c = p:get()
695+
local res = c:ping()
696+
test:ok(res, 'Connection is ok')
697+
p.queue:put('killer msg')
698+
699+
c = nil -- luacheck: no unused
700+
collectgarbage('collect')
701+
collectgarbage('collect')
702+
703+
-- Run a fiber scheduler cycle to finish gc process.
704+
fiber.yield()
705+
706+
test:ok(true, 'Tarantool is alive')
707+
708+
local ok, err = pcall(p.get, p)
709+
test:is(ok, false, 'Pool is not usable')
710+
test:like(tostring(err), 'Pool is not usable')
711+
end
712+
649713
local test = tap.test('mysql connector')
650-
test:plan(11)
714+
test:plan(12)
651715

652716
test:test('connection old api', test_old_api, conn)
653717
local pool_conn = p:get()
@@ -662,6 +726,7 @@ test:test('test_underlying_conn_closed_during_gc',
662726
test:test('ffi null printing', test_ffi_null_printing, p)
663727
test:test('test_block_fiber_inf', test_block_fiber_inf, p)
664728
test:test('test_put_to_wrong_pool', test_put_to_wrong_pool)
729+
test:test('test_conn_from_pool_gc_yield', test_conn_from_pool_gc_yield)
665730
p:close()
666731

667732
os.exit(test:check() and 0 or 1)

0 commit comments

Comments
 (0)