Skip to content

Commit b56bc4c

Browse files
committed
Use ShortestPathStable and always add graphstate vertices
These resolve engine behaving non-deterministically
1 parent 9e0f046 commit b56bc4c

20 files changed

+413
-147
lines changed

.vscode/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
"pkg/engine": true,
1818
"pkg/knowledge_base": true,
1919
"**/node_modules": true
20-
}
20+
},
21+
"go.testTimeout": "5m"
2122
}

compare.sh

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/sh
2+
3+
# This script takes in an input yaml, and runs the engine on it twice.
4+
# It saves the log output in out.log and out2.log.
5+
# It then filters the logs for important messages and outputs to out.queue.log and out2.queue.log.
6+
# These files can then be diffed to see if the engine is behaving the same way on the same input.
7+
8+
file=$1
9+
if [ ! -f "$file" ]; then
10+
file="./pkg/engine2/testdata/$1"
11+
fi
12+
13+
rm out.log out2.log
14+
export NO_COLOR=1
15+
export COLUMNS=80
16+
go run ./cmd/engine Run -i "$file" -o "./out/$(basename $file .yaml)" -v 2> out.log
17+
sleep 2
18+
go run ./cmd/engine Run -i "$file" -o "./out/$(basename $file .yaml)2" -v 2> out2.log
19+
20+
rm out.queue.log out2.queue.log
21+
grep -E -e 'op: dequeue|eval|poll-deps' -e 'Satisfied' out.log > out.queue.log
22+
grep -E -e 'op: dequeue|eval|poll-deps' -e 'Satisfied' out2.log > out2.queue.log
23+

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
github.com/pborman/ansi v1.0.0
2424
github.com/pelletier/go-toml/v2 v2.0.8-0.20230509155657-d34104d49374
2525
github.com/pkg/errors v0.9.1
26+
github.com/r3labs/diff v1.1.0
2627
github.com/schollz/progressbar/v3 v3.13.0
2728
github.com/smacker/go-tree-sitter v0.0.0-20220209044044-0d3022e933c3
2829
github.com/spf13/cobra v1.6.1
@@ -47,15 +48,14 @@ require (
4748
github.com/kr/pretty v0.3.0 // indirect
4849
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
4950
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
50-
github.com/r3labs/diff v1.1.0 // indirect
5151
github.com/rivo/uniseg v0.4.4 // indirect
5252
golang.org/x/mod v0.9.0 // indirect
5353
golang.org/x/oauth2 v0.4.0 // indirect
5454
k8s.io/client-go v0.26.0 // indirect
5555
)
5656

