summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMassimiliano Culpo <massimiliano.culpo@gmail.com>2023-10-19 16:00:45 +0200
committerGitHub <noreply@github.com>2023-10-19 16:00:45 +0200
commita1ca1a944aa27849704c525e912947a7699aab39 (patch)
tree0899772c87522e93ee2a7216a6c21a983863be00 /lib
parent4f49f7b9df1cc60a8bb5b4279362175546209772 (diff)
downloadspack-a1ca1a944aa27849704c525e912947a7699aab39.tar.gz
spack-a1ca1a944aa27849704c525e912947a7699aab39.tar.bz2
spack-a1ca1a944aa27849704c525e912947a7699aab39.tar.xz
spack-a1ca1a944aa27849704c525e912947a7699aab39.zip
ASP-based solver: single Spec instance per dag hash (#39590)
Reused specs used to be referenced directly into the built spec. This might cause issues like in issue 39570 where two objects in memory represent the same node, because two reused specs were loaded from different sources but referred to the same spec by DAG hash. The issue is solved by copying concrete specs to a dictionary keyed by dag hash.
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/solver/asp.py114
-rw-r--r--lib/spack/spack/test/concretize.py45
2 files changed, 127 insertions, 32 deletions
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
index af0ddd67ae..eba1d8a3eb 100644
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -13,7 +13,7 @@ import pprint
import re
import types
import warnings
-from typing import List, NamedTuple, Optional, Sequence, Tuple, Union
+from typing import Dict, List, NamedTuple, Optional, Sequence, Tuple, Union
import archspec.cpu
@@ -971,6 +971,70 @@ class PyclingoDriver:
return cycle_result.unsatisfiable
+class ConcreteSpecsByHash(collections.abc.Mapping):
+ """Mapping containing concrete specs keyed by DAG hash.
+
+ The mapping is ensured to be consistent, i.e. if a spec in the mapping has a dependency with
+ hash X, it is ensured to be the same object in memory as the spec keyed by X.
+ """
+
+ def __init__(self) -> None:
+ self.data: Dict[str, spack.spec.Spec] = {}
+
+ def __getitem__(self, dag_hash: str) -> spack.spec.Spec:
+ return self.data[dag_hash]
+
+ def add(self, spec: spack.spec.Spec) -> bool:
+ """Adds a new concrete spec to the mapping. Returns True if the spec was just added,
+ False if the spec was already in the mapping.
+
+ Args:
+ spec: spec to be added
+
+ Raises:
+ ValueError: if the spec is not concrete
+ """
+ if not spec.concrete:
+ msg = (
+ f"trying to store the non-concrete spec '{spec}' in a container "
+ f"that only accepts concrete"
+ )
+ raise ValueError(msg)
+
+ dag_hash = spec.dag_hash()
+ if dag_hash in self.data:
+ return False
+
+ # Here we need to iterate on the input and rewire the copy.
+ self.data[spec.dag_hash()] = spec.copy(deps=False)
+ nodes_to_reconstruct = [spec]
+
+ while nodes_to_reconstruct:
+ input_parent = nodes_to_reconstruct.pop()
+ container_parent = self.data[input_parent.dag_hash()]
+
+ for edge in input_parent.edges_to_dependencies():
+ input_child = edge.spec
+ container_child = self.data.get(input_child.dag_hash())
+ # Copy children that don't exist yet
+ if container_child is None:
+ container_child = input_child.copy(deps=False)
+ self.data[input_child.dag_hash()] = container_child
+ nodes_to_reconstruct.append(input_child)
+
+ # Rewire edges
+ container_parent.add_dependency_edge(
+ dependency_spec=container_child, depflag=edge.depflag, virtuals=edge.virtuals
+ )
+ return True
+
+ def __len__(self) -> int:
+ return len(self.data)
+
+ def __iter__(self):
+ return iter(self.data)
+
+
class SpackSolverSetup:
"""Class to set up and run a Spack concretization solve."""
@@ -994,9 +1058,7 @@ class SpackSolverSetup:
# (ID, CompilerSpec) -> dictionary of attributes
self.compiler_info = collections.defaultdict(dict)
- # hashes we've already added facts for
- self.seen_hashes = set()
- self.reusable_and_possible = {}
+ self.reusable_and_possible = ConcreteSpecsByHash()
# id for dummy variables
self._condition_id_counter = itertools.count()
@@ -2318,25 +2380,29 @@ class SpackSolverSetup:
for pkg, variant, value in self.variant_values_from_specs:
self.gen.fact(fn.pkg_fact(pkg, fn.variant_possible_value(variant, value)))
- def _facts_from_concrete_spec(self, spec, possible):
+ def register_concrete_spec(self, spec, possible):
# tell the solver about any installed packages that could
# be dependencies (don't tell it about the others)
- h = spec.dag_hash()
- if spec.name in possible and h not in self.seen_hashes:
- self.reusable_and_possible[h] = spec
- try:
- # Only consider installed packages for repo we know
- spack.repo.PATH.get(spec)
- except (spack.repo.UnknownNamespaceError, spack.repo.UnknownPackageError):
- return
+ if spec.name not in possible:
+ return
+
+ try:
+ # Only consider installed packages for repo we know
+ spack.repo.PATH.get(spec)
+ except (spack.repo.UnknownNamespaceError, spack.repo.UnknownPackageError) as e:
+ tty.debug(f"[REUSE] Issues when trying to reuse {spec.short_spec}: {str(e)}")
+ return
+
+ self.reusable_and_possible.add(spec)
+ def concrete_specs(self):
+ """Emit facts for reusable specs"""
+ for h, spec in self.reusable_and_possible.items():
# this indicates that there is a spec like this installed
self.gen.fact(fn.installed_hash(spec.name, h))
-
# this describes what constraints it imposes on the solve
self.impose(h, spec, body=True)
self.gen.newline()
-
# Declare as possible parts of specs that are not in package.py
# - Add versions to possible versions
# - Add OS to possible OS's
@@ -2347,15 +2413,12 @@ class SpackSolverSetup:
)
self.possible_oses.add(dep.os)
- # add the hash to the one seen so far
- self.seen_hashes.add(h)
-
def define_concrete_input_specs(self, specs, possible):
# any concrete specs in the input spec list
for input_spec in specs:
for spec in input_spec.traverse():
if spec.concrete:
- self._facts_from_concrete_spec(spec, possible)
+ self.register_concrete_spec(spec, possible)
def setup(
self,
@@ -2422,14 +2485,13 @@ class SpackSolverSetup:
# get possible compilers
self.possible_compilers = self.generate_possible_compilers(specs)
- self.gen.h1("Concrete input spec definitions")
+ self.gen.h1("Reusable concrete specs")
self.define_concrete_input_specs(specs, self.pkgs)
-
if reuse:
- self.gen.h1("Reusable specs")
self.gen.fact(fn.optimize_for_reuse())
for reusable_spec in reuse:
- self._facts_from_concrete_spec(reusable_spec, self.pkgs)
+ self.register_concrete_spec(reusable_spec, self.pkgs)
+ self.concrete_specs()
self.gen.h1("Generic statements on possible packages")
node_counter.possible_packages_facts(self.gen, fn)
@@ -2620,7 +2682,6 @@ class SpecBuilder:
self._specs = {}
self._result = None
self._command_line_specs = specs
- self._hash_specs = []
self._flag_sources = collections.defaultdict(lambda: set())
self._flag_compiler_defaults = set()
@@ -2631,7 +2692,6 @@ class SpecBuilder:
def hash(self, node, h):
if node not in self._specs:
self._specs[node] = self._hash_lookup[h]
- self._hash_specs.append(node)
def node(self, node):
if node not in self._specs:
@@ -2869,12 +2929,10 @@ class SpecBuilder:
# fix flags after all specs are constructed
self.reorder_flags()
- # cycle detection
- roots = [spec.root for spec in self._specs.values() if not spec.root.installed]
-
# inject patches -- note that we' can't use set() to unique the
# roots here, because the specs aren't complete, and the hash
# function will loop forever.
+ roots = [spec.root for spec in self._specs.values() if not spec.root.installed]
roots = dict((id(r), r) for r in roots)
for root in roots.values():
spack.spec.Spec.inject_patches_variant(root)
diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py
index 53f9c64d5e..48334e70a1 100644
--- a/lib/spack/spack/test/concretize.py
+++ b/lib/spack/spack/test/concretize.py
@@ -1170,7 +1170,7 @@ class TestConcretize:
)
@pytest.mark.parametrize("mock_db", [True, False])
def test_reuse_does_not_overwrite_dev_specs(
- self, dev_first, spec, mock_db, tmpdir, monkeypatch
+ self, dev_first, spec, mock_db, tmpdir, temporary_store, monkeypatch
):
"""Test that reuse does not mix dev specs with non-dev specs.
@@ -1182,8 +1182,7 @@ class TestConcretize:
# dev and non-dev specs that are otherwise identical
spec = Spec(spec)
dev_spec = spec.copy()
- dev_constraint = "dev_path=%s" % tmpdir.strpath
- dev_spec["dev-build-test-install"].constrain(dev_constraint)
+ dev_spec["dev-build-test-install"].constrain(f"dev_path={tmpdir.strpath}")
# run the test in both orders
first_spec = dev_spec if dev_first else spec
@@ -1196,7 +1195,7 @@ class TestConcretize:
return [first_spec]
if mock_db:
- monkeypatch.setattr(spack.store.STORE.db, "query", mock_fn)
+ temporary_store.db.add(first_spec, None)
else:
monkeypatch.setattr(spack.binary_distribution, "update_cache_and_get_specs", mock_fn)
@@ -2112,6 +2111,24 @@ class TestConcretize:
# when checksums are required
Spec("a@=3.0").concretized()
+ @pytest.mark.regression("39570")
+ @pytest.mark.db
+ def test_reuse_python_from_cli_and_extension_from_db(self, mutable_database):
+ """Tests that reusing python with and explicit request on the command line, when the spec
+ also reuses a python extension from the DB, doesn't fail.
+ """
+ s = Spec("py-extension1").concretized()
+ python_hash = s["python"].dag_hash()
+ s.package.do_install(fake=True, explicit=True)
+
+ with spack.config.override("concretizer:reuse", True):
+ with_reuse = Spec(f"py-extension2 ^/{python_hash}").concretized()
+
+ with spack.config.override("concretizer:reuse", False):
+ without_reuse = Spec("py-extension2").concretized()
+
+ assert with_reuse.dag_hash() == without_reuse.dag_hash()
+
@pytest.fixture()
def duplicates_test_repository():
@@ -2246,3 +2263,23 @@ class TestConcretizeSeparately:
def test_drop_moving_targets(v_str, v_opts, checksummed):
v = Version(v_str)
assert spack.solver.asp._is_checksummed_version((v, v_opts)) == checksummed
+
+
+class TestConcreteSpecsByHash:
+ """Tests the container of concrete specs"""
+
+ @pytest.mark.parametrize("input_specs", [["a"], ["a foobar=bar", "b"], ["a foobar=baz", "b"]])
+ def test_adding_specs(self, input_specs, default_mock_concretization):
+ """Tests that concrete specs in the container are equivalent, but stored as different
+ objects in memory.
+ """
+ container = spack.solver.asp.ConcreteSpecsByHash()
+ input_specs = [Spec(s).concretized() for s in input_specs]
+ for s in input_specs:
+ container.add(s)
+
+ for root in input_specs:
+ for node in root.traverse(root=True):
+ assert node == container[node.dag_hash()]
+ assert node.dag_hash() in container
+ assert node is not container[node.dag_hash()]