Skip to content

Commit

Permalink
Use ShortestPathStable and always add graphstate vertices
Browse files Browse the repository at this point in the history
These resolve engine behaving non-deterministically
  • Loading branch information
gordon-klotho committed Dec 8, 2023
1 parent df87741 commit c1bf5c6
Show file tree
Hide file tree
Showing 20 changed files with 413 additions and 147 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
"pkg/engine": true,
"pkg/knowledge_base": true,
"**/node_modules": true
}
},
"go.testTimeout": "5m"
}
23 changes: 23 additions & 0 deletions compare.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh

# This script takes in an input yaml, and runs the engine on it twice.
# It saves the log output in out.log and out2.log.
# It then filters the logs for important messages and outputs to out.queue.log and out2.queue.log.
# These files can then be diffed to see if the engine is behaving the same way on the same input.

file=$1
if [ ! -f "$file" ]; then
file="./pkg/engine2/testdata/$1"
fi

rm out.log out2.log
export NO_COLOR=1
export COLUMNS=80
go run ./cmd/engine Run -i "$file" -o "./out/$(basename $file .yaml)" -v 2> out.log
sleep 2
go run ./cmd/engine Run -i "$file" -o "./out/$(basename $file .yaml)2" -v 2> out2.log

rm out.queue.log out2.queue.log
grep -E -e 'op: dequeue|eval|poll-deps' -e 'Satisfied' out.log > out.queue.log
grep -E -e 'op: dequeue|eval|poll-deps' -e 'Satisfied' out2.log > out2.queue.log