5757
replace (
58-
github.com/dominikbraun/graph => github.com/klothoplatform/graph v0.24.6
58+
github.com/dominikbraun/graph => github.com/klothoplatform/graph v0.24.7
5959

6060
// github.com/dominikbraun/graph => github.com/klothoplatform/graph v0.24.3
6161
github.com/smacker/go-tree-sitter => github.com/klothoplatform/go-tree-sitter v0.1.1

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -383,8 +383,8 @@ github.com/klauspost/compress v1.15.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UH
383383
github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
384384
github.com/klothoplatform/go-tree-sitter v0.1.1 h1:UO9bDCP6jJfKHUPv0P+8wLM6FJ4tCRNu3Hj2EQE51wk=
385385
github.com/klothoplatform/go-tree-sitter v0.1.1/go.mod h1:q99oHDsbP0xRwmn7Vmob8gbSMNyvJ83OauXPSuHQuKE=
386-
github.com/klothoplatform/graph v0.24.6 h1:eLI4Pr9aqk6trjqbxlX3K9aOCdAnTxO+8Nc/wEpZSlw=
387-
github.com/klothoplatform/graph v0.24.6/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
386+
github.com/klothoplatform/graph v0.24.7 h1:eE3or9a8a5xgk0WaQPYVEUzbvaXafbc38+PpYpl3t1E=
387+
github.com/klothoplatform/graph v0.24.7/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
388388
github.com/klothoplatform/yaml/v3 v3.0.1 h1:9POX/TEuMqoXsMJ6ypU2FdaRtnt47bxx+klOQ0PSuys=
389389
github.com/klothoplatform/yaml/v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
390390
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=

pkg/construct2/dot.go

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package construct2
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"os"
9+
"path/filepath"
10+
"sort"
11+
12+
"github.com/klothoplatform/klotho/pkg/dot"
13+
)
14+
15+
func dotAttributes(r *Resource) map[string]string {
16+
a := make(map[string]string)
17+
a["label"] = r.ID.String()
18+
a["shape"] = "box"
19+
return a
20+
}
21+
22+
func dotEdgeAttributes(e ResourceEdge) map[string]string {
23+
a := make(map[string]string)
24+
_ = e.Source.WalkProperties(func(path PropertyPath, nerr error) error {
25+
v := path.Get()
26+
if v == e.Target.ID {
27+
a["label"] = path.String()
28+
return StopWalk
29+
}
30+
return nil
31+
})
32+
return a
33+
}
34+
35+
func GraphToDOT(g Graph, out io.Writer) error {
36+
ids, err := ToplogicalSort(g)
37+
if err != nil {
38+
return err
39+
}
40+
nodes, err := ResolveIds(g, ids)
41+
if err != nil {
42+
return err
43+
}
44+
var errs error
45+
printf := func(s string, args ...any) {
46+
_, err := fmt.Fprintf(out, s, args...)
47+
errs = errors.Join(errs, err)
48+
}
49+
printf(`digraph {
50+
rankdir = TB
51+
`)
52+
for _, n := range nodes {
53+
printf(" %q%s\n", n.ID, dot.AttributesToString(dotAttributes(n)))
54+
}
55+
56+
topoIndex := func(id ResourceId) int {
57+
for i, id2 := range ids {
58+
if id2 == id {
59+
return i
60+
}
61+
}
62+
return -1
63+
}
64+
edges, err := g.Edges()
65+
if err != nil {
66+
return err
67+
}
68+
sort.Slice(edges, func(i, j int) bool {
69+
ti, tj := topoIndex(edges[i].Source), topoIndex(edges[j].Source)
70+
if ti != tj {
71+
return ti < tj
72+
}
73+
ti, tj = topoIndex(edges[i].Target), topoIndex(edges[j].Target)
74+
return ti < tj
75+
})
76+
for _, e := range edges {
77+
edge, err := g.Edge(e.Source, e.Target)
78+
if err != nil {
79+
errs = errors.Join(errs, err)
80+
continue
81+
}
82+
printf(" %q -> %q%s\n", e.Source, e.Target, dot.AttributesToString(dotEdgeAttributes(edge)))
83+
}
84+
printf("}\n")
85+
return errs
86+
}
87+
88+
func GraphToSVG(g Graph, prefix string) error {
89+
if debugDir := os.Getenv("KLOTHO_DEBUG_DIR"); debugDir != "" {
90+
prefix = filepath.Join(debugDir, prefix)
91+
}
92+
f, err := os.Create(prefix + ".gv")
93+
if err != nil {
94+
return err
95+
}
96+
defer f.Close()
97+
98+
dotContent := new(bytes.Buffer)
99+
err = GraphToDOT(g, io.MultiWriter(f, dotContent))
100+
if err != nil {
101+
return fmt.Errorf("could not render graph to file %s: %v", prefix+".gv", err)
102+
}
103+
104+
svgContent, err := dot.ExecPan(bytes.NewReader(dotContent.Bytes()))
105+
if err != nil {
106+
return fmt.Errorf("could not run 'dot' for %s: %v", prefix+".gv", err)
107+
}
108+
109+
svgFile, err := os.Create(prefix + ".gv.svg")
110+
if err != nil {
111+
return fmt.Errorf("could not create file %s: %v", prefix+".gv.svg", err)
112+
}
113+
defer svgFile.Close()
114+
_, err = fmt.Fprint(svgFile, svgContent)
115+
return err
116+
}

pkg/construct2/paths.go

+15-15
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"errors"
55
"fmt"
66
"math"
7-
"sort"
87
"strings"
98

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

