Merge pull request #1081 from hashicorp/f-trans-reduct
Perform Transitive Reduction on Graph
This commit is contained in:
commit
58cc129caa
72
dag/dag.go
72
dag/dag.go
|
@ -40,6 +40,44 @@ func (g *AcyclicGraph) Root() (Vertex, error) {
|
||||||
return roots[0], nil
|
return roots[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TransitiveReduction performs the transitive reduction of graph g in place.
|
||||||
|
// The transitive reduction of a graph is a graph with as few edges as
|
||||||
|
// possible with the same reachability as the original graph. This means
|
||||||
|
// that if there are three nodes A => B => C, and A connects to both
|
||||||
|
// B and C, and B connects to C, then the transitive reduction is the
|
||||||
|
// same graph with only a single edge between A and B, and a single edge
|
||||||
|
// between B and C.
|
||||||
|
//
|
||||||
|
// The graph must be valid for this operation to behave properly. If
|
||||||
|
// Validate() returns an error, the behavior is undefined and the results
|
||||||
|
// will likely be unexpected.
|
||||||
|
//
|
||||||
|
// Complexity: O(V(V+E)), or asymptotically O(VE)
|
||||||
|
func (g *AcyclicGraph) TransitiveReduction() {
|
||||||
|
// For each vertex u in graph g, do a DFS starting from each vertex
|
||||||
|
// v such that the edge (u,v) exists (v is a direct descendant of u).
|
||||||
|
//
|
||||||
|
// For each v-prime reachable from v, remove the edge (u, v-prime).
|
||||||
|
|
||||||
|
for _, u := range g.Vertices() {
|
||||||
|
uTargets := g.DownEdges(u)
|
||||||
|
vs := make([]Vertex, uTargets.Len())
|
||||||
|
for i, vRaw := range uTargets.List() {
|
||||||
|
vs[i] = vRaw.(Vertex)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.depthFirstWalk(vs, func(v Vertex) error {
|
||||||
|
shared := uTargets.Intersection(g.DownEdges(v))
|
||||||
|
for _, raw := range shared.List() {
|
||||||
|
vPrime := raw.(Vertex)
|
||||||
|
g.RemoveEdge(BasicEdge(u, vPrime))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate validates the DAG. A DAG is valid if it has a single root
|
// Validate validates the DAG. A DAG is valid if it has a single root
|
||||||
// with no cycles.
|
// with no cycles.
|
||||||
func (g *AcyclicGraph) Validate() error {
|
func (g *AcyclicGraph) Validate() error {
|
||||||
|
@ -161,3 +199,37 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error {
|
||||||
<-doneCh
|
<-doneCh
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// depthFirstWalk does a depth-first walk of the graph starting from
|
||||||
|
// the vertices in start. This is not exported now but it would make sense
|
||||||
|
// to export this publicly at some point.
|
||||||
|
func (g *AcyclicGraph) depthFirstWalk(start []Vertex, cb WalkFunc) error {
|
||||||
|
seen := make(map[Vertex]struct{})
|
||||||
|
frontier := make([]Vertex, len(start))
|
||||||
|
copy(frontier, start)
|
||||||
|
for len(frontier) > 0 {
|
||||||
|
// Pop the current vertex
|
||||||
|
n := len(frontier)
|
||||||
|
current := frontier[n-1]
|
||||||
|
frontier = frontier[:n-1]
|
||||||
|
|
||||||
|
// Check if we've seen this already and return...
|
||||||
|
if _, ok := seen[current]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[current] = struct{}{}
|
||||||
|
|
||||||
|
// Visit the current node
|
||||||
|
if err := cb(current); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit targets of this in reverse order.
|
||||||
|
targets := g.DownEdges(current).List()
|
||||||
|
for i := len(targets) - 1; i >= 0; i-- {
|
||||||
|
frontier = append(frontier, targets[i].(Vertex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package dag
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -48,6 +49,44 @@ func TestAcyclicGraphRoot_multiple(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAyclicGraphTransReduction(t *testing.T) {
|
||||||
|
var g AcyclicGraph
|
||||||
|
g.Add(1)
|
||||||
|
g.Add(2)
|
||||||
|
g.Add(3)
|
||||||
|
g.Connect(BasicEdge(1, 2))
|
||||||
|
g.Connect(BasicEdge(1, 3))
|
||||||
|
g.Connect(BasicEdge(2, 3))
|
||||||
|
g.TransitiveReduction()
|
||||||
|
|
||||||
|
actual := strings.TrimSpace(g.String())
|
||||||
|
expected := strings.TrimSpace(testGraphTransReductionStr)
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad: %s", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAyclicGraphTransReduction_more(t *testing.T) {
|
||||||
|
var g AcyclicGraph
|
||||||
|
g.Add(1)
|
||||||
|
g.Add(2)
|
||||||
|
g.Add(3)
|
||||||
|
g.Add(4)
|
||||||
|
g.Connect(BasicEdge(1, 2))
|
||||||
|
g.Connect(BasicEdge(1, 3))
|
||||||
|
g.Connect(BasicEdge(1, 4))
|
||||||
|
g.Connect(BasicEdge(2, 3))
|
||||||
|
g.Connect(BasicEdge(2, 4))
|
||||||
|
g.Connect(BasicEdge(3, 4))
|
||||||
|
g.TransitiveReduction()
|
||||||
|
|
||||||
|
actual := strings.TrimSpace(g.String())
|
||||||
|
expected := strings.TrimSpace(testGraphTransReductionMoreStr)
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad: %s", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAcyclicGraphValidate(t *testing.T) {
|
func TestAcyclicGraphValidate(t *testing.T) {
|
||||||
var g AcyclicGraph
|
var g AcyclicGraph
|
||||||
g.Add(1)
|
g.Add(1)
|
||||||
|
@ -156,3 +195,21 @@ func TestAcyclicGraphWalk_error(t *testing.T) {
|
||||||
|
|
||||||
t.Fatalf("bad: %#v", visits)
|
t.Fatalf("bad: %#v", visits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const testGraphTransReductionStr = `
|
||||||
|
1
|
||||||
|
2
|
||||||
|
2
|
||||||
|
3
|
||||||
|
3
|
||||||
|
`
|
||||||
|
|
||||||
|
const testGraphTransReductionMoreStr = `
|
||||||
|
1
|
||||||
|
2
|
||||||
|
2
|
||||||
|
3
|
||||||
|
3
|
||||||
|
4
|
||||||
|
4
|
||||||
|
`
|
||||||
|
|
14
dag/set.go
14
dag/set.go
|
@ -36,6 +36,20 @@ func (s *Set) Include(v interface{}) bool {
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intersection computes the set intersection with other.
|
||||||
|
func (s *Set) Intersection(other *Set) *Set {
|
||||||
|
result := new(Set)
|
||||||
|
if other != nil {
|
||||||
|
for _, v := range s.m {
|
||||||
|
if other.Include(v) {
|
||||||
|
result.Add(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// Len is the number of items in the set.
|
// Len is the number of items in the set.
|
||||||
func (s *Set) Len() int {
|
func (s *Set) Len() int {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
|
|
|
@ -109,5 +109,9 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer {
|
||||||
|
|
||||||
// Make sure we create one root
|
// Make sure we create one root
|
||||||
&RootTransformer{},
|
&RootTransformer{},
|
||||||
|
|
||||||
|
// Perform the transitive reduction to make our graph a bit
|
||||||
|
// more sane if possible (it usually is possible).
|
||||||
|
&TransitiveReductionTransformer{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,16 +125,12 @@ const testBasicGraphBuilderStr = `
|
||||||
const testBuiltinGraphBuilderBasicStr = `
|
const testBuiltinGraphBuilderBasicStr = `
|
||||||
aws_instance.db
|
aws_instance.db
|
||||||
aws_instance.db (destroy tainted)
|
aws_instance.db (destroy tainted)
|
||||||
provider.aws
|
|
||||||
aws_instance.db (destroy tainted)
|
aws_instance.db (destroy tainted)
|
||||||
aws_instance.web (destroy tainted)
|
aws_instance.web (destroy tainted)
|
||||||
provider.aws
|
|
||||||
aws_instance.web
|
aws_instance.web
|
||||||
aws_instance.db
|
aws_instance.db
|
||||||
aws_instance.web (destroy tainted)
|
aws_instance.web (destroy tainted)
|
||||||
provider.aws
|
provider.aws
|
||||||
aws_instance.web (destroy tainted)
|
|
||||||
provider.aws
|
|
||||||
provider.aws
|
provider.aws
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
resource "aws_instance" "A" {}
|
||||||
|
|
||||||
|
resource "aws_instance" "B" {
|
||||||
|
A = "${aws_instance.A.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "C" {
|
||||||
|
A = "${aws_instance.A.id}"
|
||||||
|
B = "${aws_instance.B.id}"
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
// TransitiveReductionTransformer is a GraphTransformer that performs
|
||||||
|
// finds the transitive reduction of the graph. For a definition of
|
||||||
|
// transitive reduction, see Wikipedia.
|
||||||
|
type TransitiveReductionTransformer struct{}
|
||||||
|
|
||||||
|
func (t *TransitiveReductionTransformer) Transform(g *Graph) error {
|
||||||
|
// If the graph isn't valid, skip the transitive reduction.
|
||||||
|
// We don't error here because Terraform itself handles graph
|
||||||
|
// validation in a better way, or we assume it does.
|
||||||
|
if err := g.Validate(); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do it
|
||||||
|
g.TransitiveReduction()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransitiveReductionTransformer(t *testing.T) {
|
||||||
|
mod := testModule(t, "transform-trans-reduce-basic")
|
||||||
|
|
||||||
|
g := Graph{Path: RootModulePath}
|
||||||
|
{
|
||||||
|
tf := &ConfigTransformer{Module: mod}
|
||||||
|
if err := tf.Transform(&g); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
transform := &TransitiveReductionTransformer{}
|
||||||
|
if err := transform.Transform(&g); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := strings.TrimSpace(g.String())
|
||||||
|
expected := strings.TrimSpace(testTransformTransReduceBasicStr)
|
||||||
|
if actual != expected {
|
||||||
|
t.Fatalf("bad:\n\n%s", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testTransformTransReduceBasicStr = `
|
||||||
|
aws_instance.A
|
||||||
|
aws_instance.B
|
||||||
|
aws_instance.A
|
||||||
|
aws_instance.C
|
||||||
|
aws_instance.B
|
||||||
|
`
|
Loading…
Reference in New Issue