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``
^^^^^^^^^^^^^^^
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
accumulate in the staging area.
accumulate.
When called with ``--stage`` or without arguments this removes all staged
files.
When called with ``--downloads`` this will clear all resources
:ref:`cached <caching>` during installs.
The ``--downloads`` option removes cached :ref:`cached <caching>` downloads.
When called with ``--user-cache`` this will remove caches in the user home
directory, including cached virtual indices.
You can force the removal of all install failure tracking markers using the
``--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``.
When called with positional arguments, cleans up temporary files only
for a particular package. If ``fetch``, ``stage``, or ``install``
When called with positional arguments, this command cleans up temporary files
only for a particular package. If ``fetch``, ``stage``, or ``install``
are run again after this, Spack's build process will start from scratch.

View file

@ -23,9 +23,9 @@
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):
parser.parse_args(['-sdmp'], namespace=namespace)
parser.parse_args(['-sdfmp'], namespace=namespace)
def setup_parser(subparser):
@ -35,6 +35,9 @@ def setup_parser(subparser):
subparser.add_argument(
'-d', '--downloads', action='store_true',
help="remove cached downloads")
subparser.add_argument(
'-f', '--failures', action='store_true',
help="force removal of all install failure tracking markers")
subparser.add_argument(
'-m', '--misc-cache', action='store_true',
help="remove long-lived caches, like the virtual package index")
@ -42,15 +45,15 @@ def setup_parser(subparser):
'-p', '--python-cache', action='store_true',
help="remove .pyc, .pyo files and __pycache__ folders")
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'])
def clean(parser, args):
# If nothing was set, activate the default
if not any([args.specs, args.stage, args.downloads, args.misc_cache,
args.python_cache]):
if not any([args.specs, args.stage, args.downloads, args.failures,
args.misc_cache, args.python_cache]):
args.stage = True
# Then do the cleaning falling through the cases
@ -70,6 +73,10 @@ def clean(parser, args):
tty.msg('Removing cached downloads')
spack.caches.fetch_cache.destroy()
if args.failures:
tty.msg('Removing install failure marks')
spack.installer.clear_failures()
if args.misc_cache:
tty.msg('Removing cached information on repositories')
spack.caches.misc_cache.destroy()

View file

@ -23,6 +23,7 @@
import contextlib
import datetime
import os
import six
import socket
import sys
import time
@ -33,14 +34,14 @@
_use_uuid = False
pass
import llnl.util.filesystem as fs
import llnl.util.tty as tty
import six
import spack.repo
import spack.spec
import spack.store
import spack.util.lock as lk
import spack.util.spack_json as sjson
from llnl.util.filesystem import mkdirp
from spack.directory_layout import DirectoryLayoutError
from spack.error import SpackError
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
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:
mkdirp(self._failure_dir)
fs.mkdirp(self._failure_dir)
self.is_upstream = is_upstream
self.last_seen_verifier = ''
@ -373,6 +374,23 @@ def _failed_spec_path(self, spec):
return os.path.join(self._failure_dir,
'{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):
"""
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
def clear_failures():
"""
Remove all failure tracking markers for the Spack instance.
"""
spack.store.db.clear_all_failures()
def dump_packages(spec, path):
"""
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)
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}'
try:
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)
monkeypatch.setattr(
spack.caches.misc_cache, 'destroy', Counter())
monkeypatch.setattr(
spack.installer, 'clear_failures', Counter())
@pytest.mark.usefixtures(
'mock_packages', 'config', 'mock_calls_for_clean'
)
@pytest.mark.parametrize('command_line,counters', [
('mpileaks', [1, 0, 0, 0]),
('-s', [0, 1, 0, 0]),
('-sd', [0, 1, 1, 0]),
('-m', [0, 0, 0, 1]),
('-a', [0, 1, 1, 1]),
('', [0, 0, 0, 0]),
('mpileaks', [1, 0, 0, 0, 0]),
('-s', [0, 1, 0, 0, 0]),
('-sd', [0, 1, 1, 0, 0]),
('-m', [0, 0, 0, 1, 0]),
('-f', [0, 0, 0, 0, 1]),
('-a', [0, 1, 1, 1, 1]),
('', [0, 0, 0, 0, 0]),
])
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.caches.fetch_cache.destroy.call_count == counters[2]
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
def test_cache_only_fails(tmpdir, mock_fetch, install_mockery, capfd):
msg = ''
with capfd.disabled():
try:
install('--cache-only', 'libdwarf')
except spack.installer.InstallError as e:
msg = str(e)
def test_cache_only_fails(tmpdir, mock_fetch, install_mockery):
# libelf from cache fails to install, which automatically removes the
# the libdwarf build task and flags the package as failed to install.
err_msg = 'Installation of libdwarf failed'
with pytest.raises(spack.installer.InstallError, match=err_msg):
install('--cache-only', 'libdwarf')
# libelf from cache failed to install, which automatically removed the
# the libdwarf build task and flagged the package as failed to install.
assert 'Installation of libdwarf failed' in msg
# Check that failure prefix locks are still cached
failure_lock_prefixes = ','.join(spack.store.db._prefix_failures.keys())
assert 'libelf' in failure_lock_prefixes
assert 'libdwarf' in failure_lock_prefixes
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()
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')
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
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):
spec, installer = create_installer('a')
@ -669,7 +721,7 @@ def _raise_except(lock):
installer._cleanup_failed(pkg_id)
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

View file

@ -484,7 +484,7 @@ _spack_ci_rebuild() {
_spack_clean() {
if $list_options
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
_all_packages
fi