angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Python file I/O on Windows and Linux are two different things

When using threads you can run into problems caused by different behaviour of file I/O functions.

8 December 2021 Updated 8 December 2021
In Python
post main image
https://unsplash.com/@momofactory

I have a Python program running fine on Linux. Few months ago I wanted to run this on Windows.

This was the first time I used Python on Windows. Install Python app, create virtual environment, copy and run. No problems ... oh but there was a problem. My session sometimes disappeared ... WTF! I noticed the problem by repeatly hitting F5 in a very short time. Time for a thorough investigation.

The application and the session key-value store

The Python app is a Flask application. It uses the file system interface of Flask-Session to store sessions as files. Flask-Session uses another PyPi package to handle all file I/O. As you can imagine this package implements a key-value store.

There are two main methods in this package:

  • set(key, value), to write/update the session data
  • get(key), to get the session data

In the package, the set(key, value) method creates a temporary file and then calls the Python os.replace() function to replace the session file. The get(key) method calls the Python read function.

Test using threads

When you run Flask in development mode, you will see a lot of requests to the session store because Flask also serves images, CSS files, etc. In a production environment where you serve static content via a webserver you are less likely to run into this problem, but it is still there!
This is how I came up with the idea to write a small test using threads.

import cachelib
import threading

fsc = cachelib.file.FileSystemCache('.')

def set_get(i):
    fsc.set('key', 'val')
    val = fsc.get('key')

for i in range(10):
    t = threading.Thread(target=set_get, args=(i,))
    t.start()

On Linux there were no errors, nothing. But on Windows this raised randomly generated exceptions:

[WinError 5] Access is denied
...
[Errno 13] Permission denied

The [WinError 5] exception was generated by the Python os.replace() function. The [Errno 13] exception was generated by the Python read() function.

What is going on here?

File I/O on Windows and Linux are two different things

I assumed that Python would shield me from platform specific implementations. It does so in many functions but not in all. Especially when using threads you can run into problems caused by different behaviour of file I/O functions.

From Python Bug Tracker Issue46003:

As they say, there's no such thing as 'portable software', only 'software that has been ported'.
Especially in an area like file I/O: once you move beyond simple 'one process opens, writes, and closes' and another process then 'opens, reads, and closes', there are a lot of platform-specific issues. Python does not try to abstract away all possible file I/O issues.

Python read() function

In the library I am using, the get(key) method uses the Python read() function.

On Linux it is sufficient to put the read() function in a try-except:

    try:
        with open(f, 'r') as fo:
            return fo.read()
    except Exception as e:
        return None

The function will wait until the data is available. An exception will only be raised if there is a timeout, which on most Linux systems is 60 seconds, or some other unexpected error.

On Windows this will fail immediately if the file is accessed by another thread. To create the same behaviour we have with Linux we must add retries and a delay, for example:

    max_sleep_time = 10
    total_sleep_time = 0
    sleep_time = 0.02
    while total_sleep_time < max_sleep_time:
        try:
            with open(f, 'r') as fo:
                return fo.read()
        except OSError as e:
            errno = getattr(e, 'errno', None)
            if errno == 13:
                # permission error
                time.sleep(sleep_time)
                total_sleep_time += sleep_time
                sleep_time *= 2
            else:
                # some other error             
                return None
        except Exception as e:
            return None

    # out of retries
    return None

Python os.replace() function

In the library I am using, the set(key, value) method uses the Python os.replace() function.

On Linux it is sufficient to put the os.replace() function in a try-except:

    try:
        os.replace(src, dst)
        return True
    except Exception as e:
        return False

The function will wait until the file can be replaced. An exception will only be raised if there is a timeout, which on most Linux systems is 60 seconds, or some other unexpected error.

On Windows this will fail immediately if the file is accessed by another thread. To create the same behaviour we have with Linux we must add retries and a delay, for example:

    max_sleep_time = 10
    total_sleep_time = 0
    sleep_time = 0.02
    while total_sleep_time < max_sleep_time:
        try:
            os.replace(src, dst)
            return True
        except Exception as e:
            winerror = getattr(e, 'winerror', None)
            if winerror == 5:
                time.sleep(sleep_time)
                total_sleep_time += sleep_time
                sleep_time *= 2
            else:
                # some other error
                return False

    # out of retries
    return False

Conclusion

Creating a Python program that can run on multiple platforms can be complicated because you can run into problems like the ones described above. At first I was surprised that Python didn't hide the Windows complexity from me. Coming from Linux I thought, Python why don't you make this work on Windows the way it works on Linux?
But that's the choice the Python developers made. It may not be possible either. I couldn't find a single line in the Python docs online that alerted me and noticed that many people struggle with this. I submitted a bug report to ask that a warning be added for developers when developing for multiple platforms. But later refrained from doing so because I realize I am very biased coming from Linux.

Links / credits

backports.py
https://github.com/flennerhag/mlens/blob/master/mlens/externals/joblib/backports.py

os.replace
https://docs.python.org/3/library/os.html?highlight=os%20replace#os.replace

os.replace is not cross-platform: at least improve documentation
https://bugs.python.org/issue46003

Read more

Threads

Leave a comment

Comment anonymously or log in to comment.

Comments

Leave a reply

Reply anonymously or log in to reply.