405 lines
9.1 KiB
Go
405 lines
9.1 KiB
Go
package graph
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"sync"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Task represents a node in the dependency graph
|
||
|
|
type Task struct {
|
||
|
|
ID string
|
||
|
|
Title string
|
||
|
|
Status string // pending, ready, running, completed, failed
|
||
|
|
DependsOn []string
|
||
|
|
Metadata map[string]interface{}
|
||
|
|
}
|
||
|
|
|
||
|
|
// DependencyGraph manages task dependencies
|
||
|
|
type DependencyGraph struct {
|
||
|
|
mu sync.RWMutex
|
||
|
|
tasks map[string]*Task
|
||
|
|
adjacencyList map[string][]string // task -> dependent tasks
|
||
|
|
reverseList map[string][]string // task -> dependencies
|
||
|
|
topologicalOrder []string
|
||
|
|
cycleDetected bool
|
||
|
|
status map[string]string // task -> status
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewDependencyGraph creates a new dependency graph
|
||
|
|
func NewDependencyGraph() *DependencyGraph {
|
||
|
|
return &DependencyGraph{
|
||
|
|
tasks: make(map[string]*Task),
|
||
|
|
adjacencyList: make(map[string][]string),
|
||
|
|
reverseList: make(map[string][]string),
|
||
|
|
topologicalOrder: make([]string, 0),
|
||
|
|
status: make(map[string]string),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// AddTask adds a task to the graph
|
||
|
|
func (dg *DependencyGraph) AddTask(task *Task) error {
|
||
|
|
if task == nil || task.ID == "" {
|
||
|
|
return fmt.Errorf("task cannot be nil and must have an ID")
|
||
|
|
}
|
||
|
|
|
||
|
|
dg.mu.Lock()
|
||
|
|
defer dg.mu.Unlock()
|
||
|
|
|
||
|
|
if _, exists := dg.tasks[task.ID]; exists {
|
||
|
|
return fmt.Errorf("task already exists: %s", task.ID)
|
||
|
|
}
|
||
|
|
|
||
|
|
dg.tasks[task.ID] = task
|
||
|
|
dg.status[task.ID] = "pending"
|
||
|
|
|
||
|
|
// Initialize adjacency lists
|
||
|
|
if _, exists := dg.adjacencyList[task.ID]; !exists {
|
||
|
|
dg.adjacencyList[task.ID] = make([]string, 0)
|
||
|
|
}
|
||
|
|
if _, exists := dg.reverseList[task.ID]; !exists {
|
||
|
|
dg.reverseList[task.ID] = make([]string, 0)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// AddDependency adds a dependency: dependent depends on prerequisite
|
||
|
|
func (dg *DependencyGraph) AddDependency(dependent, prerequisite string) error {
|
||
|
|
dg.mu.Lock()
|
||
|
|
defer dg.mu.Unlock()
|
||
|
|
|
||
|
|
if _, exists := dg.tasks[dependent]; !exists {
|
||
|
|
return fmt.Errorf("dependent task not found: %s", dependent)
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, exists := dg.tasks[prerequisite]; !exists {
|
||
|
|
return fmt.Errorf("prerequisite task not found: %s", prerequisite)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for duplicate
|
||
|
|
for _, dep := range dg.reverseList[dependent] {
|
||
|
|
if dep == prerequisite {
|
||
|
|
return fmt.Errorf("dependency already exists: %s -> %s", dependent, prerequisite)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
dg.reverseList[dependent] = append(dg.reverseList[dependent], prerequisite)
|
||
|
|
dg.adjacencyList[prerequisite] = append(dg.adjacencyList[prerequisite], dependent)
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// ValidateGraph checks for cycles and structural integrity
|
||
|
|
func (dg *DependencyGraph) ValidateGraph() error {
|
||
|
|
dg.mu.Lock()
|
||
|
|
defer dg.mu.Unlock()
|
||
|
|
|
||
|
|
// Check for cycles using DFS
|
||
|
|
visited := make(map[string]bool)
|
||
|
|
recStack := make(map[string]bool)
|
||
|
|
|
||
|
|
for taskID := range dg.tasks {
|
||
|
|
if !visited[taskID] {
|
||
|
|
if dg.hasCycleLocked(taskID, visited, recStack) {
|
||
|
|
dg.cycleDetected = true
|
||
|
|
return fmt.Errorf("cycle detected in dependency graph")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// hasCycleLocked detects cycles using DFS (must be called with lock held)
|
||
|
|
func (dg *DependencyGraph) hasCycleLocked(node string, visited, recStack map[string]bool) bool {
|
||
|
|
visited[node] = true
|
||
|
|
recStack[node] = true
|
||
|
|
|
||
|
|
for _, dep := range dg.reverseList[node] {
|
||
|
|
if !visited[dep] {
|
||
|
|
if dg.hasCycleLocked(dep, visited, recStack) {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
} else if recStack[dep] {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
recStack[node] = false
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetTopologicalOrder returns tasks in execution order
|
||
|
|
func (dg *DependencyGraph) GetTopologicalOrder() ([]string, error) {
|
||
|
|
dg.mu.Lock()
|
||
|
|
defer dg.mu.Unlock()
|
||
|
|
|
||
|
|
if dg.cycleDetected {
|
||
|
|
return nil, fmt.Errorf("graph contains cycles")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Kahn's algorithm
|
||
|
|
inDegree := make(map[string]int)
|
||
|
|
for taskID := range dg.tasks {
|
||
|
|
inDegree[taskID] = len(dg.reverseList[taskID])
|
||
|
|
}
|
||
|
|
|
||
|
|
queue := make([]string, 0)
|
||
|
|
for taskID, degree := range inDegree {
|
||
|
|
if degree == 0 {
|
||
|
|
queue = append(queue, taskID)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
topOrder := make([]string, 0)
|
||
|
|
for len(queue) > 0 {
|
||
|
|
current := queue[0]
|
||
|
|
queue = queue[1:]
|
||
|
|
topOrder = append(topOrder, current)
|
||
|
|
|
||
|
|
for _, dependent := range dg.adjacencyList[current] {
|
||
|
|
inDegree[dependent]--
|
||
|
|
if inDegree[dependent] == 0 {
|
||
|
|
queue = append(queue, dependent)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(topOrder) != len(dg.tasks) {
|
||
|
|
return nil, fmt.Errorf("topological sort failed - graph may have cycles")
|
||
|
|
}
|
||
|
|
|
||
|
|
dg.topologicalOrder = topOrder
|
||
|
|
return topOrder, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetReadyTasks returns tasks that have no remaining dependencies
|
||
|
|
func (dg *DependencyGraph) GetReadyTasks() []string {
|
||
|
|
dg.mu.RLock()
|
||
|
|
defer dg.mu.RUnlock()
|
||
|
|
|
||
|
|
ready := make([]string, 0)
|
||
|
|
|
||
|
|
for taskID, deps := range dg.reverseList {
|
||
|
|
allDepsComplete := true
|
||
|
|
for _, dep := range deps {
|
||
|
|
if dg.status[dep] != "completed" {
|
||
|
|
allDepsComplete = false
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if allDepsComplete && dg.status[taskID] == "pending" {
|
||
|
|
ready = append(ready, taskID)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return ready
|
||
|
|
}
|
||
|
|
|
||
|
|
// MarkCompleted marks a task as completed and updates dependents
|
||
|
|
func (dg *DependencyGraph) MarkCompleted(taskID string) error {
|
||
|
|
dg.mu.Lock()
|
||
|
|
defer dg.mu.Unlock()
|
||
|
|
|
||
|
|
if _, exists := dg.tasks[taskID]; !exists {
|
||
|
|
return fmt.Errorf("task not found: %s", taskID)
|
||
|
|
}
|
||
|
|
|
||
|
|
dg.status[taskID] = "completed"
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// MarkFailed marks a task as failed
|
||
|
|
func (dg *DependencyGraph) MarkFailed(taskID string) error {
|
||
|
|
dg.mu.Lock()
|
||
|
|
defer dg.mu.Unlock()
|
||
|
|
|
||
|
|
if _, exists := dg.tasks[taskID]; !exists {
|
||
|
|
return fmt.Errorf("task not found: %s", taskID)
|
||
|
|
}
|
||
|
|
|
||
|
|
dg.status[taskID] = "failed"
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetTaskStatus returns the status of a task
|
||
|
|
func (dg *DependencyGraph) GetTaskStatus(taskID string) (string, error) {
|
||
|
|
dg.mu.RLock()
|
||
|
|
defer dg.mu.RUnlock()
|
||
|
|
|
||
|
|
status, exists := dg.status[taskID]
|
||
|
|
if !exists {
|
||
|
|
return "", fmt.Errorf("task not found: %s", taskID)
|
||
|
|
}
|
||
|
|
|
||
|
|
return status, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetDependencies returns all dependencies of a task
|
||
|
|
func (dg *DependencyGraph) GetDependencies(taskID string) ([]string, error) {
|
||
|
|
dg.mu.RLock()
|
||
|
|
defer dg.mu.RUnlock()
|
||
|
|
|
||
|
|
deps, exists := dg.reverseList[taskID]
|
||
|
|
if !exists {
|
||
|
|
return nil, fmt.Errorf("task not found: %s", taskID)
|
||
|
|
}
|
||
|
|
|
||
|
|
result := make([]string, len(deps))
|
||
|
|
copy(result, deps)
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetDependents returns all tasks that depend on this task
|
||
|
|
func (dg *DependencyGraph) GetDependents(taskID string) ([]string, error) {
|
||
|
|
dg.mu.RLock()
|
||
|
|
defer dg.mu.RUnlock()
|
||
|
|
|
||
|
|
deps, exists := dg.adjacencyList[taskID]
|
||
|
|
if !exists {
|
||
|
|
return nil, fmt.Errorf("task not found: %s", taskID)
|
||
|
|
}
|
||
|
|
|
||
|
|
result := make([]string, len(deps))
|
||
|
|
copy(result, deps)
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetTask returns a task by ID
|
||
|
|
func (dg *DependencyGraph) GetTask(taskID string) (*Task, bool) {
|
||
|
|
dg.mu.RLock()
|
||
|
|
defer dg.mu.RUnlock()
|
||
|
|
|
||
|
|
task, exists := dg.tasks[taskID]
|
||
|
|
return task, exists
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetAllTasks returns all tasks
|
||
|
|
func (dg *DependencyGraph) GetAllTasks() map[string]*Task {
|
||
|
|
dg.mu.RLock()
|
||
|
|
defer dg.mu.RUnlock()
|
||
|
|
|
||
|
|
result := make(map[string]*Task)
|
||
|
|
for id, task := range dg.tasks {
|
||
|
|
result[id] = task
|
||
|
|
}
|
||
|
|
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetGraphStats returns statistics about the graph
|
||
|
|
func (dg *DependencyGraph) GetGraphStats() map[string]interface{} {
|
||
|
|
dg.mu.RLock()
|
||
|
|
defer dg.mu.RUnlock()
|
||
|
|
|
||
|
|
pending := 0
|
||
|
|
completed := 0
|
||
|
|
failed := 0
|
||
|
|
|
||
|
|
for _, status := range dg.status {
|
||
|
|
switch status {
|
||
|
|
case "pending":
|
||
|
|
pending++
|
||
|
|
case "completed":
|
||
|
|
completed++
|
||
|
|
case "failed":
|
||
|
|
failed++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return map[string]interface{}{
|
||
|
|
"total_tasks": len(dg.tasks),
|
||
|
|
"pending_tasks": pending,
|
||
|
|
"completed_tasks": completed,
|
||
|
|
"failed_tasks": failed,
|
||
|
|
"cycle_detected": dg.cycleDetected,
|
||
|
|
"total_edges": dg.countEdgesLocked(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// countEdgesLocked counts total dependencies (must be called with lock held)
|
||
|
|
func (dg *DependencyGraph) countEdgesLocked() int {
|
||
|
|
count := 0
|
||
|
|
for _, deps := range dg.reverseList {
|
||
|
|
count += len(deps)
|
||
|
|
}
|
||
|
|
return count
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear clears all tasks and dependencies
|
||
|
|
func (dg *DependencyGraph) Clear() {
|
||
|
|
dg.mu.Lock()
|
||
|
|
defer dg.mu.Unlock()
|
||
|
|
|
||
|
|
dg.tasks = make(map[string]*Task)
|
||
|
|
dg.adjacencyList = make(map[string][]string)
|
||
|
|
dg.reverseList = make(map[string][]string)
|
||
|
|
dg.topologicalOrder = make([]string, 0)
|
||
|
|
dg.status = make(map[string]string)
|
||
|
|
dg.cycleDetected = false
|
||
|
|
}
|
||
|
|
|
||
|
|
// CanExecuteTask checks if a task can be executed (all deps complete)
|
||
|
|
func (dg *DependencyGraph) CanExecuteTask(taskID string) bool {
|
||
|
|
dg.mu.RLock()
|
||
|
|
defer dg.mu.RUnlock()
|
||
|
|
|
||
|
|
deps, exists := dg.reverseList[taskID]
|
||
|
|
if !exists {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, dep := range deps {
|
||
|
|
if dg.status[dep] != "completed" {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetCriticalPath returns the longest path through the graph
|
||
|
|
func (dg *DependencyGraph) GetCriticalPath() []string {
|
||
|
|
dg.mu.RLock()
|
||
|
|
defer dg.mu.RUnlock()
|
||
|
|
|
||
|
|
// Use longest path algorithm
|
||
|
|
distances := make(map[string]int)
|
||
|
|
parent := make(map[string]string)
|
||
|
|
|
||
|
|
for taskID := range dg.tasks {
|
||
|
|
distances[taskID] = 0
|
||
|
|
}
|
||
|
|
|
||
|
|
// Process in topological order
|
||
|
|
for _, taskID := range dg.topologicalOrder {
|
||
|
|
for _, dependent := range dg.adjacencyList[taskID] {
|
||
|
|
if distances[dependent] < distances[taskID]+1 {
|
||
|
|
distances[dependent] = distances[taskID] + 1
|
||
|
|
parent[dependent] = taskID
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find task with maximum distance
|
||
|
|
maxDist := 0
|
||
|
|
endTask := ""
|
||
|
|
for taskID, dist := range distances {
|
||
|
|
if dist > maxDist {
|
||
|
|
maxDist = dist
|
||
|
|
endTask = taskID
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reconstruct path
|
||
|
|
path := make([]string, 0)
|
||
|
|
current := endTask
|
||
|
|
for current != "" {
|
||
|
|
path = append([]string{current}, path...)
|
||
|
|
current = parent[current]
|
||
|
|
}
|
||
|
|
|
||
|
|
return path
|
||
|
|
}
|