关于缓存:Python中的Race-condition创建文件夹

Race-condition creating folder in Python

我有一个URLLIB2缓存模块,由于以下代码偶尔崩溃:

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

问题是,在执行第二行时,文件夹可能存在,并将出错:

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

这是因为脚本同时启动了无数次,由第三方代码我无法控制。

代码(在我尝试修复bug之前)可以在Github上的此处找到

我不能使用tempfile.mkstemp,因为它使用一个随机命名的目录(这里是tempfile.py源)来解决争用条件,这会破坏缓存的目的。

我不想简单地放弃错误,因为如果文件夹名作为文件存在(不同的错误),则会引发相同的错误errno 17错误,例如:

1
2
3
4
5
6
7
8
$ 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存储而不是文件重写缓存模块。


而不是

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

你可以做到

1
2
3
4
try:
    os.makedirs(self.cache_location)
except OSError:
    pass

因为你最终会得到相同的功能。

免责声明:我不知道这可能是怎样的Python。

使用SQLite3可能有点过分,但会给您的用例增加很多功能和灵活性。

如果您需要做大量的"选择"、并发插入和过滤,那么使用SQLite3是一个好主意,因为它不会比简单文件增加太多的复杂性(可以说它消除了复杂性)。

重读你的问题(和评论),我能更好地理解你的问题。

文件可以创建相同的竞争条件的可能性是什么?

如果它足够小,那么我会做如下的事情:

1
2
3
4
5
if not os.path.isfile(self.cache_location):
    try:
        os.makedirs(self.cache_location)
    except OSError:
        pass

而且,读你的代码,我会改变

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

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

因为它确实是您想要的,所以python要重新发出完全相同的异常(just nitpicking)

还有一件事,可能这对您有用(仅限于Unix)。


我最后得到的代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(跳跃前先看)更有效。

错误检查策略


您能否捕获异常,然后测试该文件是否作为目录存在?