Add deterministic dependency scheduler
This commit is contained in:
200
internal/engine/dependency_order.go
Normal file
200
internal/engine/dependency_order.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// dependencyKey is an immutable tuple used to order dependency-graph nodes.
|
||||
// Callers provide unique keys and valid edge endpoints.
|
||||
type dependencyKey []string
|
||||
|
||||
// dependencyEdge points from a prerequisite to the node that depends on it.
|
||||
type dependencyEdge struct {
|
||||
prerequisite int
|
||||
dependent int
|
||||
}
|
||||
|
||||
type dependencyReadyHeap struct {
|
||||
nodes []int
|
||||
keys []dependencyKey
|
||||
}
|
||||
|
||||
func (h dependencyReadyHeap) Len() int { return len(h.nodes) }
|
||||
|
||||
func (h dependencyReadyHeap) Less(i, j int) bool {
|
||||
return dependencyKeyLess(h.keys[h.nodes[i]], h.keys[h.nodes[j]])
|
||||
}
|
||||
|
||||
func (h dependencyReadyHeap) Swap(i, j int) {
|
||||
h.nodes[i], h.nodes[j] = h.nodes[j], h.nodes[i]
|
||||
}
|
||||
|
||||
func (h *dependencyReadyHeap) Push(value any) {
|
||||
h.nodes = append(h.nodes, value.(int))
|
||||
}
|
||||
|
||||
func (h *dependencyReadyHeap) Pop() any {
|
||||
last := len(h.nodes) - 1
|
||||
node := h.nodes[last]
|
||||
h.nodes = h.nodes[:last]
|
||||
return node
|
||||
}
|
||||
|
||||
// dependencyKeyLess compares tuple components bytewise, with a shorter tuple
|
||||
// sorting first when all of its components match the corresponding prefix.
|
||||
func dependencyKeyLess(left, right dependencyKey) bool {
|
||||
for i := 0; i < len(left) && i < len(right); i++ {
|
||||
if left[i] != right[i] {
|
||||
return left[i] < right[i]
|
||||
}
|
||||
}
|
||||
return len(left) < len(right)
|
||||
}
|
||||
|
||||
// scheduleDependencies applies Kahn's algorithm, always selecting the
|
||||
// smallest currently-ready key. Duplicate edges count only once. If scheduling
|
||||
// stalls, an explicit depth-first traversal of the residual graph returns an
|
||||
// actual directed cycle, rotated to its smallest key and closed by repeating
|
||||
// its first node; blocked non-cycle dependents are excluded.
|
||||
//
|
||||
// Nodes are visited by precomputed key rank, which also makes each adjacency
|
||||
// list deterministic without sorting edges. The work is O(E + V log V), with
|
||||
// O(V + E) additional memory, apart from key copying and tuple comparisons.
|
||||
func scheduleDependencies(keys []dependencyKey, edges []dependencyEdge) (order, cycle []int) {
|
||||
ownedKeys := make([]dependencyKey, len(keys))
|
||||
for i, key := range keys {
|
||||
ownedKeys[i] = append(dependencyKey(nil), key...)
|
||||
}
|
||||
|
||||
nodeOrder := make([]int, len(ownedKeys))
|
||||
for i := range nodeOrder {
|
||||
nodeOrder[i] = i
|
||||
}
|
||||
sort.Slice(nodeOrder, func(i, j int) bool {
|
||||
return dependencyKeyLess(ownedKeys[nodeOrder[i]], ownedKeys[nodeOrder[j]])
|
||||
})
|
||||
|
||||
incoming := make([][]int, len(ownedKeys))
|
||||
for _, edge := range edges {
|
||||
incoming[edge.dependent] = append(incoming[edge.dependent], edge.prerequisite)
|
||||
}
|
||||
|
||||
outgoing := make([][]int, len(ownedKeys))
|
||||
indegree := make([]int, len(ownedKeys))
|
||||
for _, dependent := range nodeOrder {
|
||||
seen := make(map[int]struct{}, len(incoming[dependent]))
|
||||
for _, prerequisite := range incoming[dependent] {
|
||||
if _, duplicate := seen[prerequisite]; duplicate {
|
||||
continue
|
||||
}
|
||||
seen[prerequisite] = struct{}{}
|
||||
indegree[dependent]++
|
||||
// Dependents are visited by key rank, so each outgoing list is
|
||||
// already ordered by key.
|
||||
outgoing[prerequisite] = append(outgoing[prerequisite], dependent)
|
||||
}
|
||||
}
|
||||
|
||||
ready := dependencyReadyHeap{keys: ownedKeys}
|
||||
for _, node := range nodeOrder {
|
||||
if indegree[node] == 0 {
|
||||
ready.nodes = append(ready.nodes, node)
|
||||
}
|
||||
}
|
||||
heap.Init(&ready)
|
||||
|
||||
order = make([]int, 0, len(ownedKeys))
|
||||
for ready.Len() != 0 {
|
||||
node := heap.Pop(&ready).(int)
|
||||
order = append(order, node)
|
||||
for _, dependent := range outgoing[node] {
|
||||
indegree[dependent]--
|
||||
if indegree[dependent] == 0 {
|
||||
heap.Push(&ready, dependent)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(order) == len(ownedKeys) {
|
||||
return order, nil
|
||||
}
|
||||
return nil, dependencyCycle(ownedKeys, nodeOrder, outgoing, indegree)
|
||||
}
|
||||
|
||||
type dependencyDFSFrame struct {
|
||||
node int
|
||||
next int
|
||||
}
|
||||
|
||||
// dependencyCycle searches the residual graph iteratively to avoid recursion
|
||||
// depth depending on input size.
|
||||
func dependencyCycle(keys []dependencyKey, nodeOrder []int, outgoing [][]int, indegree []int) []int {
|
||||
const (
|
||||
unvisited = uint8(0)
|
||||
active = uint8(1)
|
||||
finished = uint8(2)
|
||||
)
|
||||
|
||||
state := make([]uint8, len(keys))
|
||||
position := make([]int, len(keys))
|
||||
for i := range position {
|
||||
position[i] = -1
|
||||
}
|
||||
path := make([]int, 0, len(keys))
|
||||
frames := make([]dependencyDFSFrame, 0, len(keys))
|
||||
|
||||
for _, root := range nodeOrder {
|
||||
if indegree[root] == 0 || state[root] != unvisited {
|
||||
continue
|
||||
}
|
||||
state[root] = active
|
||||
position[root] = len(path)
|
||||
path = append(path, root)
|
||||
frames = append(frames, dependencyDFSFrame{node: root})
|
||||
|
||||
for len(frames) != 0 {
|
||||
frame := &frames[len(frames)-1]
|
||||
if frame.next == len(outgoing[frame.node]) {
|
||||
state[frame.node] = finished
|
||||
position[frame.node] = -1
|
||||
path = path[:len(path)-1]
|
||||
frames = frames[:len(frames)-1]
|
||||
continue
|
||||
}
|
||||
|
||||
next := outgoing[frame.node][frame.next]
|
||||
frame.next++
|
||||
if indegree[next] == 0 {
|
||||
continue
|
||||
}
|
||||
switch state[next] {
|
||||
case unvisited:
|
||||
state[next] = active
|
||||
position[next] = len(path)
|
||||
path = append(path, next)
|
||||
frames = append(frames, dependencyDFSFrame{node: next})
|
||||
case active:
|
||||
return canonicalDependencyCycle(keys, path[position[next]:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A stalled Kahn traversal always leaves a cycle in the residual graph.
|
||||
return nil
|
||||
}
|
||||
|
||||
func canonicalDependencyCycle(keys []dependencyKey, path []int) []int {
|
||||
start := 0
|
||||
for i := 1; i < len(path); i++ {
|
||||
if dependencyKeyLess(keys[path[i]], keys[path[start]]) {
|
||||
start = i
|
||||
}
|
||||
}
|
||||
|
||||
cycle := make([]int, len(path)+1)
|
||||
for i := range path {
|
||||
cycle[i] = path[(start+i)%len(path)]
|
||||
}
|
||||
cycle[len(path)] = cycle[0]
|
||||
return cycle
|
||||
}
|
||||
Reference in New Issue
Block a user