Merge pull request #2138 from sbt/wip/cached-resolution-circular2

[0.13.9-RC3] Fixes #2129. break up circular dependency loops in cached resolution
This commit is contained in:
Josh Suereth 2015-08-03 12:22:36 -04:00
commit 856a30b43d
2 changed files with 80 additions and 3 deletions

View File

@ -405,6 +405,80 @@ private[sbt] trait CachedResolutionResolveEngine extends ResolveEngine {
val xs = reports1 filter { oar => oar.organization == organization && oar.name == name }
((organization, name), xs)
}: _*)
// this returns a List of Lists of (org, name). should be deterministic
def detectLoops(allModules: Map[(String, String), Vector[OrganizationArtifactReport]]): List[List[(String, String)]] =
{
val loopSets: mutable.Set[Set[(String, String)]] = mutable.Set.empty
val loopLists: mutable.ListBuffer[List[(String, String)]] = mutable.ListBuffer.empty
def testLoop(m: (String, String), current: (String, String), history: List[(String, String)]): Unit =
{
val callers =
(for {
oar <- allModules.getOrElse(current, Vector())
mr <- oar.modules
c <- mr.callers
} yield (c.caller.organization, c.caller.name)).distinct
callers foreach { c =>
if (history contains c) {
val loop = (c :: history.takeWhile(_ != c)) ::: List(c)
if (!loopSets(loop.toSet)) {
loopSets += loop.toSet
loopLists += loop
val loopStr = (loop map { case (o, n) => s"$o:$n" }).mkString("->")
log.warn(s"""avoid circular dependency while using cached resolution: $loopStr""")
}
} else testLoop(m, c, c :: history)
}
}
orgNamePairs map { orgname =>
testLoop(orgname, orgname, List(orgname))
}
loopLists.toList
}
@tailrec def breakLoops(loops: List[List[(String, String)]],
allModules1: Map[(String, String), Vector[OrganizationArtifactReport]]): Map[(String, String), Vector[OrganizationArtifactReport]] =
loops match {
case Nil =>
allModules1
case loop :: rest =>
loop match {
case Nil =>
breakLoops(rest, allModules1)
case loop =>
val sortedLoop = loop sortBy { x =>
(for {
oar <- allModules0(x)
mr <- oar.modules
c <- mr.callers
} yield c).size
}
val moduleWithMostCallers = sortedLoop.reverse.head
val next: (String, String) = loop(loop.indexOf(moduleWithMostCallers) + 1)
// remove the module with most callers as the caller of next.
// so, A -> C, B -> C, and C -> A. C has the most callers, and C -> A will be removed.
val nextMap = allModules1 map {
case (k: (String, String), oars0) if k == next =>
val oars: Vector[OrganizationArtifactReport] = oars0 map { oar =>
val mrs = oar.modules map { mr =>
val callers0 = mr.callers
val callers = callers0 filterNot { c => (c.caller.organization, c.caller.name) == moduleWithMostCallers }
if (callers.size == callers0.size) mr
else {
log.debug(s":: $rootModuleConf: removing caller $moduleWithMostCallers -> $next for sorting")
mr.copy(callers = callers)
}
}
OrganizationArtifactReport(oar.organization, oar.name, mrs)
}
(k, oars)
case (k, v) => (k, v)
}
breakLoops(rest, nextMap)
}
}
val loop = detectLoops(allModules0)
log.debug(s":: $rootModuleConf: loop: $loop")
val allModules2 = breakLoops(loop, allModules0)
// sort the all modules such that less called modules comes earlier
@tailrec def sortModules(cs: Vector[(String, String)],
acc: Vector[(String, String)], extra: Vector[(String, String)],
@ -413,7 +487,7 @@ private[sbt] trait CachedResolutionResolveEngine extends ResolveEngine {
// println(s"sortModules: $n / $guard")
val keys = cs.toSet
val (called, notCalled) = cs partition { k =>
val reports = allModules0(k)
val reports = allModules2(k)
reports exists {
_.modules.exists {
_.callers exists { caller =>
@ -425,7 +499,7 @@ private[sbt] trait CachedResolutionResolveEngine extends ResolveEngine {
}
lazy val result0 = acc ++ notCalled ++ called ++ extra
def warnCircular(): Unit = {
log.warn(s"""avoid circular dependency while using cached resolution: ${cs.mkString(",")}""")
log.warn(s"""unexpected circular dependency while using cached resolution: ${cs.mkString(",")}""")
}
(if (n > guard) {
warnCircular
@ -463,6 +537,8 @@ private[sbt] trait CachedResolutionResolveEngine extends ResolveEngine {
}
val guard0 = (orgNamePairs.size * orgNamePairs.size) + 1
val sorted: Vector[(String, String)] = sortModules(orgNamePairs, Vector(), Vector(), 0, guard0)
val sortedStr = (sorted map { case (o, n) => s"$o:$n" }).mkString(", ")
log.debug(s":: sort result: $sortedStr")
val result = resolveConflicts(sorted.toList, allModules0)
result.toVector
}

View File

@ -50,6 +50,7 @@
[2009]: https://github.com/sbt/sbt/pull/2009
[2046]: https://github.com/sbt/sbt/issues/2046
[2097]: https://github.com/sbt/sbt/pull/2097
[2129]: https://github.com/sbt/sbt/pull/2129
### Fixes with compatibility implications
@ -130,7 +131,7 @@ Under some circumstances, libraries that shouldn't have been evicted was being e
This occured when library `A1` depended on `B2`, but a newer `A2` dropped the dependency,
and `A2` and `B1` are also is in the graph. This is fixed by sorting the graph prior to eviction.
[#2030][2030]/[#1721][1721]/[#2014][2014]/[#2046][2046]/[#2097][2097] by [@eed3si9n][@eed3si9n]
[#2030][2030]/[#1721][1721]/[#2014][2014]/[#2046][2046]/[#2097][2097]/[#2129][2129] by [@eed3si9n][@eed3si9n]
### Force GC