Skip to content

Commit 159aac2

Browse files
authored
fix: stackoverflow on circular dependencies (#137)
* throws on circular dependency * fix test flakyness due to parallel * add @VisibleForTesting annotation
1 parent 4e6aa59 commit 159aac2

File tree

3 files changed

+48
-4
lines changed

3 files changed

+48
-4
lines changed

cli/src/main/kotlin/com/bazel_diff/hash/RuleHasher.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,39 @@ package com.bazel_diff.hash
33
import com.bazel_diff.bazel.BazelRule
44
import com.bazel_diff.bazel.BazelSourceFileTarget
55
import com.bazel_diff.log.Logger
6+
import com.google.common.annotations.VisibleForTesting
67
import org.koin.core.component.KoinComponent
78
import org.koin.core.component.inject
89
import java.util.concurrent.ConcurrentMap
910

1011
class RuleHasher : KoinComponent {
1112
private val logger: Logger by inject()
1213
private val sourceFileHasher: SourceFileHasher by inject()
14+
@VisibleForTesting
15+
class CircularDependencyException(message: String) : Exception(message)
16+
17+
18+
private fun raiseCircularDependency(depPath: LinkedHashSet<String>, begin: String): CircularDependencyException {
19+
val sb = StringBuilder().appendLine("Circular dependency detected: ").append(begin).append(" -> ")
20+
val circularPath = depPath.toList().takeLastWhile { it != begin }
21+
circularPath.forEach { sb.append(it).append(" -> ") }
22+
sb.append(begin)
23+
return CircularDependencyException(sb.toString())
24+
}
1325

1426
fun digest(
1527
rule: BazelRule,
1628
allRulesMap: Map<String, BazelRule>,
1729
ruleHashes: ConcurrentMap<String, ByteArray>,
1830
sourceDigests: ConcurrentMap<String, ByteArray>,
19-
seedHash: ByteArray?
31+
seedHash: ByteArray?,
32+
depPath: LinkedHashSet<String>?
2033
): ByteArray {
34+
val depPathClone = if (depPath != null) LinkedHashSet(depPath) else LinkedHashSet()
35+
if (depPathClone.contains(rule.name)) {
36+
throw raiseCircularDependency(depPathClone, rule.name)
37+
}
38+
depPathClone.add(rule.name)
2139
ruleHashes[rule.name]?.let { return it }
2240

2341
val finalHashValue = sha256 {
@@ -38,7 +56,8 @@ class RuleHasher : KoinComponent {
3856
allRulesMap,
3957
ruleHashes,
4058
sourceDigests,
41-
seedHash
59+
seedHash,
60+
depPathClone,
4261
)
4362
safePutBytes(ruleInputHash)
4463
}

cli/src/main/kotlin/com/bazel_diff/hash/TargetHasher.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ class TargetHasher : KoinComponent {
2929
allRulesMap,
3030
ruleHashes,
3131
sourceDigests,
32-
seedHash
32+
seedHash,
33+
depPath = null
3334
)
3435
}
3536
}
3637
is BazelTarget.Rule -> {
37-
ruleHasher.digest(target.rule, allRulesMap, ruleHashes, sourceDigests, seedHash)
38+
ruleHasher.digest(target.rule, allRulesMap, ruleHashes, sourceDigests, seedHash, depPath = null)
3839
}
3940
is BazelTarget.SourceFile -> sha256 {
4041
safePutBytes(sourceDigests[target.sourceFileName])

cli/src/test/kotlin/com/bazel_diff/hash/BuildGraphHasherTest.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.bazel_diff.hash
22

3+
import assertk.all
4+
import assertk.assertAll
35
import assertk.assertThat
46
import assertk.assertions.*
7+
import assertk.assertions.any
58
import com.bazel_diff.bazel.BazelClient
69
import com.bazel_diff.bazel.BazelRule
710
import com.bazel_diff.bazel.BazelTarget
@@ -18,6 +21,7 @@ import org.koin.test.mock.MockProviderRule
1821
import org.koin.test.mock.declareMock
1922
import org.mockito.Mockito
2023
import org.mockito.junit.MockitoJUnit
24+
import org.mockito.kotlin.any
2125
import org.mockito.kotlin.mock
2226
import org.mockito.kotlin.whenever
2327
import java.util.*
@@ -129,6 +133,26 @@ class BuildGraphHasherTest : KoinTest {
129133
)
130134
}
131135

136+
@Test
137+
fun testCircularDependency() = runBlocking {
138+
val rule3 = createRuleTarget("rule3", listOf("rule2", "rule4"), "digest3")
139+
val rule4 = createRuleTarget("rule4", listOf("rule1", "rule3"), "digest4")
140+
defaultTargets.add(rule3)
141+
defaultTargets.add(rule4)
142+
whenever(bazelClientMock.queryAllTargets()).thenReturn(defaultTargets)
143+
whenever(bazelClientMock.queryAllSourcefileTargets()).thenReturn(emptyList())
144+
assertThat {
145+
hasher.hashAllBazelTargetsAndSourcefiles()
146+
}.isFailure().all {
147+
isInstanceOf(RuleHasher.CircularDependencyException::class)
148+
// they are run in parallel, so we don't know whether rule3 or rule4 will be processed first
149+
message().matchesPredicate {
150+
it!!.contains("\\brule3 -> rule4 -> rule3\\b".toRegex()) ||
151+
it.contains("\\brule4 -> rule3 -> rule4\\b".toRegex())
152+
}
153+
}
154+
}
155+
132156
@Test
133157
fun testGeneratedTargets() = runBlocking {
134158
val generator = createRuleTarget("rule1", emptyList(), "rule1Digest")

0 commit comments

Comments
 (0)