Skip to content

Instantly share code, notes, and snippets.

@ghale
Last active May 20, 2021 00:00
Show Gist options
  • Save ghale/227ec1dcb98aea35df4e146957e36527 to your computer and use it in GitHub Desktop.
Save ghale/227ec1dcb98aea35df4e146957e36527 to your computer and use it in GitHub Desktop.
Detecting overlapping outputs
task checkForOverlappingOutputs {
doLast {
def rootNode = new TreeNode()
rootNode.name = ''
// Gather the outputs of all tasks
allprojects {
tasks.all { task ->
try {
task.outputs.files.each { file ->
def segments = file.absolutePath.split("/").drop(1)
rootNode.addChildren(segments).tasks.add(task.path)
}
} catch(Exception e) {
println "Failed to resolve task outputs of ${task.path}!"
}
}
}
// Walk the tree of outputs and look for overlaps
def overlaps = rootNode.getOverlaps()
// Remove any overlaps where the only overlaps are on the same task (i.e. the task has
// multiple outputs that overlap on each other)
def iterator = overlaps.iterator()
while (iterator.hasNext()) {
def overlap = iterator.next()
if (overlap.nodes.collect { it.tasks }.flatten().unique().size() < 2) {
iterator.remove()
}
}
def error = ""
overlaps.each { overlap ->
error += "\nOverlaps found for ${overlap.root}:\n"
overlap.nodes.each { node ->
error += "\t${node.toPath()} is an output of ${node.tasks.join(", ")}\n"
}
}
// Fail the task if we find any overlaps
if (overlaps.size() > 0) {
throw new RuntimeException("Found overlapping outputs:\n${error}")
}
}
}
class TreeNode {
String name
List<String> tasks = []
TreeNode parent
Map<String, TreeNode> children = [:]
TreeNode addChildren(String[] segments) {
assert segments?.size() > 0
def childName = segments[0]
def child
if (children[childName] != null) {
child = children[childName]
} else {
child = new TreeNode()
child.name = childName
child.parent = this
children[child.name] = child
}
if (segments.size() > 1) {
def grandChildren = segments.drop(1)
return child.addChildren(grandChildren)
} else {
return child
}
}
List<Overlap> getOverlaps() {
// If there are no children, we are at a leaf node, so it's only a question of whether there
// are overlaps on this node.
if (children.size() == 0) {
if (tasks.size() > 1) {
return [new Overlap(toPath(), [this])]
} else {
// this is a leaf node and it has no overlaps
return []
}
} else {
// Otherwise, if this node has children, but no tasks, it is not the root of any overlap
if (tasks.isEmpty()) {
// Look for overlaps in children
return children.values().collect { it.getOverlaps() }.flatten()
} else {
// If it does have tasks and children, it is the root of an overlap
def overlap = new Overlap(toPath())
overlap.nodes.addAll(getChildNodesWithTasks())
return [overlap]
}
}
}
List<TreeNode> getChildNodesWithTasks() {
def nodes = []
if (! tasks.empty) {
nodes << this
}
if (children.size() > 0) {
nodes.addAll(children.values().collect { it.getChildNodesWithTasks() }.flatten())
}
return nodes
}
String toPath() {
if (parent) {
return "${parent.toPath()}/${name}"
} else {
return name
}
}
}
class Overlap {
String root
List<TreeNode> nodes
Overlap(String root, List<TreeNode> nodes = []) {
this.root = root
this.nodes = nodes
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment