• Uncategorized

About python : What-is-the-best-way-to-open-a-file-for-exclusive-access-in-Python

Question Detail

What is the most elegant way to solve this:

  • open a file for reading, but only if it is not already opened for writing
  • open a file for writing, but only if it is not already opened for reading or writing

The built-in functions work like this

>>> path = r"c:\scr.txt"
>>> file1 = open(path, "w")
>>> print file1
<open file 'c:\scr.txt', mode 'w' at 0x019F88D8>
>>> file2 = open(path, "w")
>>> print file2
<open file 'c:\scr.txt', mode 'w' at 0x02332188>
>>> file1.write("111")
>>> file2.write("222")
>>> file1.close()

scr.txt now contains ‘111’.

>>> file2.close()

scr.txt was overwritten and now contains ‘222’ (on Windows, Python 2.4).

The solution should work inside the same process (like in the example above) as well as when another process has opened the file.
It is preferred, if a crashing program will not keep the lock open.

Question Answer

I don’t think there is a fully crossplatform way. On unix, the fcntl module will do this for you. However on windows (which I assume you are by the paths), you’ll need to use the win32file module.

Fortunately, there is a portable implementation (portalocker) using the platform appropriate method at the python cookbook.

To use it, open the file, and then call:

portalocker.lock(file, flags)

where flags are portalocker.LOCK_EX for exclusive write access, or LOCK_SH for shared, read access.

The solution should work inside the same process (like in the example above) as well as when another process has opened the file.

If by ‘another process’ you mean ‘whatever process’ (i.e. not your program), in Linux there’s no way to accomplish this relying only on system calls (fcntl & friends). What you want is mandatory locking, and the Linux way to obtain it is a bit more involved:

Remount the partition that contains your file with the mand option:

# mount -o remount,mand /dev/hdXY

Set the sgid flag for your file:

# chmod g-x,g+s yourfile

In your Python code, obtain an exclusive lock on that file:

fcntl.flock(fd, fcntl.LOCK_EX)

Now even cat will not be able to read the file until you release the lock.

EDIT: I solved it myself! By using directory existence & age as a locking mechanism! Locking by file is safe only on Windows (because Linux silently overwrites), but locking by directory works perfectly both on Linux and Windows. See my GIT where I created an easy to use class ‘lockbydir.DLock’ for that:

https://github.com/drandreaskrueger/lockbydir

At the bottom of the readme, you find 3 GITplayers where you can see the code examples execute live in your browser! Quite cool, isn’t it? 🙂

Thanks for your attention


This was my original question:

