[BUG] Master/API/Minion resource leaks under sustained load (process, FD, memory)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 15.7k
- Forks
- 5.6k
- Avg merge
- 2d 44m
- Merged PRs (30d)
- 80
Description
Description
Under sustained load on 3008.x (thousands of events/sec, rapid-fire netapi logins, highstate loops), the master, salt-api, and minion processes leak processes, file descriptors, and memory. Master RSS balloons past 7 GB and eventually OOMs. Investigation on 3006.x reproduced the same root causes; they are present in 3008.x head.
Filing as one tracking issue for the whole set — each sub-item has an independent fix.
Reproduction
Stress harness in tests/monitoring/ (docker-compose master + minions + salt-api + Prometheus/Grafana). Drivers:
flood_events.py— fire salt events at ~5,000/sec into the event bus.stress_test.sh— parallelstate.apply/test.ping/manage.statusloops.stress_api.sh— CherryPy netapi login +client=local/client=runnerin a tight loop.
Observed within minutes: MWorker RSS climbs unbounded, master FD count climbs, zombie children accumulate. Within an hour master hits multi-GB RSS.
Leak inventory (root causes identified)
Process / FD leaks
salt/utils/timed_subprocess.py—TimedProc.kill()does notwait(), orphaning zombies to PID 1 after timeout.salt/utils/event.py—SaltEvent/MasterEventhave no context-manager protocol and no full teardown; ZMQ sockets and event-bus connections outlive their callers.salt/netapi/__init__.py—NetapiClientinstantiatesLocalClient/RunnerClient/WheelClient/SSHClientper request without teardown. Primary source of the CherryPy TCP/RAM leak.salt/runner.py,salt/wheel/__init__.py— nodestroy(); callers using bare instantiation leak per-call.
Memory leaks / cache bloat
salt/loader/lazy.py—LazyLoadernever purges_dict,loaded_modules, orsys.modulesentries. Every reload leaks the module set → "module creep" on both master and minion.salt/utils/context.py,salt/utils/ctx.py—ContextDict/RequestContext.current_requestnot cleared on teardown; per-request state pinned indefinitely.salt/master.py,salt/daemons/masterapi.py—AESFuncs/RemoteFuncsbound methods (e.g._serve_file) create refcycles that pinFileserverand friends past worker recycle.salt/utils/args.py—get_function_argspec()defined_ArgSpec = namedtuple(...)at function scope, dynamically compiling a fresh class (C-level struct) on every module call. At high call rates this is a fast, unbounded leak.- Worker recycling + PyZMQ
asyncio.Task: closingZMQStreamsockets mid-flight while_run_callbackhas wrapped coroutines viaasyncio.ensure_futurepermanently leaks the Task objects into the running loop. Aggressiveworker_resource_backcountrecycling made this worse, not better. - Destructor cascades missing on the loader-owning classes —
Fileserver,Cache,Roster,MasterMinion,RemoteFuncs/LocalFuncs,NetapiClient,LoadAuth,Runner. Even when callers do the right thing, the ownedLazyLoaders stay insys.modules.
Scalability
salt/tokens/localfs.py—list_tokensusesos.walkand returns a full list; on token dirs with 1M+ entries this is a GB-scale spike duringclean_expired_tokens. Same code path insalt/daemons/masterapi.pyMaintenance.
IPC
EventPublisherinherits Tornado's defaultipc_write_buffer(~27 GB effective). A slow consumer causes unbounded RAM growth on the publisher instead of a disconnect.
Fix status (staged locally on 3006.x branch dwoz/3006leak; forward-porting to 3008.x)
TimedProc.wait()afterkill().SaltEvent/MasterEventcontext managers + fulldestroy().NetapiClientuseswithfor all sub-clients;Runner/Wheelgaindestroy()and context-manager use.LazyLoader.destroy()explicitly purges_dict,loaded_modules, and matchingsys.moduleskeys.ContextDict.destroy();RequestContextclearscurrent_request.AESFuncs.destroy()/RemoteFuncs.destroy()nullify bound-method attributes (self._serve_file = None, etc.) to break the refcycle._ArgSpecnamedtuple hoisted to module scope insalt/utils/args.py.worker_resource_backcountrecycle path removed — workers cache execution context; saves ~600 MB globally and eliminates the PyZMQ Task leak. (If a bounded reset is still wanted, it must first drain outstanding ZMQStream callbacks.)- Destructor cascades wired into
Fileserver,Cache,Roster,MasterMinion,RemoteFuncs,LocalFuncs,NetapiClient,LoadAuth,Runner. list_tokensconverted toos.scandir-based generator;Maintenance.clean_expired_tokensconsumes the generator (constant memory).- Recommend a sane default cap for
ipc_write_bufferonEventPublisher(or hard cap + disconnect on overflow).
Post-fix validation
- Master baseline ~70 MB / peak ~155 MB RSS (was 7 GB+ before OOM).
- Minion ~108 MB stable (was 1.3 GB).
- salt-api ~53 MB stable.
- Master FDs stable at ~1,300 under ~5,700 events/sec.
Remaining slow leak (unresolved)
After the above, MWorker still creeps ~25 MB/hr under sustained stress; MWorkerQueue occasionally accumulates. Profiler shows ReferenceType, Context, generator, and Task counts ticking upward. Suspects (unconfirmed):
- Residual
salt.ext.tornado.zmq.eventloop.zmqstreamfuture retention. - Unbounded cache inside a hot execution module (
test.ping,manage.status,grains.items) or in CherryPy session handling. IOLoop.add_futurecallbacks not clearing on external-auth paths.
Will file a follow-up narrowly-scoped issue once tracemalloc/objgraph isolate it.
Target
3008.x. 3006.x carries the same code paths (verified) but is LTS — separate backport scope.
salt --versions-report
3008.x head (branch tip at time of filing). Reproduction environment is Ubuntu 24.04 containers under docker-compose (see tests/monitoring/).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the stress harness in tests/monitoring/ and choose one independently scoped leak item, then read its named entry point such as salt/utils/timed_subprocess.py, salt/utils/event.py, or salt/loader/lazy.py. Run the relevant stress driver and add or update focused tests where the subsystem provides them. Done means the selected resource remains bounded under the stated workload and the harness reproduces the reported validation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker-compose, grafana, prometheus, python
- Domain
- backend, distributed-systems, observability-sre, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100