380 lines
9.9 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
] -> list {
let service_def = (get-service-definition $service_name)
if ($service_def.dependencies | is-empty) {
return []
}
# Recursively resolve dependencies
let mut all_deps = []
for dep in $service_def.dependencies {
# Add the dependency itself
if $dep not-in $all_deps {
$all_deps = ($all_deps | append $dep)
}
# Recursively add its dependencies
let sub_deps = (resolve-dependencies $dep)
for sub_dep in $sub_deps {
if $sub_dep not-in $all_deps {
$all_deps = ($all_deps | append $sub_dep)
}
}
}
$all_deps
}
# Get dependency tree
export def get-dependency-tree [
service_name: string
] -> record {
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
] -> list {
let mut sorted = []
let mut visited = {}
let mut visiting = {}
# Recursive DFS
def visit [
node: string
dep_map: record
visited: record
visiting: record
sorted: list
] -> record {
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 mut visiting = ($visiting | insert $node true)
let mut sorted = $sorted
let mut visited = $visited
let deps = if $node in ($dep_map | columns) {
$dep_map | get $node
} else {
[]
}
for dep in $deps {
let result = (visit $dep $dep_map $visited $visiting $sorted)
$visited = $result.visited
$visiting = $result.visiting
$sorted = $result.sorted
}
$visiting = ($visiting | reject $node)
$visited = ($visited | insert $node true)
$sorted = ($sorted | append $node)
{ visited: $visited, visiting: $visiting, sorted: $sorted }
}
# Visit all nodes
let mut state = { visited: {}, visiting: {}, sorted: [] }
for service in $services {
if $service not-in ($state.visited | columns) {
$state = (visit $service $dep_map $state.visited $state.visiting $state.sorted)
}
}
$state.sorted
}
# Start services in dependency order
export def start-services-with-deps [
service_names: list
] -> record {
# Build dependency map
let registry = (load-service-registry)
let dep_map = (
$registry
| transpose name config
| reduce -f {} { |row, acc|
$acc | insert $row.name $row.config.dependencies
}
)
# Get all services to start (including dependencies)
let mut all_services = []
for service in $service_names {
if $service not-in $all_services {
$all_services = ($all_services | append $service)
}
let deps = (resolve-dependencies $service)
for dep in $deps {
if $dep not-in $all_services {
$all_services = ($all_services | append $dep)
}
}
}
# Get startup order
let startup_order = (topological-sort $all_services $dep_map)
print $"Starting services in order: ($startup_order | str join ' -> ')"
# Start services
let mut started = []
let mut failed = []
for service in $startup_order {
use manager.nu is-service-running start-service
if (is-service-running $service) {
print $"✅ ($service) already running"
$started = ($started | append $service)
continue
}
print $"Starting ($service)..."
try {
let result = (start-service $service)
if $result {
$started = ($started | append $service)
print $"✅ ($service) started"
} else {
$failed = ($failed | append $service)
print $"❌ Failed to start ($service)"
break
}
} catch {
$failed = ($failed | append $service)
print $"❌ Error starting ($service)"
break
}
}
{
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 [] -> record {
let registry = (load-service-registry)
let dep_map = (
$registry
| transpose name config
| reduce -f {} { |row, acc|
$acc | insert $row.name $row.config.dependencies
}
)
let services = ($registry | columns)
# Try to topologically sort - will error if cycle detected
try {
let sorted = (topological-sort $services $dep_map)
{
valid: true
has_cycles: false
message: "Dependency graph is valid (no cycles)"
startup_order: $sorted
}
} catch { |err|
{
valid: false
has_cycles: true
message: $"Dependency graph has cycles: ($err.msg)"
error: $err
}
}
}
# Get startup order
export def get-startup-order [
service_names: list
] -> list {
let registry = (load-service-registry)
let dep_map = (
$registry
| transpose name config
| reduce -f {} { |row, acc|
$acc | insert $row.name $row.config.dependencies
}
)
# Get all services to include (with dependencies)
let mut all_services = []
for service in $service_names {
if $service not-in $all_services {
$all_services = ($all_services | append $service)
}
let deps = (resolve-dependencies $service)
for dep in $deps {
if $dep not-in $all_services {
$all_services = ($all_services | append $dep)
}
}
}
# 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
try {
topological-sort $services_with_order $dep_map
} catch {
# 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
] -> list {
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 [] -> string {
let registry = (load-service-registry)
let mut output = ["# Service Dependency Graph", ""]
for service in ($registry | columns | sort) {
let service_def = (get-service-definition $service)
$output = ($output | append $"## ($service)")
$output = ($output | append $"- Type: ($service_def.type)")
$output = ($output | append $"- Category: ($service_def.category)")
if not ($service_def.dependencies | is-empty) {
$output = ($output | append "- Dependencies:")
for dep in $service_def.dependencies {
$output = ($output | append $" - ($dep)")
}
}
let reverse_deps = (get-reverse-dependencies $service)
if not ($reverse_deps | is-empty) {
$output = ($output | append "- Required by:")
for dep in $reverse_deps {
$output = ($output | append $" - ($dep)")
}
}
if not ($service_def.conflicts | is-empty) {
$output = ($output | append "- Conflicts:")
for conflict in $service_def.conflicts {
$output = ($output | append $" - ($conflict)")
}
}
$output = ($output | append "")
}
$output | str join "\n"
}
# Check if service can be stopped safely
export def can-stop-service [
service_name: string
] -> record {
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"
})
}
}