features: Add install failure tracking removal through spack clean (#15314)

* Add ability to force removal of install failure tracking data through spack clean

* Add clean failures option to packaging guide
This commit is contained in:
Tamara Dahlgren 2020-06-24 18:28:53 -07:00 committed by GitHub
parent bc53bb9b7c
commit 48d3e8d350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 140 additions and 36 deletions

View File

@ -4252,23 +4252,29 @@ Does this in one of two ways:
``spack clean`` ``spack clean``
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
Cleans up all of Spack's temporary and cached files. This can be used to Cleans up Spack's temporary and cached files. This command can be used to
recover disk space if temporary files from interrupted or failed installs recover disk space if temporary files from interrupted or failed installs
accumulate in the staging area. accumulate.
When called with ``--stage`` or without arguments this removes all staged When called with ``--stage`` or without arguments this removes all staged
files. files.
When called with ``--downloads`` this will clear all resources The ``--downloads`` option removes cached :ref:`cached <caching>` downloads.
:ref:`cached <caching>` during installs.
When called with ``--user-cache`` this will remove caches in the user home You can force the removal of all install failure tracking markers using the
directory, including cached virtual indices. ``--failures`` option. Note that ``spack install`` will automatically clear
relevant failure markings prior to performing the requested installation(s).
Long-lived caches, like the virtual package index, are removed using the
``--misc-cache`` option.
The ``--python-cache`` option removes `.pyc`, `.pyo`, and `__pycache__`
folders.
To remove all of the above, the command can be called with ``--all``. To remove all of the above, the command can be called with ``--all``.
When called with positional arguments, cleans up temporary files only When called with positional arguments, this command cleans up temporary files
for a particular package. If ``fetch``, ``stage``, or ``install`` only for a particular package. If ``fetch``, ``stage``, or ``install``
are run again after this, Spack's build process will start from scratch. are run again after this, Spack's build process will start from scratch.

View File

@ -23,9 +23,9 @@
class AllClean(argparse.Action): class AllClean(argparse.Action):
"""Activates flags -s -d -m and -p simultaneously""" """Activates flags -s -d -f -m and -p simultaneously"""
def __call__(self, parser, namespace, values, option_string=None): def __call__(self, parser, namespace, values, option_string=None):
parser.parse_args(['-sdmp'], namespace=namespace) parser.parse_args(['-sdfmp'], namespace=namespace)
def setup_parser(subparser): def setup_parser(subparser):
@ -35,6 +35,9 @@ def setup_parser(subparser):
subparser.add_argument( subparser.add_argument(
'-d', '--downloads', action='store_true', '-d', '--downloads', action='store_true',
help="remove cached downloads") help="remove cached downloads")
subparser.add_argument(
'-f', '--failures', action='store_true',
help="force removal of all install failure tracking markers")
subparser.add_argument( subparser.add_argument(
'-m', '--misc-cache', action='store_true', '-m', '--misc-cache', action='store_true',
help="remove long-lived caches, like the virtual package index") help="remove long-lived caches, like the virtual package index")
@ -42,15 +45,15 @@ def setup_parser(subparser):
'-p', '--python-cache', action='store_true', '-p', '--python-cache', action='store_true',
help="remove .pyc, .pyo files and __pycache__ folders") help="remove .pyc, .pyo files and __pycache__ folders")
subparser.add_argument( subparser.add_argument(
'-a', '--all', action=AllClean, help="equivalent to -sdmp", nargs=0 '-a', '--all', action=AllClean, help="equivalent to -sdfmp", nargs=0
) )
arguments.add_common_arguments(subparser, ['specs']) arguments.add_common_arguments(subparser, ['specs'])
def clean(parser, args): def clean(parser, args):
# If nothing was set, activate the default # If nothing was set, activate the default
if not any([args.specs, args.stage, args.downloads, args.misc_cache, if not any([args.specs, args.stage, args.downloads, args.failures,
args.python_cache]): args.misc_cache, args.python_cache]):
args.stage = True args.stage = True
# Then do the cleaning falling through the cases # Then do the cleaning falling through the cases
@ -70,6 +73,10 @@ def clean(parser, args):
tty.msg('Removing cached downloads') tty.msg('Removing cached downloads')
spack.caches.fetch_cache.destroy() spack.caches.fetch_cache.destroy()
if args.failures:
tty.msg('Removing install failure marks')
spack.installer.clear_failures()
if args.misc_cache: if args.misc_cache:
tty.msg('Removing cached information on repositories') tty.msg('Removing cached information on repositories')
spack.caches.misc_cache.destroy() spack.caches.misc_cache.destroy()

View File

@ -23,6 +23,7 @@
import contextlib import contextlib
import datetime import datetime
import os import os
import six
import socket import socket
import sys import sys
import time import time
@ -33,14 +34,14 @@
_use_uuid = False _use_uuid = False
pass pass
import llnl.util.filesystem as fs
import llnl.util.tty as tty import llnl.util.tty as tty
import six
import spack.repo import spack.repo
import spack.spec import spack.spec
import spack.store import spack.store
import spack.util.lock as lk import spack.util.lock as lk
import spack.util.spack_json as sjson import spack.util.spack_json as sjson
from llnl.util.filesystem import mkdirp
from spack.directory_layout import DirectoryLayoutError from spack.directory_layout import DirectoryLayoutError
from spack.error import SpackError from spack.error import SpackError
from spack.filesystem_view import YamlFilesystemView from spack.filesystem_view import YamlFilesystemView
@ -316,10 +317,10 @@ def __init__(self, root, db_dir=None, upstream_dbs=None,
# Create needed directories and files # Create needed directories and files
if not os.path.exists(self._db_dir): if not os.path.exists(self._db_dir):
mkdirp(self._db_dir) fs.mkdirp(self._db_dir)
if not os.path.exists(self._failure_dir) and not is_upstream: if not os.path.exists(self._failure_dir) and not is_upstream:
mkdirp(self._failure_dir) fs.mkdirp(self._failure_dir)
self.is_upstream = is_upstream self.is_upstream = is_upstream
self.last_seen_verifier = '' self.last_seen_verifier = ''
@ -373,6 +374,23 @@ def _failed_spec_path(self, spec):
return os.path.join(self._failure_dir, return os.path.join(self._failure_dir,
'{0}-{1}'.format(spec.name, spec.full_hash())) '{0}-{1}'.format(spec.name, spec.full_hash()))
def clear_all_failures(self):
"""Force remove install failure tracking files."""
tty.debug('Releasing prefix failure locks')
for pkg_id in list(self._prefix_failures.keys()):
lock = self._prefix_failures.pop(pkg_id, None)
if lock:
lock.release_write()
# Remove all failure markings (aka files)
tty.debug('Removing prefix failure tracking files')
for fail_mark in os.listdir(self._failure_dir):
try:
os.remove(os.path.join(self._failure_dir, fail_mark))
except OSError as exc:
tty.warn('Unable to remove failure marking file {0}: {1}'
.format(fail_mark, str(exc)))
def clear_failure(self, spec, force=False): def clear_failure(self, spec, force=False):
""" """
Remove any persistent and cached failure tracking for the spec. Remove any persistent and cached failure tracking for the spec.

View File

@ -373,6 +373,13 @@ def _update_explicit_entry_in_db(pkg, rec, explicit):
rec.explicit = True rec.explicit = True
def clear_failures():
"""
Remove all failure tracking markers for the Spack instance.
"""
spack.store.db.clear_all_failures()
def dump_packages(spec, path): def dump_packages(spec, path):
""" """
Dump all package information for a spec and its dependencies. Dump all package information for a spec and its dependencies.
@ -835,7 +842,7 @@ def _cleanup_failed(self, pkg_id):
""" """
lock = self.failed.get(pkg_id, None) lock = self.failed.get(pkg_id, None)
if lock is not None: if lock is not None:
err = "{0} exception when removing failure mark for {1}: {2}" err = "{0} exception when removing failure tracking for {1}: {2}"
msg = 'Removing failure mark on {0}' msg = 'Removing failure mark on {0}'
try: try:
tty.verbose(msg.format(pkg_id)) tty.verbose(msg.format(pkg_id))

View File

@ -28,18 +28,21 @@ def __call__(self, *args, **kwargs):
spack.caches.fetch_cache, 'destroy', Counter(), raising=False) spack.caches.fetch_cache, 'destroy', Counter(), raising=False)
monkeypatch.setattr( monkeypatch.setattr(
spack.caches.misc_cache, 'destroy', Counter()) spack.caches.misc_cache, 'destroy', Counter())
monkeypatch.setattr(
spack.installer, 'clear_failures', Counter())
@pytest.mark.usefixtures( @pytest.mark.usefixtures(
'mock_packages', 'config', 'mock_calls_for_clean' 'mock_packages', 'config', 'mock_calls_for_clean'
) )
@pytest.mark.parametrize('command_line,counters', [ @pytest.mark.parametrize('command_line,counters', [
('mpileaks', [1, 0, 0, 0]), ('mpileaks', [1, 0, 0, 0, 0]),
('-s', [0, 1, 0, 0]), ('-s', [0, 1, 0, 0, 0]),
('-sd', [0, 1, 1, 0]), ('-sd', [0, 1, 1, 0, 0]),
('-m', [0, 0, 0, 1]), ('-m', [0, 0, 0, 1, 0]),
('-a', [0, 1, 1, 1]), ('-f', [0, 0, 0, 0, 1]),
('', [0, 0, 0, 0]), ('-a', [0, 1, 1, 1, 1]),
('', [0, 0, 0, 0, 0]),
]) ])
def test_function_calls(command_line, counters): def test_function_calls(command_line, counters):
@ -52,3 +55,4 @@ def test_function_calls(command_line, counters):
assert spack.stage.purge.call_count == counters[1] assert spack.stage.purge.call_count == counters[1]
assert spack.caches.fetch_cache.destroy.call_count == counters[2] assert spack.caches.fetch_cache.destroy.call_count == counters[2]
assert spack.caches.misc_cache.destroy.call_count == counters[3] assert spack.caches.misc_cache.destroy.call_count == counters[3]
assert spack.installer.clear_failures.call_count == counters[4]

