Pasta de criação de raça em python
-
22-09-2019 - |
Pergunta
Eu tenho um módulo de cache de urllib2, que trava esporadicamente por causa do seguinte código:
if not os.path.exists(self.cache_location):
os.mkdir(self.cache_location)
O problema é que, quando a segunda linha está sendo executada, a pasta pode existir e errará:
File ".../cache.py", line 103, in __init__ os.mkdir(self.cache_location) OSError: [Errno 17] File exists: '/tmp/examplecachedir/'
Isso ocorre porque o script é lançado simultaneamente várias vezes, por código de terceiros sobre o qual não tenho controle.
O código (antes de tentar corrigir o bug) pode ser encontrado Aqui, no Github
Eu não posso usar o tempfile.mkstemp, como resolve a condição de corrida usando um diretório nomeado aleatoriamente (Fonte tempfile.py aqui), que derrotaria o objetivo do cache.
Não quero simplesmente descartar o erro, pois o mesmo erro errno 17 é levantado se o nome da pasta existir como um arquivo (um erro diferente), por exemplo:
$ touch blah $ python >>> import os >>> os.mkdir("blah") Traceback (most recent call last): File "", line 1, in OSError: [Errno 17] File exists: 'blah' >>>
Eu não posso usar threading.RLock
como o código é chamado de vários processos.
Então, tentei escrever um bloqueio simples baseado em arquivos (Essa versão pode ser encontrada aqui), mas isso tem um problema: cria o arquivo de bloqueio um de nível, então /tmp/example.lock
por /tmp/example/
, que quebra se você usa /tmp/
como um diretor de cache (como tenta fazer /tmp.lock
)..
Em suma, eu preciso armazenar em cache urllib2
respostas ao disco. Para fazer isso, preciso acessar um diretório conhecido (criando -o, se necessário), de maneira segura multiprocesso. Ele precisa funcionar no OS X, Linux e Windows.
Pensamentos? A única solução alternativa em que consigo pensar é reescrever o módulo de cache usando o armazenamento SQLITE3, em vez de arquivos.
Solução
No Python 3.x, você pode usar os.makedirs(path, exists_ok=True)
, o que não levantará nenhuma exceção se esse diretório existir. Ele irá aumentar FileExistsError: [Errno 17]
Se existir um arquivo com o mesmo nome do diretório solicitado (path
).
Verifique com:
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)
Outras dicas
Ao invés de
if not os.path.exists(self.cache_location):
os.mkdir(self.cache_location)
você poderia fazer
try:
os.makedirs(self.cache_location)
except OSError:
pass
Como você acabaria com o mesmo funcionalidade.
Isenção de responsabilidade: não sei o quão pitônico isso pode ser.
Usando SQLite3
, poderia ser um pouco de exagero, mas adicionaria um muito de funcionalidade e flexibilidade no seu caso de uso.
Se você tiver que fazer muitas "selecionando", inserção e filtragem simultâneas, é uma ótima ideia usar SQLite3
, pois não adicionará muita complexidade sobre arquivos simples (pode -se argumentar que ele remove a complexidade).
Relendo sua pergunta (e comentários), posso entender melhor o seu problema.
Qual é a possibilidade de que um Arquivo poderia criar a mesma condição de corrida?
Se for pequeno o suficiente, então eu faria algo como:
if not os.path.isfile(self.cache_location):
try:
os.makedirs(self.cache_location)
except OSError:
pass
Além disso, lendo seu código, eu mudaria
else:
# Our target dir is already a file, or different error,
# relay the error!
raise OSError(e)
para
else:
# Our target dir is already a file, or different error,
# relay the error!
raise
Como é realmente o que você quer, Python para reorrair exatamente a mesma exceção (apenas nitpicking).
Mais uma coisa, pode ser isto Pode ser útil para você (apenas como UNIX).
O código com quem acabei foi:
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
Você poderia pegar a exceção e depois testar se o arquivo existe como um diretório ou não?
Quando você tem condições de corrida, muitas vezes o EAFP (mais fácil de perguntar perdão do que permissão) funciona melhor que o LBYL (olhe antes de saltar)