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.
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
Recent
- Database UUID primaire sleutels van je webapplicatie verbergen
- Don't Repeat Yourself (DRY) met Jinja2
- SQLAlchemy, PostgreSQL, maximum aantal rijen per user
- Toon de waarden in SQLAlchemy dynamische filters
- Veilige gegevensoverdracht met Public Key versleuteling en pyNaCl
- rqlite: een alternatief voor SQLite met hoge beschikbaarheid en distributed
Meest bekeken
- Met behulp van Python's pyOpenSSL om SSL-certificaten die van een host zijn gedownload te controleren
- Gebruik van UUIDs in plaats van Integer Autoincrement Primary Keys met SQLAlchemy en MariaDb
- Maak verbinding met een dienst op een Docker host vanaf een Docker container
- PyInstaller en Cython gebruiken om een Python executable te maken
- SQLAlchemy: Gebruik van Cascade Deletes om verwante objecten te verwijderen
- Flask RESTful API verzoekparametervalidatie met Marshmallow-schema's