4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ require (
github.com/pborman/ansi v1.0.0
github.com/pelletier/go-toml/v2 v2.0.8-0.20230509155657-d34104d49374
github.com/pkg/errors v0.9.1
github.com/r3labs/diff v1.1.0
github.com/schollz/progressbar/v3 v3.13.0
github.com/smacker/go-tree-sitter v0.0.0-20220209044044-0d3022e933c3
github.com/spf13/cobra v1.6.1
Expand All @@ -47,15 +48,14 @@ require (
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/r3labs/diff v1.1.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/oauth2 v0.4.0 // indirect
k8s.io/client-go v0.26.0 // indirect
)

replace (
github.com/dominikbraun/graph => github.com/klothoplatform/graph v0.24.6
github.com/dominikbraun/graph => github.com/klothoplatform/graph v0.24.7

// github.com/dominikbraun/graph => github.com/klothoplatform/graph v0.24.3
github.com/smacker/go-tree-sitter => github.com/klothoplatform/go-tree-sitter v0.1.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,8 @@ github.com/klauspost/compress v1.15.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UH
github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klothoplatform/go-tree-sitter v0.1.1 h1:UO9bDCP6jJfKHUPv0P+8wLM6FJ4tCRNu3Hj2EQE51wk=
github.com/klothoplatform/go-tree-sitter v0.1.1/go.mod h1:q99oHDsbP0xRwmn7Vmob8gbSMNyvJ83OauXPSuHQuKE=
github.com/klothoplatform/graph v0.24.6 h1:eLI4Pr9aqk6trjqbxlX3K9aOCdAnTxO+8Nc/wEpZSlw=
github.com/klothoplatform/graph v0.24.6/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/klothoplatform/graph v0.24.7 h1:eE3or9a8a5xgk0WaQPYVEUzbvaXafbc38+PpYpl3t1E=
github.com/klothoplatform/graph v0.24.7/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/klothoplatform/yaml/v3 v3.0.1 h1:9POX/TEuMqoXsMJ6ypU2FdaRtnt47bxx+klOQ0PSuys=
github.com/klothoplatform/yaml/v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
Expand Down
116 changes: 116 additions & 0 deletions pkg/construct2/dot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package construct2

import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"

"github.com/klothoplatform/klotho/pkg/dot"
)

func dotAttributes(r *Resource) map[string]string {
a := make(map[string]string)
a["label"] = r.ID.String()
a["shape"] = "box"
return a
}

func dotEdgeAttributes(e ResourceEdge) map[string]string {
a := make(map[string]string)
_ = e.Source.WalkProperties(func(path PropertyPath, nerr error) error {
v := path.Get()
if v == e.Target.ID {
a["label"] = path.String()
return StopWalk
}
return nil
})
return a
}

func GraphToDOT(g Graph, out io.Writer) error {
ids, err := ToplogicalSort(g)
if err != nil {
return err
}
nodes, err := ResolveIds(g, ids)
if err != nil {
return err
}
var errs error
printf := func(s string, args ...any) {
_, err := fmt.Fprintf(out, s, args...)
errs = errors.Join(errs, err)
}
printf(`digraph {
rankdir = TB
`)
for _, n := range nodes {
printf(" %q%s\n", n.ID, dot.AttributesToString(dotAttributes(n)))
}

topoIndex := func(id ResourceId) int {
for i, id2 := range ids {
if id2 == id {
return i
}
}
return -1
}
edges, err := g.Edges()
if err != nil {
return err
}
sort.Slice(edges, func(i, j int) bool {
ti, tj := topoIndex(edges[i].Source), topoIndex(edges[j].Source)
if ti != tj {
return ti < tj
}
ti, tj = topoIndex(edges[i].Target), topoIndex(edges[j].Target)
return ti < tj
})
for _, e := range edges {
edge, err := g.Edge(e.Source, e.Target)
if err != nil {
errs = errors.Join(errs, err)
continue
}
printf(" %q -> %q%s\n", e.Source, e.Target, dot.AttributesToString(dotEdgeAttributes(edge)))
}
printf("}\n")
return errs
}

func GraphToSVG(g Graph, prefix string) error {
if debugDir := os.Getenv("KLOTHO_DEBUG_DIR"); debugDir != "" {
prefix = filepath.Join(debugDir, prefix)
}
f, err := os.Create(prefix + ".gv")
if err != nil {
return err
}
defer f.Close()

dotContent := new(bytes.Buffer)
err = GraphToDOT(g, io.MultiWriter(f, dotContent))
if err != nil {
return fmt.Errorf("could not render graph to file %s: %v", prefix+".gv", err)
}

svgContent, err := dot.ExecPan(bytes.NewReader(dotContent.Bytes()))
if err != nil {
return fmt.Errorf("could not run 'dot' for %s: %v", prefix+".gv", err)
}

svgFile, err := os.Create(prefix + ".gv.svg")
if err != nil {
return fmt.Errorf("could not create file %s: %v", prefix+".gv.svg", err)
}
defer svgFile.Close()
_, err = fmt.Fprint(svgFile, svgContent)
return err
}
30 changes: 15 additions & 15 deletions pkg/construct2/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"math"
"sort"
"strings"

"github.com/dominikbraun/graph"
Expand Down Expand Up @@ -120,37 +119,38 @@ func bellmanFord(g Graph, source ResourceId, skipEdge func(Edge) bool) (*bellman
}
dist[source] = 0

// Sort the keys to ensure deterministic results. It adds +O(N) to the runtime, but
// when it's already O(N * E), it doesn't matter.
sortedKeys := make([]ResourceId, 0, len(adjacencyMap))
for key := range adjacencyMap {
sortedKeys = append(sortedKeys, key)
}
sort.Sort(SortedIds(sortedKeys))

for i := 0; i < len(adjacencyMap)-1; i++ {
for _, key := range sortedKeys {
edges := adjacencyMap[key]
for key, edges := range adjacencyMap {
for _, edge := range edges {
if skipEdge(edge) {
continue
}
newDist := dist[key] + edge.Properties.Weight
edgeWeight := edge.Properties.Weight
if !g.Traits().IsWeighted {
edgeWeight = 1
}

newDist := dist[key] + edgeWeight
if newDist < dist[edge.Target] {
dist[edge.Target] = newDist
prev[edge.Target] = key
} else if newDist == dist[edge.Target] && ResourceIdLess(key, prev[edge.Target]) {
prev[edge.Target] = key
}
}
}
}

for _, key := range sortedKeys {
edges := adjacencyMap[key]
for _, edges := range adjacencyMap {
for _, edge := range edges {
if skipEdge(edge) {
continue
}
if newDist := dist[edge.Source] + edge.Properties.Weight; newDist < dist[edge.Target] {
edgeWeight := edge.Properties.Weight
if !g.Traits().IsWeighted {
edgeWeight = 1
}
if newDist := dist[edge.Source] + edgeWeight; newDist < dist[edge.Target] {
return nil, errors.New("graph contains a negative-weight cycle")
}
}
Expand Down
47 changes: 38 additions & 9 deletions pkg/engine2/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import (
"reflect"
"runtime/pprof"
"strings"
"sync"

"github.com/iancoleman/strcase"
"github.com/klothoplatform/klotho/pkg/analytics"
"github.com/klothoplatform/klotho/pkg/closenicely"
construct "github.com/klothoplatform/klotho/pkg/construct2"
"github.com/klothoplatform/klotho/pkg/engine2/constraints"
"github.com/klothoplatform/klotho/pkg/engine2/solution_context"
"github.com/klothoplatform/klotho/pkg/io"
knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
"github.com/klothoplatform/klotho/pkg/knowledge_base2/reader"
Expand Down Expand Up @@ -264,8 +266,11 @@ func (em *EngineMain) RunEngine(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
var input construct.YamlGraph

context := &EngineContext{}

if architectureEngineCfg.inputGraph != "" {
var input FileFormat
zap.S().Info("Loading input graph")
inputF, err := os.Open(architectureEngineCfg.inputGraph)
if err != nil {
Expand All @@ -276,25 +281,29 @@ func (em *EngineMain) RunEngine(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
context.InitialState = input.Graph
if architectureEngineCfg.constraints == "" {
context.Constraints = input.Constraints
}
} else {
input.Graph = construct.NewGraph()
context.InitialState = construct.NewGraph()
}
zap.S().Info("Loading constraints")

runConstraints, err := constraints.LoadConstraintsFromFile(architectureEngineCfg.constraints)
if err != nil {
return errors.Errorf("failed to load constraints: %s", err.Error())
}
context := &EngineContext{
InitialState: input.Graph,
Constraints: runConstraints,
if architectureEngineCfg.constraints != "" {
runConstraints, err := constraints.LoadConstraintsFromFile(architectureEngineCfg.constraints)
if err != nil {
return errors.Errorf("failed to load constraints: %s", err.Error())
}
context.Constraints = runConstraints
}

zap.S().Info("Running engine")
err = em.Engine.Run(context)
if err != nil {
return errors.Errorf("failed to run engine: %s", err.Error())
}
writeDebugGraphs(context.Solutions[0])
zap.S().Info("Engine finished running... Generating views")
var files []io.File
files, err = em.Engine.VisualizeViews(context.Solutions[0])
Expand Down Expand Up @@ -407,3 +416,23 @@ func (em *EngineMain) GetValidEdgeTargets(cmd *cobra.Command, args []string) err
}
return nil
}

func writeDebugGraphs(sol solution_context.SolutionContext) {
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
err := construct.GraphToSVG(sol.DataflowGraph(), "dataflow")
if err != nil {
zap.S().Errorf("failed to write dataflow graph: %s", err.Error())
}
}()
go func() {
defer wg.Done()
err := construct.GraphToSVG(sol.DeploymentGraph(), "iac")
if err != nil {
zap.S().Errorf("failed to write iac graph: %s", err.Error())
}
}()
wg.Wait()
}
Loading

0 comments on commit c1bf5c6

Please sign in to comment.