Public source validation / validate (push) Failing after 3m8s
136 lines
4.2 KiB
Go
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")
|