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

Les E/S du fichier Python sur Windows et Linux sont deux choses différentes.

Lorsque vous utilisez des threads, vous pouvez rencontrer des problèmes causés par le comportement différent des fonctions d'entrée/sortie de fichiers.

8 décembre 2021
Dans Python
post main image
https://unsplash.com/@momofactory

J'ai un programme Python qui fonctionne bien sur Linux. Il y a quelques mois, j'ai voulu le faire fonctionner sur Windows.

C'était la première fois que j'utilisais Python sur Windows. Installer l'application Python , créer virtual environment, copier et exécuter. Aucun problème ... oh mais il y avait un problème. Ma session disparaissait parfois ... WTF ! J'ai remarqué le problème en appuyant plusieurs fois sur F5 dans un laps de temps très court. Il est temps de procéder à une enquête approfondie.

L'application et le magasin de valeurs clés de la session

L'application Python est une application Flask . Elle utilise l'interface de système de fichiers de Flask-Session pour stocker les sessions sous forme de fichiers. Flask-Session utilise un autre paquet PyPi pour gérer toutes les entrées/sorties de fichiers. Comme vous pouvez l'imaginer, ce paquetage implémente un magasin de valeurs clés.

Il y a deux méthodes principales dans ce paquetage :

  • set(key, value), pour écrire/mettre à jour les données de la session
  • get(key), pour récupérer les données de la session.

Dans le paquetage, la méthode set(key, value) crée un fichier temporaire, puis appelle la fonction Python os.replace() pour remplacer le fichier de session. La méthode get(key) appelle la fonction read de Python .

Test à l'aide de threads

Lorsque vous exécutez Flask en mode développement, vous verrez beaucoup de demandes au magasin de sessions car Flask sert également images, des fichiers CSS, etc. Dans un environnement de production où vous servez du contenu statique via un serveur web, vous avez moins de chances de rencontrer ce problème, mais il est toujours présent !
C'est ainsi que m'est venue l'idée d'écrire un petit test utilisant des 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()

Sur Linux , il n'y avait aucune erreur, rien. Mais sur Windows cela a soulevé des exceptions générées aléatoirement :

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

L'exception [WinError 5] a été générée par la fonction os.replace() de Python . L'exception [Errno 13] a été générée par la fonction read() de Python .

Que se passe-t-il ici ?

Les E/S de fichiers sur Windows et Linux sont deux choses différentes.

Je pensais que la Python me protégerait des implémentations spécifiques aux plateformes. C'est le cas dans de nombreuses fonctions, mais pas dans toutes. En particulier, lorsque vous utilisez des threads, vous pouvez rencontrer des problèmes causés par le comportement différent des fonctions d'entrée/sortie de fichiers.

Extrait de Python Bug Tracker Issue46003 :

Comme on dit, il n'y a pas de "logiciel portable", seulement des "logiciels qui ont été portés".
Surtout dans un domaine comme les E/S de fichiers : dès que l'on va au-delà du simple "un processus ouvre, écrit et ferme" et qu'un autre processus "ouvre, lit et ferme", il y a beaucoup de problèmes spécifiques à la plate-forme. Python n'essaie pas de faire abstraction de tous les problèmes possibles d'E/S de fichiers.

Fonction read() de Python

Dans la bibliothèque que j'utilise, la méthode get(key) utilise la fonction Python read().

Sur Linux il suffit de mettre la fonction read() dans un try-except :

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

La fonction attendra que les données soient disponibles. Une exception ne sera levée qu'en cas de dépassement du délai d'attente, qui est de 60 secondes sur la plupart des systèmes Linux , ou en cas d'erreur inattendue.

Sur Windows , l'échec sera immédiat si le fichier est accédé par un autre thread. Pour créer le même comportement qu'avec la Linux , nous devons ajouter des tentatives et un délai, par exemple :

    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

Fonction Python os.replace()

Dans la bibliothèque que j'utilise, la méthode set(key, value) utilise la fonction Python os.replace().

Sur Linux il suffit de mettre la fonction os.replace() dans un try-except :

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

La fonction attendra que le fichier puisse être remplacé. Une exception ne sera levée qu'en cas de dépassement du délai d'attente, qui est de 60 secondes sur la plupart des systèmes Linux , ou en cas d'erreur inattendue.

Sur Windows , l'échec sera immédiat si le fichier est accédé par un autre thread. Pour créer le même comportement que nous avons avec Linux , nous devons ajouter des tentatives et un délai, par exemple :

    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

Créer un programme Python qui peut fonctionner sur plusieurs plateformes peut être compliqué car vous pouvez rencontrer des problèmes comme ceux décrits ci-dessus. Au début, j'ai été surpris que Python ne me cache pas la complexité de Windows . Venant de Linux je me suis dit, Python pourquoi ne pas faire fonctionner cela sur Windows de la même manière que sur Linux ?
Mais c'est le choix qu'ont fait les développeurs de Python . Il se peut que ce ne soit pas possible non plus. Je n'ai pas trouvé une seule ligne dans les docs Python en ligne qui m'ait alerté et j'ai remarqué que beaucoup de gens ont du mal avec ça. J'ai soumis un rapport de bogue pour demander qu'un avertissement soit ajouté pour les développeurs lorsqu'ils développent pour plusieurs plateformes. Mais je me suis abstenu de le faire par la suite car je me rends compte que je suis très partial, venant de Linux.

Liens / crédits

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

En savoir plus...

Threads

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.