Files
ITWorx-Pulse-Public/internal/service/dependency.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

136 lines
4.2 KiB
Go

package service
import (
"errors"
"fmt"
"sort"
)
type DependencySuppressionInput struct {
DependencyID string `json:"dependencyId"`
ServiceID string `json:"serviceId"`
DependsOnServiceID string `json:"dependsOnServiceId"`
SourceID string `json:"sourceId,omitempty"`
Confidence float64 `json:"confidence"`
Confirmed bool `json:"confirmed"`
UpstreamState string `json:"upstreamState"`
Suppress bool `json:"suppress"`
Reason string `json:"reason"`
}
// ValidateDependencyGraph rejects dependency cycles deterministically. Only
// depends_on edges participate in propagation; backs/exposes remain visible
// topology links without changing service state.
func ValidateDependencyGraph(dependencies []Dependency) error {
edges := append([]Dependency(nil), dependencies...)
SortDependencies(edges)
seen := make(map[string]struct{}, len(edges))
adjacency := make(map[string][]string)
for _, dependency := range edges {
if err := dependency.Validate(); err != nil {
return err
}
key := dependencyKey(dependency)
if _, exists := seen[key]; exists {
return fmt.Errorf("duplicate service dependency: %s", key)
}
seen[key] = struct{}{}
if dependency.RelationType == RelationDependsOn {
adjacency[dependency.ServiceID] = append(adjacency[dependency.ServiceID], dependency.DependsOnServiceID)
}
}
for source := range adjacency {
sort.Strings(adjacency[source])
}
colors := make(map[string]uint8)
var visit func(string) error
visit = func(node string) error {
switch colors[node] {
case 1:
return fmt.Errorf("%w includes %s", ErrDependencyCycle, node)
case 2:
return nil
}
colors[node] = 1
for _, target := range adjacency[node] {
if err := visit(target); err != nil {
return err
}
}
colors[node] = 2
return nil
}
nodes := make([]string, 0, len(adjacency))
for node := range adjacency {
nodes = append(nodes, node)
}
sort.Strings(nodes)
for _, node := range nodes {
if err := visit(node); err != nil {
return err
}
}
return nil
}
func SortDependencies(dependencies []Dependency) {
sort.SliceStable(dependencies, func(i, j int) bool {
left, right := dependencies[i], dependencies[j]
if left.ServiceID != right.ServiceID {
return left.ServiceID < right.ServiceID
}
if left.DependsOnServiceID != right.DependsOnServiceID {
return left.DependsOnServiceID < right.DependsOnServiceID
}
if left.RelationType != right.RelationType {
return left.RelationType < right.RelationType
}
if left.SourceID != right.SourceID {
return left.SourceID < right.SourceID
}
return left.ID < right.ID
})
}
func BuildSuppressionInputs(dependencies []Dependency, states map[string]string) ([]DependencySuppressionInput, error) {
if states == nil {
states = map[string]string{}
}
if err := ValidateDependencyGraph(dependencies); err != nil {
return nil, err
}
ordered := append([]Dependency(nil), dependencies...)
SortDependencies(ordered)
inputs := make([]DependencySuppressionInput, 0, len(ordered))
for _, dependency := range ordered {
if dependency.RelationType != RelationDependsOn {
continue
}
state := states[dependency.DependsOnServiceID]
input := DependencySuppressionInput{DependencyID: dependency.ID, ServiceID: dependency.ServiceID, DependsOnServiceID: dependency.DependsOnServiceID, SourceID: dependency.SourceID, Confidence: dependency.Confidence, Confirmed: dependency.Confirmed, UpstreamState: state}
switch state {
case StateDown, StateDegraded:
input.Suppress = dependency.Confirmed || dependency.Confidence >= .75
if input.Suppress && dependency.Confirmed {
input.Reason = "confirmed_dependency_failure"
} else if input.Suppress {
input.Reason = "inferred_dependency_failure"
} else {
input.Reason = "low_confidence_dependency_failure"
}
case StateUnknown:
input.Reason = "dependency_unknown"
default:
input.Reason = "dependency_healthy"
}
inputs = append(inputs, input)
}
return inputs, nil
}
func dependencyKey(dependency Dependency) string {
return dependency.ServiceID + "|" + dependency.DependsOnServiceID + "|" + dependency.RelationType + "|" + dependency.SourceID
}
var ErrDependencyCycle = errors.New("service dependency cycle")