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

Python archivo I/O en Windows y Linux son dos cosas diferentes

Cuando se utilizan hilos se pueden encontrar problemas causados por el diferente comportamiento de las funciones de E/S de los archivos.

8 diciembre 2021
En Python
post main image
https://unsplash.com/@momofactory

Tengo un programa Python que funciona bien en Linux. Hace unos meses quise ejecutarlo en Windows.

Esta fue la primera vez que usé Python en Windows. Instale Python app, cree virtual environment, copie y ejecute. Sin problemas ... oh, pero había un problema. Mi sesión a veces desaparecía ... ¡WTF! Me di cuenta del problema al pulsar repetidamente F5 en muy poco tiempo. Es hora de investigar a fondo.

La aplicación y el almacén de claves y valores de la sesión

La aplicación Python es una aplicación Flask . Utiliza la interfaz del sistema de archivos de Flask-Session para almacenar las sesiones como archivos. Flask-Session utiliza otro paquete de PyPi para manejar toda la E/S de los archivos. Como puedes imaginar este paquete implementa un almacén de valores clave.

Hay dos métodos principales en este paquete:

  • set(key, value), para escribir/actualizar los datos de la sesión
  • get(key), para obtener los datos de la sesión

En el paquete, el método set(key, value) crea un archivo temporal y luego llama a la función Python os.replace() para reemplazar el archivo de sesión. El método get(key) llama a la función de lectura Python .

Prueba con hilos

Cuando ejecutes Flask en modo de desarrollo, verás muchas peticiones al almacén de sesiones porque Flask también sirve images, archivos CSS, etc. En un entorno de producción en el que se sirve contenido estático a través de un servidor web es menos probable que te encuentres con este problema, ¡pero sigue existiendo!
Así es como se me ocurrió escribir una pequeña prueba utilizando hilos.

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

En Linux no había errores, nada. Pero en Windows esto levantó excepciones generadas aleatoriamente:

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

La excepción [WinError 5] fue generada por la función Python os.replace(). La excepción [Errno 13] fue generada por la función Python read().

¿Qué está pasando aquí?

La E/S de archivos en Windows y Linux son dos cosas diferentes

Supuse que Python me protegería de las implementaciones específicas de la plataforma. Lo hace en muchas funciones pero no en todas. Especialmente cuando se usan hilos se pueden encontrar problemas causados por el diferente comportamiento de las funciones de E/S de los archivos.

De Python Bug Tracker Issue46003:

Como dicen, no existe el "software portable", sólo el "software que ha sido portado".
Especialmente en un área como la E/S de archivos: una vez que se va más allá del simple "un proceso abre, escribe y cierra" y otro proceso "abre, lee y cierra", hay un montón de problemas específicos de la plataforma. Python no trata de abstraer todos los posibles problemas de E/S de archivos.

Python función read()

En la biblioteca que estoy utilizando, el método get(key) utiliza la función Python read().

En Linux basta con poner la función read() en un try-except:

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

La función esperará hasta que los datos estén disponibles. Sólo se lanzará una excepción si hay un tiempo de espera, que en la mayoría de los sistemas Linux es de 60 segundos, o algún otro error inesperado.

En Windows esto fallará inmediatamente si el archivo es accedido por otro hilo. Para crear el mismo comportamiento que tenemos con Linux debemos añadir reintentos y un retraso, por ejemplo:

    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 función os.replace()

En la librería que estoy utilizando, el método set(key, value) utiliza la función Python os.replace().

En Linux basta con poner la función os.replace() en un try-except:

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

La función esperará hasta que el archivo pueda ser reemplazado. Sólo se lanzará una excepción si hay un tiempo de espera, que en la mayoría de los sistemas Linux es de 60 segundos, o algún otro error inesperado.

En Windows esto fallará inmediatamente si el archivo es accedido por otro hilo. Para crear el mismo comportamiento que tenemos con Linux debemos añadir reintentos y un retraso, por ejemplo:

    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

Conclusión

Crear un programa Python que pueda ejecutarse en múltiples plataformas puede ser complicado porque puedes encontrarte con problemas como los descritos anteriormente. Al principio me sorprendió que Python no me ocultara la complejidad de Windows . Viniendo de Linux pensé, Python ¿por qué no hacéis que esto funcione en Windows como funciona en Linux?
Pero esa es la elección que hicieron los desarrolladores de Python . Puede que tampoco sea posible. No pude encontrar ni una sola línea en los documentos de Python en línea que me alertara y me di cuenta de que mucha gente tiene problemas con esto. Envié un informe de error para pedir que se añadiera una advertencia para los desarrolladores cuando se desarrollara para múltiples plataformas. Pero más tarde me abstuve de hacerlo porque me doy cuenta de que soy muy parcial viniendo de Linux.

Enlaces / créditos

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

Leer más

Threads

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.