380 lines
9.9 KiB
Plaintext
380 lines
9.9 KiB
Plaintext
#!/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"
|
|
})
|
|
}
|
|
}
|