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.
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
Recientes
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
- Transferencia de datos segura con cifrado de Public Key y pyNaCl
- rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando PyInstaller y Cython para crear un ejecutable de Python
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow