@@ -33,6 +33,18 @@ class SafeFileCache(SeparateBodyBaseCache):
3333 """
3434 A file based cache which is safe to use even when the target directory may
3535 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.
3648 """
3749
3850 def __init__ (self , directory : str ) -> None :
@@ -49,9 +61,13 @@ def _get_cache_path(self, name: str) -> str:
4961 return os .path .join (self .directory , * parts )
5062
5163 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
5369 with suppressed_cache_errors ():
54- with open (path , "rb" ) as f :
70+ with open (metadata_path , "rb" ) as f :
5571 return f .read ()
5672
5773 def _write (self , path : str , data : bytes ) -> None :
@@ -77,9 +93,13 @@ def delete(self, key: str) -> None:
7793 os .remove (path + ".body" )
7894
7995 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
81101 with suppressed_cache_errors ():
82- return open (path , "rb" )
102+ return open (body_path , "rb" )
83103
84104 def set_body (self , key : str , body : bytes ) -> None :
85105 path = self ._get_cache_path (key ) + ".body"
0 commit comments