Skip to content

Commit a514081

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 b40ae9e commit a514081

File tree

2 files changed

+117
-3
lines changed

2 files changed

+117
-3
lines changed

mysql/init.lua

Lines changed: 25 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,23 @@ 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, conn_id)
34+
local success = pool.queue:put(POOL_EMPTY_SLOT, CONN_GC_HOOK_TIMEOUT)
35+
if not success then
36+
log.error('mysql pool %s internal queue unexpected state: there are no ' ..
37+
'empty slots, connection %s cannot be put back. It is likely ' ..
38+
'that someone had messed with pool.queue manually. Closing ' ..
39+
'the pool...', pool, conn_id)
40+
log.error(debug.traceback)
41+
42+
pool:close()
43+
end
44+
end
45+
2846
-- get connection from pool
2947
local function conn_get(pool, timeout)
3048
local mysql_conn = pool.queue:get(timeout)
@@ -44,11 +62,17 @@ local function conn_get(pool, timeout)
4462
end
4563

4664
local conn = conn_create(mysql_conn)
65+
local conn_id = tostring(conn)
4766
-- we can use ffi gc to return mysql connection to pool
4867
conn.__gc_hook = ffi.gc(ffi.new('void *'),
4968
function(self)
5069
mysql_conn:close()
51-
pool.queue:put(POOL_EMPTY_SLOT)
70+
-- Fiber yields are prohibited in gc since Tarantool
71+
-- * 2.6.0-138-gd3f1dd720
72+
-- * 2.5.1-105-gc690b3337
73+
-- * 2.4.2-89-g83037df15
74+
-- * 1.10.7-47-g8099cb053
75+
fiber.new(conn_gc_hook, pool, conn_id)
5276
end)
5377
-- If the connection belongs to a connection pool, it must be returned to
5478
-- the pool when calling "close" without actually closing the connection.

test/mysql.test.lua

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ local function test_gc(test, pool)
9090

9191
-- Collect lost connections.
9292
collectgarbage('collect')
93+
collectgarbage('collect')
94+
95+
-- Run a fiber scheduler cycle to finish gc process.
96+
fiber.yield()
9397

9498
-- Verify that a pool is aware of collected connections.
9599
test:is(pool.queue:count(), pool.size, 'all connections are put back')
@@ -110,6 +114,10 @@ local function test_gc(test, pool)
110114

111115
-- Collect the lost connection.
112116
collectgarbage('collect')
117+
collectgarbage('collect')
118+
119+
-- Run a fiber scheduler cycle to finish gc process.
120+
fiber.yield()
113121

114122
-- Verify that a pool is aware of collected connections.
115123
test:is(pool.queue:count(), pool.size, 'all connections are put back')
@@ -128,6 +136,10 @@ local function test_gc(test, pool)
128136

129137
-- Collect the lost connection.
130138
collectgarbage('collect')
139+
collectgarbage('collect')
140+
141+
-- Run a fiber scheduler cycle to finish gc process.
142+
fiber.yield()
131143

132144
-- Verify that a pool is aware of collected connections.
133145
test:is(pool.queue:count(), pool.size, 'all connections are put back')
@@ -321,6 +333,10 @@ local function test_connection_pool(test, pool)
321333
pool:put(conn)
322334
conn = nil -- luacheck: no unused
323335
collectgarbage('collect')
336+
collectgarbage('collect')
337+
338+
-- Run a fiber scheduler cycle to finish gc process.
339+
fiber.yield()
324340

325341
-- Verify that the pool is full.
326342
test:ok(pool.queue:is_full(), 'a connection was given back')
@@ -358,6 +374,10 @@ local function test_connection_pool(test, pool)
358374
pool:put(conn)
359375
conn = nil -- luacheck: no unused
360376
collectgarbage('collect')
377+
collectgarbage('collect')
378+
379+
-- Run a fiber scheduler cycle to finish gc process.
380+
fiber.yield()
361381

362382
-- Verify that the pool is full
363383
test:ok(pool.queue:is_full(), 'a broken connection was given back')
@@ -389,6 +409,10 @@ local function test_connection_pool(test, pool)
389409

390410
conn = nil -- luacheck: no unused
391411
collectgarbage('collect')
412+
collectgarbage('collect')
413+
414+
-- Run a fiber scheduler cycle to finish gc process.
415+
fiber.yield()
392416

