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:
parent
bc53bb9b7c
commit
48d3e8d350
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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.
|
||||||
|
@ -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))
|
||||||
|
@ -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]
|
||||||
|
@ -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):
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user