Files
entropy/lib/entropy/locks.py

466 lines
14 KiB
Python

# -*- coding: utf-8 -*-
"""
@author: Fabio Erculiani <lxnay@sabayon.org>
@contact: lxnay@sabayon.org
@copyright: Fabio Erculiani
@license: GPL-2
B{Entropy resources lock functions module}.
"""
import contextlib
import errno
import fcntl
import threading
import os
from entropy.cache import EntropyCacher
from entropy.const import etpConst, const_setup_directory, const_setup_file
from entropy.core.settings.base import SystemSettings
from entropy.i18n import _
from entropy.misc import FlockFile
from entropy.output import TextInterface, blue, darkred, darkgreen, teal
class SimpleFileLock(object):
"""
Helper class that makes it easy to acquire and release file based locks.
"""
@classmethod
def acquire_lock(cls, lock_file, lock_map):
"""
Make possible to protect a code region using an EXCLUSIVE, non-blocking
file lock. A lock map (dict) is required in order to register the lock
data (usually lock file object) and then unlock it using release_lock().
@param lock_file: path to lock file used for locking
@type lock_file: string
@param lock_map: lock map (dict object) that can be used to
record the lock data in order to unlock it on release_lock().
@type lock_map: dict
@return: True, if lock has been acquired, False otherwise
@rtype: bool
"""
lock_f = open(lock_file, "a+")
try:
fcntl.flock(lock_f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
lock_f.truncate()
lock_f.write(str(os.getpid()))
lock_f.flush()
lock_map[lock_file] = lock_f
return True
except IOError as err:
lock_f.close()
if err.errno not in (errno.EACCES, errno.EAGAIN,):
# ouch, wtf?
raise
return False # lock already acquired
except Exception:
lock_f.close()
raise
@classmethod
def release_lock(cls, lock_file, lock_map):
"""
Release a previously acquired lock through acquire_lock().
@param lock_file: path to lock file used for locking
@type lock_file: string
@param lock_map: lock map (dict object) that can be used to
record the lock data in order to unlock it on release_lock().
@type lock_map: dict
"""
try:
lock_f = lock_map.pop(lock_file)
except KeyError:
lock_f = None
if lock_f is not None:
fcntl.flock(lock_f.fileno(), fcntl.LOCK_UN)
lock_f.close()
try:
os.remove(lock_file)
except OSError as err:
# cope with possible race conditions
if err.errno != errno.ENOENT:
raise
class ResourceLock(object):
"""
Generic Entropy Resource Lock abstract class.
"""
_TLS = threading.local()
def __init__(self, lock_map, lock_mutex, output=None):
"""
Object constructor.
@keyword output: a TextInterface interface
@type output: entropy.output.TextInterface or None
"""
self._lock_map = lock_map
self._lock_mutex = lock_mutex
if output is not None:
self._out = output
else:
self._out = TextInterface
def path(self):
"""
Return the path to the lock file.
"""
raise NotImplementedError()
def _file_lock_setup(self, file_path):
"""
Setup _FILE_LOCK_MAP for file_path, allocating locking information.
"""
mapped = self._lock_map.get(file_path)
if mapped is None:
mapped = {
'count': 0,
'ref': None,
'path': file_path,
'shared': None,
}
self._lock_map[file_path] = mapped
return mapped
def _lock_resource(self, blocking, shared):
"""
Internal function that does the locking given a lock
file path.
"""
lock_path = self.path()
with self._lock_mutex:
mapped = self._file_lock_setup(lock_path)
# I asked for an exclusive lock, but
# I am only holding a shared one, don't
# return True.
want_exclusive_when_shared = not shared and mapped['shared']
if mapped['ref'] is not None:
if not want_exclusive_when_shared:
# reentrant lock, already acquired
mapped['count'] += 1
return True
else:
mapped['shared'] = shared
# watch for deadlocks using TLS
recursed = getattr(self._TLS, "recursed", False)
if recursed and want_exclusive_when_shared:
# deadlock, raise exception
raise RuntimeError(
"want exclusive lock when shared acquired")
# not the same thread requested an exclusive lock when shared
self._TLS.recursed = True
# fall through, we won't deadlock
path = mapped['path']
acquired, flock_f = self._file_lock_create(
path, blocking=blocking, shared=shared)
if acquired:
with self._lock_mutex:
mapped['count'] += 1
if flock_f is not None:
mapped['ref'] = flock_f
return acquired
def _unlock_resource(self):
"""
Internal function that does the unlocking of a given
lock file.
"""
lock_path = self.path()
with self._lock_mutex:
mapped = self._file_lock_setup(lock_path)
if mapped['count'] == 0:
raise RuntimeError("releasing a non-acquired lock")
# decrement lock counter
if mapped['count'] > 0:
mapped['count'] -= 1
# if lock counter > 0, still locked
# waiting for other upper-level calls
if mapped['count'] > 0:
return
ref_obj = mapped['ref']
if ref_obj is not None:
# do not remove!
ref_obj.release()
ref_obj.close()
mapped['ref'] = None
# allow the same thread to acquire the lock again.
self._TLS.recursed = False
def _file_lock_create(self, lock_path, blocking=False, shared=False):
"""
Create and allocate the lock file pointed by lock_data structure.
"""
lock_dir = os.path.dirname(lock_path)
try:
os.makedirs(lock_dir, 0o775)
except OSError as err:
if err.errno != errno.EEXIST:
raise
const_setup_directory(lock_dir)
try:
fmode = 0o664
if shared:
fd = os.open(lock_path, os.O_CREAT | os.O_RDONLY, fmode)
else:
fd = os.open(lock_path, os.O_CREAT | os.O_APPEND, fmode)
except OSError as err:
if err.errno in (errno.ENOENT, errno.EACCES):
# cannot get lock or dir doesn't exist
return False, None
raise
# ensure that entropy group can write on that
try:
const_setup_file(lock_path, etpConst['entropygid'], 0o664)
except OSError:
pass
flock_f = FlockFile(lock_path, fd=fd)
if blocking:
if shared:
flock_f.acquire_shared()
else:
flock_f.acquire_exclusive()
else:
acquired = False
if shared:
acquired = flock_f.try_acquire_shared()
else:
acquired = flock_f.try_acquire_exclusive()
if not acquired:
return False, None
return True, flock_f
def _wait_resource(self, shared):
"""
Try to acquire the resource, on failure, print a warning
and block until acquired.
"""
if shared:
acquired = self.try_acquire_shared()
else:
acquired = self.try_acquire_exclusive()
if acquired:
return True
if shared:
msg = "%s %s ..." % (
blue(_("Acquiring shared lock on")),
darkgreen(self.path()),)
else:
msg = "%s %s ..." % (
blue(_("Acquiring exclusive lock on")),
darkgreen(self.path()),)
self._out.output(
msg,
importance=0,
back=True,
level="warning"
)
if shared:
self.acquire_shared()
else:
self.acquire_exclusive()
return True
@contextlib.contextmanager
def exclusive(self):
"""
Acquire an exclusive file lock for this repository (context manager).
"""
self.wait_exclusive()
try:
yield
finally:
self.release()
def acquire_exclusive(self):
"""
Acquire the resource in exclusive blocking mode.
"""
self._lock_resource(True, False)
def try_acquire_exclusive(self):
"""
Acquire the resource in exclusive non-blocking mode.
Return True if resource is acquired, False otherwise.
"""
return self._lock_resource(False, False)
def wait_exclusive(self):
"""
Try to acquire the resource in non-blocking mode, on
failure, print a warning and then acquire the resource
in blocking mode.
"""
return self._wait_resource(False)
@contextlib.contextmanager
def shared(self):
"""
Acquire a shared file lock for this repository (context manager).
"""
self.wait_shared()
try:
yield
finally:
self.release()
def acquire_shared(self):
"""
Acquire the resource in shared blocking mode.
"""
self._lock_resource(True, True)
def try_acquire_shared(self):
"""
Acquire the resource in shared non-blocking mode.
Return True if resource is acquired, False otherwise.
"""
return self._lock_resource(False, True)
def wait_shared(self):
"""
Try to acquire the resource in non-blocking mode, on
failure, print a warning and then acquire the resource
in blocking mode.
"""
return self._wait_resource(True)
def release(self):
"""
Release the previously acquired resource.
"""
self._unlock_resource()
class EntropyResourcesLock(ResourceLock):
"""
Entropy Resources Lock (or Big Entropy Lock, BEL) class.
This class wraps the interface to the Entropy Resources Lock,
for acquiring shared or exclusive access to the Entropy Package
Manager.
Typically, routines acquire this lock in shared mode and
only a handful of code paths require exclusive locking.
"""
_FILE_LOCK_MUTEX = threading.Lock()
_FILE_LOCK_MAP = {}
# List of callables that will be triggered upon lock acquisition.
# It can be used to execute cache cleanups.
_POST_ACQUIRE_HOOK_LOCK = threading.Lock()
_POST_ACQUIRE_HOOKS = {}
_POST_ACQUIRE_HOOK_COUNT = 0
@classmethod
def add_post_acquire_hook(cls, callab):
"""
Add a hook that will be executed once the lock is acquired.
This can be used to execute cache cleanups or other activities
of the same sort. The callable is called with the EntropyCacher
lock held.
This method returns a reference number that must be used to perform
the removal of the hook.
"""
with EntropyResourcesLock._POST_ACQUIRE_HOOK_LOCK:
EntropyResourcesLock._POST_ACQUIRE_HOOK_COUNT += 1
index = EntropyResourcesLock._POST_ACQUIRE_HOOK_COUNT
EntropyResourcesLock._POST_ACQUIRE_HOOKS[index] = callab
return index
@classmethod
def remove_post_acquire_hook(cls, index):
"""
Remove a previously added hook using its reference.
This method raises KeyError if the hook is not present.
"""
with EntropyResourcesLock._POST_ACQUIRE_HOOK_LOCK:
EntropyResourcesLock._POST_ACQUIRE_HOOKS.pop(index)
def __init__(self, output=None):
"""
Object constructor.
@keyword output: a TextInterface interface
@type output: entropy.output.TextInterface or None
"""
super(EntropyResourcesLock, self).__init__(
EntropyResourcesLock._FILE_LOCK_MAP,
EntropyResourcesLock._FILE_LOCK_MUTEX,
output=output)
def path(self):
"""
Return the path to the lock file.
"""
return os.path.join(etpConst['entropyworkdir'],
'.using_resources')
def _clear_resources_after_lock(self):
"""
Clear resources that could have become stale after
the Entropy Lock acquisition.
"""
cacher = EntropyCacher()
with cacher:
SystemSettings().clear()
cacher.discard()
with EntropyResourcesLock._POST_ACQUIRE_HOOK_LOCK:
callables = list(
EntropyResourcesLock._POST_ACQUIRE_HOOKS.values()
)
for callab in callables:
callab()
cacher.sync()
def _lock_resource(self, blocking, shared):
"""
Overridden from ResourceLock.
Add hooks support code.
"""
acquired = super(EntropyResourcesLock, self)._lock_resource(
blocking, shared)
if acquired:
self._clear_resources_after_lock()
return acquired