I would like to answer to parity3 (https://meta.stackoverflow.com/users/1454536/parity3) but I can neither comment directly (‘You must have 50 reputation to comment’), nor do I see any way to contact him/her directly. What do you suggest to me, to get through to him?

My question:

I have implemented something similiar to what parity3 suggested here as an answer: https://stackoverflow.com/a/21444311/3693375 (“Assuming your Python interpreter, and the …”)

And it works brilliantly – on Windows. (I am using it to implement a locking mechanism that works across independently started processes. https://github.com/drandreaskrueger/lockbyfile )

But other than parity3 says, it does NOT work the same on Linux:

os.rename(src, dst)

Rename the file or directory src to dst. … On Unix, if dst exists
and is a file,
it will be replaced silently if the user has permission.
The operation may fail on some Unix flavors if src and dst
are on different filesystems. If successful, the renaming will
be an atomic operation (this is a POSIX requirement).
On Windows, if dst already exists, OSError will be raised
(https://docs.python.org/2/library/os.html#os.rename)

The silent replacing is the problem. On Linux.
The “if dst already exists, OSError will be raised” is great for my purposes. But only on Windows, sadly.

I guess parity3’s example still works most of the time, because of his if condition

if not os.path.exists(lock_filename):
    try:
        os.rename(tmp_filename,lock_filename)

But then the whole thing is not atomic anymore.

Because the if condition might be true in two parallel processes, and then both will rename, but only one will win the renaming race. And no exception raised (in Linux).

Any suggestions? Thanks!

P.S.: I know this is not the proper way, but I am lacking an alternative. PLEASE don’t punish me with lowering my reputation. I looked around a lot, to solve this myself. How to PM users in here? And meh why can’t I?

Here’s a start on the win32 half of a portable implementation, that does not need a seperate locking mechanism.

Requires the Python for Windows Extensions to get down to the win32 api, but that’s pretty much mandatory for python on windows already, and can alternatively be done with ctypes. The code could be adapted to expose more functionality if it’s needed (such as allowing FILE_SHARE_READ rather than no sharing at all). See also the MSDN documentation for the CreateFile and WriteFile system calls, and the article on Creating and Opening Files.

As has been mentioned, you can use the standard fcntl module to implement the unix half of this, if required.

import winerror, pywintypes, win32file

class LockError(StandardError):
    pass

class WriteLockedFile(object):
    """
    Using win32 api to achieve something similar to file(path, 'wb')
    Could be adapted to handle other modes as well.
    """
    def __init__(self, path):
        try:
            self._handle = win32file.CreateFile(
                path,
                win32file.GENERIC_WRITE,
                0,
                None,
                win32file.OPEN_ALWAYS,
                win32file.FILE_ATTRIBUTE_NORMAL,
                None)
        except pywintypes.error, e:
            if e[0] == winerror.ERROR_SHARING_VIOLATION:
                raise LockError(e[2])
            raise
    def close(self):
        self._handle.close()
    def write(self, str):
        win32file.WriteFile(self._handle, str)

Here’s how your example from above behaves:

>>> path = "C:\\scr.txt"
>>> file1 = WriteLockedFile(path)
>>> file2 = WriteLockedFile(path) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
    ...
LockError: ...
>>> file1.write("111")
>>> file1.close()
>>> print file(path).read()
111

I prefer to use filelock, a cross-platform Python library which barely requires any additional code. Here’s an example of how to use it:

from filelock import FileLock

lockfile = r"c:\scr.txt"
lock = FileLock(lockfile + ".lock")
with lock:
    file = open(path, "w")
    file.write("111")
    file.close()

Any code within the with lock: block is thread-safe, meaning that it will be finished before another process has access to the file.

Assuming your Python interpreter, and the underlying os and filesystem treat os.rename as an atomic operation and it will error when the destination exists, the following method is free of race conditions. I’m using this in production on a linux machine. Requires no third party libs and is not os dependent, and aside from an extra file create, the performance hit is acceptable for many use cases. You can easily apply python’s function decorator pattern or a ‘with_statement’ contextmanager here to abstract out the mess.

You’ll need to make sure that lock_filename does not exist before a new process/task begins.

import os,time
def get_tmp_file():
    filename='tmp_%s_%s'%(os.getpid(),time.time())
    open(filename).close()
    return filename

def do_exclusive_work():
    print 'exclusive work being done...'

num_tries=10
wait_time=10
lock_filename='filename.lock'
acquired=False
for try_num in xrange(num_tries):
    tmp_filename=get_tmp_file()
    if not os.path.exists(lock_filename):
        try:
            os.rename(tmp_filename,lock_filename)
            acquired=True
        except (OSError,ValueError,IOError), e:
            pass
    if acquired:
        try:
            do_exclusive_work()
        finally:
            os.remove(lock_filename)
        break
    os.remove(tmp_filename)
    time.sleep(wait_time)
assert acquired, 'maximum tries reached, failed to acquire lock file'

EDIT

It has come to light that os.rename silently overwrites the destination on a non-windows OS. Thanks for pointing this out @ akrueger!

Here is a workaround, gathered from here:

Instead of using os.rename you can use:

try:
    if os.name != 'nt': # non-windows needs a create-exclusive operation
        fd = os.open(lock_filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
        os.close(fd)
    # non-windows os.rename will overwrite lock_filename silently.
    # We leave this call in here just so the tmp file is deleted but it could be refactored so the tmp file is never even generated for a non-windows OS
    os.rename(tmp_filename,lock_filename)
    acquired=True
except (OSError,ValueError,IOError), e:
    if os.name != 'nt' and not 'File exists' in str(e): raise

@ akrueger You’re probably just fine with your directory based solution, just giving you an alternate method.

To make you safe when opening files within one application, you could try something like this:

import time
class ExclusiveFile(file):
    openFiles = {}
    fileLocks = []

    class FileNotExclusiveException(Exception):
        pass

    def __init__(self, *args):

        sMode = 'r'
        sFileName = args[0]
        try:
            sMode = args[1]
        except:
            pass
        while sFileName in ExclusiveFile.fileLocks:
            time.sleep(1)

        ExclusiveFile.fileLocks.append(sFileName)

        if not sFileName in ExclusiveFile.openFiles.keys() or (ExclusiveFile.openFiles[sFileName] == 'r' and sMode == 'r'):
            ExclusiveFile.openFiles[sFileName] = sMode
            try:
                file.__init__(self, sFileName, sMode)
            finally:
                ExclusiveFile.fileLocks.remove(sFileName)
         else:
            ExclusiveFile.fileLocks.remove(sFileName)
            raise self.FileNotExclusiveException(sFileName)

    def close(self):
        del ExclusiveFile.openFiles[self.name]
        file.close(self)

That way you subclass the file class. Now just do:

>>> f = ExclusiveFile('/tmp/a.txt', 'r')
>>> f
<open file '/tmp/a.txt', mode 'r' at 0xb7d7cc8c>
>>> f1 = ExclusiveFile('/tmp/a.txt', 'r')
>>> f1
<open file '/tmp/a.txt', mode 'r' at 0xb7d7c814>
>>> f2 = ExclusiveFile('/tmp/a.txt', 'w') # can't open it for writing now
exclfile.FileNotExclusiveException: /tmp/a.txt

If you open it first with ‘w’ mode, it won’t allow anymore opens, even in read mode, just as you wanted…

You may also like...

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.