mbox locking: combine locking functions into one and swap lock order

We used to create a dotlock file first and then lock with fcntl; swap that
order, since locking first with fcntl seems to be more common.

This patch also adds general mbox lock/unlock methods, which call the
dotlock and fcntl-lock methods, and moves the retry logic there.

When the dotlock and fcntl methods fail to acquire a lock, they now raise
a custom exception "LockUnavailable", which gets caught in the general
lock() method.  That way, if we succeed to acquire one lock but fail to
acquire the other, we can release our locks at the upper level and retry.
This commit is contained in:
Nikolaus Schulz 2009-05-22 19:40:07 +02:00
parent d706409c59
commit 11103e2de2
2 changed files with 70 additions and 40 deletions

View File

@ -80,6 +80,8 @@ class UserError(ArchivemailException):
pass pass
class UnexpectedError(ArchivemailException): class UnexpectedError(ArchivemailException):
pass pass
class LockUnavailable(ArchivemailException):
pass
class Stats: class Stats:
"""Class to collect and print statistics about mailbox archival""" """Class to collect and print statistics about mailbox archival"""
@ -177,9 +179,9 @@ class Options:
dry_run = 0 dry_run = 0
filter_append = None filter_append = None
include_flagged = 0 include_flagged = 0
lockfile_attempts = 5 locking_attempts = 5
lockfile_extension = ".lock" lockfile_extension = ".lock"
lockfile_sleep = 1 lock_sleep = 1
no_compress = 0 no_compress = 0
only_archive_read = 0 only_archive_read = 0
output_dir = None output_dir = None
@ -328,6 +330,7 @@ class Mbox(mailbox.UnixMailbox):
path -- file name of the 'mbox' file to be opened path -- file name of the 'mbox' file to be opened
""" """
assert(path) assert(path)
self._locked = False
try: try:
self.original_atime = os.path.getatime(path) self.original_atime = os.path.getatime(path)
self.original_mtime = os.path.getmtime(path) self.original_mtime = os.path.getmtime(path)
@ -351,18 +354,54 @@ class Mbox(mailbox.UnixMailbox):
os.utime(self.mbox_file_name, (self.original_atime, \ os.utime(self.mbox_file_name, (self.original_atime, \
self.original_mtime)) self.original_mtime))
def posix_lock(self): def lock(self):
"""Set an exclusive posix lock on the 'mbox' mailbox""" """Lock this mbox with both a dotlock and a posix lock."""
vprint("obtaining exclusive lock on file '%s'" % self.mbox_file_name) assert(not self._locked)
fcntl.lockf(self.mbox_file, fcntl.LOCK_EX|fcntl.LOCK_NB) attempt = 1
while True:
try:
self._posix_lock()
self._dotlock_lock()
break
except LockUnavailable, e:
self._posix_unlock()
attempt += 1
if (attempt > options.locking_attempts):
unexpected_error(str(e))
vprint("%s - sleeping..." % e)
time.sleep(options.lock_sleep)
except:
self._posix_unlock()
raise
self._locked = True
def posix_unlock(self): def unlock(self):
"""Unlock this mbox."""
assert(self._locked)
self._dotlock_unlock()
self._posix_unlock()
self._locked = False
def _posix_lock(self):
"""Set an exclusive posix lock on the 'mbox' mailbox"""
vprint("trying to acquire posix lock on file '%s'" % self.mbox_file_name)
try:
fcntl.lockf(self.mbox_file, fcntl.LOCK_EX|fcntl.LOCK_NB)
except IOError, e:
if e.errno in (errno.EAGAIN, errno.EACCES):
raise LockUnavailable("posix lock for '%s' unavailable" % \
self.mbox_file_name)
else:
raise
vprint("acquired posix lock on file '%s'" % self.mbox_file_name)
def _posix_unlock(self):
"""Unset any posix lock on the 'mbox' mailbox""" """Unset any posix lock on the 'mbox' mailbox"""
vprint("dropping posix lock on file '%s'" % self.mbox_file_name) vprint("dropping posix lock on file '%s'" % self.mbox_file_name)
fcntl.lockf(self.mbox_file, fcntl.LOCK_UN) fcntl.lockf(self.mbox_file, fcntl.LOCK_UN)
def dotlock_lock(self): def _dotlock_lock(self):
"""Create a dotlock file on the 'mbox' mailbox""" """Create a dotlock file for the 'mbox' mailbox"""
import socket import socket
hostname = socket.gethostname() hostname = socket.gethostname()
pid = os.getpid() pid = os.getpid()
@ -371,36 +410,29 @@ class Mbox(mailbox.UnixMailbox):
plfd, prelock_name = tempfile.mkstemp(prelock_suffix, prelock_prefix, plfd, prelock_name = tempfile.mkstemp(prelock_suffix, prelock_prefix,
dir=box_dir) dir=box_dir)
lock_name = self.mbox_file_name + options.lockfile_extension lock_name = self.mbox_file_name + options.lockfile_extension
attempt = 0
try: try:
while True: try:
attempt = attempt + 1 os.link(prelock_name, lock_name)
try: # We've got the lock.
os.link(prelock_name, lock_name) except OSError, e:
# We've got the lock. if os.fstat(plfd)[stat.ST_NLINK] == 2:
break # The Linux man page for open(2) claims that in this
except OSError, e: # case we have actually succeeded to create the link,
if os.fstat(plfd)[stat.ST_NLINK] == 2: # and this assumption seems to be folklore.
# The Linux man page for open(2) claims that in this # So we've got the lock.
# case we have actually succeeded to create the link, pass
# and this assumption seems to be folklore. elif e.errno == errno.EEXIST:
# So we've got the lock. raise LockUnavailable("Dotlock for '%s' unavailable" % self.mbox_file_name)
break else:
if e.errno != errno.EEXIST: raise raise
# Lockfile already existed, someone else has the lock.
if (attempt >= options.lockfile_attempts):
unexpected_error("Giving up waiting for "
"dotlock '%s'" % lock_name)
vprint("lockfile '%s' exists - sleeping..." % lock_name)
time.sleep(options.lockfile_sleep)
finally: finally:
os.close(plfd) os.close(plfd)
os.unlink(prelock_name) os.unlink(prelock_name)
_stale.dotlock_lock = lock_name _stale.dotlock_lock = lock_name
vprint("acquired lockfile '%s'" % lock_name) vprint("acquired lockfile '%s'" % lock_name)
def dotlock_unlock(self): def _dotlock_unlock(self):
"""Delete the dotlock file on the 'mbox' mailbox""" """Delete the dotlock file for the 'mbox' mailbox."""
assert(self.mbox_file_name) assert(self.mbox_file_name)
lock_name = self.mbox_file_name + options.lockfile_extension lock_name = self.mbox_file_name + options.lockfile_extension
vprint("removing lockfile '%s'" % lock_name) vprint("removing lockfile '%s'" % lock_name)
@ -1132,8 +1164,7 @@ def _archive_mbox(mailbox_name, final_archive_name):
else: else:
archive = ArchiveMbox(final_archive_name) archive = ArchiveMbox(final_archive_name)
original.dotlock_lock() original.lock()
original.posix_lock()
msg = original.next() msg = original.next()
if not msg and (original.starting_size > 0): if not msg and (original.starting_size > 0):
user_error("'%s' is not a valid mbox-format mailbox" % mailbox_name) user_error("'%s' is not a valid mbox-format mailbox" % mailbox_name)
@ -1164,10 +1195,9 @@ def _archive_mbox(mailbox_name, final_archive_name):
archive.finalise() archive.finalise()
if retain: if retain:
retain.finalise() retain.finalise()
original.posix_unlock() original.unlock()
original.close() original.close()
original.reset_timestamps() original.reset_timestamps()
original.dotlock_unlock()
if not options.quiet: if not options.quiet:
stats.display() stats.display()

