* Fixes #1720: spack reindex fails with invalid ref count. - Database graph wasn't being built properly; dependencies were set up incorrectly in the nodes that ended up in the graph on reindex. - Reworked _add to increment ref count properly and to always build bottom-up to make the logic simpler to understand. * Add checks to ensure DB is a valid merkle tree.
This commit is contained in:
parent
66c2ac0bc9
commit
cb229f0842
@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
import spack.spec
|
import spack.spec
|
||||||
from spack.version import Version
|
from spack.version import Version
|
||||||
from spack.spec import Spec
|
from spack.spec import *
|
||||||
from spack.error import SpackError
|
from spack.error import SpackError
|
||||||
from spack.repository import UnknownPackageError
|
from spack.repository import UnknownPackageError
|
||||||
import spack.util.spack_yaml as syaml
|
import spack.util.spack_yaml as syaml
|
||||||
@ -64,6 +64,9 @@
|
|||||||
# Default timeout for spack database locks is 5 min.
|
# Default timeout for spack database locks is 5 min.
|
||||||
_db_lock_timeout = 60
|
_db_lock_timeout = 60
|
||||||
|
|
||||||
|
# Types of dependencies tracked by the database
|
||||||
|
_tracked_deps = nobuild
|
||||||
|
|
||||||
|
|
||||||
def _autospec(function):
|
def _autospec(function):
|
||||||
"""Decorator that automatically converts the argument of a single-arg
|
"""Decorator that automatically converts the argument of a single-arg
|
||||||
@ -232,8 +235,6 @@ def _assign_dependencies(self, hash_key, installs, data):
|
|||||||
spec.format('$_$#'), dname, dhash[:7]))
|
spec.format('$_$#'), dname, dhash[:7]))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# defensive copy (not sure everything handles extra
|
|
||||||
# parent links yet)
|
|
||||||
child = data[dhash].spec
|
child = data[dhash].spec
|
||||||
spec._add_dependency(child, dtypes)
|
spec._add_dependency(child, dtypes)
|
||||||
|
|
||||||
@ -328,7 +329,7 @@ def invalid_record(hash_key, error):
|
|||||||
self._data = data
|
self._data = data
|
||||||
|
|
||||||
def reindex(self, directory_layout):
|
def reindex(self, directory_layout):
|
||||||
"""Build database index from scratch based from a directory layout.
|
"""Build database index from scratch based on a directory layout.
|
||||||
|
|
||||||
Locks the DB if it isn't locked already.
|
Locks the DB if it isn't locked already.
|
||||||
|
|
||||||
@ -359,9 +360,6 @@ def _read_suppress_error():
|
|||||||
|
|
||||||
# Ask the directory layout to traverse the filesystem.
|
# Ask the directory layout to traverse the filesystem.
|
||||||
for spec in directory_layout.all_specs():
|
for spec in directory_layout.all_specs():
|
||||||
# Create a spec for each known package and add it.
|
|
||||||
path = directory_layout.path_for_spec(spec)
|
|
||||||
|
|
||||||
# Try to recover explicit value from old DB, but
|
# Try to recover explicit value from old DB, but
|
||||||
# default it to False if DB was corrupt.
|
# default it to False if DB was corrupt.
|
||||||
explicit = False
|
explicit = False
|
||||||
@ -370,7 +368,7 @@ def _read_suppress_error():
|
|||||||
if old_info is not None:
|
if old_info is not None:
|
||||||
explicit = old_info.explicit
|
explicit = old_info.explicit
|
||||||
|
|
||||||
self._add(spec, path, directory_layout, explicit=explicit)
|
self._add(spec, directory_layout, explicit=explicit)
|
||||||
|
|
||||||
self._check_ref_counts()
|
self._check_ref_counts()
|
||||||
|
|
||||||
@ -389,10 +387,7 @@ def _check_ref_counts(self):
|
|||||||
counts = {}
|
counts = {}
|
||||||
for key, rec in self._data.items():
|
for key, rec in self._data.items():
|
||||||
counts.setdefault(key, 0)
|
counts.setdefault(key, 0)
|
||||||
# XXX(deptype): This checks all dependencies, but build
|
for dep in rec.spec.dependencies(_tracked_deps):
|
||||||
# dependencies might be able to be dropped in the
|
|
||||||
# future.
|
|
||||||
for dep in rec.spec.dependencies():
|
|
||||||
dep_key = dep.dag_hash()
|
dep_key = dep.dag_hash()
|
||||||
counts.setdefault(dep_key, 0)
|
counts.setdefault(dep_key, 0)
|
||||||
counts[dep_key] += 1
|
counts[dep_key] += 1
|
||||||
@ -450,52 +445,62 @@ def _read(self):
|
|||||||
# reindex() takes its own write lock, so no lock here.
|
# reindex() takes its own write lock, so no lock here.
|
||||||
self.reindex(spack.install_layout)
|
self.reindex(spack.install_layout)
|
||||||
|
|
||||||
def _add(self, spec, path, directory_layout=None, explicit=False):
|
def _add(self, spec, directory_layout=None, explicit=False):
|
||||||
"""Add an install record for spec at path to the database.
|
"""Add an install record for this spec to the database.
|
||||||
|
|
||||||
This assumes that the spec is not already installed. It
|
Assumes spec is installed in ``layout.path_for_spec(spec)``.
|
||||||
updates the ref counts on dependencies of the spec in the DB.
|
|
||||||
|
|
||||||
This operation is in-memory, and does not lock the DB.
|
Also ensures dependencies are present and updated in the DB as
|
||||||
|
either intsalled or missing.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
key = spec.dag_hash()
|
if not spec.concrete:
|
||||||
if key in self._data:
|
raise NonConcreteSpecAddError(
|
||||||
rec = self._data[key]
|
"Specs added to DB must be concrete.")
|
||||||
rec.installed = True
|
|
||||||
|
|
||||||
# TODO: this overwrites a previous install path (when path !=
|
for dep in spec.dependencies(_tracked_deps):
|
||||||
# self._data[key].path), and the old path still has a
|
dkey = dep.dag_hash()
|
||||||
# dependent in the DB. We could consider re-RPATH-ing the
|
if dkey not in self._data:
|
||||||
# dependents. This case is probably infrequent and may not be
|
self._add(dep, directory_layout, explicit=False)
|
||||||
# worth fixing, but this is where we can discover it.
|
|
||||||
rec.path = path
|
|
||||||
|
|
||||||
else:
|
|
||||||
self._data[key] = InstallRecord(spec, path, True,
|
|
||||||
explicit=explicit)
|
|
||||||
for dep in spec.dependencies(('link', 'run')):
|
|
||||||
self._increment_ref_count(dep, directory_layout)
|
|
||||||
|
|
||||||
def _increment_ref_count(self, spec, directory_layout=None):
|
|
||||||
"""Recursively examine dependencies and update their DB entries."""
|
|
||||||
key = spec.dag_hash()
|
key = spec.dag_hash()
|
||||||
if key not in self._data:
|
if key not in self._data:
|
||||||
installed = False
|
installed = False
|
||||||
path = None
|
path = None
|
||||||
if directory_layout:
|
if directory_layout:
|
||||||
path = directory_layout.path_for_spec(spec)
|
path = directory_layout.path_for_spec(spec)
|
||||||
installed = os.path.isdir(path)
|
try:
|
||||||
|
directory_layout.check_installed(spec)
|
||||||
|
installed = True
|
||||||
|
except DirectoryLayoutError as e:
|
||||||
|
tty.warn(
|
||||||
|
'Dependency missing due to corrupt install directory:',
|
||||||
|
path, str(e))
|
||||||
|
|
||||||
self._data[key] = InstallRecord(spec.copy(), path, installed)
|
# Create a new install record with no deps initially.
|
||||||
|
new_spec = spec.copy(deps=False)
|
||||||
|
self._data[key] = InstallRecord(
|
||||||
|
new_spec, path, installed, ref_count=0, explicit=explicit)
|
||||||
|
|
||||||
for dep in spec.dependencies(('link', 'run')):
|
# Connect dependencies from the DB to the new copy.
|
||||||
self._increment_ref_count(dep)
|
for name, dep in spec.dependencies_dict(_tracked_deps).iteritems():
|
||||||
|
dkey = dep.spec.dag_hash()
|
||||||
|
new_spec._add_dependency(self._data[dkey].spec, dep.deptypes)
|
||||||
|
self._data[dkey].ref_count += 1
|
||||||
|
|
||||||
self._data[key].ref_count += 1
|
# Mark concrete once everything is built, and preserve
|
||||||
|
# the original hash of concrete specs.
|
||||||
|
new_spec._mark_concrete()
|
||||||
|
new_spec._hash = key
|
||||||
|
|
||||||
|
else:
|
||||||
|
# If it is already there, mark it as installed.
|
||||||
|
self._data[key].installed = True
|
||||||
|
|
||||||
|
self._data[key].explicit = explicit
|
||||||
|
|
||||||
@_autospec
|
@_autospec
|
||||||
def add(self, spec, path, explicit=False):
|
def add(self, spec, directory_layout, explicit=False):
|
||||||
"""Add spec at path to database, locking and reading DB to sync.
|
"""Add spec at path to database, locking and reading DB to sync.
|
||||||
|
|
||||||
``add()`` will lock and read from the DB on disk.
|
``add()`` will lock and read from the DB on disk.
|
||||||
@ -504,7 +509,7 @@ def add(self, spec, path, explicit=False):
|
|||||||
# TODO: ensure that spec is concrete?
|
# TODO: ensure that spec is concrete?
|
||||||
# Entire add is transactional.
|
# Entire add is transactional.
|
||||||
with self.write_transaction():
|
with self.write_transaction():
|
||||||
self._add(spec, path, explicit=explicit)
|
self._add(spec, directory_layout, explicit=explicit)
|
||||||
|
|
||||||
def _get_matching_spec_key(self, spec, **kwargs):
|
def _get_matching_spec_key(self, spec, **kwargs):
|
||||||
"""Get the exact spec OR get a single spec that matches."""
|
"""Get the exact spec OR get a single spec that matches."""
|
||||||
@ -534,7 +539,7 @@ def _decrement_ref_count(self, spec):
|
|||||||
|
|
||||||
if rec.ref_count == 0 and not rec.installed:
|
if rec.ref_count == 0 and not rec.installed:
|
||||||
del self._data[key]
|
del self._data[key]
|
||||||
for dep in spec.dependencies('link'):
|
for dep in spec.dependencies(_tracked_deps):
|
||||||
self._decrement_ref_count(dep)
|
self._decrement_ref_count(dep)
|
||||||
|
|
||||||
def _remove(self, spec):
|
def _remove(self, spec):
|
||||||
@ -548,7 +553,7 @@ def _remove(self, spec):
|
|||||||
return rec.spec
|
return rec.spec
|
||||||
|
|
||||||
del self._data[key]
|
del self._data[key]
|
||||||
for dep in rec.spec.dependencies('link'):
|
for dep in rec.spec.dependencies(_tracked_deps):
|
||||||
self._decrement_ref_count(dep)
|
self._decrement_ref_count(dep)
|
||||||
|
|
||||||
# Returns the concrete spec so we know it in the case where a
|
# Returns the concrete spec so we know it in the case where a
|
||||||
@ -657,6 +662,10 @@ class CorruptDatabaseError(SpackError):
|
|||||||
"""Raised when errors are found while reading the database."""
|
"""Raised when errors are found while reading the database."""
|
||||||
|
|
||||||
|
|
||||||
|
class NonConcreteSpecAddError(SpackError):
|
||||||
|
"""Raised when attemptint to add non-concrete spec to DB."""
|
||||||
|
|
||||||
|
|
||||||
class InvalidDatabaseVersionError(SpackError):
|
class InvalidDatabaseVersionError(SpackError):
|
||||||
|
|
||||||
def __init__(self, expected, found):
|
def __init__(self, expected, found):
|
||||||
|
@ -1053,7 +1053,8 @@ def build_process():
|
|||||||
|
|
||||||
# note: PARENT of the build process adds the new package to
|
# note: PARENT of the build process adds the new package to
|
||||||
# the database, so that we don't need to re-read from file.
|
# the database, so that we don't need to re-read from file.
|
||||||
spack.installed_db.add(self.spec, self.prefix, explicit=explicit)
|
spack.installed_db.add(
|
||||||
|
self.spec, spack.install_layout, explicit=explicit)
|
||||||
|
|
||||||
def sanity_check_prefix(self):
|
def sanity_check_prefix(self):
|
||||||
"""This function checks whether install succeeded."""
|
"""This function checks whether install succeeded."""
|
||||||
|
@ -124,6 +124,19 @@ def test_015_write_and_read(self):
|
|||||||
self.assertEqual(new_rec.path, rec.path)
|
self.assertEqual(new_rec.path, rec.path)
|
||||||
self.assertEqual(new_rec.installed, rec.installed)
|
self.assertEqual(new_rec.installed, rec.installed)
|
||||||
|
|
||||||
|
def _check_merkleiness(self):
|
||||||
|
"""Ensure the spack database is a valid merkle graph."""
|
||||||
|
all_specs = spack.installed_db.query(installed=any)
|
||||||
|
|
||||||
|
seen = {}
|
||||||
|
for spec in all_specs:
|
||||||
|
for dep in spec.dependencies():
|
||||||
|
hash_key = dep.dag_hash()
|
||||||
|
if hash_key not in seen:
|
||||||
|
seen[hash_key] = id(dep)
|
||||||
|
else:
|
||||||
|
self.assertEqual(seen[hash_key], id(dep))
|
||||||
|
|
||||||
def _check_db_sanity(self):
|
def _check_db_sanity(self):
|
||||||
"""Utiilty function to check db against install layout."""
|
"""Utiilty function to check db against install layout."""
|
||||||
expected = sorted(spack.install_layout.all_specs())
|
expected = sorted(spack.install_layout.all_specs())
|
||||||
@ -133,10 +146,17 @@ def _check_db_sanity(self):
|
|||||||
for e, a in zip(expected, actual):
|
for e, a in zip(expected, actual):
|
||||||
self.assertEqual(e, a)
|
self.assertEqual(e, a)
|
||||||
|
|
||||||
|
self._check_merkleiness()
|
||||||
|
|
||||||
def test_020_db_sanity(self):
|
def test_020_db_sanity(self):
|
||||||
"""Make sure query() returns what's actually in the db."""
|
"""Make sure query() returns what's actually in the db."""
|
||||||
self._check_db_sanity()
|
self._check_db_sanity()
|
||||||
|
|
||||||
|
def test_025_reindex(self):
|
||||||
|
"""Make sure reindex works and ref counts are valid."""
|
||||||
|
spack.installed_db.reindex(spack.install_layout)
|
||||||
|
self._check_db_sanity()
|
||||||
|
|
||||||
def test_030_db_sanity_from_another_process(self):
|
def test_030_db_sanity_from_another_process(self):
|
||||||
def read_and_modify():
|
def read_and_modify():
|
||||||
self._check_db_sanity() # check that other process can read DB
|
self._check_db_sanity() # check that other process can read DB
|
||||||
@ -203,9 +223,10 @@ def _check_remove_and_add_package(self, spec):
|
|||||||
self.assertTrue(concrete_spec not in remaining)
|
self.assertTrue(concrete_spec not in remaining)
|
||||||
|
|
||||||
# add it back and make sure everything is ok.
|
# add it back and make sure everything is ok.
|
||||||
self.installed_db.add(concrete_spec, "")
|
self.installed_db.add(concrete_spec, spack.install_layout)
|
||||||
installed = self.installed_db.query()
|
installed = self.installed_db.query()
|
||||||
self.assertEqual(len(installed), len(original))
|
self.assertTrue(concrete_spec in installed)
|
||||||
|
self.assertEqual(installed, original)
|
||||||
|
|
||||||
# sanity check against direcory layout and check ref counts.
|
# sanity check against direcory layout and check ref counts.
|
||||||
self._check_db_sanity()
|
self._check_db_sanity()
|
||||||
@ -233,7 +254,7 @@ def test_080_root_ref_counts(self):
|
|||||||
self.assertEqual(self.installed_db.get_record('mpich').ref_count, 1)
|
self.assertEqual(self.installed_db.get_record('mpich').ref_count, 1)
|
||||||
|
|
||||||
# Put the spec back
|
# Put the spec back
|
||||||
self.installed_db.add(rec.spec, rec.path)
|
self.installed_db.add(rec.spec, spack.install_layout)
|
||||||
|
|
||||||
# record is present again
|
# record is present again
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
Loading…
Reference in New Issue
Block a user