Skip to content

Fix 2^n slowdown in wrap_sysconfig by making it idempotent (#321) - #325

Merged
dwoz merged 2 commits into
mainfrom
fix/issue-321-wrap-sysconfig-idempotence
Aug 26, 2026
Merged

dwoz merged 2 commits into
mainfrom
fix/issue-321-wrap-sysconfig-idempotence

Conversation

@dwoz

@dwoz dwoz commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Closes #321.

Summary

`bootstrap()` re-runs every time the relenv `.pth` file is re-executed, and `importlib.reload(site)` does exactly that. Salt calls `importlib.reload(site)` from `state.module_refresh()`, so on a long-lived minion each state refresh stacks another wrapper around `sysconfig.get_config_var`, `get_config_vars`, and `get_paths`.

Because `get_config_vars_wrapper` calls its wrapped function twice per invocation, N stacked layers cost 2^N calls to the innermost stdlib function. Reporter measured 46 layers on a wedged minion, at which depth a single `sysconfig.get_config_var()` call takes on the order of years to return; the process spins at 100% CPU forever and highstates never complete.

Fix

Guard `wrap_sysconfig` with an early-return when the wrapper is already installed. `functools.wraps` sets `wrapped` on our wrapper, so its presence is a reliable "already wrapped" marker (per the reporter's minimal verified patch in the issue).

Test plan

  • New regression test `test_wrap_sysconfig_is_idempotent` walks the `wrapped` chain to recover the raw stdlib functions (previous tests in the module may have already wrapped), calls `wrap_sysconfig` three times, and asserts the module attributes are still the same wrapper objects the first call installed — no stacking.
  • Confirmed the test fails without the runtime fix and passes with it (`git stash push -- relenv/runtime.py` verification).
  • All 86 tests in `tests/test_runtime.py` pass locally.

dwoz added 2 commits August 25, 2026 17:59
bootstrap() is re-entered every time the relenv .pth file is re-executed,
and importlib.reload(site) does exactly that. Salt calls
importlib.reload(site) from state.module_refresh(), so on a long-lived
minion each state refresh stacks another wrapper around
sysconfig.get_config_var, get_config_vars, and get_paths.

Because get_config_vars_wrapper calls its wrapped function twice per
invocation, N stacked layers cost 2**N calls to the innermost stdlib
function. Reported measurement: 46 layers on a wedged minion, at which
depth a single sysconfig.get_config_var() call takes on the order of
years to return and the process spins at 100% CPU forever. Highstates
never finish.

Guard wrap_sysconfig with an early-return when the wrapper is already
installed. functools.wraps sets __wrapped__ on our wrapper, so its
presence is a reliable 'already wrapped' marker.

Regression test walks the __wrapped__ chain to recover the raw stdlib
functions (previous tests in the module may have already wrapped),
calls wrap_sysconfig three times, and asserts the module attributes
are still the same wrapper objects that the first call installed --
no stacking.
The previous version referenced Any without importing it, which
ruff caught as F821. object is sufficient here since _unwrap only
uses getattr and does not need any specific typing.Any semantics.
@dwoz
dwoz merged commit 013b754 into main Aug 26, 2026
79 checks passed
dwoz added a commit to saltstack/salt that referenced this pull request Aug 26, 2026
Picks up the wrap_sysconfig idempotence fix (saltstack/relenv#325,
fixes saltstack/relenv#321) that stops the 2^n wrapper stacking that
made state.module_refresh() hang long-lived Python 3.13+ onedir
minions on highstates. Also picks up openssl 3.5.8 from 0.22.24.

Regenerated .github/workflows/{ci,nightly,scheduled,staging}.yml via
the generate-workflows pre-commit hook.

Refs #70142
@bchill

bchill commented Aug 28, 2026

Copy link
Copy Markdown

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wrap_sysconfig re-wraps without an idempotence guard, causing 2ⁿ slowdown that hangs Salt highstates indefinitely

2 participants