| OLD | NEW |
| 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 optparse | 9 import optparse |
| 9 import os | 10 import os |
| 10 import random | 11 import random |
| 11 import re | 12 import re |
| 12 import string | 13 import string |
| 13 | 14 |
| 14 from utils import lru | 15 from utils import lru |
| 15 from utils import file_path | 16 from utils import file_path |
| 16 from utils import fs | 17 from utils import fs |
| 17 from utils import threading_utils | 18 from utils import threading_utils |
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 91 | 92 |
| 92 Requires NamedCache to be open. | 93 Requires NamedCache to be open. |
| 93 """ | 94 """ |
| 94 self._lock.assert_locked() | 95 self._lock.assert_locked() |
| 95 assert isinstance(name, basestring), name | 96 assert isinstance(name, basestring), name |
| 96 path = self._lru.get(name) | 97 path = self._lru.get(name) |
| 97 create_named_link = False | 98 create_named_link = False |
| 98 if path is None: | 99 if path is None: |
| 99 path = self._allocate_dir() | 100 path = self._allocate_dir() |
| 100 create_named_link = True | 101 create_named_link = True |
| 102 logging.info('Created %r for %r', path, name) |
| 101 abs_path = os.path.join(self.root_dir, path) | 103 abs_path = os.path.join(self.root_dir, path) |
| 102 | 104 |
| 105 # TODO(maruel): That's weird, it should exist already. |
| 103 file_path.ensure_tree(abs_path) | 106 file_path.ensure_tree(abs_path) |
| 104 self._lru.add(name, path) | 107 self._lru.add(name, path) |
| 105 | 108 |
| 106 if create_named_link: | 109 if create_named_link: |
| 107 # Create symlink <root_dir>/<named>/<name> -> <root_dir>/<short name> | 110 # Create symlink <root_dir>/<named>/<name> -> <root_dir>/<short name> |
| 108 # for user convenience. | 111 # for user convenience. |
| 109 named_path = self._get_named_path(name) | 112 named_path = self._get_named_path(name) |
| 110 if os.path.exists(named_path): | 113 if os.path.exists(named_path): |
| 111 file_path.remove(named_path) | 114 file_path.remove(named_path) |
| 112 else: | 115 else: |
| 113 file_path.ensure_tree(os.path.dirname(named_path)) | 116 file_path.ensure_tree(os.path.dirname(named_path)) |
| 117 logging.info('Symlink %r to %r', named_path, abs_path) |
| 114 fs.symlink(abs_path, named_path) | 118 fs.symlink(abs_path, named_path) |
| 115 | 119 |
| 116 return abs_path | 120 return abs_path |
| 117 | 121 |
| 118 def get_oldest(self): | 122 def get_oldest(self): |
| 119 """Returns name of the LRU cache or None. | 123 """Returns name of the LRU cache or None. |
| 120 | 124 |
| 121 Requires NamedCache to be open. | 125 Requires NamedCache to be open. |
| 122 """ | 126 """ |
| 123 self._lock.assert_locked() | 127 self._lock.assert_locked() |
| (...skipping 17 matching lines...) Expand all Loading... |
| 141 """Creates symlinks in |root| for specified named_caches. | 145 """Creates symlinks in |root| for specified named_caches. |
| 142 | 146 |
| 143 named_caches must be a list of (name, path) tuples. | 147 named_caches must be a list of (name, path) tuples. |
| 144 | 148 |
| 145 Requires NamedCache to be open. | 149 Requires NamedCache to be open. |
| 146 | 150 |
| 147 Raises Error if cannot create a symlink. | 151 Raises Error if cannot create a symlink. |
| 148 """ | 152 """ |
| 149 self._lock.assert_locked() | 153 self._lock.assert_locked() |
| 150 for name, path in named_caches: | 154 for name, path in named_caches: |
| 155 logging.info('Named cache %r -> %r', name, path) |
| 151 try: | 156 try: |
| 152 if os.path.isabs(path): | 157 if os.path.isabs(path): |
| 153 raise Error('named cache path must not be absolute') | 158 raise Error('named cache path must not be absolute') |
| 154 if '..' in path.split(os.path.sep): | 159 if '..' in path.split(os.path.sep): |
| 155 raise Error('named cache path must not contain ".."') | 160 raise Error('named cache path must not contain ".."') |
| 156 symlink_path = os.path.abspath(os.path.join(root, path)) | 161 symlink_path = os.path.abspath(os.path.join(root, path)) |
| 157 file_path.ensure_tree(os.path.dirname(symlink_path)) | 162 file_path.ensure_tree(os.path.dirname(symlink_path)) |
| 158 fs.symlink(self.request(name), symlink_path) | 163 requested = self.request(name) |
| 164 logging.info('Symlink %r to %r', symlink_path, requested) |
| 165 fs.symlink(requested, symlink_path) |
| 159 except (OSError, Error) as ex: | 166 except (OSError, Error) as ex: |
| 160 raise Error( | 167 raise Error( |
| 161 'cannot create a symlink for cache named "%s" at "%s": %s' % ( | 168 'cannot create a symlink for cache named "%s" at "%s": %s' % ( |
| 162 name, symlink_path, ex)) | 169 name, symlink_path, ex)) |
| 163 | 170 |
| 164 def trim(self, min_free_space): | 171 def trim(self, min_free_space): |
| 165 """Purges cache. | 172 """Purges cache. |
| 166 | 173 |
| 167 Removes cache directories that were not accessed for a long time | 174 Removes cache directories that were not accessed for a long time |
| 168 until there is enough free space and the number of caches is sane. | 175 until there is enough free space and the number of caches is sane. |
| 169 | 176 |
| 170 If min_free_space is None, disk free space is not checked. | 177 If min_free_space is None, disk free space is not checked. |
| 171 | 178 |
| 172 Requires NamedCache to be open. | 179 Requires NamedCache to be open. |
| 173 """ | 180 """ |
| 174 self._lock.assert_locked() | 181 self._lock.assert_locked() |
| 175 if not os.path.isdir(self.root_dir): | 182 if not os.path.isdir(self.root_dir): |
| 176 return | 183 return |
| 177 | 184 |
| 178 free_space = 0 | 185 free_space = 0 |
| 179 if min_free_space is not None: | 186 if min_free_space: |
| 180 file_path.get_free_space(self.root_dir) | 187 free_space = file_path.get_free_space(self.root_dir) |
| 181 while ((min_free_space is not None and free_space < min_free_space) | 188 while ((min_free_space and free_space < min_free_space) |
| 182 or len(self._lru) > MAX_CACHE_SIZE): | 189 or len(self._lru) > MAX_CACHE_SIZE): |
| 190 logging.info( |
| 191 'Making space for named cache %s > %s or %s > %s', |
| 192 free_space, min_free_space, len(self._lru), MAX_CACHE_SIZE) |
| 183 try: | 193 try: |
| 184 name, (path, _) = self._lru.get_oldest() | 194 name, (path, _) = self._lru.get_oldest() |
| 185 except KeyError: | 195 except KeyError: |
| 186 return | 196 return |
| 187 named_dir = self._get_named_path(name) | 197 named_dir = self._get_named_path(name) |
| 188 if fs.islink(named_dir): | 198 if fs.islink(named_dir): |
| 189 fs.unlink(named_dir) | 199 fs.unlink(named_dir) |
| 190 path_abs = os.path.join(self.root_dir, path) | 200 path_abs = os.path.join(self.root_dir, path) |
| 191 if os.path.isdir(path_abs): | 201 if os.path.isdir(path_abs): |
| 202 logging.info('Removing named cache %s', path_abs) |
| 192 file_path.rmtree(path_abs) | 203 file_path.rmtree(path_abs) |
| 193 if min_free_space is not None: | 204 if min_free_space: |
| 194 free_space = file_path.get_free_space(self.root_dir) | 205 free_space = file_path.get_free_space(self.root_dir) |
| 195 self._lru.pop(name) | 206 self._lru.pop(name) |
| 196 | 207 |
| 197 _DIR_ALPHABET = string.ascii_letters + string.digits | 208 _DIR_ALPHABET = string.ascii_letters + string.digits |
| 198 | 209 |
| 199 def _allocate_dir(self): | 210 def _allocate_dir(self): |
| 200 """Creates and returns relative path of a new cache directory.""" | 211 """Creates and returns relative path of a new cache directory.""" |
| 201 # We randomly generate directory names that have two lower/upper case | 212 # We randomly generate directory names that have two lower/upper case |
| 202 # letters or digits. Total number of possibilities is (26*2 + 10)^2 = 3844. | 213 # letters or digits. Total number of possibilities is (26*2 + 10)^2 = 3844. |
| 203 abc_len = len(self._DIR_ALPHABET) | 214 abc_len = len(self._DIR_ALPHABET) |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 244 parser.error('--named-cache is specified, but --named-cache-root is empty') | 255 parser.error('--named-cache is specified, but --named-cache-root is empty') |
| 245 for name, path in options.named_caches: | 256 for name, path in options.named_caches: |
| 246 if not CACHE_NAME_RE.match(name): | 257 if not CACHE_NAME_RE.match(name): |
| 247 parser.error( | 258 parser.error( |
| 248 'cache name "%s" does not match %s' % (name, CACHE_NAME_RE.pattern)) | 259 'cache name "%s" does not match %s' % (name, CACHE_NAME_RE.pattern)) |
| 249 if not path: | 260 if not path: |
| 250 parser.error('cache path cannot be empty') | 261 parser.error('cache path cannot be empty') |
| 251 if options.named_cache_root: | 262 if options.named_cache_root: |
| 252 return CacheManager(os.path.abspath(options.named_cache_root)) | 263 return CacheManager(os.path.abspath(options.named_cache_root)) |
| 253 return None | 264 return None |
| OLD | NEW |