From 9d1b963f9ae6362dc9242bdb23627d979175dc8a Mon Sep 17 00:00:00 2001 From: Michael Hanselmann <hansmi@google.com> Date: Mon, 10 Jan 2011 17:58:52 +0100 Subject: [PATCH] utils: Move code related to file locking into separate file Signed-off-by: Michael Hanselmann <hansmi@google.com> Reviewed-by: Iustin Pop <iustin@google.com> --- Makefile.am | 2 + lib/utils/__init__.py | 149 +------------------- lib/utils/filelock.py | 179 +++++++++++++++++++++++++ test/ganeti.utils.filelock_unittest.py | 152 +++++++++++++++++++++ test/ganeti.utils_unittest.py | 116 ---------------- 5 files changed, 334 insertions(+), 264 deletions(-) create mode 100644 lib/utils/filelock.py create mode 100755 test/ganeti.utils.filelock_unittest.py diff --git a/Makefile.am b/Makefile.am index bbec35280..7c44969db 100644 --- a/Makefile.am +++ b/Makefile.am @@ -214,6 +214,7 @@ server_PYTHON = \ utils_PYTHON = \ lib/utils/__init__.py \ lib/utils/algo.py \ + lib/utils/filelock.py \ lib/utils/hash.py \ lib/utils/log.py \ lib/utils/mlock.py \ @@ -486,6 +487,7 @@ python_tests = \ test/ganeti.ssh_unittest.py \ test/ganeti.uidpool_unittest.py \ test/ganeti.utils.algo_unittest.py \ + test/ganeti.utils.filelock_unittest.py \ test/ganeti.utils.hash_unittest.py \ test/ganeti.utils.mlock_unittest.py \ test/ganeti.utils.retry_unittest.py \ diff --git a/lib/utils/__init__.py b/lib/utils/__init__.py index eb8d1b028..a7b603341 100644 --- a/lib/utils/__init__.py +++ b/lib/utils/__init__.py @@ -60,6 +60,7 @@ from ganeti.utils.mlock import * # pylint: disable-msg=W0401 from ganeti.utils.log import * # pylint: disable-msg=W0401 from ganeti.utils.hash import * # pylint: disable-msg=W0401 from ganeti.utils.wrapper import * # pylint: disable-msg=W0401 +from ganeti.utils.filelock import * # pylint: disable-msg=W0401 #: when set to True, L{RunCmd} is disabled @@ -2549,21 +2550,6 @@ def RunInSeparateProcess(fn, *args): return bool(exitcode) -def LockFile(fd): - """Locks a file using POSIX locks. - - @type fd: int - @param fd: the file descriptor we need to lock - - """ - try: - fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError, err: - if err.errno == errno.EAGAIN: - raise errors.LockError("File already locked") - raise - - def ReadWatcherPauseFile(filename, now=None, remove_after=3600): """Reads the watcher pause file. @@ -2658,139 +2644,6 @@ def GenerateSelfSignedSslCert(filename, common_name=constants.X509_CERT_CN, WriteFile(filename, mode=0400, data=key_pem + cert_pem) -class FileLock(object): - """Utility class for file locks. - - """ - def __init__(self, fd, filename): - """Constructor for FileLock. - - @type fd: file - @param fd: File object - @type filename: str - @param filename: Path of the file opened at I{fd} - - """ - self.fd = fd - self.filename = filename - - @classmethod - def Open(cls, filename): - """Creates and opens a file to be used as a file-based lock. - - @type filename: string - @param filename: path to the file to be locked - - """ - # Using "os.open" is necessary to allow both opening existing file - # read/write and creating if not existing. Vanilla "open" will truncate an - # existing file -or- allow creating if not existing. - return cls(os.fdopen(os.open(filename, os.O_RDWR | os.O_CREAT), "w+"), - filename) - - def __del__(self): - self.Close() - - def Close(self): - """Close the file and release the lock. - - """ - if hasattr(self, "fd") and self.fd: - self.fd.close() - self.fd = None - - def _flock(self, flag, blocking, timeout, errmsg): - """Wrapper for fcntl.flock. - - @type flag: int - @param flag: operation flag - @type blocking: bool - @param blocking: whether the operation should be done in blocking mode. - @type timeout: None or float - @param timeout: for how long the operation should be retried (implies - non-blocking mode). - @type errmsg: string - @param errmsg: error message in case operation fails. - - """ - assert self.fd, "Lock was closed" - assert timeout is None or timeout >= 0, \ - "If specified, timeout must be positive" - assert not (flag & fcntl.LOCK_NB), "LOCK_NB must not be set" - - # When a timeout is used, LOCK_NB must always be set - if not (timeout is None and blocking): - flag |= fcntl.LOCK_NB - - if timeout is None: - self._Lock(self.fd, flag, timeout) - else: - try: - Retry(self._Lock, (0.1, 1.2, 1.0), timeout, - args=(self.fd, flag, timeout)) - except RetryTimeout: - raise errors.LockError(errmsg) - - @staticmethod - def _Lock(fd, flag, timeout): - try: - fcntl.flock(fd, flag) - except IOError, err: - if timeout is not None and err.errno == errno.EAGAIN: - raise RetryAgain() - - logging.exception("fcntl.flock failed") - raise - - def Exclusive(self, blocking=False, timeout=None): - """Locks the file in exclusive mode. - - @type blocking: boolean - @param blocking: whether to block and wait until we - can lock the file or return immediately - @type timeout: int or None - @param timeout: if not None, the duration to wait for the lock - (in blocking mode) - - """ - self._flock(fcntl.LOCK_EX, blocking, timeout, - "Failed to lock %s in exclusive mode" % self.filename) - - def Shared(self, blocking=False, timeout=None): - """Locks the file in shared mode. - - @type blocking: boolean - @param blocking: whether to block and wait until we - can lock the file or return immediately - @type timeout: int or None - @param timeout: if not None, the duration to wait for the lock - (in blocking mode) - - """ - self._flock(fcntl.LOCK_SH, blocking, timeout, - "Failed to lock %s in shared mode" % self.filename) - - def Unlock(self, blocking=True, timeout=None): - """Unlocks the file. - - According to C{flock(2)}, unlocking can also be a nonblocking - operation:: - - To make a non-blocking request, include LOCK_NB with any of the above - operations. - - @type blocking: boolean - @param blocking: whether to block and wait until we - can lock the file or return immediately - @type timeout: int or None - @param timeout: if not None, the duration to wait for the lock - (in blocking mode) - - """ - self._flock(fcntl.LOCK_UN, blocking, timeout, - "Failed to unlock %s" % self.filename) - - def SignalHandled(signums): """Signal Handled decoration. diff --git a/lib/utils/filelock.py b/lib/utils/filelock.py new file mode 100644 index 000000000..58b32bcff --- /dev/null +++ b/lib/utils/filelock.py @@ -0,0 +1,179 @@ +# +# + +# Copyright (C) 2006, 2007, 2010, 2011 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +"""Utility functions for file-based locks. + +""" + +import fcntl +import errno +import os +import logging + +from ganeti import errors +from ganeti.utils import retry + + +def LockFile(fd): + """Locks a file using POSIX locks. + + @type fd: int + @param fd: the file descriptor we need to lock + + """ + try: + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError, err: + if err.errno == errno.EAGAIN: + raise errors.LockError("File already locked") + raise + + +class FileLock(object): + """Utility class for file locks. + + """ + def __init__(self, fd, filename): + """Constructor for FileLock. + + @type fd: file + @param fd: File object + @type filename: str + @param filename: Path of the file opened at I{fd} + + """ + self.fd = fd + self.filename = filename + + @classmethod + def Open(cls, filename): + """Creates and opens a file to be used as a file-based lock. + + @type filename: string + @param filename: path to the file to be locked + + """ + # Using "os.open" is necessary to allow both opening existing file + # read/write and creating if not existing. Vanilla "open" will truncate an + # existing file -or- allow creating if not existing. + return cls(os.fdopen(os.open(filename, os.O_RDWR | os.O_CREAT), "w+"), + filename) + + def __del__(self): + self.Close() + + def Close(self): + """Close the file and release the lock. + + """ + if hasattr(self, "fd") and self.fd: + self.fd.close() + self.fd = None + + def _flock(self, flag, blocking, timeout, errmsg): + """Wrapper for fcntl.flock. + + @type flag: int + @param flag: operation flag + @type blocking: bool + @param blocking: whether the operation should be done in blocking mode. + @type timeout: None or float + @param timeout: for how long the operation should be retried (implies + non-blocking mode). + @type errmsg: string + @param errmsg: error message in case operation fails. + + """ + assert self.fd, "Lock was closed" + assert timeout is None or timeout >= 0, \ + "If specified, timeout must be positive" + assert not (flag & fcntl.LOCK_NB), "LOCK_NB must not be set" + + # When a timeout is used, LOCK_NB must always be set + if not (timeout is None and blocking): + flag |= fcntl.LOCK_NB + + if timeout is None: + self._Lock(self.fd, flag, timeout) + else: + try: + retry.Retry(self._Lock, (0.1, 1.2, 1.0), timeout, + args=(self.fd, flag, timeout)) + except retry.RetryTimeout: + raise errors.LockError(errmsg) + + @staticmethod + def _Lock(fd, flag, timeout): + try: + fcntl.flock(fd, flag) + except IOError, err: + if timeout is not None and err.errno == errno.EAGAIN: + raise retry.RetryAgain() + + logging.exception("fcntl.flock failed") + raise + + def Exclusive(self, blocking=False, timeout=None): + """Locks the file in exclusive mode. + + @type blocking: boolean + @param blocking: whether to block and wait until we + can lock the file or return immediately + @type timeout: int or None + @param timeout: if not None, the duration to wait for the lock + (in blocking mode) + + """ + self._flock(fcntl.LOCK_EX, blocking, timeout, + "Failed to lock %s in exclusive mode" % self.filename) + + def Shared(self, blocking=False, timeout=None): + """Locks the file in shared mode. + + @type blocking: boolean + @param blocking: whether to block and wait until we + can lock the file or return immediately + @type timeout: int or None + @param timeout: if not None, the duration to wait for the lock + (in blocking mode) + + """ + self._flock(fcntl.LOCK_SH, blocking, timeout, + "Failed to lock %s in shared mode" % self.filename) + + def Unlock(self, blocking=True, timeout=None): + """Unlocks the file. + + According to C{flock(2)}, unlocking can also be a nonblocking + operation:: + + To make a non-blocking request, include LOCK_NB with any of the above + operations. + + @type blocking: boolean + @param blocking: whether to block and wait until we + can lock the file or return immediately + @type timeout: int or None + @param timeout: if not None, the duration to wait for the lock + (in blocking mode) + + """ + self._flock(fcntl.LOCK_UN, blocking, timeout, + "Failed to unlock %s" % self.filename) diff --git a/test/ganeti.utils.filelock_unittest.py b/test/ganeti.utils.filelock_unittest.py new file mode 100755 index 000000000..29e7f6446 --- /dev/null +++ b/test/ganeti.utils.filelock_unittest.py @@ -0,0 +1,152 @@ +#!/usr/bin/python +# + +# Copyright (C) 2006, 2007, 2010, 2011 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +"""Script for testing ganeti.utils.filelock""" + +import os +import tempfile +import unittest + +from ganeti import constants +from ganeti import utils +from ganeti import errors + +import testutils + + +class _BaseFileLockTest: + """Test case for the FileLock class""" + + def testSharedNonblocking(self): + self.lock.Shared(blocking=False) + self.lock.Close() + + def testExclusiveNonblocking(self): + self.lock.Exclusive(blocking=False) + self.lock.Close() + + def testUnlockNonblocking(self): + self.lock.Unlock(blocking=False) + self.lock.Close() + + def testSharedBlocking(self): + self.lock.Shared(blocking=True) + self.lock.Close() + + def testExclusiveBlocking(self): + self.lock.Exclusive(blocking=True) + self.lock.Close() + + def testUnlockBlocking(self): + self.lock.Unlock(blocking=True) + self.lock.Close() + + def testSharedExclusiveUnlock(self): + self.lock.Shared(blocking=False) + self.lock.Exclusive(blocking=False) + self.lock.Unlock(blocking=False) + self.lock.Close() + + def testExclusiveSharedUnlock(self): + self.lock.Exclusive(blocking=False) + self.lock.Shared(blocking=False) + self.lock.Unlock(blocking=False) + self.lock.Close() + + def testSimpleTimeout(self): + # These will succeed on the first attempt, hence a short timeout + self.lock.Shared(blocking=True, timeout=10.0) + self.lock.Exclusive(blocking=False, timeout=10.0) + self.lock.Unlock(blocking=True, timeout=10.0) + self.lock.Close() + + @staticmethod + def _TryLockInner(filename, shared, blocking): + lock = utils.FileLock.Open(filename) + + if shared: + fn = lock.Shared + else: + fn = lock.Exclusive + + try: + # The timeout doesn't really matter as the parent process waits for us to + # finish anyway. + fn(blocking=blocking, timeout=0.01) + except errors.LockError, err: + return False + + return True + + def _TryLock(self, *args): + return utils.RunInSeparateProcess(self._TryLockInner, self.tmpfile.name, + *args) + + def testTimeout(self): + for blocking in [True, False]: + self.lock.Exclusive(blocking=True) + self.failIf(self._TryLock(False, blocking)) + self.failIf(self._TryLock(True, blocking)) + + self.lock.Shared(blocking=True) + self.assert_(self._TryLock(True, blocking)) + self.failIf(self._TryLock(False, blocking)) + + def testCloseShared(self): + self.lock.Close() + self.assertRaises(AssertionError, self.lock.Shared, blocking=False) + + def testCloseExclusive(self): + self.lock.Close() + self.assertRaises(AssertionError, self.lock.Exclusive, blocking=False) + + def testCloseUnlock(self): + self.lock.Close() + self.assertRaises(AssertionError, self.lock.Unlock, blocking=False) + + +class TestFileLockWithFilename(testutils.GanetiTestCase, _BaseFileLockTest): + TESTDATA = "Hello World\n" * 10 + + def setUp(self): + testutils.GanetiTestCase.setUp(self) + + self.tmpfile = tempfile.NamedTemporaryFile() + utils.WriteFile(self.tmpfile.name, data=self.TESTDATA) + self.lock = utils.FileLock.Open(self.tmpfile.name) + + # Ensure "Open" didn't truncate file + self.assertFileContent(self.tmpfile.name, self.TESTDATA) + + def tearDown(self): + self.assertFileContent(self.tmpfile.name, self.TESTDATA) + + testutils.GanetiTestCase.tearDown(self) + + +class TestFileLockWithFileObject(unittest.TestCase, _BaseFileLockTest): + def setUp(self): + self.tmpfile = tempfile.NamedTemporaryFile() + self.lock = utils.FileLock(open(self.tmpfile.name, "w"), self.tmpfile.name) + + +if __name__ == "__main__": + testutils.GanetiTestProgram() diff --git a/test/ganeti.utils_unittest.py b/test/ganeti.utils_unittest.py index baaca37be..e7b0298f8 100755 --- a/test/ganeti.utils_unittest.py +++ b/test/ganeti.utils_unittest.py @@ -1165,122 +1165,6 @@ class TestTailFile(testutils.GanetiTestCase): self.failUnlessEqual(TailFile(fname, lines=i), data[-i:]) -class _BaseFileLockTest: - """Test case for the FileLock class""" - - def testSharedNonblocking(self): - self.lock.Shared(blocking=False) - self.lock.Close() - - def testExclusiveNonblocking(self): - self.lock.Exclusive(blocking=False) - self.lock.Close() - - def testUnlockNonblocking(self): - self.lock.Unlock(blocking=False) - self.lock.Close() - - def testSharedBlocking(self): - self.lock.Shared(blocking=True) - self.lock.Close() - - def testExclusiveBlocking(self): - self.lock.Exclusive(blocking=True) - self.lock.Close() - - def testUnlockBlocking(self): - self.lock.Unlock(blocking=True) - self.lock.Close() - - def testSharedExclusiveUnlock(self): - self.lock.Shared(blocking=False) - self.lock.Exclusive(blocking=False) - self.lock.Unlock(blocking=False) - self.lock.Close() - - def testExclusiveSharedUnlock(self): - self.lock.Exclusive(blocking=False) - self.lock.Shared(blocking=False) - self.lock.Unlock(blocking=False) - self.lock.Close() - - def testSimpleTimeout(self): - # These will succeed on the first attempt, hence a short timeout - self.lock.Shared(blocking=True, timeout=10.0) - self.lock.Exclusive(blocking=False, timeout=10.0) - self.lock.Unlock(blocking=True, timeout=10.0) - self.lock.Close() - - @staticmethod - def _TryLockInner(filename, shared, blocking): - lock = utils.FileLock.Open(filename) - - if shared: - fn = lock.Shared - else: - fn = lock.Exclusive - - try: - # The timeout doesn't really matter as the parent process waits for us to - # finish anyway. - fn(blocking=blocking, timeout=0.01) - except errors.LockError, err: - return False - - return True - - def _TryLock(self, *args): - return utils.RunInSeparateProcess(self._TryLockInner, self.tmpfile.name, - *args) - - def testTimeout(self): - for blocking in [True, False]: - self.lock.Exclusive(blocking=True) - self.failIf(self._TryLock(False, blocking)) - self.failIf(self._TryLock(True, blocking)) - - self.lock.Shared(blocking=True) - self.assert_(self._TryLock(True, blocking)) - self.failIf(self._TryLock(False, blocking)) - - def testCloseShared(self): - self.lock.Close() - self.assertRaises(AssertionError, self.lock.Shared, blocking=False) - - def testCloseExclusive(self): - self.lock.Close() - self.assertRaises(AssertionError, self.lock.Exclusive, blocking=False) - - def testCloseUnlock(self): - self.lock.Close() - self.assertRaises(AssertionError, self.lock.Unlock, blocking=False) - - -class TestFileLockWithFilename(testutils.GanetiTestCase, _BaseFileLockTest): - TESTDATA = "Hello World\n" * 10 - - def setUp(self): - testutils.GanetiTestCase.setUp(self) - - self.tmpfile = tempfile.NamedTemporaryFile() - utils.WriteFile(self.tmpfile.name, data=self.TESTDATA) - self.lock = utils.FileLock.Open(self.tmpfile.name) - - # Ensure "Open" didn't truncate file - self.assertFileContent(self.tmpfile.name, self.TESTDATA) - - def tearDown(self): - self.assertFileContent(self.tmpfile.name, self.TESTDATA) - - testutils.GanetiTestCase.tearDown(self) - - -class TestFileLockWithFileObject(unittest.TestCase, _BaseFileLockTest): - def setUp(self): - self.tmpfile = tempfile.NamedTemporaryFile() - self.lock = utils.FileLock(open(self.tmpfile.name, "w"), self.tmpfile.name) - - class TestTimeFunctions(unittest.TestCase): """Test case for time functions""" -- GitLab