View File

@ -610,17 +610,17 @@ def test_build_warning_output(tmpdir, mock_fetch, install_mockery, capfd):
assert 'foo.c:89: warning: some weird warning!' in msg assert 'foo.c:89: warning: some weird warning!' in msg
def test_cache_only_fails(tmpdir, mock_fetch, install_mockery, capfd): def test_cache_only_fails(tmpdir, mock_fetch, install_mockery):
msg = '' # libelf from cache fails to install, which automatically removes the
with capfd.disabled(): # the libdwarf build task and flags the package as failed to install.
try: err_msg = 'Installation of libdwarf failed'
install('--cache-only', 'libdwarf') with pytest.raises(spack.installer.InstallError, match=err_msg):
except spack.installer.InstallError as e: install('--cache-only', 'libdwarf')
msg = str(e)
# libelf from cache failed to install, which automatically removed the # Check that failure prefix locks are still cached
# the libdwarf build task and flagged the package as failed to install. failure_lock_prefixes = ','.join(spack.store.db._prefix_failures.keys())
assert 'Installation of libdwarf failed' in msg assert 'libelf' in failure_lock_prefixes
assert 'libdwarf' in failure_lock_prefixes
def test_install_only_dependencies(tmpdir, mock_fetch, install_mockery): def test_install_only_dependencies(tmpdir, mock_fetch, install_mockery):

View File

@ -601,6 +601,16 @@ def install_mockery(tmpdir, config, mock_packages, monkeypatch):
tmpdir.join('opt').remove() tmpdir.join('opt').remove()
spack.store.store = real_store spack.store.store = real_store
# Also wipe out any cached prefix failure locks (associated with
# the session-scoped mock archive).
for pkg_id in list(spack.store.db._prefix_failures.keys()):
lock = spack.store.db._prefix_failures.pop(pkg_id, None)
if lock:
try:
lock.release_write()
except Exception:
pass
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def install_mockery_mutable_config( def install_mockery_mutable_config(

View File

@ -462,6 +462,58 @@ def _repoerr(repo, name):
assert "Couldn't copy in provenance for cmake" in out assert "Couldn't copy in provenance for cmake" in out
def test_clear_failures_success(install_mockery):
"""Test the clear_failures happy path."""
# Set up a test prefix failure lock
lock = lk.Lock(spack.store.db.prefix_fail_path, start=1, length=1,
default_timeout=1e-9, desc='test')
try:
lock.acquire_write()
except lk.LockTimeoutError:
tty.warn('Failed to write lock the test install failure')
spack.store.db._prefix_failures['test'] = lock
# Set up a fake failure mark (or file)
fs.touch(os.path.join(spack.store.db._failure_dir, 'test'))
# Now clear failure tracking
inst.clear_failures()
# Ensure there are no cached failure locks or failure marks
assert len(spack.store.db._prefix_failures) == 0
assert len(os.listdir(spack.store.db._failure_dir)) == 0
# Ensure the core directory and failure lock file still exist
assert os.path.isdir(spack.store.db._failure_dir)
assert os.path.isfile(spack.store.db.prefix_fail_path)
def test_clear_failures_errs(install_mockery, monkeypatch, capsys):
"""Test the clear_failures exception paths."""
orig_fn = os.remove
err_msg = 'Mock os remove'
def _raise_except(path):
raise OSError(err_msg)
# Set up a fake failure mark (or file)
fs.touch(os.path.join(spack.store.db._failure_dir, 'test'))
monkeypatch.setattr(os, 'remove', _raise_except)
# Clear failure tracking
inst.clear_failures()
# Ensure expected warning generated
out = str(capsys.readouterr()[1])
assert 'Unable to remove failure' in out
assert err_msg in out
# Restore remove for teardown
monkeypatch.setattr(os, 'remove', orig_fn)
def test_check_deps_status_install_failure(install_mockery, monkeypatch): def test_check_deps_status_install_failure(install_mockery, monkeypatch):
spec, installer = create_installer('a') spec, installer = create_installer('a')
@ -669,7 +721,7 @@ def _raise_except(lock):
installer._cleanup_failed(pkg_id) installer._cleanup_failed(pkg_id)
out = str(capsys.readouterr()[1]) out = str(capsys.readouterr()[1])
assert 'exception when removing failure mark' in out assert 'exception when removing failure tracking' in out
assert msg in out assert msg in out

View File

@ -484,7 +484,7 @@ _spack_ci_rebuild() {
_spack_clean() { _spack_clean() {
if $list_options if $list_options
then then
SPACK_COMPREPLY="-h --help -s --stage -d --downloads -m --misc-cache -p --python-cache -a --all" SPACK_COMPREPLY="-h --help -s --stage -d --downloads -f --failures -m --misc-cache -p --python-cache -a --all"
else else
_all_packages _all_packages
fi fi