1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Contains on-disk caching functionality."""
7 from __future__ import print_function
14 from chromite.lib import cros_build_lib
15 from chromite.lib import locking
16 from chromite.lib import osutils
17 from chromite.lib import retry_util
19 # pylint: disable=W0212
22 """Decorator that provides monitor access control."""
23 def new_f(self, *args, **kwargs):
24 # Ensure we don't have a read lock before potentially blocking while trying
25 # to access the monitor.
28 'Cannot call %s while holding a read lock.' % f.__name__)
30 with self._entry_lock:
31 self._entry_lock.write_lock()
32 return f(self, *args, **kwargs)
37 """Decorator that takes a write lock."""
38 def new_f(self, *args, **kwargs):
39 with self._lock.write_lock():
40 return f(self, *args, **kwargs)
44 class CacheReference(object):
45 """Encapsulates operations on a cache key reference.
47 CacheReferences are returned by the DiskCache.Lookup() function. They are
48 used to read from and insert into the cache.
50 A typical example of using a CacheReference:
52 @contextlib.contextmanager
54 with cache.Lookup(key) as ref:
55 # If entry doesn't exist in cache already, generate it ourselves, and
56 # insert it into the cache, acquiring a read lock on it in the process.
57 # If the entry does exist, we grab a read lock on it.
58 if not ref.Exists(lock=True):
60 ref.SetDefault(path, lock=True)
62 # yield the path to the cached entry to consuming code.
66 def __init__(self, cache, key):
70 self.read_locked = False
71 self._lock = cache._LockForKey(key)
72 self._entry_lock = cache._LockForKey(key, suffix='.entry_lock')
76 """Returns on-disk path to the cached item."""
77 return self._cache._GetKeyPath(self.key)
80 """Prepare the cache reference for operation.
82 This must be called (either explicitly or through entering a 'with'
83 context) before calling any methods that acquire locks, or mutates
88 'Attempting to acquire an already acquired reference.')
91 self._lock.__enter__()
94 """Release the cache reference. Causes any held locks to be released."""
97 'Attempting to release an unacquired reference.')
100 self._lock.__exit__(None, None, None)
106 def __exit__(self, *args):
110 self._lock.read_lock()
111 self.read_locked = True
114 def _Assign(self, path):
115 self._cache._Insert(self.key, path)
118 def _AssignText(self, text):
119 self._cache._InsertText(self.key, text)
122 def _Remove(self, key):
123 self._cache._Remove(key)
126 return self._cache._KeyExists(self.key)
129 def Assign(self, path):
130 """Insert a file or a directory into the cache at the referenced key."""
134 def AssignText(self, text):
135 """Create a file containing |text| and assign it to the key.
138 text: Can be a string or an iterable.
140 self._AssignText(text)
143 def Remove(self, key):
144 """Removes the key entry from the cache."""
148 def Exists(self, lock=False):
149 """Tests for existence of entry.
152 lock: If the entry exists, acquire and maintain a read lock on it.
161 def SetDefault(self, default_path, lock=False):
162 """Assigns default_path if the entry doesn't exist.
165 default_path: The path to assign if the entry doesn't exist.
166 lock: Acquire and maintain a read lock on the entry.
168 if not self._Exists():
169 self._Assign(default_path)
174 """Release read lock on the reference."""
178 class DiskCache(object):
179 """Locked file system cache keyed by tuples.
181 Key entries can be files or directories. Access to the cache is provided
182 through CacheReferences, which are retrieved by using the cache Lookup()
185 # TODO(rcui): Add LRU cleanup functionality.
187 _STAGING_DIR = 'staging'
189 def __init__(self, cache_dir):
190 self._cache_dir = cache_dir
191 self.staging_dir = os.path.join(cache_dir, self._STAGING_DIR)
193 osutils.SafeMakedirsNonRoot(self._cache_dir)
194 osutils.SafeMakedirsNonRoot(self.staging_dir)
196 def _KeyExists(self, key):
197 return os.path.exists(self._GetKeyPath(key))
199 def _GetKeyPath(self, key):
200 """Get the on-disk path of a key."""
201 return os.path.join(self._cache_dir, '+'.join(key))
203 def _LockForKey(self, key, suffix='.lock'):
204 """Returns an unacquired lock associated with a key."""
205 key_path = self._GetKeyPath(key)
206 osutils.SafeMakedirsNonRoot(os.path.dirname(key_path))
207 lock_path = os.path.join(self._cache_dir, os.path.dirname(key_path),
208 os.path.basename(key_path) + suffix)
209 return locking.FileLock(lock_path)
211 def _TempDirContext(self):
212 return osutils.TempDir(base_dir=self.staging_dir)
214 def _Insert(self, key, path):
215 """Insert a file or a directory into the cache at a given key."""
217 key_path = self._GetKeyPath(key)
218 osutils.SafeMakedirsNonRoot(os.path.dirname(key_path))
219 shutil.move(path, key_path)
221 def _InsertText(self, key, text):
222 """Inserts a file containing |text| into the cache."""
223 with self._TempDirContext() as tempdir:
224 file_path = os.path.join(tempdir, 'tempfile')
225 osutils.WriteFile(file_path, text)
226 self._Insert(key, file_path)
228 def _Remove(self, key):
229 """Remove a key from the cache."""
230 if self._KeyExists(key):
231 with self._TempDirContext() as tempdir:
232 shutil.move(self._GetKeyPath(key), tempdir)
234 def Lookup(self, key):
235 """Get a reference to a given key."""
236 return CacheReference(self, key)
239 def Untar(path, cwd, sudo=False):
240 """Untar a tarball."""
241 functor = cros_build_lib.SudoRunCommand if sudo else cros_build_lib.RunCommand
242 functor(['tar', '-xpf', path], cwd=cwd, debug_level=logging.DEBUG)
245 class TarballCache(DiskCache):
246 """Supports caching of extracted tarball contents."""
248 def __init__(self, cache_dir):
249 DiskCache.__init__(self, cache_dir)
251 def _Fetch(self, url, local_path):
252 """Fetch a remote file."""
253 # We have to nest the import because gs.GSContext uses us to cache its own
254 # gsutil tarball. We know we won't get into a recursive loop though as it
255 # only fetches files via non-gs URIs.
256 from chromite.lib import gs
258 if url.startswith(gs.BASE_GS_URL):
260 ctx.Copy(url, local_path)
262 retry_util.RunCurl([url, '-o', local_path], debug_level=logging.DEBUG)
264 def _Insert(self, key, tarball_path):
265 """Insert a tarball and its extracted contents into the cache.
267 Download the tarball first if a URL is provided as tarball_path.
269 with osutils.TempDir(prefix='tarball-cache',
270 base_dir=self.staging_dir) as tempdir:
272 o = urlparse.urlsplit(tarball_path)
275 tarball_path = os.path.join(tempdir, os.path.basename(o.path))
276 self._Fetch(url, tarball_path)
278 extract_path = os.path.join(tempdir, 'extract')
279 os.mkdir(extract_path)
280 Untar(tarball_path, extract_path)
281 DiskCache._Insert(self, key, extract_path)