123-
// Sort the keys to ensure deterministic results. It adds +O(N) to the runtime, but
124-
// when it's already O(N * E), it doesn't matter.
125-
sortedKeys := make([]ResourceId, 0, len(adjacencyMap))
126-
for key := range adjacencyMap {
127-
sortedKeys = append(sortedKeys, key)
128-
}
129-
sort.Sort(SortedIds(sortedKeys))
130-
131122
for i := 0; i < len(adjacencyMap)-1; i++ {
132-
for _, key := range sortedKeys {
133-
edges := adjacencyMap[key]
123+
for key, edges := range adjacencyMap {
134124
for _, edge := range edges {
135125
if skipEdge(edge) {
136126
continue
137127
}
138-
newDist := dist[key] + edge.Properties.Weight
128+
edgeWeight := edge.Properties.Weight
129+
if !g.Traits().IsWeighted {
130+
edgeWeight = 1
131+
}
132+
133+
newDist := dist[key] + edgeWeight
139134
if newDist < dist[edge.Target] {
140135
dist[edge.Target] = newDist
141136
prev[edge.Target] = key
137+
} else if newDist == dist[edge.Target] && ResourceIdLess(key, prev[edge.Target]) {
138+
prev[edge.Target] = key
142139
}
143140
}
144141
}
145142
}
146143

147-
for _, key := range sortedKeys {
148-
edges := adjacencyMap[key]
144+
for _, edges := range adjacencyMap {
149145
for _, edge := range edges {
150146
if skipEdge(edge) {
151147
continue
152148
}
153-
if newDist := dist[edge.Source] + edge.Properties.Weight; newDist < dist[edge.Target] {
149+
edgeWeight := edge.Properties.Weight
150+
if !g.Traits().IsWeighted {
151+
edgeWeight = 1
152+
}
153+
if newDist := dist[edge.Source] + edgeWeight; newDist < dist[edge.Target] {
154154
return nil, errors.New("graph contains a negative-weight cycle")
155155
}
156156
}

pkg/engine2/cli.go

+38-9
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import (
88
"reflect"
99
"runtime/pprof"
1010
"strings"
11+
"sync"
1112

1213
"github.com/iancoleman/strcase"
1314
"github.com/klothoplatform/klotho/pkg/analytics"
1415
"github.com/klothoplatform/klotho/pkg/closenicely"
1516
construct "github.com/klothoplatform/klotho/pkg/construct2"
1617
"github.com/klothoplatform/klotho/pkg/engine2/constraints"
18+
"github.com/klothoplatform/klotho/pkg/engine2/solution_context"
1719
"github.com/klothoplatform/klotho/pkg/io"
1820
knowledgebase "github.com/klothoplatform/klotho/pkg/knowledge_base2"
1921
"github.com/klothoplatform/klotho/pkg/knowledge_base2/reader"
@@ -264,8 +266,11 @@ func (em *EngineMain) RunEngine(cmd *cobra.Command, args []string) error {
264266
if err != nil {
265267
return err
266268
}
267-
var input construct.YamlGraph
269+
270+
context := &EngineContext{}
271+
268272
if architectureEngineCfg.inputGraph != "" {
273+
var input FileFormat
269274
zap.S().Info("Loading input graph")
270275
inputF, err := os.Open(architectureEngineCfg.inputGraph)
271276
if err != nil {
@@ -276,25 +281,29 @@ func (em *EngineMain) RunEngine(cmd *cobra.Command, args []string) error {
276281
if err != nil {
277282
return err
278283
}
284+
context.InitialState = input.Graph
285+
if architectureEngineCfg.constraints == "" {
286+
context.Constraints = input.Constraints
287+
}
279288
} else {
280-
input.Graph = construct.NewGraph()
289+
context.InitialState = construct.NewGraph()
281290
}
282291
zap.S().Info("Loading constraints")
283292

284-
runConstraints, err := constraints.LoadConstraintsFromFile(architectureEngineCfg.constraints)
285-
if err != nil {
286-
return errors.Errorf("failed to load constraints: %s", err.Error())
287-
}
288-
context := &EngineContext{
289-
InitialState: input.Graph,
290-
Constraints: runConstraints,
293+
if architectureEngineCfg.constraints != "" {
294+
runConstraints, err := constraints.LoadConstraintsFromFile(architectureEngineCfg.constraints)
295+
if err != nil {
296+
return errors.Errorf("failed to load constraints: %s", err.Error())
297+
}
298+
context.Constraints = runConstraints
291299
}
292300

293301
zap.S().Info("Running engine")
294302
err = em.Engine.Run(context)
295303
if err != nil {
296304
return errors.Errorf("failed to run engine: %s", err.Error())
297305
}
306+
writeDebugGraphs(context.Solutions[0])
298307
zap.S().Info("Engine finished running... Generating views")
299308
var files []io.File
300309
files, err = em.Engine.VisualizeViews(context.Solutions[0])
@@ -407,3 +416,23 @@ func (em *EngineMain) GetValidEdgeTargets(cmd *cobra.Command, args []string) err
407416
}
408417
return nil
409418
}
419+
420+
func writeDebugGraphs(sol solution_context.SolutionContext) {
421+
wg := sync.WaitGroup{}
422+
wg.Add(2)
423+
go func() {
424+
defer wg.Done()
425+
err := construct.GraphToSVG(sol.DataflowGraph(), "dataflow")
426+
if err != nil {
427+
zap.S().Errorf("failed to write dataflow graph: %s", err.Error())
428+
}
429+
}()
430+
go func() {
431+
defer wg.Done()
432+
err := construct.GraphToSVG(sol.DeploymentGraph(), "iac")
433+
if err != nil {
434+
zap.S().Errorf("failed to write iac graph: %s", err.Error())
435+
}
436+
}()
437+
wg.Wait()
438+
}

0 commit comments

Comments
 (0)