Skip to content

Commit 2e5b23a

Browse files
author
Arnau Orriols
committed
Fix deadlock when connection fails
Wraps sqlite3.connect() in try/except and stores the exception to raise it in check_raise_error()
1 parent 39e8dce commit 2e5b23a

File tree

2 files changed

+72
-43
lines changed

2 files changed

+72
-43
lines changed

sqlitedict.py

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,12 @@ def __init__(self, filename=None, tablename='unnamed', flag='c',
170170

171171
logger.info("opening Sqlite table %r in %s" % (tablename, filename))
172172
MAKE_TABLE = 'CREATE TABLE IF NOT EXISTS "%s" (key TEXT PRIMARY KEY, value BLOB)' % self.tablename
173-
self.conn = self._new_conn()
174-
self.conn.execute(MAKE_TABLE)
175-
self.conn.commit()
173+
try:
174+
self.conn = self._new_conn()
175+
self.conn.execute(MAKE_TABLE)
176+
self.conn.commit()
177+
except sqlite3.OperationalError as e:
178+
raise RuntimeError(str(e))
176179
if flag == 'w':
177180
self.clear()
178181

@@ -381,20 +384,14 @@ def __init__(self, filename, autocommit, journal_mode):
381384
self.setDaemon(True) # python2.5-compatible
382385
self.exception = None
383386
self.log = logging.getLogger('sqlitedict.SqliteMultithread')
387+
self.connect()
384388
self.start()
385389

386390
def run(self):
387-
if self.autocommit:
388-
conn = sqlite3.connect(self.filename, isolation_level=None, check_same_thread=False)
389-
else:
390-
conn = sqlite3.connect(self.filename, check_same_thread=False)
391-
conn.execute('PRAGMA journal_mode = %s' % self.journal_mode)
392-
conn.text_factory = str
393-
cursor = conn.cursor()
394-
conn.commit()
395-
cursor.execute('PRAGMA synchronous=OFF')
396391

397392
res = None
393+
conn = None
394+
cursor = None
398395
while True:
399396
req, arg, res, outer_stack = self.reqs.get()
400397
if req == '--close--':
@@ -404,35 +401,25 @@ def run(self):
404401
conn.commit()
405402
if res:
406403
res.put('--no more--')
404+
elif req == '--connect--':
405+
try:
406+
if self.autocommit:
407+
conn = sqlite3.connect(self.filename, isolation_level=None, check_same_thread=False)
408+
else:
409+
conn = sqlite3.connect(self.filename, check_same_thread=False)
410+
except Exception as err:
411+
self.store_error(outer_stack, err)
412+
else:
413+
conn.execute('PRAGMA journal_mode = %s' % self.journal_mode)
414+
conn.text_factory = str
415+
cursor = conn.cursor()
416+
conn.commit()
417+
cursor.execute('PRAGMA synchronous=OFF')
407418
else:
408419
try:
409420
cursor.execute(req, arg)
410421
except Exception as err:
411-
self.exception = (e_type, e_value, e_tb) = sys.exc_info()
412-
inner_stack = traceback.extract_stack()
413-
414-
# An exception occurred in our thread, but we may not
415-
# immediately able to throw it in our calling thread, if it has
416-
# no return `res` queue: log as level ERROR both the inner and
417-
# outer exception immediately.
418-
#
419-
# Any iteration of res.get() or any next call will detect the
420-
# inner exception and re-raise it in the calling Thread; though
421-
# it may be confusing to see an exception for an unrelated
422-
# statement, an ERROR log statement from the 'sqlitedict.*'
423-
# namespace contains the original outer stack location.
424-
self.log.error('Inner exception:')
425-
for item in traceback.format_list(inner_stack):
426-
self.log.error(item)
427-
self.log.error('') # deliniate traceback & exception w/blank line
428-
for item in traceback.format_exception_only(e_type, e_value):
429-
self.log.error(item)
430-
431-
self.log.error('') # exception & outer stack w/blank line
432-
self.log.error('Outer stack:')
433-
for item in traceback.format_list(outer_stack):
434-
self.log.error(item)
435-
self.log.error('Exception will be re-raised at next call.')
422+
self.store_error(outer_stack, err)
436423

437424
if res:
438425
for rec in cursor:
@@ -443,9 +430,38 @@ def run(self):
443430
conn.commit()
444431

445432
self.log.debug('received: %s, send: --no more--', req)
446-
conn.close()
433+
if conn is not None:
434+
conn.close()
447435
res.put('--no more--')
448436

437+
def store_error(self, outer_stack, err):
438+
""" Store error to be raised in any next call """
439+
self.exception = (e_type, e_value, e_tb) = sys.exc_info()
440+
inner_stack = traceback.extract_stack()
441+
442+
# An exception occurred in our thread, but we may not
443+
# immediately able to throw it in our calling thread, if it has
444+
# no return `res` queue: log as level ERROR both the inner and
445+
# outer exception immediately.
446+
#
447+
# Any iteration of res.get() or any next call will detect the
448+
# inner exception and re-raise it in the calling Thread; though
449+
# it may be confusing to see an exception for an unrelated
450+
# statement, an ERROR log statement from the 'sqlitedict.*'
451+
# namespace contains the original outer stack location.
452+
self.log.error('Inner exception:')
453+
for item in traceback.format_list(inner_stack):
454+
self.log.error(item)
455+
self.log.error('') # deliniate traceback & exception w/blank line
456+
for item in traceback.format_exception_only(e_type, e_value):
457+
self.log.error(item)
458+
459+
self.log.error('') # exception & outer stack w/blank line
460+
self.log.error('Outer stack:')
461+
for item in traceback.format_list(outer_stack):
462+
self.log.error(item)
463+
self.log.error('Exception will be re-raised at next call.')
464+
449465
def check_raise_error(self):
450466
"""
451467
Check for and raise exception for any previous sqlite query.
@@ -527,6 +543,9 @@ def commit(self, blocking=True):
527543
# otherwise, we fire and forget as usual.
528544
self.execute('--commit--')
529545

546+
def connect(self):
547+
self.execute('--connect--')
548+
530549
def close(self, force=False):
531550
if force:
532551
# If a SqliteDict is being killed or garbage-collected, then select_one()

tests/test_core.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ def test_directory_notfound(self):
5959
with self.assertRaises(RuntimeError):
6060
SqliteDict(filename=os.path.join(folder, 'nonexistent'))
6161

62+
def test_failed_connection(self):
63+
""" Verify error when connecting does not deadlock """
64+
# given: a non-existent directory,
65+
folder = tempfile.mkdtemp(prefix='sqlitedict-test')
66+
# os.rmdir(folder)
67+
# exercise,
68+
with self.assertRaises(RuntimeError):
69+
SqliteDict(filename=folder)
70+
os.rmdir(folder)
71+
6272
def test_commit_nonblocking(self):
6373
"""Coverage for non-blocking commit."""
6474
# given,
@@ -133,8 +143,8 @@ def attempt_clear():
133143
def attempt_terminate():
134144
readonly_db.terminate()
135145

136-
attempt_funcs = [attempt_write,
137-
attempt_update,
146+
attempt_funcs = [attempt_write,
147+
attempt_update,
138148
attempt_delete,
139149
attempt_clear,
140150
attempt_terminate]
@@ -159,7 +169,7 @@ def test_irregular_tablenames(self):
159169

160170
with self.assertRaisesRegexp(ValueError, r'^Invalid tablename '):
161171
SqliteDict(':memory:', '"')
162-
172+
163173
def test_overwrite_using_flag_w(self):
164174
"""Re-opening of a database with flag='w' destroys only the target table."""
165175
# given,
@@ -277,6 +287,6 @@ def test_tablenames(self):
277287
self.assertEqual(SqliteDict.get_tablenames(fname), ['table1'])
278288
with SqliteDict(fname,tablename='table2') as db2:
279289
self.assertEqual(SqliteDict.get_tablenames(fname), ['table1','table2'])
280-
290+
281291
tablenames = SqliteDict.get_tablenames('tests/db/tablenames-test-2.sqlite')
282-
self.assertEqual(tablenames, ['table1','table2'])
292+
self.assertEqual(tablenames, ['table1','table2'])

0 commit comments

Comments
 (0)