diff --git a/src/python/pants/build_graph/target.py b/src/python/pants/build_graph/target.py index 8e1ed82dac6..8f4f0788edf 100644 --- a/src/python/pants/build_graph/target.py +++ b/src/python/pants/build_graph/target.py @@ -20,6 +20,7 @@ from pants.base.payload_field import PrimitiveField from pants.base.validation import assert_list from pants.build_graph.address import Address, Addresses +from pants.build_graph.address_lookup_error import AddressLookupError from pants.build_graph.target_addressable import TargetAddressable from pants.build_graph.target_scopes import Scope from pants.source.payload_fields import DeferredSourcesField, SourcesField @@ -130,6 +131,14 @@ class Target(AbstractTarget): :API: public """ + + class RecursiveDepthError(AddressLookupError): + """Raised when there are too many recursive calls to calculate the fingerprint.""" + pass + + + _MAX_RECURSION_DEPTH=300 + class WrongNumberOfAddresses(Exception): """Internal error, too many elements in Addresses @@ -444,7 +453,7 @@ def mark_invalidation_hash_dirty(self): self.mark_extra_invalidation_hash_dirty() self.payload.mark_dirty() - def transitive_invalidation_hash(self, fingerprint_strategy=None): + def transitive_invalidation_hash(self, fingerprint_strategy=None, depth=0): """ :API: public @@ -455,15 +464,25 @@ def transitive_invalidation_hash(self, fingerprint_strategy=None): did not contribute to the fingerprint, according to the provided FingerprintStrategy. :rtype: string """ + if depth > self._MAX_RECURSION_DEPTH: + # NB(zundel) without this catch, we'll eventually hit the python stack limit + # RuntimeError: maximum recursion depth exceeded while calling a Python object + raise self.RecursiveDepthError("Max depth of {} exceeded.".format(self._MAX_RECURSION_DEPTH)) + fingerprint_strategy = fingerprint_strategy or DefaultFingerprintStrategy() if fingerprint_strategy not in self._cached_transitive_fingerprint_map: hasher = sha1() def dep_hash_iter(): for dep in self.dependencies: - dep_hash = dep.transitive_invalidation_hash(fingerprint_strategy) - if dep_hash is not None: - yield dep_hash + try: + dep_hash = dep.transitive_invalidation_hash(fingerprint_strategy, depth=depth+1) + if dep_hash is not None: + yield dep_hash + except self.RecursiveDepthError as e: + raise self.RecursiveDepthError("{message}\n referenced from {spec}" + .format(message=e, spec=dep.address.spec)) + dep_hashes = sorted(list(dep_hash_iter())) for dep_hash in dep_hashes: hasher.update(dep_hash) diff --git a/tests/python/pants_test/build_graph/test_target.py b/tests/python/pants_test/build_graph/test_target.py index e30858aeb1b..6ee97d2f9a5 100644 --- a/tests/python/pants_test/build_graph/test_target.py +++ b/tests/python/pants_test/build_graph/test_target.py @@ -138,3 +138,11 @@ def test_sources_with_more_than_one_address_fails(self): t.create_sources_field(sources=addresses, sources_rel_path='', key_arg='cool_field') self.assertIn("Expected 'cool_field' to be a single address to from_target() as argument", str(cm.exception)) + + def test_max_recursion(self): + target_a = self.make_target('a', Target) + target_b = self.make_target('b', Target, dependencies=[target_a]) + self.make_target('c', Target, dependencies=[target_b]) + target_a.inject_dependency(Address.parse('c')) + with self.assertRaises(Target.RecursiveDepthError): + target_a.transitive_invalidation_hash()