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

Python bestand I/O op Windows en Linux zijn twee verschillende dingen

Bij gebruik van threads kun je problemen krijgen door verschillend gedrag van bestands-I/O functies.

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

Ik heb een Python programma dat prima draait op Linux. Een paar maanden geleden wilde ik dit programma op Windows laten draaien.

Dit was de eerste keer dat ik Python op Windows gebruikte. Installeer Python app, maak virtual environment, kopieer en voer uit. Geen problemen ... oh maar er was een probleem. Mijn sessie verdween soms ... WTF! Ik merkte het probleem door herhaaldelijk op F5 te drukken in een zeer korte tijd. Tijd voor een grondig onderzoek.

De applicatie en de session key-value store

De Python app is een Flask toepassing. Het gebruikt de bestandssysteeminterface van Flask-Session om sessies als bestanden op te slaan. Flask-Session gebruikt een ander PyPi pakket om alle bestands I/O af te handelen. Zoals u zich kunt voorstellen implementeert dit pakket een "key-value store".

Er zijn twee belangrijke methodes in dit pakket:

  • set(key, value), om de sessie gegevens te schrijven/bij te werken
  • get(key), om de sessie gegevens op te halen

In het pakket maakt de methode set(key, value) een tijdelijk bestand en roept dan de functie Python os.replace() aan om het sessiebestand te vervangen. De methode get(key) roept de Python leesfunctie aan.

Testen met threads

Wanneer je Flask in ontwikkelingsmodus draait, zul je veel requests naar de session store zien omdat Flask ook images, CSS bestanden, etc. serveert. In een productie omgeving waar je statische inhoud serveert via een webserver is de kans kleiner dat je tegen dit probleem aanloopt, maar het is er nog steeds!
Zo kwam ik op het idee om een kleine test te schrijven die gebruik maakt van 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()

Op Linux waren er geen fouten, niets. Maar op Windows veroorzaakte dit willekeurig gegenereerde uitzonderingen:

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

De [WinError 5] uitzondering werd gegenereerd door de Python os.replace() functie. De [Errno 13] uitzondering werd gegenereerd door de Python read() functie.

Wat is hier aan de hand?

Bestands-I/O op Windows en Linux zijn twee verschillende dingen

Ik nam aan dat Python mij zou beschermen tegen platform specifieke implementaties. Dat doet het in veel functies, maar niet in alle. Vooral bij gebruik van threads kun je problemen krijgen door verschillend gedrag van file I/O functies.

Van Python Bug Tracker Issue46003:

Zoals ze zeggen, er bestaat niet zoiets als 'portable software', alleen 'software die geporteerd is'.
Vooral op een gebied als file I/O: zodra je verder gaat dan simpel 'een proces opent, schrijft en sluit' en een ander proces vervolgens 'opent, leest en sluit', zijn er een hoop platform-specifieke problemen. Python probeert niet om alle mogelijke bestands I/O problemen weg te abstraheren.

Python read() functie

In de bibliotheek die ik gebruik, gebruikt de get(key) methode de Python read() functie.

Op Linux is het voldoende om de read() functie in een try-except te zetten:

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

De functie zal wachten tot de gegevens beschikbaar zijn. Er wordt alleen een exceptie gemaakt als er een time-out is, die op de meeste Linux systemen 60 seconden is, of een andere onverwachte fout.

Op Windows zal dit onmiddellijk mislukken als het bestand door een andere thread wordt benaderd. Om hetzelfde gedrag te krijgen als we met Linux hebben, moeten we bijvoorbeeld retries en een vertraging toevoegen:

    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() functie

In de bibliotheek die ik gebruik, gebruikt de set(key, value) methode de Python os.replace() functie.

Op Linux is het voldoende om de os.replace() functie in een try-except te zetten:

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

De functie zal wachten tot het bestand kan worden vervangen. Er wordt alleen een exceptie gemaakt als er een time-out is, die op de meeste Linux systemen 60 seconden is, of een andere onverwachte fout.

Op Windows zal dit onmiddellijk mislukken als het bestand door een andere thread wordt benaderd. Om hetzelfde gedrag te krijgen dat we met Linux hebben, moeten we bijvoorbeeld retries en een vertraging toevoegen:

    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

Conclusie

Het maken van een Python programma dat op meerdere platformen kan draaien kan gecompliceerd zijn omdat je tegen problemen kunt aanlopen zoals hierboven beschreven. In het begin was ik verbaasd dat Python de complexiteit van Windows niet voor me verborgen hield. Komende van Linux dacht ik, Python waarom laat je dit niet werken op Windows zoals het werkt op Linux?
Maar dat is de keuze die de Python ontwikkelaars hebben gemaakt. Misschien is het ook niet mogelijk. Ik kon geen enkele regel in de Python docs online vinden die mij waarschuwde en merkte dat veel mensen hiermee worstelen. Ik diende een bug report in om te vragen een waarschuwing toe te voegen voor ontwikkelaars bij het ontwikkelen voor meerdere platformen. Maar later zag ik daarvan af omdat ik me realiseer dat ik erg bevooroordeeld ben als ik van Linux kom.

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

Lees meer

Threads

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.