Race condition la création du dossier en Python
-
22-09-2019 - |
Question
J'ai un module de mise en cache de urllib2, qui tombe en panne de façon sporadique en raison du code suivant:
if not os.path.exists(self.cache_location):
os.mkdir(self.cache_location)
Le problème est, au moment où la deuxième ligne est en cours d'exécution, le dossier peut exister, et l'erreur:
File ".../cache.py", line 103, in __init__ os.mkdir(self.cache_location) OSError: [Errno 17] File exists: '/tmp/examplecachedir/'
En effet, le script est lancé en même temps à plusieurs reprises, par code tiers que je ne contrôle pas.
Le code (avant que je tenté de corriger le bug) peut être trouvé ici, sur GitHub
Je ne peux pas utiliser le tempfile.mkstemp , comme permet de résoudre la condition de course en utilisant un répertoire nommé de façon aléatoire ( source de tempfile.py ici ), ce qui irait à l'encontre du cache.
Je ne veux pas simplement jeter l'erreur, comme la même erreur erreur Errno 17 est augmentée si le nom du dossier existe sous la forme d'un fichier (une autre erreur), par exemple:
$ touch blah $ python >>> import os >>> os.mkdir("blah") Traceback (most recent call last): File "", line 1, in OSError: [Errno 17] File exists: 'blah' >>>
Je ne peux pas en utilisant threading.RLock
que le code est appelé à partir de plusieurs processus.
Alors, j'ai essayé d'écrire simple verrouillage basé sur des fichiers ( cette version peut être trouvé ici ), mais cela a un problème: il crée le lockfile un niveau, donc /tmp/example.lock
pour /tmp/example/
, qui brise si vous utilisez /tmp/
comme un cache dir (comme il essaie de faire /tmp.lock
) ..
En bref, je dois mettre en cache des réponses urllib2
à disque. Pour ce faire, je dois accéder à un répertoire connu (création, le cas échéant), dans un multiprocessus de manière sûre. Il a besoin de travailler sur OS X, Linux et Windows.
Pensées? La seule solution de rechange que je peux penser est de réécrire le module de cache à l'aide de stockage SQLite3, plutôt que des fichiers.
La solution
En Python 3.x, vous pouvez utiliser os.makedirs(path, exists_ok=True)
, qui ne soulèvera pas d'exception si ce répertoire existe. Il soulèvera FileExistsError: [Errno 17]
si un fichier existe avec le même nom que le répertoire demandé (path
).
Vérifier avec:
import os
parent = os.path.dirname(__file__)
target = os.path.join(parent, 'target')
os.makedirs(target, exist_ok=True)
os.makedirs(target, exist_ok=True)
os.rmdir(target)
with open(target, 'w'):
pass
os.makedirs(target, exist_ok=True)
Autres conseils
Au lieu de
if not os.path.exists(self.cache_location):
os.mkdir(self.cache_location)
vous pouvez faire
try:
os.makedirs(self.cache_location)
except OSError:
pass
Comme vous finiriez avec le même fonctionnalité .
DISCLAIMER: Je ne sais pas comment cela pourrait être Pythonic
. Utilisation SQLite3
, peut être un peu exagéré, mais ajouterait un beaucoup de la fonctionnalité et la flexibilité de votre cas d'utilisation.
Si vous devez faire beaucoup de « sélection », l'insertion simultanée et le filtrage, il est une excellente idée d'utiliser SQLite3
, car il ajoute habitude trop de complexité sur les fichiers simples (on pourrait dire qu'il supprime la complexité).
Relire votre question (et commentaires) je peux mieux comprendre votre problème.
Quelle est la possibilité qu'un fichier pourrait créer la même condition de course?
S'il est assez petit, alors je ferais quelque chose comme:
if not os.path.isfile(self.cache_location):
try:
os.makedirs(self.cache_location)
except OSError:
pass
En outre, la lecture de votre code, je changerais
else:
# Our target dir is already a file, or different error,
# relay the error!
raise OSError(e)
à
else:
# Our target dir is already a file, or different error,
# relay the error!
raise
comme il est vraiment ce que vous voulez, Python reraise la même exception exacte (juste tatillonne) .
Une autre chose, peut-être script shell cette pourrait être d'utilisation pour vous (Unix uniquement).
Le code j'ai fini avec était:
import os
import errno
folder_location = "/tmp/example_dir"
try:
os.mkdir(folder_location)
except OSError as 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
Pouvez-vous attraper l'exception, puis vérifier si le fichier existe comme un répertoire ou non?
Lorsque vous avez des conditions de course souvent EAFP (plus facile de demander pardon que la permission) fonctionne mieux que LBYL (regarder avant de sauter)