Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(900)

Side by Side Diff: client/named_cache.py

Issue 2866283002: named caches: move instead of symlinking (Closed)
Patch Set: self review Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | client/run_isolated.py » ('j') | client/tests/named_cache_test.py » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2016 The LUCI Authors. All rights reserved. 1 # Copyright 2016 The LUCI Authors. All rights reserved.
2 # Use of this source code is governed under the Apache License, Version 2.0 2 # Use of this source code is governed under the Apache License, Version 2.0
3 # that can be found in the LICENSE file. 3 # that can be found in the LICENSE file.
4 4
5 """This file implements Named Caches.""" 5 """This file implements Named Caches."""
6 6
7 import contextlib 7 import contextlib
8 import logging 8 import logging
9 import optparse 9 import optparse
10 import os 10 import os
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
46 """ 46 """
47 assert file_path.isabs(root_dir), root_dir 47 assert file_path.isabs(root_dir), root_dir
48 self.root_dir = unicode(root_dir) 48 self.root_dir = unicode(root_dir)
49 self._lock = threading_utils.LockWithAssert() 49 self._lock = threading_utils.LockWithAssert()
50 # LRU {cache_name -> cache_location} 50 # LRU {cache_name -> cache_location}
51 # It is saved to |root_dir|/state.json. 51 # It is saved to |root_dir|/state.json.
52 self._lru = None 52 self._lru = None
53 53
54 @contextlib.contextmanager 54 @contextlib.contextmanager
55 def open(self, time_fn=None): 55 def open(self, time_fn=None):
56 """Opens NamedCaches for mutation operations, such as request or trim. 56 """Opens NamedCaches for mutation operations, such as install.
57 57
58 Only on caller can open the cache manager at a time. If the same thread 58 Only one caller can open the cache manager at a time. If the same thread
59 calls this function after opening it earlier, the call will deadlock. 59 calls this function after opening it earlier, the call will deadlock.
60 60
61 time_fn is a function that returns timestamp (float) and used to take 61 time_fn is a function that returns timestamp (float) and used to take
62 timestamps when new caches are requested. 62 timestamps when new caches are requested.
63 63
64 Returns a context manager that must be closed as soon as possible. 64 Returns a context manager that must be closed as soon as possible.
65 """ 65 """
66 with self._lock: 66 with self._lock:
67 state_path = os.path.join(self.root_dir, u'state.json') 67 state_path = os.path.join(self.root_dir, u'state.json')
68 assert self._lru is None, 'acquired lock, but self._lru is not None' 68 assert self._lru is None, 'acquired lock, but self._lru is not None'
(...skipping 10 matching lines...) Expand all
79 try: 79 try:
80 yield 80 yield
81 finally: 81 finally:
82 file_path.ensure_tree(self.root_dir) 82 file_path.ensure_tree(self.root_dir)
83 self._lru.save(state_path) 83 self._lru.save(state_path)
84 self._lru = None 84 self._lru = None
85 85
86 def __len__(self): 86 def __len__(self):
87 """Returns number of items in the cache. 87 """Returns number of items in the cache.
88 88
89 Requires NamedCache to be open. 89 NamedCache must be open.
90 """ 90 """
91 return len(self._lru) 91 return len(self._lru)
92 92
93 def request(self, name):
94 """Returns an absolute path to the directory of the named cache.
95
96 Creates a cache directory if it does not exist yet.
97
98 Requires NamedCache to be open.
99 """
100 self._lock.assert_locked()
101 assert isinstance(name, basestring), name
102 path = self._lru.get(name)
103 create_named_link = False
104 if path is None:
105 path = self._allocate_dir()
106 create_named_link = True
107 logging.info('Created %r for %r', path, name)
108 abs_path = os.path.join(self.root_dir, path)
109
110 # TODO(maruel): That's weird, it should exist already.
111 file_path.ensure_tree(abs_path)
112 self._lru.add(name, path)
113
114 if create_named_link:
115 # Create symlink <root_dir>/<named>/<name> -> <root_dir>/<short name>
116 # for user convenience.
117 named_path = self._get_named_path(name)
118 if os.path.exists(named_path):
119 file_path.remove(named_path)
120 else:
121 file_path.ensure_tree(os.path.dirname(named_path))
122 logging.info('Symlink %r to %r', named_path, abs_path)
123 fs.symlink(abs_path, named_path)
124
125 return abs_path
126
127 def get_oldest(self): 93 def get_oldest(self):
128 """Returns name of the LRU cache or None. 94 """Returns name of the LRU cache or None.
129 95
130 Requires NamedCache to be open. 96 NamedCache must be open..
M-A Ruel 2017/05/09 18:35:14 one dot
nodir 2017/05/09 23:20:09 Done.
131 """ 97 """
132 self._lock.assert_locked() 98 self._lock.assert_locked()
133 try: 99 try:
134 return self._lru.get_oldest()[0] 100 return self._lru.get_oldest()[0]
135 except KeyError: 101 except KeyError:
136 return None 102 return None
137 103
138 def get_timestamp(self, name): 104 def get_timestamp(self, name):
139 """Returns timestamp of last use of an item. 105 """Returns timestamp of last use of an item.
140 106
141 Requires NamedCache to be open. 107 NamedCache must be open.
142 108
143 Raises KeyError if cache is not found. 109 Raises KeyError if cache is not found.
144 """ 110 """
145 self._lock.assert_locked() 111 self._lock.assert_locked()
146 assert isinstance(name, basestring), name 112 assert isinstance(name, basestring), name
147 return self._lru.get_timestamp(name) 113 return self._lru.get_timestamp(name)
148 114
149 @contextlib.contextmanager 115 @property
150 def create_symlinks(self, root, named_caches): 116 def available(self):
151 """Creates symlinks in |root| for the specified named_caches. 117 """Returns a set of names of available caches.
118
119 NamedCache must be open.
120 """
121 self._lock.assert_locked()
122 return self._lru.keys_set()
123
124 def install(self, root, named_caches):
M-A Ruel 2017/05/09 18:35:14 I'd prefer to have this function accept one named_
nodir 2017/05/09 23:20:09 do'h! Yeah, this is much simpler
125 """Moves directories of the specified named caches to |root|.
152 126
153 named_caches must be a list of (name, path) tuples. 127 named_caches must be a list of (name, path) tuples.
154 128
155 Requires NamedCache to be open. 129 NamedCache must be open..
M-A Ruel 2017/05/09 18:35:14 same
nodir 2017/05/09 23:20:09 Done.
156 130
157 Raises Error if cannot create a symlink. 131 Raises Error if cannot create a symlink.
158 """ 132 """
159 self._lock.assert_locked() 133 self._lock.assert_locked()
160 for name, path in named_caches: 134 for name, rel_install in named_caches:
161 logging.info('Named cache %r -> %r', name, path) 135 logging.info('Installing named cache %r to %r', name, rel_install)
162 try: 136 try:
163 _validate_named_cache_path(path) 137 _validate_named_cache_path(rel_install)
164 symlink_path = os.path.abspath(os.path.join(root, path)) 138 rel_install = unicode(rel_install)
165 file_path.ensure_tree(os.path.dirname(symlink_path)) 139 abs_install = os.path.abspath(os.path.join(root, rel_install))
166 requested = self.request(name) 140
167 logging.info('Symlink %r to %r', symlink_path, requested) 141 rel_cache = self._lru.get(name)
168 fs.symlink(requested, symlink_path) 142 if rel_cache:
143 abs_cache = os.path.join(self.root_dir, rel_cache)
144 if os.path.isdir(abs_cache):
145 logging.info('Moving %r to %r', abs_cache, abs_install)
146 file_path.ensure_tree(os.path.dirname(abs_install))
147 fs.rename(abs_cache, abs_install)
148 self._remove(name)
149 continue
150
151 logging.warning('directory for named cache %r does not exist', name)
152 self._remove(name)
153
154 # The named cache does not exist.
155 # Just create an empty directory in the root.
156 # When uninstalling, we will move it back to the cache and create an
157 # an entry.
158 file_path.ensure_tree(abs_install)
169 except (OSError, Error) as ex: 159 except (OSError, Error) as ex:
170 raise Error( 160 raise Error(
171 'cannot create a symlink for cache named "%s" at "%s": %s' % ( 161 'cannot install cache named %r at %r: %s' % (
172 name, symlink_path, ex)) 162 name, abs_install, ex))
173 163
174 def delete_symlinks(self, root, named_caches): 164 def uninstall(self, root, named_caches):
M-A Ruel 2017/05/09 18:35:14 same about one call per named cache. Makes the fun
nodir 2017/05/09 23:20:09 Done.
175 """Deletes symlinks from |root| for the specified named_caches. 165 """Moves directories of the specified named caches back from |root|.
176 166
177 named_caches must be a list of (name, path) tuples. 167 named_caches must be a list of (name, path) tuples.
178 """ 168 """
179 for name, path in named_caches: 169 for name, rel_install in named_caches:
180 logging.info('Unlinking named cache "%s"', name) 170 logging.info('Uninstalling named cache %r from %r', name, rel_install)
181 try: 171 try:
182 _validate_named_cache_path(path) 172 _validate_named_cache_path(rel_install)
183 symlink_path = os.path.abspath(os.path.join(root, path)) 173 rel_install = unicode(rel_install)
M-A Ruel 2017/05/09 18:35:14 don't, make sure caller is fixed.
nodir 2017/05/09 23:20:09 Done.
184 fs.unlink(symlink_path) 174 abs_install = os.path.abspath(os.path.join(root, rel_install))
175 if not os.path.isdir(abs_install):
176 logging.warning(
177 'Directory %r does not exist anymore. Cache lost.', abs_install)
178 continue
179
180 rel_cache = self._lru.get(name)
181 if rel_cache:
182 # Do not crash because cache already exists.
183 logging.warning('overwriting an existing named cache %r', name)
184 create_named_link = False
185 else:
186 rel_cache = self._allocate_dir()
187 create_named_link = True
188
189 # Move the dir and create an entry for the named cache.
190 abs_cache = os.path.join(self.root_dir, rel_cache)
191 logging.info('Moving %r to %r', abs_install, abs_cache)
192 file_path.ensure_tree(os.path.dirname(abs_cache))
193 fs.rename(abs_install, abs_cache)
194 self._lru.add(name, rel_cache)
195
196 if create_named_link:
197 # Create symlink <root_dir>/<named>/<name> -> <root_dir>/<short name>
198 # for user convenience.
199 named_path = self._get_named_path(name)
200 if os.path.exists(named_path):
201 file_path.remove(named_path)
202 else:
203 file_path.ensure_tree(os.path.dirname(named_path))
204 fs.symlink(abs_cache, named_path)
205 logging.info('Created symlink %r for %r', named_path, name)
185 except (OSError, Error) as ex: 206 except (OSError, Error) as ex:
186 raise Error( 207 raise Error(
187 'cannot unlink cache named "%s" at "%s": %s' % ( 208 'cannot uninstall cache named %r at %r: %s' % (
188 name, symlink_path, ex)) 209 name, abs_install, ex))
189 210
190 def trim(self, min_free_space): 211 def trim(self, min_free_space):
191 """Purges cache. 212 """Purges cache.
192 213
193 Removes cache directories that were not accessed for a long time 214 Removes cache directories that were not accessed for a long time
194 until there is enough free space and the number of caches is sane. 215 until there is enough free space and the number of caches is sane.
195 216
196 If min_free_space is None, disk free space is not checked. 217 If min_free_space is None, disk free space is not checked.
197 218
198 Requires NamedCache to be open. 219 NamedCache must be open..
M-A Ruel 2017/05/09 18:35:14 same
199 220
200 Returns: 221 Returns:
201 Number of caches deleted. 222 Number of caches deleted.
202 """ 223 """
203 self._lock.assert_locked() 224 self._lock.assert_locked()
204 if not os.path.isdir(self.root_dir): 225 if not os.path.isdir(self.root_dir):
205 return 0 226 return 0
206 227
207 total = 0 228 total = 0
208 free_space = 0 229 free_space = 0
209 if min_free_space: 230 if min_free_space:
210 free_space = file_path.get_free_space(self.root_dir) 231 free_space = file_path.get_free_space(self.root_dir)
211 while ((min_free_space and free_space < min_free_space) 232 while ((min_free_space and free_space < min_free_space)
212 or len(self._lru) > MAX_CACHE_SIZE): 233 or len(self._lru) > MAX_CACHE_SIZE):
213 logging.info( 234 logging.info(
214 'Making space for named cache %s > %s or %s > %s', 235 'Making space for named cache %d > %d or %d > %d',
215 free_space, min_free_space, len(self._lru), MAX_CACHE_SIZE) 236 free_space, min_free_space, len(self._lru), MAX_CACHE_SIZE)
216 try: 237 try:
217 name, (path, _) = self._lru.get_oldest() 238 name, (path, _) = self._lru.get_oldest()
218 except KeyError: 239 except KeyError:
219 return total 240 return total
220 named_dir = self._get_named_path(name) 241 logging.info('Removing named cache %r', name)
221 if fs.islink(named_dir): 242 self._remove(name)
222 fs.unlink(named_dir)
223 path_abs = os.path.join(self.root_dir, path)
224 if os.path.isdir(path_abs):
225 logging.info('Removing named cache %s', path_abs)
226 file_path.rmtree(path_abs)
227 if min_free_space: 243 if min_free_space:
228 free_space = file_path.get_free_space(self.root_dir) 244 free_space = file_path.get_free_space(self.root_dir)
229 self._lru.pop(name)
230 total += 1 245 total += 1
231 return total 246 return total
232 247
233 _DIR_ALPHABET = string.ascii_letters + string.digits 248 _DIR_ALPHABET = string.ascii_letters + string.digits
234 249
235 def _allocate_dir(self): 250 def _allocate_dir(self):
236 """Creates and returns relative path of a new cache directory.""" 251 """Creates and returns relative path of a new cache directory."""
237 # We randomly generate directory names that have two lower/upper case 252 # We randomly generate directory names that have two lower/upper case
238 # letters or digits. Total number of possibilities is (26*2 + 10)^2 = 3844. 253 # letters or digits. Total number of possibilities is (26*2 + 10)^2 = 3844.
239 abc_len = len(self._DIR_ALPHABET) 254 abc_len = len(self._DIR_ALPHABET)
240 tried = set() 255 tried = set()
241 while len(tried) < 1000: 256 while len(tried) < 1000:
242 i = random.randint(0, abc_len * abc_len - 1) 257 i = random.randint(0, abc_len * abc_len - 1)
243 rel_path = ( 258 rel_path = (
244 self._DIR_ALPHABET[i / abc_len] + 259 self._DIR_ALPHABET[i / abc_len] +
245 self._DIR_ALPHABET[i % abc_len]) 260 self._DIR_ALPHABET[i % abc_len])
246 if rel_path in tried: 261 if rel_path in tried:
247 continue 262 continue
248 abs_path = os.path.join(self.root_dir, rel_path) 263 abs_path = os.path.join(self.root_dir, rel_path)
249 if not fs.exists(abs_path): 264 if not fs.exists(abs_path):
250 return rel_path 265 return rel_path
251 tried.add(rel_path) 266 tried.add(rel_path)
252 raise Error('could not allocate a new cache dir, too many cache dirs') 267 raise Error('could not allocate a new cache dir, too many cache dirs')
253 268
269 def _remove(self, name):
270 """Removes a cache directory and entry.
271
272 NamedCache must be open.
273
274 Returns:
275 Number of caches deleted.
276 """
277 self._lock.assert_locked()
278 rel_path = self._lru.get(name)
279 if not rel_path:
280 return
281
282 named_dir = self._get_named_path(name)
283 if fs.islink(named_dir):
284 fs.unlink(named_dir)
285
286 abs_path = os.path.join(self.root_dir, rel_path)
287 if os.path.isdir(abs_path):
288 file_path.rmtree(abs_path)
289 self._lru.pop(name)
290
254 def _get_named_path(self, name): 291 def _get_named_path(self, name):
255 return os.path.join(self.root_dir, 'named', name) 292 return os.path.join(self.root_dir, 'named', name)
256 293
257 294
258 def add_named_cache_options(parser): 295 def add_named_cache_options(parser):
259 group = optparse.OptionGroup(parser, 'Named caches') 296 group = optparse.OptionGroup(parser, 'Named caches')
260 group.add_option( 297 group.add_option(
261 '--named-cache', 298 '--named-cache',
262 dest='named_caches', 299 dest='named_caches',
263 action='append', 300 action='append',
(...skipping 10 matching lines...) Expand all
274 parser.add_option_group(group) 311 parser.add_option_group(group)
275 312
276 313
277 def process_named_cache_options(parser, options): 314 def process_named_cache_options(parser, options):
278 """Validates named cache options and returns a CacheManager.""" 315 """Validates named cache options and returns a CacheManager."""
279 if options.named_caches and not options.named_cache_root: 316 if options.named_caches and not options.named_cache_root:
280 parser.error('--named-cache is specified, but --named-cache-root is empty') 317 parser.error('--named-cache is specified, but --named-cache-root is empty')
281 for name, path in options.named_caches: 318 for name, path in options.named_caches:
282 if not CACHE_NAME_RE.match(name): 319 if not CACHE_NAME_RE.match(name):
283 parser.error( 320 parser.error(
284 'cache name "%s" does not match %s' % (name, CACHE_NAME_RE.pattern)) 321 'cache name %r does not match %r' % (name, CACHE_NAME_RE.pattern))
285 if not path: 322 if not path:
286 parser.error('cache path cannot be empty') 323 parser.error('cache path cannot be empty')
287 if options.named_cache_root: 324 if options.named_cache_root:
288 return CacheManager(os.path.abspath(options.named_cache_root)) 325 return CacheManager(os.path.abspath(options.named_cache_root))
289 return None 326 return None
290 327
291 328
292 def _validate_named_cache_path(path): 329 def _validate_named_cache_path(path):
330 if not isinstance(path, basestring):
331 raise Error('named cache path must be a string')
293 if os.path.isabs(path): 332 if os.path.isabs(path):
294 raise Error('named cache path must not be absolute') 333 raise Error('named cache path must not be absolute')
295 if '..' in path.split(os.path.sep): 334 if '..' in path.split(os.path.sep):
296 raise Error('named cache path must not contain ".."') 335 raise Error('named cache path must not contain ".."')
OLDNEW
« no previous file with comments | « no previous file | client/run_isolated.py » ('j') | client/tests/named_cache_test.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698