@@ -33,6 +33,18 @@ class SafeFileCache(SeparateBodyBaseCache):
33
33
"""
34
34
A file based cache which is safe to use even when the target directory may
35
35
not be accessible or writable.
36
+
37
+ There is a race condition when two processes try to write and/or read the
38
+ same entry at the same time, since each entry consists of two separate
39
+ files (https://github.com/psf/cachecontrol/issues/324). We therefore have
40
+ additional logic that makes sure that both files to be present before
41
+ returning an entry; this fixes the read side of the race condition.
42
+
43
+ For the write side, we assume that the server will only ever return the
44
+ same data for the same URL, which ought to be the case for files pip is
45
+ downloading. PyPI does not have a mechanism to swap out a wheel for
46
+ another wheel, for example. If this assumption is not true, the
47
+ CacheControl issue will need to be fixed.
36
48
"""
37
49
38
50
def __init__ (self , directory : str ) -> None :
@@ -49,9 +61,13 @@ def _get_cache_path(self, name: str) -> str:
49
61
return os .path .join (self .directory , * parts )
50
62
51
63
def get (self , key : str ) -> Optional [bytes ]:
52
- path = self ._get_cache_path (key )
64
+ # The cache entry is only valid if both metadata and body exist.
65
+ metadata_path = self ._get_cache_path (key )
66
+ body_path = metadata_path + ".body"
67
+ if not (os .path .exists (metadata_path ) and os .path .exists (body_path )):
68
+ return None
53
69
with suppressed_cache_errors ():
54
- with open (path , "rb" ) as f :
70
+ with open (metadata_path , "rb" ) as f :
55
71
return f .read ()
56
72
57
73
def _write (self , path : str , data : bytes ) -> None :
@@ -77,9 +93,13 @@ def delete(self, key: str) -> None:
77
93
os .remove (path + ".body" )
78
94
79
95
def get_body (self , key : str ) -> Optional [BinaryIO ]:
80
- path = self ._get_cache_path (key ) + ".body"
96
+ # The cache entry is only valid if both metadata and body exist.
97
+ metadata_path = self ._get_cache_path (key )
98
+ body_path = metadata_path + ".body"
99
+ if not (os .path .exists (metadata_path ) and os .path .exists (body_path )):
100
+ return None
81
101
with suppressed_cache_errors ():
82
- return open (path , "rb" )
102
+ return open (body_path , "rb" )
83
103
84
104
def set_body (self , key : str , body : bytes ) -> None :
85
105
path = self ._get_cache_path (key ) + ".body"
0 commit comments