Skip to content

Commit 0846576

Browse files
committed
feat(tree): add Components() to group cost-coupled resources
Components() partitions a tree's resources into connected components based on their Relationships, so callers can reason about which resources' costs are coupled. Modifying a resource can only change the cost of resources in its own component, which lets savings be recomputed over one component instead of the whole tree. Resources are matched by Definition.Address rather than pointer identity so value-copied relationship targets (e.g. []T fields) are handled the same as pointer targets.
1 parent 77d9049 commit 0846576

2 files changed

Lines changed: 431 additions & 0 deletions

File tree

pkg/tree/components.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package tree
2+
3+
import "reflect"
4+
5+
// relationshipsField is the conventional name of the struct field, present on
6+
// resource types that link to other resources, that holds those links (e.g.
7+
// ec2.Instance.Relationships). It is the only place cross-resource pointers
8+
// live, so it is the only field Components walks. The field is tagged `tree:"-"`
9+
// so it is never serialized.
10+
const relationshipsField = "Relationships"
11+
12+
// Component is a set of resources whose costs are coupled: each resource is
13+
// reachable from the others by following Relationships links (in either
14+
// direction). Because a resource's cost function can only observe other
15+
// resources through its Relationships, modifying one resource can only change
16+
// the cost of resources within the same component. This lets callers (e.g. the
17+
// savings calculation) recompute costs over a single component instead of the
18+
// whole tree.
19+
type Component []Resource
20+
21+
// Addresses returns the string form of every resource address in the component,
22+
// in the component's resource order. Resources with no address are skipped.
23+
func (c Component) Addresses() []string {
24+
out := make([]string, 0, len(c))
25+
for _, r := range c {
26+
if addr := r.GetBase().Definition.Address; addr != nil {
27+
out = append(out, addr.String())
28+
}
29+
}
30+
return out
31+
}
32+
33+
// Components partitions the tree's supported resources into connected
34+
// components based on their Relationships. Two resources share a component when
35+
// one references the other — directly or transitively — through a Relationships
36+
// field. A resource with no relationships forms a component of one.
37+
//
38+
// Relationships must already be linked, so call PostProcess before Components
39+
// (FromProto callers get this via the standard round-trip). Resources are
40+
// matched by Definition.Address, so relationship targets held by value (e.g.
41+
// []T fields) are treated identically to pointer targets. Resources without an
42+
// address cannot be matched and are returned as singleton components.
43+
//
44+
// The result is deterministic: components appear in the order their first
45+
// resource appears in ToResources, and resources within a component keep that
46+
// same order.
47+
func (t *Tree) Components() []Component {
48+
resources := t.ToResources(false)
49+
n := len(resources)
50+
51+
uf := newUnionFind(n)
52+
53+
// Index resources by address so relationship links (which may be value
54+
// copies, not pointers into the canonical slices) can be resolved back to
55+
// the resource they name.
56+
indexByAddress := make(map[string]int, n)
57+
for i, r := range resources {
58+
if addr := r.GetBase().Definition.Address; addr != nil {
59+
indexByAddress[addr.String()] = i
60+
}
61+
}
62+
63+
for i, r := range resources {
64+
rel := reflect.ValueOf(r).Elem().FieldByName(relationshipsField)
65+
if !rel.IsValid() {
66+
continue
67+
}
68+
forEachRelatedAddress(rel, func(addr string) {
69+
if j, ok := indexByAddress[addr]; ok {
70+
uf.union(i, j)
71+
}
72+
})
73+
}
74+
75+
// Group resources by their component root, preserving first-seen order.
76+
groups := make(map[int]Component, n)
77+
order := make([]int, 0, n)
78+
for i, r := range resources {
79+
root := uf.find(i)
80+
if _, seen := groups[root]; !seen {
81+
order = append(order, root)
82+
}
83+
groups[root] = append(groups[root], r)
84+
}
85+
86+
out := make([]Component, 0, len(order))
87+
for _, root := range order {
88+
out = append(out, groups[root])
89+
}
90+
return out
91+
}
92+
93+
// forEachRelatedAddress walks a Relationships struct value and calls visit with
94+
// the address of every resource it links to, across *T, []*T and []T fields.
95+
func forEachRelatedAddress(rel reflect.Value, visit func(string)) {
96+
for i := range rel.NumField() {
97+
if !rel.Type().Field(i).IsExported() {
98+
continue
99+
}
100+
collectResourceAddresses(rel.Field(i), visit)
101+
}
102+
}
103+
104+
func collectResourceAddresses(v reflect.Value, visit func(string)) {
105+
switch v.Kind() {
106+
case reflect.Pointer:
107+
if v.IsNil() {
108+
return
109+
}
110+
if addr, ok := addressOf(v); ok {
111+
visit(addr)
112+
}
113+
case reflect.Slice:
114+
for i := range v.Len() {
115+
collectResourceAddresses(v.Index(i), visit)
116+
}
117+
case reflect.Struct:
118+
// A value (non-pointer) relationship target — GetBase has a pointer
119+
// receiver, so take the element's address to satisfy the interface.
120+
if v.CanAddr() {
121+
if addr, ok := addressOf(v.Addr()); ok {
122+
visit(addr)
123+
}
124+
}
125+
}
126+
}
127+
128+
// addressOf returns the address string of v if it is a resource with a
129+
// non-nil address.
130+
func addressOf(v reflect.Value) (string, bool) {
131+
if !v.CanInterface() {
132+
return "", false
133+
}
134+
r, ok := v.Interface().(Resource)
135+
if !ok {
136+
return "", false
137+
}
138+
base := r.GetBase()
139+
if base == nil || base.Definition.Address == nil {
140+
return "", false
141+
}
142+
return base.Definition.Address.String(), true
143+
}
144+
145+
// unionFind is a disjoint-set structure with path halving and union by size.
146+
type unionFind struct {
147+
parent []int
148+
size []int
149+
}
150+
151+
func newUnionFind(n int) *unionFind {
152+
uf := &unionFind{parent: make([]int, n), size: make([]int, n)}
153+
for i := range uf.parent {
154+
uf.parent[i] = i
155+
uf.size[i] = 1
156+
}
157+
return uf
158+
}
159+
160+
func (uf *unionFind) find(x int) int {
161+
for uf.parent[x] != x {
162+
uf.parent[x] = uf.parent[uf.parent[x]] // path halving
163+
x = uf.parent[x]
164+
}
165+
return x
166+
}
167+
168+
func (uf *unionFind) union(a, b int) {
169+
ra, rb := uf.find(a), uf.find(b)
170+
if ra == rb {
171+
return
172+
}
173+
if uf.size[ra] < uf.size[rb] {
174+
ra, rb = rb, ra
175+
}
176+
uf.parent[rb] = ra
177+
uf.size[ra] += uf.size[rb]
178+
}

0 commit comments

Comments
 (0)