393417
-- Verify that the pool is full
394418
test:ok(pool.queue:is_full(), 'a broken connection was given back')
@@ -510,7 +534,12 @@ local function test_underlying_conn_closed_during_gc(test)
510534

511535
-- Somehow we lost the connection handle.
512536
conn = nil
513-
collectgarbage()
537+
collectgarbage('collect')
538+
collectgarbage('collect')
539+
540+
-- Run a fiber scheduler cycle to finish gc process.
541+
fiber.yield()
542+
514543
ffi.cdef([[ int fcntl(int fd, int cmd, ...); ]])
515544
local F_GETFD = 1
516545
test:ok(ffi.C.fcntl(handle, F_GETFD) == -1, 'descriptor is closed')
@@ -646,8 +675,68 @@ local function test_put_to_wrong_pool(test)
646675
format(p1, p2))
647676
end
648677

678+
-- gh-67: Check that garbage collection of a connection from a connection pool
679+
-- not yields. Yielding in gc is prohibited since Tarantool
680+
-- 2.6.0-138-gd3f1dd720, 2.5.1-105-gc690b3337, 2.4.2-89-g83037df15,
681+
-- 1.10.7-47-g8099cb053. If fiber yield happens in object gc, Tarantool process
682+
-- exits with code 1.
683+
--
684+
-- Gc hook of a connection from a connection pool calls `pool.queue:put(...)`.
685+
-- Fiber `channel:put()` may yield if the channel is full.
686+
-- `channel:put()`/`channel:get()` in mysql driver connection pool is balanced:
687+
-- * the channel capacity is equal to the connection count;
688+
-- * pool create fills the channel with multiple `channel:put()`s
689+
-- all the way through;
690+
-- * `pool:get()` causes single `channel:get()` and creates a connection object
691+
-- with gc hook;
692+
-- * `pool:put()` causes single `channel:put()` and removes gc hook;
693+
-- * gc hook causes single `channel:put()`.
694+
--
695+
-- There are no other cases of `channel:put()`. `pool:put()` and gc hook cannot
696+
-- be executed both in the same time: either a connection is used inside
697+
-- `pool:put()` and won't be garbage collected or a connection is no longer used
698+
-- anywhere else (including `pool:put()`) and it will be garbage collected.
699+
-- So now there are no valid cases when gc hook may yield unless someone messes
700+
-- up with pool queue. Messing up with pool queue anyway breaks a connection
701+
-- pool internal logic, but at least it won't be causing a process exit.
702+
local function test_conn_from_pool_gc_yield(test)
703+
test:plan(2)
704+
705+
local p = mysql.pool_create({
706+
host = host,
707+
port = port,
708+
user = user,
709+
password = password,
710+
db = db,
711+
size = 1
712+
})
713+
714+
local c = p:get()
715+
local res = c:ping()
716+
test:ok(res, 'Connection is ok')
717+
p.queue:put('killer msg')
718+
719+
c = nil -- luacheck: no unused
720+
collectgarbage('collect')
721+
collectgarbage('collect')
722+
723+
-- Pool should became unavailable after gc hook finish.
724+
-- It may be a couple of fiber scheduler cycles.
725+
local pool_is_usable = true
726+
for _= 1,10 do
727+
fiber.yield()
728+
729+
pool_is_usable = p.usable
730+
if pool_is_usable == false then
731+
break
732+
end
733+
end
734+
735+
test:is(pool_is_usable, false, 'Pool is not usable')
736+
end
737+
649738
local test = tap.test('mysql connector')
650-
test:plan(11)
739+
test:plan(12)
651740

652741
test:test('connection old api', test_old_api, conn)
653742
local pool_conn = p:get()
@@ -662,6 +751,7 @@ test:test('test_underlying_conn_closed_during_gc',
662751
test:test('ffi null printing', test_ffi_null_printing, p)
663752
test:test('test_block_fiber_inf', test_block_fiber_inf, p)
664753
test:test('test_put_to_wrong_pool', test_put_to_wrong_pool)
754+
test:test('test_conn_from_pool_gc_yield', test_conn_from_pool_gc_yield)
665755
p:close()
666756

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

0 commit comments

Comments
 (0)