Add methods for topological sorts

A topological walk was previously only done in Terraform via the
concurrent method used for walking the primary dependency graph in core.
Sometime however we want a dependency ordering without the overhead of
instantiating the concurrent walk with the channel-based edges.

Add TopologicalOrder and ReverseTopologicalOrder to obtain a list of
nodes which can be used to visit each while ensuring that all
dependencies are satisfied.
This commit is contained in:
James Bardin 2022-07-22 12:04:50 -04:00
parent 95019e3d02
commit ca272b2107
2 changed files with 99 additions and 1 deletions

View File

@ -179,6 +179,69 @@ type vertexAtDepth struct {
Depth int
}
// TopologicalOrder returns a topological sort of the given graph. The nodes
// are not sorted, and any valid order may be returned. This function will
// panic if it encounters a cycle.
func (g *AcyclicGraph) TopologicalOrder() []Vertex {
return g.topoOrder(upOrder)
}
// ReverseTopologicalOrder returns a topological sort of the given graph,
// following each edge in reverse. The nodes are not sorted, and any valid
// order may be returned. This function will panic if it encounters a cycle.
func (g *AcyclicGraph) ReverseTopologicalOrder() []Vertex {
return g.topoOrder(downOrder)
}
func (g *AcyclicGraph) topoOrder(order walkType) []Vertex {
// Use a dfs-based sorting algorithm, similar to that used in
// TransitiveReduction.
sorted := make([]Vertex, 0, len(g.vertices))
// tmp track the current working node to check for cycles
tmp := map[Vertex]bool{}
// perm tracks completed nodes to end the recursion
perm := map[Vertex]bool{}
var visit func(v Vertex)
visit = func(v Vertex) {
if perm[v] {
return
}
if tmp[v] {
panic("cycle found in dag")
}
tmp[v] = true
var next Set
switch {
case order&downOrder != 0:
next = g.downEdgesNoCopy(v)
case order&upOrder != 0:
next = g.upEdgesNoCopy(v)
default:
panic(fmt.Sprintln("invalid order", order))
}
for _, u := range next {
visit(u)
}
tmp[v] = false
perm[v] = true
sorted = append(sorted, v)
}
for _, v := range g.Vertices() {
visit(v)
}
return sorted
}
type walkType uint64
const (

View File

@ -429,7 +429,7 @@ func TestAcyclicGraphWalkOrder(t *testing.T) {
*/
var g AcyclicGraph
for i := 0; i <= 11; i++ {
for i := 1; i <= 11; i++ {
g.Add(i)
}
g.Connect(BasicEdge(1, 3))
@ -509,6 +509,41 @@ func TestAcyclicGraphWalkOrder(t *testing.T) {
t.Errorf("expected visits:\n%v\ngot:\n%v\n", expect, visits)
}
})
t.Run("TopologicalOrder", func(t *testing.T) {
order := g.topoOrder(downOrder)
// Validate the order by checking it against the initial graph. We only
// need to verify that each node has it's direct dependencies
// satisfied.
completed := map[Vertex]bool{}
for _, v := range order {
deps := g.DownEdges(v)
for _, dep := range deps {
if !completed[dep] {
t.Fatalf("walking node %v, but dependency %v was not yet seen", v, dep)
}
}
completed[v] = true
}
})
t.Run("ReverseTopologicalOrder", func(t *testing.T) {
order := g.topoOrder(upOrder)
// Validate the order by checking it against the initial graph. We only
// need to verify that each node has it's direct dependencies
// satisfied.
completed := map[Vertex]bool{}
for _, v := range order {
deps := g.UpEdges(v)
for _, dep := range deps {
if !completed[dep] {
t.Fatalf("walking node %v, but dependency %v was not yet seen", v, dep)
}
}
completed[v] = true
}
})
}
const testGraphTransReductionStr = `