-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgraph.go
121 lines (105 loc) · 2.79 KB
/
graph.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package main
import (
"fmt"
"strings"
)
// Node is a graph node. It's used to represent a dependency graph
type Node struct {
Job
// ID is a unique ID of the node within a graph
ID string
// Dependencies is a set of nodes that are required to resolve a node
Dependencies map[*Node]struct{}
// Dependents is a set of nodes that are waiting for a node to resolve
Dependents map[*Node]struct{}
}
// NewNode is a constructor for a node
func NewNode(j Job, id string) *Node {
return &Node{
Job: j,
ID: id,
Dependencies: make(map[*Node]struct{}),
Dependents: make(map[*Node]struct{}),
}
}
// detectCircularDependency traverses the whole graph and find a circular dependency.
// When a circular dependency, the function will return an error with a friendly message
// to show where the circular dependency occurred.
func detectCircularDependency(root *Node) error {
unresolved := make(map[*Node]struct{})
resolved := make(map[*Node]struct{})
var resolve func(*Node) []*Node
resolve = func(n *Node) []*Node {
if n == nil {
return nil
}
unresolved[n] = struct{}{}
for dependent := range n.Dependents {
if _, ok := resolved[dependent]; ok {
continue
}
// Detect a cycle
if _, ok := unresolved[dependent]; ok {
return []*Node{dependent, n}
}
cycle := resolve(dependent)
if len(cycle) > 0 {
return append(cycle, n)
}
}
delete(unresolved, n)
resolved[n] = struct{}{}
return nil
}
cycle := resolve(root)
if len(cycle) > 0 {
deps := make([]string, len(cycle)-1)
for i, c := range cycle[:len(cycle)-1] {
deps[i] = c.ID
}
depsStr := strings.Join(deps, "->")
return fmt.Errorf("detected a circular dependency: %s", depsStr)
}
return nil
}
// NewGraph constructs a dependency graph based on given config
func NewGraph(cfg Config) (*Node, error) {
nodes := make(map[string]*Node)
for id, job := range cfg.Jobs {
nodes[id] = NewNode(job, id)
}
if len(nodes) == 0 {
return nil, fmt.Errorf("there are no jobs")
}
for id, task := range cfg.Jobs {
node := nodes[id]
for _, depID := range task.Needs {
dep, ok := nodes[depID]
if !ok {
return nil, fmt.Errorf("failed to find %s dependency", depID)
}
node.Dependencies[dep] = struct{}{}
dep.Dependents[node] = struct{}{}
}
}
rootNode := NewNode(Job{}, "root")
for _, node := range nodes {
if len(node.Dependencies) == 0 {
rootNode.Dependents[node] = struct{}{}
}
}
// If this is true, it's definitely a cycle. But, we'll still attach one
// of the nodes so that we can still detect what dependencies that caused
// the cycle
if len(rootNode.Dependents) == 0 {
for _, node := range nodes {
rootNode.Dependents[node] = struct{}{}
break
}
}
err := detectCircularDependency(rootNode)
if err != nil {
return nil, err
}
return rootNode, nil
}