blob: fe0997fc8fceb9855cc6c1274edf1f76130ed6cf [file] [log] [blame]
# 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)