Папка создания условий гонки в Python
-
22-09-2019 - |
Вопрос
У меня есть модуль кэширования urllib2, который периодически выходит из строя из-за следующего кода:
if not os.path.exists(self.cache_location):
os.mkdir(self.cache_location)
Проблема в том, что к моменту выполнения второй строки папка может существовать и выдать ошибку:
File ".../cache.py", line 103, in __init__ os.mkdir(self.cache_location) OSError: [Errno 17] File exists: '/tmp/examplecachedir/'
Это связано с тем, что скрипт одновременно запускается множество раз с помощью стороннего кода, который я не контролирую.
Код (до того, как я попытался исправить ошибку) можно найти здесь, на гитхабе
Я не могу использовать tempfile.mkstemp, поскольку он решает проблему гонки, используя каталог со случайным именем (источник tempfile.py здесь), что противоречит цели кэша.
Я не хочу просто сбрасывать ошибку, так как возникает та же ошибка Errno 17, если имя папки существует в виде файла (другая ошибка), например:
$ touch blah $ python >>> import os >>> os.mkdir("blah") Traceback (most recent call last): File "", line 1, in OSError: [Errno 17] File exists: 'blah' >>>
я не могу использовать threading.RLock
поскольку код вызывается из нескольких процессов.
Итак, я попробовал написать простую файловую блокировку (эту версию можно найти здесь), но здесь есть проблема:он создает файл блокировки на один уровень выше, поэтому /tmp/example.lock
для /tmp/example/
, который ломается, если вы используете /tmp/
в качестве каталога кэша (поскольку он пытается сделать /tmp.lock
)..
Короче говоря, мне нужно кэшировать urllib2
ответы на диск.Для этого мне нужно получить доступ к известному каталогу (создав его, если необходимо) безопасным многопроцессорным способом.Он должен работать на OS X, Linux и Windows.
Мысли?Единственное альтернативное решение, которое я могу придумать, — это переписать модуль кэша, используя хранилище SQLite3, а не файлы.
Решение
В Python 3.x вы можете использовать os.makedirs(path, exists_ok=True)
, что не вызовет никаких исключений, если такой каталог существует.Это поднимет FileExistsError: [Errno 17]
если существует файл с тем же именем, что и запрошенный каталог (path
).
Проверьте это с помощью:
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)
Другие советы
Вместо
if not os.path.exists(self.cache_location):
os.mkdir(self.cache_location)
ты мог бы сделать
try:
os.makedirs(self.cache_location)
except OSError:
pass
Поскольку вы получите то же самое функциональность.
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ:Я не знаю, насколько это может быть Pythonic.
С использованием SQLite3
, мощь это немного перебор, но я бы добавил много функциональности и гибкости в соответствии с вашим вариантом использования.
Если вам приходится много «выбирать», одновременно вставлять и фильтровать, отличная идея использовать SQLite3
, поскольку он не усложняет простые файлы (можно утверждать, что он усложняет).
Перечитывая ваш вопрос (и комментарии), я могу лучше понять вашу проблему.
Какова вероятность того, что файл могло бы создать такое же состояние гонки?
Если он достаточно мал, я бы сделал что-то вроде:
if not os.path.isfile(self.cache_location):
try:
os.makedirs(self.cache_location)
except OSError:
pass
Кроме того, читая ваш код, я бы изменил
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
поскольку это действительно то, что вы хотите, Python повторно вызовет то же самое исключение (просто придирки).
Еще одна вещь, может быть этот может быть вам полезен (только для Unix).
Код, который у меня получился, был:
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
Не могли бы вы уловить исключение, а затем проверить, существует ли файл как каталог или нет?
Когда у вас возникают состояния гонки, EAFP (проще попросить прощения, чем разрешения) работает лучше, чем LBYL (посмотрите, прежде чем прыгать)