Fix SQLite named_locks

The named_lock context manager rely on READ_COMMITED isolation layer to
create locks in the database.
Unfortunately sqlite does not support sur isolation layer.

Since years the tests were running fine but the new eventlet release
(0.38.2) seems to order the green threads differently, leading to race
conditions.

To fix this, let's introduce a threading semaphore lock inside the
named_lock when sqlite is used.

Change-Id: I6d0e7e3b2fb7498becd637477a9ccd60cd0da515
Signed-off-by: Arnaud M <arnaud.morin@gmail.com>
This commit is contained in:
Arnaud M 2024-12-21 22:27:42 +01:00
parent f74f45903d
commit add8a2eb97
3 changed files with 23 additions and 5 deletions

View File

@ -33,6 +33,14 @@ def acquire_lock(obj_id, session):
_locks[obj_id] = (session, tup[1])
def release_lock(obj_id, session):
with _mutex:
if obj_id in _locks:
tup = _locks.get(obj_id)
tup[1].release()
del _locks[obj_id]
def release_locks(session):
with _mutex:
for obj_id, tup in _locks.items():

View File

@ -2080,6 +2080,13 @@ def delete_event_triggers(session=None, **kwargs):
@b.session_aware()
def create_named_lock(name, session=None):
if b.get_driver_name() == 'sqlite':
# In case of 'sqlite' we need to apply a manual lock because sqlite
# is not READ_COMMITED able
# This is useful for testing purpose as sqlite is not supposed to be
# used for prod
sqlite_lock.acquire_lock(name, session)
# This method has to work not through SQLAlchemy session because
# session may not immediately issue an SQL query to a database
# and instead just schedule it whereas we need to make sure to
@ -2103,7 +2110,7 @@ def get_named_locks(session=None, **kwargs):
@b.session_aware()
def delete_named_lock(lock_id, session=None):
def delete_named_lock(lock_id, name, session=None):
# This method has to work without SQLAlchemy session because
# session may not immediately issue an SQL query to a database
# and instead just schedule it whereas we need to make sure to
@ -2118,6 +2125,9 @@ def delete_named_lock(lock_id, session=None):
session.flush()
if b.get_driver_name() == 'sqlite':
sqlite_lock.release_lock(name, session)
@contextlib.contextmanager
def named_lock(name):
@ -2131,7 +2141,7 @@ def named_lock(name):
yield
delete_named_lock(lock_id)
delete_named_lock(lock_id, name)
@b.session_aware()

View File

@ -3536,20 +3536,20 @@ class LockTest(SQLAlchemyTest):
self.assertEqual('lock1', locks[0].name)
db_api.delete_named_lock('invalid_lock_id')
db_api.delete_named_lock('invalid_lock_id', 'fake')
locks = db_api.get_named_locks()
self.assertEqual(1, len(locks))
db_api.delete_named_lock(locks[0].id)
db_api.delete_named_lock(locks[0].id, 'lock1')
locks = db_api.get_named_locks()
self.assertEqual(0, len(locks))
def test_with_named_lock(self):
name = 'lock1'
name = 'lock42'
with db_api.named_lock(name):
# Make sure that within 'with' section the lock record exists.