Skip to content

Commit 2e4a62d

Browse files
authored
Merge pull request #35873 from hashicorp/jbardin/ancestors
core: use more efficient methods for finding ancestors
2 parents 06db25c + 146a52a commit 2e4a62d

File tree

4 files changed

+172
-14
lines changed

4 files changed

+172
-14
lines changed

internal/dag/dag.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,56 @@ func (g *AcyclicGraph) Ancestors(vs ...Vertex) Set {
5151
return s
5252
}
5353

54+
// FirstAncestorsWith returns a Set that includes every Vertex yielded by
55+
// walking down from the provided starting Vertex v, and stopping each branch when
56+
// match returns true. This will return the set of all first ancestors
57+
// encountered which match some criteria.
58+
func (g *AcyclicGraph) FirstAncestorsWith(v Vertex, match func(Vertex) bool) Set {
59+
s := make(Set)
60+
searchFunc := func(v Vertex, d int) error {
61+
if match(v) {
62+
s.Add(v)
63+
return errStopWalkBranch
64+
}
65+
66+
return nil
67+
}
68+
69+
start := make(Set)
70+
for _, dep := range g.downEdgesNoCopy(v) {
71+
start.Add(dep)
72+
}
73+
74+
// our memoFunc doesn't return an error
75+
g.DepthFirstWalk(start, searchFunc)
76+
77+
return s
78+
}
79+
80+
// MatchAncestor returns true if the given match function returns true for any
81+
// descendants of the given Vertex.
82+
func (g *AcyclicGraph) MatchAncestor(v Vertex, match func(Vertex) bool) bool {
83+
var ret bool
84+
matchFunc := func(v Vertex, d int) error {
85+
if match(v) {
86+
ret = true
87+
return errStopWalk
88+
}
89+
90+
return nil
91+
}
92+
93+
start := make(Set)
94+
for _, dep := range g.downEdgesNoCopy(v) {
95+
start.Add(dep)
96+
}
97+
98+
// our memoFunc doesn't return an error
99+
g.DepthFirstWalk(start, matchFunc)
100+
101+
return ret
102+
}
103+
54104
// Descendants returns a Set that includes every Vertex yielded by walking up
55105
// from the provided starting Vertex v.
56106
func (g *AcyclicGraph) Descendants(v Vertex) Set {

internal/dag/dag_test.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ func TestAcyclicGraphFindDescendants(t *testing.T) {
317317
return v.(int) == 1
318318
})
319319
if !foundOne {
320-
t.Fatal("did not match 6 in the graph")
320+
t.Fatal("did not match 1 in the graph")
321321
}
322322

