Fixes #1720: spack reindex fails with invalid ref count. (#1867)

* 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:
Todd Gamblin 2016-09-28 15:00:26 -04:00 committed by GitHub
parent 66c2ac0bc9
commit cb229f0842
3 changed files with 79 additions and 48 deletions

View File

@ -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):

View File

@ -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."""

View File

@ -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(