| # Copyright 2020 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from contextlib import contextmanager |
| from recipe_engine import recipe_api |
| |
| # Name of the file whose presence indicates a corrupted cache. |
| # |
| # This name is load-bearing in some cases (e.g. it's referenced in some |
| # projects' .gitignore files) so it is not safe to change. |
| GUARD_FILE_NAME = ".GUARD_FILE" |
| |
| |
| class CacheApi(recipe_api.RecipeApi): |
| @contextmanager |
| def guard(self, cache): |
| """Context wrapping Swarming named cache writes. |
| |
| Ensures the named cache is purged if an error occurs during the context, |
| which might cause the cache to become corrupted and break subsequent |
| builds. |
| """ |
| cache_path = self.m.path["cache"].join(cache) |
| |
| # The cache directory will be deleted in case of a failure, which would |
| # break the recipe engine's assumption that the cwd always exists. So |
| # prevent recipes from using a cache context *after* entering the cache |
| # dir. It's fine to enter the cache dir within the cache context. |
| cwd = self.m.context.cwd or self.m.path["start_dir"] |
| if cache_path == cwd or cache_path.is_parent_of(cwd): |
| raise ValueError("cwd is within the %s cache" % cache) |
| |
| guard_file = cache_path.join(GUARD_FILE_NAME) |
| if self.m.path.exists(guard_file): |
| self._purge(cache, cache_path) |
| if not self.m.path.exists(cache_path): |
| self.m.file.ensure_directory("ensure %s cache dir" % cache, cache_path) |
| self.m.file.write_raw( |
| name="write %s cache guard file" % cache, dest=guard_file, data="" |
| ) |
| try: |
| yield |
| except: |
| if not self.m.runtime.in_global_shutdown: |
| self._purge(cache, cache_path) |
| raise |
| else: |
| self.m.file.remove( |
| name="remove %s cache guard file" % cache, source=guard_file |
| ) |
| |
| def _purge(self, cache, cache_path): |
| """Atomically purge the named cache. |
| |
| Removing the cache directory in-place is not atomic because all of its |
| contents need to be individually deleted before deleting the directory |
| itself. |
| |
| Instead, we first move the cache into the cleanup directory (where it is |
| guaranteed to be cleaned up after the recipe completes) and then make a |
| best-effort attempt at deleting it from the cleanup directory to save |
| disk space in case this is running during the setup of a cache having |
| encountered a dirty cache from the previous build. |
| """ |
| with self.m.step.nest("%s cache corrupted; purging" % cache): |
| new_path = self.m.path.mkdtemp("%s_cache_cleanup" % cache) |
| self.m.file.move("move cache dir to cleanup", cache_path, new_path) |
| self.m.file.rmtree("delete cache dir", new_path) |