Commit 34bf32a0 authored by Jim Fulton's avatar Jim Fulton

Refactored file locking support:

- Found and added a test for a race condition arising from creating
  and removing lock files.

- Change to leave lock files behind.

- Use the windows locking implementation from msvcrt from the standard
  library, rather than using a custom extension module.
parent 2dfd67fd
...@@ -17,31 +17,49 @@ import errno ...@@ -17,31 +17,49 @@ import errno
import logging import logging
logger = logging.getLogger("ZODB.lock_file") logger = logging.getLogger("ZODB.lock_file")
class LockError(Exception):
"""Couldn't lock a file
"""
try: try:
import fcntl import fcntl
except ImportError: except ImportError:
try: try:
from winlock import LockFile as _LockFile import msvcrt
from winlock import UnlockFile as _UnlockFile
except ImportError: except ImportError:
def lock_file(file): def _lock_file(file):
logger.info('No file-locking support on this platform') raise TypeError('No file-locking support on this platform')
def _unlock_file(file):
raise TypeError('No file-locking support on this platform')
else:
# Windows # Windows
def lock_file(file): def _lock_file(file):
# Lock just the first byte # Lock just the first byte
_LockFile(file.fileno(), 0, 0, 1, 0) try:
msvcrt.locking(file.fileno(), msvcrt.LK_NBLCK, 1)
except IOError:
raise LockError("Couldn't lock %r" % file.name)
def _unlock_file(file):
try:
file.seek(0)
msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1)
except IOError:
raise LockError("Couldn't unlock %r" % file.name)
def unlock_file(file):
_UnlockFile(file.fileno(), 0, 0, 1, 0)
else: else:
# Unix # Unix
_flags = fcntl.LOCK_EX | fcntl.LOCK_NB _flags = fcntl.LOCK_EX | fcntl.LOCK_NB
def lock_file(file): def _lock_file(file):
try:
fcntl.flock(file.fileno(), _flags) fcntl.flock(file.fileno(), _flags)
except IOError:
raise LockError("Couldn't lock %r" % file.name)
def unlock_file(file): def _unlock_file(file):
# File is automatically unlocked on close # File is automatically unlocked on close
pass pass
...@@ -51,25 +69,29 @@ else: ...@@ -51,25 +69,29 @@ else:
# Creating the instance acquires the lock. The file remains open. Calling # Creating the instance acquires the lock. The file remains open. Calling
# close both closes and unlocks the lock file. # close both closes and unlocks the lock file.
class LockFile: class LockFile:
_fp = None
def __init__(self, path): def __init__(self, path):
self._path = path self._path = path
fp = open(path, 'w+')
try: try:
self._fp = open(path, 'r+') _lock_file(fp)
except IOError, e:
if e.errno <> errno.ENOENT: raise
self._fp = open(path, 'w+')
# Acquire the lock and piss on the hydrant
try:
lock_file(self._fp)
except: except:
logger.exception("Error locking file %s", path) fp.seek(1)
pid = fp.read().strip()[:20]
fp.close()
logger.exception("Error locking file", path, pid)
raise raise
print >> self._fp, os.getpid()
self._fp.flush() self._fp = fp
fp.write(" %s\n" % os.getpid())
fp.truncate()
fp.flush()
def close(self): def close(self):
if self._fp is not None: if self._fp is not None:
unlock_file(self._fp) _unlock_file(self._fp)
self._fp.close() self._fp.close()
os.unlink(self._path)
self._fp = None self._fp = None
Lock file support
=================
The ZODB lock_file module provides support for creating file system
locks. These are locks that are implemented with lock files and
OS-provided locking facilities. To create a lock, instantiate a
LockFile object with a file name:
>>> import ZODB.lock_file
>>> lock = ZODB.lock_file.LockFile('lock')
If we try to lock the same name, we'll get a lock error:
>>> try:
... ZODB.lock_file.LockFile('lock')
... except ZODB.lock_file.LockError:
... print "Can't lock file"
Can't lock file
To release the lock, use it's close method:
>>> lock.close()
The lock file is not removed. It is left behind:
>>> import os
>>> os.path.exists('lock')
True
Of course, now that we've released the lock, we can created it again:
>>> lock = ZODB.lock_file.LockFile('lock')
>>> lock.close()
##############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
import os, sys, unittest
from zope.testing import doctest
import ZODB.lock_file, time, threading
def inc():
while 1:
try:
lock = ZODB.lock_file.LockFile('f.lock')
except ZODB.lock_file.LockError:
continue
else:
break
f = open('f', 'r+b')
v = int(f.readline().strip())
time.sleep(0.01)
v += 1
f.seek(0)
f.write('%d\n' % v)
f.close()
lock.close()
def many_threads_read_and_write():
r"""
>>> open('f', 'w+b').write('0\n')
>>> open('f.lock', 'w+b').write('0\n')
>>> n = 50
>>> threads = [threading.Thread(target=inc) for i in range(n)]
>>> _ = [thread.start() for thread in threads]
>>> _ = [thread.join() for thread in threads]
>>> saved = int(open('f', 'rb').readline().strip())
>>> saved == n
True
>>> os.remove('f')
>>> os.remove('f.lock')
"""
def test_suite():
suite = unittest.TestSuite()
suite.addTest(doctest.DocFileSuite(os.path.join('..', 'lock_file.txt')))
suite.addTest(doctest.DocTestSuite())
return suite
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment