From 02f8d840a685f10e5855bf5c9bbc0367ebfc0703 Mon Sep 17 00:00:00 2001 From: Valentin David <valentin.david@codethink.co.uk> Date: Wed, 15 Aug 2018 17:25:43 +0200 Subject: [PATCH] Invalidate reverse dependencies to scheduled element with workspace Issues: Keys would change from one build to another. So building a second time an element depending a workspaced element, would fail to find the artifact that was already built. Artifact cache would be populated with artifacts with the wrong key. This is a serious issue were an artifact that was built with a workspaced dependency would not rebuild after closing the workspace. Solution: Elements depending on another element with workspace which has been scheduled should not have any keys until we have managed to built the workspaced element. Fixes #461. --- buildstream/element.py | 48 +++++++++++++++++++++++-- tests/frontend/workspace.py | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/buildstream/element.py b/buildstream/element.py index a34b1ca368..88830d0aad 100644 --- a/buildstream/element.py +++ b/buildstream/element.py @@ -194,6 +194,7 @@ class Element(Plugin): self.__runtime_dependencies = [] # Direct runtime dependency Elements self.__build_dependencies = [] # Direct build dependency Elements + self.__reverse_dependencies = [] # Direct reverse dependency Elements self.__sources = [] # List of Sources self.__weak_cache_key = None # Our cached weak cache key self.__strict_cache_key = None # Our cached cache key for strict builds @@ -892,9 +893,11 @@ class Element(Plugin): for meta_dep in meta.dependencies: dependency = Element._new_from_meta(meta_dep, artifacts) element.__runtime_dependencies.append(dependency) + dependency.__reverse_dependencies.append(element) for meta_dep in meta.build_dependencies: dependency = Element._new_from_meta(meta_dep, artifacts) element.__build_dependencies.append(dependency) + dependency.__reverse_dependencies.append(element) return element @@ -1428,6 +1431,16 @@ class Element(Plugin): self._update_state() + if workspace: + # We need to invalidate reverse dependencies + for reverse_dep in self.__get_reverse_dependencies(): + reverse_dep.__cache_key_dict = None + reverse_dep.__cache_key = None + reverse_dep.__weak_cache_key = None + reverse_dep.__strict_cache_key = None + reverse_dep.__strong_cached = None + reverse_dep._update_state() + # _assemble_done(): # # This is called in the main process after the element has been assembled @@ -1465,8 +1478,14 @@ class Element(Plugin): # This does *not* cause a race condition, because # _assemble_done is called before a cleanup job may be # launched. - # - self.__artifacts.append_required_artifacts([self]) + required_artifacts = [self] + + # Reverse dependencies can now compute their keys + for reverse_dep in self.__get_reverse_dependencies(): + reverse_dep._update_state() + required_artifacts.append(reverse_dep) + + self.__artifacts.append_required_artifacts(required_artifacts) # _assemble(): # @@ -2598,6 +2617,31 @@ class Element(Plugin): return utils._deduplicate(keys) + # __get_reverse_dependencies(): + # + # Iterates through the closure of reverse dependenices. + # + # Args: + # visited (set): The elements to skip (only for recursion) + # recursed (bool): Whether to emit the current element (only for recursion) + # + # Yields: + # (:class:`.Element`): The reverse dependent elements + def __get_reverse_dependencies(self, *, visited=None, recursed=False): + if visited is None: + visited = set() + + full_name = self._get_full_name() + + if full_name in visited: + return + + if recursed: + yield self + + for reverse_dep in self.__reverse_dependencies: + yield from reverse_dep.__get_reverse_dependencies(visited=visited, recursed=True) + def _overlap_error_detail(f, forbidden_overlap_elements, elements): if forbidden_overlap_elements: diff --git a/tests/frontend/workspace.py b/tests/frontend/workspace.py index c7af0a70fd..9826b5af00 100644 --- a/tests/frontend/workspace.py +++ b/tests/frontend/workspace.py @@ -767,3 +767,73 @@ def test_list_supported_workspace(cli, tmpdir, datafiles, workspace_cfg, expecte # Check that workspace config is converted correctly if necessary loaded_config = _yaml.node_sanitize(_yaml.load(workspace_config_path)) assert loaded_config == parse_dict_as_yaml(expected) + + +@pytest.mark.datafiles(DATA_DIR) +@pytest.mark.parametrize("kind", repo_kinds) +@pytest.mark.parametrize("strict", [("strict"), ("non-strict")]) +def test_cache_key_workspace_in_dependencies(cli, tmpdir, datafiles, kind, strict): + checkout = os.path.join(str(tmpdir), 'checkout') + element_name, project, workspace = open_workspace(cli, os.path.join(str(tmpdir), 'repo-a'), datafiles, kind, False) + + element_path = os.path.join(project, 'elements') + back_dep_element_name = 'workspace-test-{}-back-dep.bst'.format(kind) + + # Write out our test target + element = { + 'kind': 'compose', + 'depends': [ + { + 'filename': element_name, + 'type': 'build' + } + ] + } + _yaml.dump(element, + os.path.join(element_path, + back_dep_element_name)) + + # Modify workspace + shutil.rmtree(os.path.join(workspace, 'usr', 'bin')) + os.makedirs(os.path.join(workspace, 'etc')) + with open(os.path.join(workspace, 'etc', 'pony.conf'), 'w') as f: + f.write("PONY='pink'") + + # Configure strict mode + strict_mode = True + if strict != 'strict': + strict_mode = False + cli.configure({ + 'projects': { + 'test': { + 'strict': strict_mode + } + } + }) + + # Build artifact with dependency's modified workspace + assert cli.get_element_state(project, element_name) == 'buildable' + assert cli.get_element_key(project, element_name) == "{:?<64}".format('') + assert cli.get_element_state(project, back_dep_element_name) == 'waiting' + assert cli.get_element_key(project, back_dep_element_name) == "{:?<64}".format('') + result = cli.run(project=project, args=['build', back_dep_element_name]) + result.assert_success() + assert cli.get_element_state(project, element_name) == 'cached' + assert cli.get_element_key(project, element_name) != "{:?<64}".format('') + assert cli.get_element_state(project, back_dep_element_name) == 'cached' + assert cli.get_element_key(project, back_dep_element_name) != "{:?<64}".format('') + result = cli.run(project=project, args=['build', back_dep_element_name]) + result.assert_success() + + # Checkout the result + result = cli.run(project=project, args=[ + 'checkout', back_dep_element_name, checkout + ]) + result.assert_success() + + # Check that the pony.conf from the modified workspace exists + filename = os.path.join(checkout, 'etc', 'pony.conf') + assert os.path.exists(filename) + + # Check that the original /usr/bin/hello is not in the checkout + assert not os.path.exists(os.path.join(checkout, 'usr', 'bin', 'hello')) -- GitLab