View File

@ -115,9 +115,9 @@ class TestMboxDotlock(TestCaseInTempdir):
def testDotlock(self): def testDotlock(self):
"""dotlock_lock/unlock should create/delete a lockfile""" """dotlock_lock/unlock should create/delete a lockfile"""
lock = self.mbox_name + ".lock" lock = self.mbox_name + ".lock"
self.mbox.dotlock_lock() self.mbox._dotlock_lock()
assert(os.path.isfile(lock)) assert(os.path.isfile(lock))
self.mbox.dotlock_unlock() self.mbox._dotlock_unlock()
assert(not os.path.isfile(lock)) assert(not os.path.isfile(lock))
class TestMboxPosixLock(TestCaseInTempdir): class TestMboxPosixLock(TestCaseInTempdir):
@ -138,9 +138,9 @@ class TestMboxPosixLock(TestCaseInTempdir):
pid = os.fork() pid = os.fork()
if pid == 0: if pid == 0:
# In the child, lock the mailbox. # In the child, lock the mailbox.
self.mbox.posix_lock() self.mbox._posix_lock()
time.sleep(2) time.sleep(2)
self.mbox.posix_unlock() self.mbox._posix_unlock()
os._exit(0) os._exit(0)
# In the parent, sleep a bit to give the child time to acquire # In the parent, sleep a bit to give the child time to acquire