perf(interpreter): near-linear variable resolver (fix hang on large stacks) - #212
Merged
patrickchugh merged 2 commits intoSep 14, 2026
Merged
Conversation
…rge stacks) find_replace_values re-resolves the entire attribute string once per nested reference (via replace_module_vars), which is O(refs**depth) and can hang for minutes / exhaust memory on large stacks whose attributes reference several module outputs (e.g. a jsonencode() secret built from other modules' endpoints). This adds, within a single handle_metadata_vars pass: - Memoization of (value, module) -> resolved result. - A re-entrancy guard that returns the string unchanged when the same (value, module) is already being resolved on the stack (that recursion cannot make progress and is the source of the exponential fan-out). - A per-attribute bounded-cost guard (call budget + working-string length) so a value the resolver could never fully resolve anyway (it would otherwise reach the existing recursion_depth>=50 'UNKNOWN' path) bails out in bounded time instead of after a hang. Normal attributes resolve in a handful of small calls and never hit the caps, so their resolved output is unchanged.
swirle13
force-pushed
the
perf/near-linear-variable-resolver
branch
from
September 4, 2026 23:25
a808873 to
cd5c859
Compare
Owner
|
Nice work fixing this nasty bug @swirle13 I have a few small defensive suggestions before approving:
Let me know if you have any questions. Thanks! |
patrickchugh
self-requested a review
September 9, 2026 14:51
patrickchugh
requested changes
Sep 9, 2026
patrickchugh
left a comment
Owner
There was a problem hiding this comment.
Please see my comment under the issue description
…get, cap warning, tests Per @patrickchugh's review on patrickchugh#212: - Clear the in-progress (value, module) marker in a try/finally so an exception never leaves a stale _FRV_STACK entry. - Use str(module) in the cache key; short-circuit paths always return str. - Check the memo cache (and re-entrancy) BEFORE spending the cost budget, so cached hits don't consume _FRV_STATE. - Warn (once per attribute) when the cost cap is hit, so stacks that trip the guard are visible. - Add tests/interpreter_perf_unit_test.py: normal resolution, cache-hit budget, re-entrancy, exception stack-cleanup, both cost caps, and a bounded-runtime regression guard for a deeply mutually-referential attribute.
Contributor
Author
|
Thanks for the thorough review @patrickchugh — all five addressed in ac27cce:
Let me know if you'd prefer the caps exposed as CLI flags/env vars rather than module constants — easy to add. |
Owner
|
@swirle13 Thanks for the PR - appreciate your contribution. I will be making some change to the test suite to make it more robust but will merge this as is first. Cheers. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
modules.interpreter.find_replace_valuescan hang for many minutes (and grow tomultiple GB of RSS) on larger stacks. This PR makes the variable resolver
near-linear so those stacks render in seconds, while leaving the resolved
output of normal stacks unchanged.
This is unrelated to #209 (that fixed an interactive
terraformstdin prompthang). This one is an algorithmic blow-up inside the resolver itself.
Root cause
find_replace_valuesresolves a resource attribute string by regex-scanning itfor
var./local./data./module.references and substituting each.The problem is in
replace_module_vars: when a referenced module output'svalue itself contains further unresolved references, it recurses by calling
find_replace_valueson the entire attribute string again (not just on thesub-value), once per reference:
For an attribute with R references this fans out roughly O(R^depth) (capped
only by
recursion_depth >= 50), and the working string can grow on each pass asoutputs expand. The pathological shape is common: e.g. a
jsonencode({...})secret assembled from several other modules' endpoints/ARNs, where those values
are themselves references.
The fix (within one
handle_metadata_varspass)(value, module) -> resolved result.(value, module)is already beingresolved on the call stack, return it unchanged. That recursion cannot make
further progress on that exact string and is the source of the fan-out.
_FRV_MAX_CALLS) and aworking-string length cap (
_FRV_MAX_LEN). This only fires on values theresolver could never fully resolve anyway (they would otherwise reach the
existing
recursion_depth >= 50→"UNKNOWN"path) — so it fails in boundedtime instead of after a long hang.
Caches are module-level and cleared at the start of each
handle_metadata_varspass, so nothing leaks across CLI invocations.
Before / after (real-world)
Measured on real AWS Terraform stacks (planfile mode, so only the enrichment
pipeline is timed):
jsonencode()secret referencing ~8 module outputsProfiling the 15 enrichment steps on the ~115-resource stack showed
resolve_all_variableswas the sole hotspot (every other step < 0.1 s);after the patch it is ~27 s and the whole run is ~33 s.
Correctness
caps, so their resolved values are unchanged.
resulting diagram is structurally identical — same node set and same edges;
the only diff is TerraVision's own random per-run node UUIDs.
Notes
_FRV_MAX_CALLS/_FRV_MAX_LENare module constants and easy to tune; happyto make them configurable (env var / flag) or to split this into a
memoization-only change plus a separate discussion on the cap if you'd prefer.
replace_module_varsto resolve thereferenced output value in isolation and substitute it, rather than
re-resolving the whole string — but that is a larger, behavior-sensitive change
and I kept this PR minimal.