database: fix reindex with uninstalled deps (#28764)

* Fix reindex with uninstalled deps

When a prefix of a dep is removed, and the db is reindexed, it is added
through the dependent, but until now it incorrectly listed the spec as
'installed'.

There was also some questionable behavior in the db when the same spec
was added multiple times, it would always be marked installed.

* Always reserve path

* Only add installed spec's prefixes to install prefixes set

* Improve warning, and ensure ensure only ensures

* test: reindex with every file system remnant removed except for the old index; it should give a database with nothing installed, including records with installed==False,external==False,ref_count==0,explicit=True, and these should be removable from the database
This commit is contained in:
Harmen Stoppels 2022-02-04 20:31:39 +01:00
parent 9c69e1dc9e
commit cc21905b90
4 changed files with 94 additions and 42 deletions

View File

@ -937,22 +937,15 @@ def _construct_from_directory_layout(self, directory_layout, old_data):
tty.debug( tty.debug(
'RECONSTRUCTING FROM OLD DB: {0}'.format(entry.spec)) 'RECONSTRUCTING FROM OLD DB: {0}'.format(entry.spec))
try: try:
layout = spack.store.layout layout = None if entry.spec.external else spack.store.layout
if entry.spec.external: kwargs = {
layout = None 'spec': entry.spec,
install_check = True 'directory_layout': layout,
else: 'explicit': entry.explicit,
install_check = layout.check_installed(entry.spec) 'installation_time': entry.installation_time # noqa: E501
}
if install_check: self._add(**kwargs)
kwargs = { processed_specs.add(entry.spec)
'spec': entry.spec,
'directory_layout': layout,
'explicit': entry.explicit,
'installation_time': entry.installation_time # noqa: E501
}
self._add(**kwargs)
processed_specs.add(entry.spec)
except Exception as e: except Exception as e:
# Something went wrong, so the spec was not restored # Something went wrong, so the spec was not restored
# from old data # from old data
@ -1096,24 +1089,28 @@ def _add(
} }
self._add(dep, directory_layout, **extra_args) self._add(dep, directory_layout, **extra_args)
if key not in self._data: # Make sure the directory layout agrees whether the spec is installed
installed = bool(spec.external) if not spec.external and directory_layout:
path = None path = directory_layout.path_for_spec(spec)
if not spec.external and directory_layout: installed = False
path = directory_layout.path_for_spec(spec) try:
if path in self._installed_prefixes: directory_layout.ensure_installed(spec)
raise Exception("Install prefix collision.") installed = True
try:
directory_layout.check_installed(spec)
installed = True
except DirectoryLayoutError as e:
tty.warn(
'Dependency missing: may be deprecated or corrupted:',
path, str(e))
self._installed_prefixes.add(path) self._installed_prefixes.add(path)
elif spec.external_path: except DirectoryLayoutError as e:
path = spec.external_path msg = ("{0} is being {1} in the database with prefix {2}, "
"but this directory does not contain an installation of "
"the spec, due to: {3}")
action = "updated" if key in self._data else "registered"
tty.warn(msg.format(spec.short_spec, action, path, str(e)))
elif spec.external_path:
path = spec.external_path
installed = True
else:
path = None
installed = True
if key not in self._data:
# Create a new install record with no deps initially. # Create a new install record with no deps initially.
new_spec = spec.copy(deps=False) new_spec = spec.copy(deps=False)
extra_args = { extra_args = {
@ -1141,9 +1138,8 @@ def _add(
new_spec._full_hash = spec._full_hash new_spec._full_hash = spec._full_hash
else: else:
# If it is already there, mark it as installed and update # It is already in the database
# installation time self._data[key].installed = installed
self._data[key].installed = True
self._data[key].installation_time = _now() self._data[key].installation_time = _now()
self._data[key].explicit = explicit self._data[key].explicit = explicit
@ -1210,7 +1206,7 @@ def _remove(self, spec):
# This install prefix is now free for other specs to use, even if the # This install prefix is now free for other specs to use, even if the
# spec is only marked uninstalled. # spec is only marked uninstalled.
if not rec.spec.external: if not rec.spec.external and rec.installed:
self._installed_prefixes.remove(rec.path) self._installed_prefixes.remove(rec.path)
if rec.ref_count > 0: if rec.ref_count > 0:

View File

@ -233,13 +233,20 @@ def create_install_directory(self, spec):
self.write_spec(spec, self.spec_file_path(spec)) self.write_spec(spec, self.spec_file_path(spec))
def check_installed(self, spec): def ensure_installed(self, spec):
"""
Throws DirectoryLayoutError if:
1. spec prefix does not exist
2. spec prefix does not contain a spec file
3. the spec file does not correspond to the spec
"""
_check_concrete(spec) _check_concrete(spec)
path = self.path_for_spec(spec) path = self.path_for_spec(spec)
spec_file_path = self.spec_file_path(spec) spec_file_path = self.spec_file_path(spec)
if not os.path.isdir(path): if not os.path.isdir(path):
return None raise InconsistentInstallDirectoryError(
"Install prefix {0} does not exist.".format(path))
if not os.path.isfile(spec_file_path): if not os.path.isfile(spec_file_path):
raise InconsistentInstallDirectoryError( raise InconsistentInstallDirectoryError(
@ -248,7 +255,7 @@ def check_installed(self, spec):
installed_spec = self.read_spec(spec_file_path) installed_spec = self.read_spec(spec_file_path)
if installed_spec == spec: if installed_spec == spec:
return path return
# DAG hashes currently do not include build dependencies. # DAG hashes currently do not include build dependencies.
# #
@ -261,7 +268,7 @@ def check_installed(self, spec):
# may be installed. This means for example that for two instances # may be installed. This means for example that for two instances
# that differ only in CMake version used to build, only one will # that differ only in CMake version used to build, only one will
# be installed. # be installed.
return path return
if spec.dag_hash() == installed_spec.dag_hash(): if spec.dag_hash() == installed_spec.dag_hash():
raise SpecHashCollisionError(spec, installed_spec) raise SpecHashCollisionError(spec, installed_spec)

View File

@ -876,8 +876,8 @@ def __init__(self, root):
def path_for_spec(self, spec): def path_for_spec(self, spec):
return '/'.join([self.root, spec.name + '-' + spec.dag_hash()]) return '/'.join([self.root, spec.name + '-' + spec.dag_hash()])
def check_installed(self, spec): def ensure_installed(self, spec):
return True pass
@pytest.fixture() @pytest.fixture()

View File

@ -11,6 +11,7 @@
import functools import functools
import json import json
import os import os
import shutil
import pytest import pytest
@ -909,3 +910,51 @@ def test_database_works_with_empty_dir(tmpdir):
db.query() db.query()
# Check that reading an empty directory didn't create a new index.json # Check that reading an empty directory didn't create a new index.json
assert not os.path.exists(db._index_path) assert not os.path.exists(db._index_path)
def test_reindex_removed_prefix_is_not_installed(mutable_database, mock_store, capfd):
"""When a prefix of a dependency is removed and the database is reindexed,
the spec should still be added through the dependent, but should be listed as
not installed."""
# Remove libelf from the filesystem
prefix = mutable_database.query_one('libelf').prefix
assert prefix.startswith(str(mock_store))
shutil.rmtree(prefix)
# Reindex should pick up libelf as a dependency of libdwarf
spack.store.store.reindex()
# Reindexing should warn about libelf not being found on the filesystem
err = capfd.readouterr()[1]
assert 'this directory does not contain an installation of the spec' in err
# And we should still have libelf in the database, but not installed.
assert not mutable_database.query_one('libelf', installed=True)
assert mutable_database.query_one('libelf', installed=False)
def test_reindex_when_all_prefixes_are_removed(mutable_database, mock_store):
# Remove all non-external installations from the filesystem
for spec in spack.store.db.query_local():
if not spec.external:
assert spec.prefix.startswith(str(mock_store))
shutil.rmtree(spec.prefix)
# Make sure we have some explicitly installed specs
num = len(mutable_database.query_local(installed=True, explicit=True))
assert num > 0
# Reindex uses the current index to repopulate itself
spack.store.store.reindex()
# Make sure all explicit specs are still there, but are now uninstalled.
specs = mutable_database.query_local(installed=False, explicit=True)
assert len(specs) == num
# And make sure they can be removed from the database (covers the case where
# `ref_count == 0 and not installed`, which hits some obscure branches.
for s in specs:
mutable_database.remove(s)
assert len(mutable_database.query_local(installed=False, explicit=True)) == 0