Skip to content

Commit 35e2af6

Browse files
ostermanclaudeaknysh
authored
Add circular dependency detection for YAML functions (#1708)
* Add circular dependency detection for YAML functions ## what - Implement universal circular dependency detection for all Atmos YAML functions (!terraform.state, !terraform.output, atmos.Component) - Add goroutine-local resolution context for cycle tracking - Create comprehensive error messages showing dependency chains - Fix missing perf.Track() calls in Azure backend wrapper methods - Refactor code to meet golangci-lint complexity limits ## why - Users experiencing stack overflow panics from circular dependencies in component configurations - Need to detect cycles before they cause panics and provide actionable error messages - Performance tracking required for all public functions per Atmos conventions - Reduce cyclomatic complexity and function length for maintainability ## references - Fixes community-reported stack overflow issue in YAML function processing - See docs/prd/circular-dependency-detection.md for architecture - See docs/circular-dependency-detection.md for user documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove non-deliverable summary file ## what - Remove CIRCULAR_DEPENDENCY_DETECTION_SUMMARY.md ## why - This was a process artifact, not part of the deliverable - Keep only the PRD and user documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Add blog post for circular dependency detection feature ## what - Add blog post announcing YAML function circular dependency detection - Concise explanation of the feature and its benefits - Clear example of error message with call stack ## why - Minor/major PRs require blog posts (CI enforced) - Users need to know about this important bug fix and enhancement 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix goroutine safety and memory leak issues in circular dependency detection ## what - Fix getGoroutineID to use growing buffer and defensive parsing to prevent panics - Fix unsafe require.* calls inside goroutines in tests - Fix resolution context lifecycle to prevent memory leaks and cross-call contamination ## why - getGoroutineID could panic if stack trace was truncated or parsing failed - require.* calls FailNow in goroutines which is unsafe and can cause test hangs - Resolution contexts persisted indefinitely causing memory leaks across calls ## how - getGoroutineID now grows buffer dynamically (up to 8KB) and returns "unknown" on parse failure - Tests now use channels to collect errors from goroutines and assert in main goroutine - ProcessCustomYamlTags now uses scoped context: save existing, install fresh, restore on exit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Rename blog post to .mdx extension for CI detection ## what - Rename blog post from .md to .mdx extension ## why - GitHub Action checks for .mdx files specifically - CI was not detecting the changelog entry with .md extension 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Andriy Knysh <aknysh@users.noreply.github.com>
1 parent 9cad901 commit 35e2af6

20 files changed

+2210
-22
lines changed
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
# Circular Dependency Detection in Atmos
2+
3+
## Overview
4+
5+
Atmos now automatically detects circular dependencies in YAML functions and template functions, preventing infinite recursion and stack overflow errors.
6+
7+
## How It Works
8+
9+
The cycle detection system uses a goroutine-local resolution context that tracks all component dependencies during YAML function resolution. When a circular dependency is detected, Atmos immediately stops processing and returns a detailed error message.
10+
11+
## Protected Functions
12+
13+
The following functions are automatically protected against circular dependencies:
14+
15+
- **`!terraform.state`** - Reading Terraform state from backends
16+
- **`!terraform.output`** - Executing Terraform output commands
17+
- **`!store.get`** - Retrieving values from external stores
18+
- **`!store`** - Store function calls
19+
- **`atmos.Component()`** - Template function for component lookups
20+
21+
## Error Messages
22+
23+
When a circular dependency is detected, you'll see an error like this:
24+
25+
```
26+
Error: circular dependency detected in identity chain
27+
28+
Dependency chain:
29+
1. Component 'vpc' in stack 'core'
30+
→ !terraform.state vpc staging attachment_ids
31+
2. Component 'vpc' in stack 'staging'
32+
→ !terraform.state vpc core transit_gateway_id
33+
3. Component 'vpc' in stack 'core' (cycle detected)
34+
→ !terraform.state vpc staging attachment_ids
35+
36+
To fix this issue:
37+
- Review your component dependencies and break the circular reference
38+
- Consider using Terraform data sources or direct remote state instead
39+
- Ensure dependencies flow in one direction only
40+
```
41+
42+
## Common Circular Dependency Patterns
43+
44+
### Direct Circular Dependency
45+
46+
Two components that depend on each other:
47+
48+
```yaml
49+
# stacks/core.yaml
50+
components:
51+
terraform:
52+
vpc:
53+
vars:
54+
transit_gateway_attachments: !terraform.state vpc staging attachment_ids
55+
56+
# stacks/staging.yaml
57+
components:
58+
terraform:
59+
vpc:
60+
vars:
61+
transit_gateway_id: !terraform.state vpc core transit_gateway_id
62+
```
63+
64+
**Problem**: Core depends on Staging, Staging depends on Core → Cycle!
65+
66+
### Indirect Circular Dependency
67+
68+
Multiple components forming a dependency cycle:
69+
70+
```yaml
71+
# Component A depends on Component B
72+
component-a:
73+
vars:
74+
dependency: !terraform.state component-b stack-b value
75+
76+
# Component B depends on Component C
77+
component-b:
78+
vars:
79+
dependency: !terraform.state component-c stack-c value
80+
81+
# Component C depends on Component A
82+
component-c:
83+
vars:
84+
dependency: !terraform.state component-a stack-a value
85+
```
86+
87+
**Problem**: A → B → C → A creates a cycle!
88+
89+
### Mixed Function Circular Dependency
90+
91+
Cycles can occur across different function types:
92+
93+
```yaml
94+
# Component A uses !terraform.state
95+
component-a:
96+
vars:
97+
output: !terraform.state component-b stack-b value
98+
99+
# Component B uses atmos.Component() in templates
100+
component-b:
101+
vars:
102+
config: '{{ (atmos.Component "component-a" "stack-a").outputs.value }}'
103+
```
104+
105+
**Problem**: A → B → A cycle across different function types!
106+
107+
## How to Fix Circular Dependencies
108+
109+
### 1. Identify the Dependency Chain
110+
111+
Look at the error message to understand the full dependency chain. The error shows each step in the cycle.
112+
113+
### 2. Break the Cycle
114+
115+
Choose one of these strategies:
116+
117+
**Option A: Use Terraform Data Sources**
118+
119+
Instead of using `!terraform.state` bidirectionally, use Terraform data sources in one direction:
120+
121+
```hcl
122+
# In the Terraform component code
123+
data "terraform_remote_state" "core_vpc" {
124+
backend = "s3"
125+
config = {
126+
bucket = "my-terraform-state"
127+
key = "core/vpc/terraform.tfstate"
128+
}
129+
}
130+
```
131+
132+
**Option B: Introduce an Intermediate Component**
133+
134+
Create a shared component that both depend on:
135+
136+
```yaml
137+
# stacks/shared.yaml
138+
components:
139+
terraform:
140+
transit-gateway:
141+
vars:
142+
# Core configuration
143+
144+
# stacks/core.yaml
145+
components:
146+
terraform:
147+
vpc:
148+
vars:
149+
transit_gateway_id: !terraform.state transit-gateway shared tgw_id
150+
151+
# stacks/staging.yaml
152+
components:
153+
terraform:
154+
vpc:
155+
vars:
156+
transit_gateway_id: !terraform.state transit-gateway shared tgw_id
157+
```
158+
159+
**Option C: Restructure Dependencies**
160+
161+
Ensure dependencies flow in one direction only. Dependencies should form a DAG (Directed Acyclic Graph):
162+
163+
```
164+
Shared Services → Core Infrastructure → Application Stacks
165+
```
166+
167+
### 3. Apply Dependencies in Order
168+
169+
Use `atmos workflow` to apply components in the correct order:
170+
171+
```yaml
172+
# workflows/vpc-setup.yaml
173+
name: vpc-setup
174+
workflows:
175+
deploy:
176+
steps:
177+
- command: terraform apply
178+
component: transit-gateway
179+
stack: shared
180+
- command: terraform apply
181+
component: vpc
182+
stack: core
183+
- command: terraform apply
184+
component: vpc
185+
stack: staging
186+
```
187+
188+
## Architecture
189+
190+
The cycle detection uses:
191+
192+
1. **Goroutine-Local Storage**: Each goroutine maintains its own resolution context
193+
2. **Call Stack Tracking**: Records each component/stack being resolved
194+
3. **Visited Set**: O(1) lookup to detect when revisiting a component
195+
4. **Automatic Cleanup**: Resolution context is cleared after processing completes
196+
197+
## For Developers
198+
199+
### Adding Cycle Detection to New Functions
200+
201+
To add cycle detection to a new YAML or template function:
202+
203+
```go
204+
func processTagMyFunction(
205+
atmosConfig *schema.AtmosConfiguration,
206+
input string,
207+
currentStack string,
208+
resolutionCtx *ResolutionContext,
209+
) any {
210+
// ... parse input ...
211+
212+
// Add cycle detection
213+
if resolutionCtx != nil {
214+
node := DependencyNode{
215+
Component: component,
216+
Stack: stack,
217+
FunctionType: "my.function",
218+
FunctionCall: input,
219+
}
220+
221+
if err := resolutionCtx.Push(atmosConfig, node); err != nil {
222+
errUtils.CheckErrorPrintAndExit(err, "", "")
223+
}
224+
defer resolutionCtx.Pop(atmosConfig)
225+
}
226+
227+
// ... execute function logic ...
228+
}
229+
```
230+
231+
### Testing Cycle Detection
232+
233+
See `internal/exec/yaml_func_circular_deps_test.go` for comprehensive test examples.
234+
235+
## References
236+
237+
- [PRD: Circular Dependency Detection](prd/circular-dependency-detection.md)
238+
- [Error Handling Strategy](prd/error-handling-strategy.md)
239+
- [Testing Strategy](prd/testing-strategy.md)

0 commit comments

Comments
 (0)