446 lines
13 KiB
Plaintext
Raw Normal View History

2025-10-07 10:32:04 +01:00
#!/usr/bin/env nu
# Service Dependency Resolution
# Handles dependency graph analysis and startup ordering
use manager.nu [load-service-registry get-service-definition]
# Resolve service dependencies
export def resolve-dependencies [
service_name: string
]: nothing -> list {
2025-10-07 10:32:04 +01:00
let service_def = (get-service-definition $service_name)
if ($service_def.dependencies | is-empty) {
return []
}
# Recursively resolve dependencies - collect all unique deps
def accumulate-deps [deps: list, all_deps: list]: nothing -> list {
if ($deps | is-empty) {
return $all_deps
2025-10-07 10:32:04 +01:00
}
let first = ($deps | get 0)
let rest = ($deps | skip 1)
let with_first = if $first in $all_deps { $all_deps } else { $all_deps | append $first }
let sub_deps = (resolve-dependencies $first)
let all_sub_deps = ($sub_deps | append $rest)
accumulate-deps $all_sub_deps $with_first
2025-10-07 10:32:04 +01:00
}
accumulate-deps $service_def.dependencies []
2025-10-07 10:32:04 +01:00
}
# Get dependency tree
export def get-dependency-tree [
service_name: string
]: nothing -> record {
2025-10-07 10:32:04 +01:00
let service_def = (get-service-definition $service_name)
if ($service_def.dependencies | is-empty) {
return {
service: $service_name
dependencies: []
}
}
let deps = (
$service_def.dependencies
| each { |dep|
get-dependency-tree $dep
}
)
{
service: $service_name
dependencies: $deps
}
}
# Topological sort for dependency ordering
def topological-sort [
services: list
dep_map: record
]: nothing -> list {
# Recursive DFS helper function
2025-10-07 10:32:04 +01:00
def visit [
node: string
dep_map: record
visited: record
visiting: record
sorted: list
]: nothing -> record {
2025-10-07 10:32:04 +01:00
if $node in ($visiting | columns) {
error make {
msg: "Circular dependency detected"
label: {
text: $"Cycle involving ($node)"
span: (metadata $node).span
}
}
}
if $node in ($visited | columns) {
return { visited: $visited, visiting: $visiting, sorted: $sorted }
}
let new_visiting = ($visiting | insert $node true)
2025-10-07 10:32:04 +01:00
let deps = if $node in ($dep_map | columns) {
$dep_map | get $node
} else {
[]
}
# Process dependencies recursively
def visit-deps [deps: list, state: record]: nothing -> record {
if ($deps | is-empty) {
return $state
}
let first_dep = ($deps | get 0)
let rest_deps = ($deps | skip 1)
let state_after = (visit $first_dep $dep_map $state.visited $state.visiting $state.sorted)
visit-deps $rest_deps $state_after
2025-10-07 10:32:04 +01:00
}
let result_after_deps = (visit-deps $deps { visited: $visited, visiting: $new_visiting, sorted: $sorted })
2025-10-07 10:32:04 +01:00
let final_visiting = ($result_after_deps.visiting | reject $node)
let final_visited = ($result_after_deps.visited | insert $node true)
let final_sorted = ($result_after_deps.sorted | append $node)
2025-10-07 10:32:04 +01:00
{ visited: $final_visited, visiting: $final_visiting, sorted: $final_sorted }
}
2025-10-07 10:32:04 +01:00
# Visit all nodes recursively starting with empty state
def visit-services [services: list, state: record]: nothing -> record {
if ($services | is-empty) {
return $state
}
let first_service = ($services | get 0)
let rest_services = ($services | skip 1)
let state_after = if $first_service not-in ($state.visited | columns) {
visit $first_service $dep_map $state.visited $state.visiting $state.sorted
} else {
$state
2025-10-07 10:32:04 +01:00
}
visit-services $rest_services $state_after
2025-10-07 10:32:04 +01:00
}
(visit-services $services { visited: {}, visiting: {}, sorted: [] }) | get sorted
2025-10-07 10:32:04 +01:00
}
# Start services in dependency order
export def start-services-with-deps [
service_names: list
]: nothing -> record {
2025-10-07 10:32:04 +01:00
# Build dependency map
let registry = (load-service-registry)
# Helper to build dep_map from registry entries
def build-dep-map [entries: list, acc: record]: nothing -> record {
if ($entries | is-empty) {
return $acc
2025-10-07 10:32:04 +01:00
}
let first = ($entries | get 0)
let rest = ($entries | skip 1)
let new_acc = ($acc | insert $first.name $first.config.dependencies)
build-dep-map $rest $new_acc
}
2025-10-07 10:32:04 +01:00
let dep_map = (build-dep-map ($registry | transpose name config) {})
2025-10-07 10:32:04 +01:00
# Helper to collect all services with their dependencies
def collect-services [services: list, all_deps: list]: nothing -> list {
if ($services | is-empty) {
return $all_deps
2025-10-07 10:32:04 +01:00
}
let first = ($services | get 0)
let rest = ($services | skip 1)
let with_first = if $first in $all_deps { $all_deps } else { $all_deps | append $first }
let sub_deps = (resolve-dependencies $first)
collect-services (($rest | append $sub_deps)) $with_first
2025-10-07 10:32:04 +01:00
}
let all_services = (collect-services $service_names [])
2025-10-07 10:32:04 +01:00
# Get startup order
let startup_order = (topological-sort $all_services $dep_map)
print $"Starting services in order: ($startup_order | str join ' -> ')"
# Helper to start services recursively
def start-services [services: list, state: record]: nothing -> record {
if ($services | is-empty) {
return $state
2025-10-07 10:32:04 +01:00
}
let first = ($services | get 0)
let rest = ($services | skip 1)
use manager.nu [is-service-running start-service]
2025-10-07 10:32:04 +01:00
let new_state = if (is-service-running $first) {
print $"✅ ($first) already running"
{
started: ($state.started | append $first)
failed: $state.failed
}
} else {
print $"Starting [$first]..."
let start_result = (do {
start-service $first
} | complete)
if $start_result.exit_code == 0 and ($start_result.stdout == "true" or $start_result.stdout == true) {
print $"✅ [$first] started"
{
started: ($state.started | append $first)
failed: $state.failed
}
2025-10-07 10:32:04 +01:00
} else {
print $"❌ Failed to start [$first]: ($start_result.stderr)"
{
started: $state.started
failed: ($state.failed | append $first)
}
2025-10-07 10:32:04 +01:00
}
}
start-services $rest $new_state
2025-10-07 10:32:04 +01:00
}
let result = (start-services $startup_order { started: [], failed: [] })
let started = $result.started
let failed = $result.failed
2025-10-07 10:32:04 +01:00
{
success: ($failed | is-empty)
startup_order: $startup_order
started: $started
failed: $failed
message: (if ($failed | is-empty) {
$"Successfully started ($started | length) services"
} else {
$"Failed at service: ($failed | get 0)"
})
}
}
# Validate dependency graph (detect cycles)
export def validate-dependency-graph []: nothing -> record {
2025-10-07 10:32:04 +01:00
let registry = (load-service-registry)
# Helper to build dep_map from registry entries
def build-dep-map [entries: list, acc: record]: nothing -> record {
if ($entries | is-empty) {
return $acc
2025-10-07 10:32:04 +01:00
}
let first = ($entries | get 0)
let rest = ($entries | skip 1)
let new_acc = ($acc | insert $first.name $first.config.dependencies)
build-dep-map $rest $new_acc
}
let dep_map = (build-dep-map ($registry | transpose name config) {})
2025-10-07 10:32:04 +01:00
let services = ($registry | columns)
# Try to topologically sort - will error if cycle detected
let sort_result = (do {
topological-sort $services $dep_map
} | complete)
2025-10-07 10:32:04 +01:00
if $sort_result.exit_code == 0 {
2025-10-07 10:32:04 +01:00
{
valid: true
has_cycles: false
message: "Dependency graph is valid (no cycles)"
startup_order: ($sort_result.stdout | from json)
2025-10-07 10:32:04 +01:00
}
} else {
2025-10-07 10:32:04 +01:00
{
valid: false
has_cycles: true
message: $"Dependency graph has cycles: ($sort_result.stderr)"
error: $sort_result.stderr
2025-10-07 10:32:04 +01:00
}
}
}
# Get startup order
export def get-startup-order [
service_names: list
]: nothing -> list {
2025-10-07 10:32:04 +01:00
let registry = (load-service-registry)
# Helper to build dep_map from registry entries
def build-dep-map [entries: list, acc: record]: nothing -> record {
if ($entries | is-empty) {
return $acc
2025-10-07 10:32:04 +01:00
}
let first = ($entries | get 0)
let rest = ($entries | skip 1)
let new_acc = ($acc | insert $first.name $first.config.dependencies)
build-dep-map $rest $new_acc
}
2025-10-07 10:32:04 +01:00
let dep_map = (build-dep-map ($registry | transpose name config) {})
2025-10-07 10:32:04 +01:00
# Helper to collect all services with their dependencies
def collect-services [services: list, all_deps: list]: nothing -> list {
if ($services | is-empty) {
return $all_deps
2025-10-07 10:32:04 +01:00
}
let first = ($services | get 0)
let rest = ($services | skip 1)
let with_first = if $first in $all_deps { $all_deps } else { $all_deps | append $first }
let sub_deps = (resolve-dependencies $first)
collect-services (($rest | append $sub_deps)) $with_first
2025-10-07 10:32:04 +01:00
}
let all_services = (collect-services $service_names [])
2025-10-07 10:32:04 +01:00
# Also sort by start_order field
let services_with_order = (
$all_services
| each { |name|
let service_def = (get-service-definition $name)
{
name: $name
start_order: $service_def.startup.start_order
}
}
| sort-by start_order
| get name
)
# Perform topological sort
let sort_result = (do {
2025-10-07 10:32:04 +01:00
topological-sort $services_with_order $dep_map
} | complete)
if $sort_result.exit_code == 0 {
$sort_result.stdout | from json
} else {
2025-10-07 10:32:04 +01:00
# Fallback to simple order if cycle detected
print "⚠️ Warning: Circular dependency detected, using start_order fallback"
$services_with_order
}
}
# Get reverse dependencies (which services depend on this one)
export def get-reverse-dependencies [
service_name: string
]: nothing -> list {
2025-10-07 10:32:04 +01:00
let registry = (load-service-registry)
$registry
| transpose name config
| where { |row|
$service_name in $row.config.dependencies
}
| get name
}
# Get dependency graph visualization
export def visualize-dependency-graph []: nothing -> string {
2025-10-07 10:32:04 +01:00
let registry = (load-service-registry)
# Helper to format a single service's dependencies
def format-service-deps [service: string, lines: list]: nothing -> list {
2025-10-07 10:32:04 +01:00
let service_def = (get-service-definition $service)
let base_lines = (
$lines
| append $"## ($service)"
| append $"- Type: ($service_def.type)"
| append $"- Category: ($service_def.category)"
)
let with_deps = (
if not ($service_def.dependencies | is-empty) {
($base_lines | append "- Dependencies:")
| append (
$service_def.dependencies
| each { |dep| $" - ($dep)" }
)
} else {
$base_lines
2025-10-07 10:32:04 +01:00
}
)
2025-10-07 10:32:04 +01:00
let reverse_deps = (get-reverse-dependencies $service)
let with_reverse = (
if not ($reverse_deps | is-empty) {
($with_deps | append "- Required by:")
| append (
$reverse_deps
| each { |dep| $" - ($dep)" }
)
} else {
$with_deps
2025-10-07 10:32:04 +01:00
}
)
let final = (
if not ($service_def.conflicts | is-empty) {
($with_reverse | append "- Conflicts:")
| append (
$service_def.conflicts
| each { |conflict| $" - ($conflict)" }
)
} else {
$with_reverse
2025-10-07 10:32:04 +01:00
}
)
($final | append "")
}
2025-10-07 10:32:04 +01:00
# Helper to format all services recursively
def format-services [services: list, lines: list]: nothing -> list {
if ($services | is-empty) {
return $lines
}
let first = ($services | get 0)
let rest = ($services | skip 1)
let new_lines = (format-service-deps $first $lines)
format-services $rest $new_lines
2025-10-07 10:32:04 +01:00
}
let output = (
["# Service Dependency Graph", ""]
| append (format-services (($registry | columns | sort)) [])
)
2025-10-07 10:32:04 +01:00
$output | str join "\n"
}
# Check if service can be stopped safely
export def can-stop-service [
service_name: string
]: nothing -> record {
2025-10-07 10:32:04 +01:00
use manager.nu is-service-running
let reverse_deps = (get-reverse-dependencies $service_name)
# Check which dependent services are running
let running_deps = (
$reverse_deps
| where { |dep| is-service-running $dep }
)
{
service: $service_name
can_stop: ($running_deps | is-empty)
dependent_services: $reverse_deps
running_dependents: $running_deps
message: (if ($running_deps | is-empty) {
"Service can be stopped safely"
} else {
$"Cannot stop: ($running_deps | length) dependent services are running"
})
}
}