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."""
12 from chromite.lib import cros_build_lib
13 from chromite.lib import locking
14 from chromite.lib import osutils
15 from chromite.lib import retry_util
17 # pylint: disable=W0212
20 """Decorator that provides monitor access control."""
21 def new_f(self, *args, **kwargs):
22 # Ensure we don't have a read lock before potentially blocking while trying
23 # to access the monitor.
26 'Cannot call %s while holding a read lock.' % f.__name__)
28 with self._entry_lock:
29 self._entry_lock.write_lock()
30 return f(self, *args, **kwargs)
35 """Decorator that takes a write lock."""
36 def new_f(self, *args, **kwargs):
37 with self._lock.write_lock():
38 return f(self, *args, **kwargs)
42 class CacheReference(object):
43 """Encapsulates operations on a cache key reference.
45 CacheReferences are returned by the DiskCache.Lookup() function. They are
46 used to read from and insert into the cache.
48 A typical example of using a CacheReference:
50 @contextlib.contextmanager
52 with cache.Lookup(key) as ref:
53 # If entry doesn't exist in cache already, generate it ourselves, and
54 # insert it into the cache, acquiring a read lock on it in the process.
55 # If the entry does exist, we grab a read lock on it.
56 if not ref.Exists(lock=True):
58 ref.SetDefault(path, lock=True)
60 # yield the path to the cached entry to consuming code.
64 def __init__(self, cache, key):
68 self.read_locked = False
69 self._lock = cache._LockForKey(key)
70 self._entry_lock = cache._LockForKey(key, suffix='.entry_lock')
74 """Returns on-disk path to the cached item."""
75 return self._cache._GetKeyPath(self.key)
78 """Prepare the cache reference for operation.
80 This must be called (either explicitly or through entering a 'with'
81 context) before calling any methods that acquire locks, or mutates
86 'Attempting to acquire an already acquired reference.')
89 self._lock.__enter__()
92 """Release the cache reference. Causes any held locks to be released."""
95 'Attempting to release an unacquired reference.')
98 self._lock.__exit__(None, None, None)
104 def __exit__(self, *args):
108 self._lock.read_lock()
109 self.read_locked = True
112 def _Assign(self, path):
113 self._cache._Insert(self.key, path)
116 def _AssignText(self, text):
117 self._cache._InsertText(self.key, text)
120 def _Remove(self, key):
121 self._cache._Remove(key)
124 return self._cache._KeyExists(self.key)
127 def Assign(self, path):
128 """Insert a file or a directory into the cache at the referenced key."""
132 def AssignText(self, text):
133 """Create a file containing |text| and assign it to the key.
136 text: Can be a string or an iterable.
138 self._AssignText(text)
141 def Remove(self, key):
142 """Removes the key entry from the cache."""
146 def Exists(self, lock=False):
147 """Tests for existence of entry.
150 lock: If the entry exists, acquire and maintain a read lock on it.
159 def SetDefault(self, default_path, lock=False):
160 """Assigns default_path if the entry doesn't exist.
163 default_path: The path to assign if the entry doesn't exist.
164 lock: Acquire and maintain a read lock on the entry.
166 if not self._Exists():
167 self._Assign(default_path)
172 """Release read lock on the reference."""
176 class DiskCache(object):
177 """Locked file system cache keyed by tuples.
179 Key entries can be files or directories. Access to the cache is provided
180 through CacheReferences, which are retrieved by using the cache Lookup()
183 # TODO(rcui): Add LRU cleanup functionality.
185 _STAGING_DIR = 'staging'
187 def __init__(self, cache_dir):
188 self._cache_dir = cache_dir
189 self.staging_dir = os.path.join(cache_dir, self._STAGING_DIR)
191 osutils.SafeMakedirsNonRoot(self._cache_dir)
192 osutils.SafeMakedirsNonRoot(self.staging_dir)
194 def _KeyExists(self, key):
195 return os.path.exists(self._GetKeyPath(key))
197 def _GetKeyPath(self, key):
198 """Get the on-disk path of a key."""
199 return os.path.join(self._cache_dir, '+'.join(key))
201 def _LockForKey(self, key, suffix='.lock'):
202 """Returns an unacquired lock associated with a key."""
203 key_path = self._GetKeyPath(key)
204 osutils.SafeMakedirsNonRoot(os.path.dirname(key_path))
205 lock_path = os.path.join(self._cache_dir, os.path.dirname(key_path),
206 os.path.basename(key_path) + suffix)
207 return locking.FileLock(lock_path)
209 def _TempDirContext(self):
210 return osutils.TempDir(base_dir=self.staging_dir)
212 def _Insert(self, key, path):
213 """Insert a file or a directory into the cache at a given key."""
215 key_path = self._GetKeyPath(key)
216 osutils.SafeMakedirsNonRoot(os.path.dirname(key_path))
217 shutil.move(path, key_path)
219 def _InsertText(self, key, text):
220 """Inserts a file containing |text| into the cache."""
221 with self._TempDirContext() as tempdir:
222 file_path = os.path.join(tempdir, 'tempfile')
223 osutils.WriteFile(file_path, text)
224 self._Insert(key, file_path)
226 def _Remove(self, key):
227 """Remove a key from the cache."""
228 if self._KeyExists(key):
229 with self._TempDirContext() as tempdir:
230 shutil.move(self._GetKeyPath(key), tempdir)
232 def Lookup(self, key):
233 """Get a reference to a given key."""
234 return CacheReference(self, key)
237 def Untar(path, cwd, sudo=False):
238 """Untar a tarball."""
239 functor = cros_build_lib.SudoRunCommand if sudo else cros_build_lib.RunCommand
240 functor(['tar', '-xpf', path], cwd=cwd, debug_level=logging.DEBUG)
243 class TarballCache(DiskCache):
244 """Supports caching of extracted tarball contents."""
246 def __init__(self, cache_dir):
247 DiskCache.__init__(self, cache_dir)
249 def _Fetch(self, url, local_path):
250 """Fetch a remote file."""
251 # We have to nest the import because gs.GSContext uses us to cache its own
252 # gsutil tarball. We know we won't get into a recursive loop though as it
253 # only fetches files via non-gs URIs.
254 from chromite.lib import gs
256 if url.startswith(gs.BASE_GS_URL):
258 ctx.Copy(url, local_path)
260 retry_util.RunCurl([url, '-o', local_path], debug_level=logging.DEBUG)
262 def _Insert(self, key, tarball_path):
263 """Insert a tarball and its extracted contents into the cache.
265 Download the tarball first if a URL is provided as tarball_path.
267 with osutils.TempDir(prefix='tarball-cache',
268 base_dir=self.staging_dir) as tempdir:
270 o = urlparse.urlsplit(tarball_path)
273 tarball_path = os.path.join(tempdir, os.path.basename(o.path))
274 self._Fetch(url, tarball_path)
276 extract_path = os.path.join(tempdir, 'extract')
277 os.mkdir(extract_path)
278 Untar(tarball_path, extract_path)
279 DiskCache._Insert(self, key, extract_path)