2009-10-19 19 views
13

I tiene un módulo urllib2 almacenamiento en caché, que esporádicamente se bloquea por lo siguiente código:Race-condición creación de carpeta en Python

if not os.path.exists(self.cache_location): 
    os.mkdir(self.cache_location) 

El problema es que en el momento está siendo ejecutada la segunda línea, la carpeta puede existir, y se error:

 File ".../cache.py", line 103, in __init__ 
    os.mkdir(self.cache_location) 
OSError: [Errno 17] File exists: '/tmp/examplecachedir/'

Esto es porque el guión está lanzado simultáneamente en numerosas ocasiones, por el código de terceros no tengo ningún control sobre.

El código (antes he tratado de corregir el error) se puede encontrar here, on github

no puedo usar el tempfile.mkstemp, como los que resuelve la condición de carrera mediante el uso de un directorio de nombre aleatorio (tempfile.py source here), lo que arruinaría el propósito de la memoria caché

no quiero por desechar el error, ya que el mismo error se eleva Errno 17 de error si el nombre de la carpeta existe como un archivo (un error diferente), por ejemplo:

$ touch blah 
$ python 
>>> import os 
>>> os.mkdir("blah") 
Traceback (most recent call last): 
    File "", line 1, in 
OSError: [Errno 17] File exists: 'blah' 
>>>

no puedo utilizando threading.RLock ya que el código se llama desde múltiples procesos.

tanto, he intentado escribir un bloqueo basado en archivos simples (that version can be found here), pero esto tiene un problema: se crea el fichero de bloqueo en un nivel superior, por lo /tmp/example.lock para /tmp/example/, que se rompe si se utiliza /tmp/ como un directorio caché (como se intenta hacer /tmp.lock) ..

En resumen, necesito almacenar en caché urllib2 respuestas al disco. Para hacer esto, necesito acceder a un directorio conocido (crearlo, si es necesario), de una manera segura en multiprocesos. Debe funcionar en OS X, Linux y Windows.

¿Pensamientos? La única solución alternativa que puedo pensar es volver a escribir el módulo de caché utilizando SQLite3 storage, en lugar de archivos.

Respuesta

5

El código acabé con era:

import os 
import errno 

folder_location = "/tmp/example_dir" 

try: 
    os.mkdir(folder_location) 
except OSError, e: 
    if e.errno == errno.EEXIST and os.path.isdir(folder_location): 
     # File exists, and it's a directory, 
     # another process beat us to creating this dir, that's OK. 
     pass 
    else: 
     # Our target dir exists as a file, or different error, 
     # reraise the error! 
     raise 
2

¿Podría capturar la excepción y luego probar si el archivo existe como directorio o no?

+0

¡Posiblemente! Pensé en eso justo cuando estaba volviendo a leer la pregunta, antes de enviarla ... Lo he implementado (http://github.com/dbr/tvdb_api/blob/468d9f816373b14ef3a483fca07e031b69fa62f9/cache.py#L103-114), y hará que la persona que informó el error lo pruebe en breve. – dbr

+0

Esto parece funcionar perfectamente, gracias! – dbr

+1

@dbr: tenga en cuenta que en la línea 114, quiere 'raise e' ya que ya es una instancia de' OSError'. http://github.com/dbr/tvdb_api/blob/468d9f816373b14ef3a483fca07e031b69fa62f9/cache.py#L114 – nosklo

11

En lugar de

if not os.path.exists(self.cache_location): 
    os.mkdir(self.cache_location) 

que podría hacer

try: 
    os.makedirs(self.cache_location) 
except OSError: 
    pass 

Como era terminar con la misma funcionalidad .

DESCARGO DE RESPONSABILIDAD: No sé cómo podría ser Pythonic.


Usando SQLite3, podría ser un poco excesivo, pero añadiría mucho de funcionalidad y flexibilidad para su caso de uso.

Si tiene que hacer muchas "selecciones", inserción simultánea y filtrado, es una gran idea usar SQLite3, ya que no agregará demasiada complejidad a los archivos simples (se podría argumentar que elimina la complejidad).


Relectura de su pregunta (y comentarios) Puedo entender mejor su problema.

¿Cuál es la posibilidad de que un archivo podría crear la misma condición de carrera?

Si es lo suficientemente pequeño, entonces me gustaría hacer algo como:

if not os.path.isfile(self.cache_location): 
    try: 
     os.makedirs(self.cache_location) 
    except OSError: 
     pass 

Además, la lectura de su código, que cambiaría

else: 
    # Our target dir is already a file, or different error, 
    # relay the error! 
    raise OSError(e) 

a

else: 
    # Our target dir is already a file, or different error, 
    # relay the error! 
    raise 

ya que es realmente lo que quieres, Python resubirá exactamente la misma excepción (solo detalle).


Una cosa más, puede ser this podría ser de utilidad para usted (Unix solamente).

+2

+1: Es perfectamente Pythonic, IMO. –

+1

De acuerdo, aunque podría considerar el uso de 'os.makedirs', que también crea directorios principales, si es necesario. –

+0

@Eli Courtwright: buen punto. Modificado. – voyager

2

Cuando tenga las condiciones de carrera menudo EAFP (más fácil pedir perdón que permiso) funciona mejor que LBYL (mirar antes de saltar)

Error checking strategies

+0

Todo esto depende de la probabilidad de conflictos - EAFP = concurrencia optimista, que verifica el conflicto después de probar la operación, y funciona bien si hay poca probabilidad de conflictos. LBYL = concurrencia pesimista que verifica antes de la operación y es mejor si hay muchos conflictos típicamente. Para un caso simple como este, creo que pesimista es mejor. – RichVel

+0

@RichVel, con LBYL aquí tienes la posibilidad de una condición de carrera. Por lo tanto, necesita código adicional para manejar la excepción de todos modos. Entonces, es solo una operación simple y el código adicional no es mucho ahora, pero estas cosas tienen una forma de crecer durante la vida del proyecto. Hay un costo adicional cada vez que alguien necesita leer/comprender/depurar este código adicional. Creo que es probable que sea una optimización prematura e innecesaria en este caso. –

+0

No es el caso, porque mkdir es atómico, es decir, es equivalente a una sola operación de "prueba y ajuste". Codificar las operaciones LBYL con pruebas no atómicas y establecer operaciones provocará condiciones de carrera, pero eso es un error de implementación. – RichVel

Cuestiones relacionadas