diff --git a/doc/source/drivers.rst b/doc/source/drivers.rst index 75a2524e..75404bd1 100644 --- a/doc/source/drivers.rst +++ b/doc/source/drivers.rst @@ -24,6 +24,10 @@ API, some of them have different properties: some basic group primitives (with huge limitations). The lock can only be distributed locally to a computer processes. +* `file` is based on file and only implements a lock based on POSIX or Window + file locking for now. The lock can only be distributed locally to a computer + processes. + * `zake`_ is a driver using a fake implementation of ZooKeeper and can be used to use Tooz in your unit tests suite for example. diff --git a/setup.cfg b/setup.cfg index 5221a3b9..8b0bc110 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ tooz.backends = redis = tooz.drivers.redis:RedisDriver postgresql = tooz.drivers.pgsql:PostgresDriver mysql = tooz.drivers.mysql:MySQLDriver + file = tooz.drivers.file:FileDriver [build_sphinx] all_files = 1 diff --git a/tooz/drivers/file.py b/tooz/drivers/file.py new file mode 100644 index 00000000..17dccc50 --- /dev/null +++ b/tooz/drivers/file.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 2015 eNovance +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import errno +import os +import weakref + +import tooz +from tooz import coordination +from tooz.drivers import _retry +from tooz import locking + + +class FileLock(locking.Lock): + """A file based lock.""" + + def __init__(self, path): + super(FileLock, self).__init__(path) + self.acquired = False + + def acquire(self, blocking=True): + self.lockfile = open(self.name, 'a') + + @_retry.retry(stop_max_delay=blocking) + def _lock(): + # NOTE(jd) If the same process try to grab the lock, the call to + # self.lock() will succeed, so we track internally if the process + # already has the lock. + if self.acquired is True: + if blocking: + raise _retry.Retry + return False + try: + self.lock() + except IOError as e: + if e.errno in (errno.EACCESS, errno.EAGAIN): + if blocking: + raise _retry.Retry + return False + else: + self.acquired = True + return True + + return _lock() + + def release(self): + self.unlock() + self.lockfile.close() + self.acquired = False + + def lock(self): + raise NotImplementedError + + def unlock(self): + raise NotImplementedError + + +class WindowsFileLock(FileLock): + def lock(self): + msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1) + + def unlock(self): + msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1) + + +class PosixFileLock(FileLock): + def lock(self): + fcntl.lockf(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) + + def unlock(self): + fcntl.lockf(self.lockfile, fcntl.LOCK_UN) + + +if os.name == 'nt': + import msvcrt + LockClass = WindowsFileLock +else: + import fcntl + LockClass = PosixFileLock + + +class FileDriver(coordination.CoordinationDriver): + """A file based driver.""" + + LOCKS = weakref.WeakValueDictionary() + + def __init__(self, member_id, parsed_url, options): + """Initialize the file driver.""" + super(FileDriver, self).__init__() + self._lockdir = parsed_url.path + + def get_lock(self, name): + path = os.path.abspath(os.path.join(self._lockdir, name.decode())) + lock = LockClass(path) + return self.LOCKS.setdefault(path, lock) + + @staticmethod + def watch_join_group(group_id, callback): + raise tooz.NotImplemented + + @staticmethod + def unwatch_join_group(group_id, callback): + raise tooz.NotImplemented + + @staticmethod + def watch_leave_group(group_id, callback): + raise tooz.NotImplemented + + @staticmethod + def unwatch_leave_group(group_id, callback): + raise tooz.NotImplemented + + @staticmethod + def watch_elected_as_leader(group_id, callback): + raise tooz.NotImplemented + + @staticmethod + def unwatch_elected_as_leader(group_id, callback): + raise tooz.NotImplemented diff --git a/tooz/tests/test_coordination.py b/tooz/tests/test_coordination.py index c1188211..381b5916 100644 --- a/tooz/tests/test_coordination.py +++ b/tooz/tests/test_coordination.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright © 2013-2014 eNovance Inc. All Rights Reserved. +# Copyright © 2013-2015 eNovance Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -36,6 +36,7 @@ class TestAPI(testscenarios.TestWithScenarios, 'bad_url': 'memcached://localhost:1', 'timeout_capable': True}), ('ipc', {'url': 'ipc://'}), + ('file', {'url': 'file:///tmp'}), ('redis', {'url': os.getenv("TOOZ_TEST_REDIS_URL"), 'bad_url': 'redis://localhost:1', 'timeout_capable': True}),