323323
foundSix := g.MatchDescendant(6, func(v Vertex) bool {
@@ -335,6 +335,57 @@ func TestAcyclicGraphFindDescendants(t *testing.T) {
335335
}
336336
}
337337

338+
func TestAcyclicGraphFindAncestors(t *testing.T) {
339+
var g AcyclicGraph
340+
g.Add(0)
341+
g.Add(1)
342+
g.Add(2)
343+
g.Add(3)
344+
g.Add(4)
345+
g.Add(5)
346+
g.Add(6)
347+
g.Connect(BasicEdge(1, 0))
348+
g.Connect(BasicEdge(2, 1))
349+
g.Connect(BasicEdge(6, 2))
350+
g.Connect(BasicEdge(4, 3))
351+
g.Connect(BasicEdge(5, 4))
352+
g.Connect(BasicEdge(6, 5))
353+
354+
actual := g.FirstAncestorsWith(6, func(v Vertex) bool {
355+
// looking for first odd ancestors
356+
return v.(int)%2 != 0
357+
})
358+
359+
expected := make(Set)
360+
expected.Add(1)
361+
expected.Add(5)
362+
363+
if expected.Intersection(actual).Len() != expected.Len() {
364+
t.Fatalf("expected %#v, got %#v\n", expected, actual)
365+
}
366+
367+
foundOne := g.MatchAncestor(6, func(v Vertex) bool {
368+
return v.(int) == 1
369+
})
370+
if !foundOne {
371+
t.Fatal("did not match 1 in the graph")
372+
}
373+
374+
foundSix := g.MatchAncestor(6, func(v Vertex) bool {
375+
return v.(int) == 6
376+
})
377+
if foundSix {
378+
t.Fatal("6 should not be a descendant of itself")
379+
}
380+
381+
foundTen := g.MatchAncestor(6, func(v Vertex) bool {
382+
return v.(int) == 10
383+
})
384+
if foundTen {
385+
t.Fatal("10 is not in the graph at all")
386+
}
387+
}
388+
338389
func TestAcyclicGraphWalk(t *testing.T) {
339390
var g AcyclicGraph
340391
g.Add(1)

internal/terraform/context_apply2_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3582,3 +3582,53 @@ resource "test_instance" "obj" {
35823582
t.Fatal("old instance not destroyed")
35833583
}
35843584
}
3585+
3586+
// each resource only needs to record the first dependency in a chain
3587+
func TestContext2Apply_limitDependencies(t *testing.T) {
3588+
m := testModuleInline(t, map[string]string{
3589+
"main.tf": `
3590+
resource "test_object" "a" {
3591+
}
3592+
3593+
resource "test_object" "b" {
3594+
test_string = test_object.a.test_string
3595+
}
3596+
3597+
resource "test_object" "c" {
3598+
test_string = test_object.b.test_string
3599+
}
3600+
`,
3601+
})
3602+
3603+
p := simpleMockProvider()
3604+
3605+
ctx := testContext2(t, &ContextOpts{
3606+
Providers: map[addrs.Provider]providers.Factory{
3607+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
3608+
},
3609+
})
3610+
3611+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
3612+
assertNoErrors(t, diags)
3613+
3614+
state, diags := ctx.Apply(plan, m, nil)
3615+
assertNoErrors(t, diags)
3616+
3617+
for _, res := range state.RootModule().Resources {
3618+
deps := res.Instances[addrs.NoKey].Current.Dependencies
3619+
switch res.Addr.Resource.Name {
3620+
case "a":
3621+
if len(deps) > 0 {
3622+
t.Error(res.Addr, "should have no dependencies")
3623+
}
3624+
case "b":
3625+
if len(deps) != 1 || deps[0].Resource.Name != "a" {
3626+
t.Error(res.Addr, "should only depend on 'a', got", deps)
3627+
}
3628+
case "c":
3629+
if len(deps) != 1 || deps[0].Resource.Name != "b" {
3630+
t.Error(res.Addr, "should only record a dependency of 'b', got", deps)
3631+
}
3632+
}
3633+
}
3634+
}

internal/terraform/transform_reference.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -257,25 +257,30 @@ func (t AttachDependenciesTransformer) Transform(g *Graph) error {
257257
// dedupe addrs when there's multiple instances involved, or
258258
// multiple paths in the un-reduced graph
259259
depMap := map[string]addrs.ConfigResource{}
260-
for _, d := range g.Ancestors(v) {
261-
var addr addrs.ConfigResource
262260

261+
// since we need to type-switch over the nodes anyway, we're going to
262+
// insert the address directly into depMap and forget about the returned
263+
// set.
264+
g.FirstAncestorsWith(v, func(d dag.Vertex) bool {
265+
var addr addrs.ConfigResource
263266
switch d := d.(type) {
264-
case GraphNodeResourceInstance:
265-
instAddr := d.ResourceInstanceAddr()
267+
case GraphNodeCreator:
268+
// most of the time we'll hit a GraphNodeConfigResource first since that represents the config structure, but
269+
instAddr := d.CreateAddr()
266270
addr = instAddr.ContainingResource().Config()
267271
case GraphNodeConfigResource:
268272
addr = d.ResourceAddr()
269273
default:
270-
continue
274+
return false
271275
}
272276

273277
if matchesSelf(addr) {
274-
continue
278+
return false
275279
}
276280

277281
depMap[addr.String()] = addr
278-
}
282+
return true
283+
})
279284

280285
deps := make([]addrs.ConfigResource, 0, len(depMap))
281286
for _, d := range depMap {
@@ -381,10 +386,10 @@ func (m ReferenceMap) dependsOn(g *Graph, depender graphNodeDependsOn) ([]dag.Ve
381386
// upstream managed resource in order to check for a planned
382387
// change.
383388
if _, ok := rv.(GraphNodeConfigResource); !ok {
384-
for _, v := range g.Ancestors(rv) {
385-
if isDependableResource(v) {
386-
res = append(res, v)
387-
}
389+
for _, v := range g.FirstAncestorsWith(rv, func(v dag.Vertex) bool {
390+
return isDependableResource(v)
391+
}) {
392+
res = append(res, v)
388393
}
389394
}
390395
}
@@ -454,8 +459,10 @@ func (m ReferenceMap) parentModuleDependsOn(g *Graph, depender graphNodeDependsO
454459
}
455460
}
456461

457-
for _, v := range g.Ancestors(deps...) {
458-
if isDependableResource(v) {
462+
for _, dep := range deps {
463+
for _, v := range g.FirstAncestorsWith(dep, func(v dag.Vertex) bool {
464+
return isDependableResource(v)
465+
}) {
459466
res = append(res, v)
460467
}
461468
}

0 commit comments

Comments
 (0)