Skip to content

Memoize SaltStackVersion construction in warn_until() - #69924

Merged
dwoz merged 2 commits into
saltstack:3008.xfrom
dwoz:dwoz/perf/warn-until-memoize-3008x
Aug 4, 2026
Merged

Memoize SaltStackVersion construction in warn_until()#69924
dwoz merged 2 commits into
saltstack:3008.xfrom
dwoz:dwoz/perf/warn-until-memoize-3008x

Conversation

@dwoz

@dwoz dwoz commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Closes #69921

Summary

  • Wrap the two SaltStackVersion constructions inside salt.utils.versions.warn_until() with functools.lru_cache-backed helpers so hot paths that call warn_until() per event (deprecated TCPPubClient / TCPReqServer / MessageClient aliases) stop allocating fresh SaltStackVersion (and, transitively, packaging.version.Version) objects on every call
  • Fall-through path for the non-hashable inputs (SaltVersion, SaltStackVersion instances) is unchanged; only the common hashable inputs (int, tuple, str) go through the cache
  • Behaviour is otherwise identical — same emitted warning, same RuntimeError on unknown release names, same fall-through logic

Measured impact

memray on the master's EventPublisher under 4h stress showed ~1.4 M packaging.version.Version allocations totalling 2.5 GB in a 90 s window; the counting test in the new suite confirms 100% reduction on the repeated-argument path (10 000 warn_until(3009, ...) calls → 0 SaltStackVersion constructions after cache warmup).

Per-process RSS impact on the WebSocket-transport master, 30 min stress:

EventPublisher peak
Unpatched 271 MB
With this patch 214 MB (-57 / -21%)

Test plan

  • pytest tests/pytests/unit/utils/test_versions_warn_until_cache.py — 9 tests, all passing locally
  • Verify the emitted deprecation warning is unchanged in the resulting stderr
  • Verify unknown release-name inputs still raise RuntimeError with the "Incorrect spelling" message

warn_until() built two fresh SaltStackVersion instances per call — one
for the target-version comparison and one for the running version.
SaltStackVersion.__init__ transitively allocates a packaging.version.
Version. Under stress the master EventPublisher was allocating ~1.4M
Version objects (2.5 GB of transient allocation churn) per 90 s
window; on hot deprecation-warning paths (TCPPubClient, TCPReqServer,
MessageClient) this drove Python's arena high-water mark and pinned
process RSS above its actual working set.

Wrap both constructions in functools.lru_cache-backed helpers:

  _resolve_target_version_hashable  — 32-slot cache keyed on the
      common hashable inputs (int, tuple, str); returns None for
      non-hashable input so the caller falls through to the inline
      path unchanged
  _resolve_current_version          — 8-slot cache keyed on the
      version_info tuple; effectively a one-time construction for a
      given process

After the patch a 10 000-call warn_until loop makes 0 SaltStackVersion
constructions (100% reduction on the repeated-argument path).

WebSocket-transport master EventPublisher RSS peak: 271 MB -> 214 MB
(-57 MB / -21%) under 30 min stress.
Comment thread salt/utils/versions.py
twangboy
twangboy previously approved these changes Aug 3, 2026
``SaltVersion`` is a ``namedtuple`` subclass, so it matches
``isinstance(version, tuple)`` and hits the ``lru_cache`` fast path
added in saltstack#69921. But it defines ``__eq__`` without ``__hash__``, which
makes it unhashable — ``hash(version)`` raises ``TypeError`` before the
cache lookup can even start. On top of that, its tuple form is
``(name, info, released)``, not version parts, so even if it were
hashable the resolver would build the wrong ``SaltStackVersion``.

Route ``SaltVersion`` explicitly to its own branch before the fast path
and add a regression test that guards it.

Fixes CI failure in ``test_warn_until_good_version_argument[version3]``.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:full Run the full test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants