Details
-
Bug
-
Status: In Review (View Workflow)
-
Critical
-
Resolution: Unresolved
-
23.02.24
-
None
-
None
-
Rocky Linux 8 (cgroup v1), MariaDB Enterprise Server 10.6.15-10, ColumnStore 23.02.24
-
2026-11, 2026-12
Description
Summary
When PrimProc's ExeMgr memory guard (RssMonFcn) decides RSS is too high, it logs CAL0044 ("Too much memory allocated!") and calls exit(1). That exit(1) never finishes. The process stays alive with its full RSS, so systemd never restarts it (Restart=on-failure never triggers).
The effect in production: the node keeps thrashing until it is unresponsive or CMAPI fails it over. Queries on the other PMs wait on it indefinitely, which users see as stuck queries and an unresponsive node. In one of our runs, the node that took over the failed node's dbroot then hit the same guard and wedged too, until it was rebooted.
The hang is deterministic and doesn't need memory pressure. Any exit() in the forked PrimProc child hits it. The quick repro below takes about 5 minutes on an idle node.
Why it hangs
All links point to tag 23.02.24. The same code is unchanged on stable-23.10 (ab3d31bd1, checked 2026-10-06).
- A namespace-scope static FEMsgHandler::threadPool is defined at femsghandler.cpp:28. Its constructor (threadpool.cpp:40) calls init(), which starts fPruneThread (threadpool.cpp:75). This happens during static initialization, before main().
- main() → ServicePrimProc::Run() → Service::RunForking() calls fork() (service.h:53). The systemd unit starts PrimProc without -f, so production always forks. Only the forking thread exists in the child, and the child is the real PrimProc. The prune thread is gone.
- Nothing re-creates it in the child: init() is private and only called from constructors, and there is no pthread_atfork handler anywhere in the tree.
- RssMonFcn::operator() calls exit(1) (rssmonfcn.cpp:43 when MaxPct >= 95, rssmonfcn.cpp:53 otherwise). exit() runs static destructors: ~ThreadPool() → stop() → fPruneThread->join() (threadpool.cpp:135). That is an untimed boost join waiting on a done flag that fork copied as false and that no thread in the child will ever set. It waits forever.
This hazard is already known and worked around in one place. ServiceDMLProc::Parent() calls jobstepThreadPool.stop() with the comment "otherwise it would get stuck when trying to join fPruneThread" (dmlproc.cpp:116-124). That fix only covers DMLProc's parent side.
How it got here
| Commit | Date | Change |
|---|---|---|
| e2d66222c | 2017-01-25 | |
| 94dfacfe2 | 2018-09-28 | |
| ab44ef6dd | 2020-10-07 | |
| e17469635 | 2022-03-02 |
Fixing FEMsgHandler's pool alone isn't enough
- A second pre-fork pool. JobStep::jobstepThreadPool (jobstep.cpp:61, in libjoblist.so, linked into PrimProc) is also a static with a prune thread started before the fork. Shared-library statics are destroyed last, so today exit() blocks on FEMsgHandler's pool first. Fix that one and it blocks on this one next.
- stop() also joins busy workers. fThreads.join_all() (threadpool.cpp:138) waits for workers that may be mid-query. In our vmcore one FEMsgHandler worker was still in threadFcn.
- Other exit() sites reachable in the PrimProc child should hang the same way:
- rssmonfcn.cpp:43 and :53 (ExeMgr RSS guard)
- MonitorProcMem.cpp:70 (PrimProc's own memory guard, started at primproc.cpp:554)
- primitiveserver.cpp:2302 (block-cache allocation failure)
- inetstreamsocket.cpp:193 and :202 (setsockopt failures)
- dbrm.cpp:128
- ServicePrimProc::Child() returning, which returns from main() (primproc.cpp:726) and so calls exit() (primproc.cpp:337, :708)
- Signal-driven exits are not affected, because crashtrace re-raises with SIG_DFL.
How to reproduce (no data needed, about 5 minutes)
Tested on Rocky 8 (cgroup v1), ES 10.6.15-10 / ColumnStore 23.02.24, idle PrimProc. This drives the real RssMonFcn code path with no debugger injection. fMemTotal comes from the cgroup named in SystemConfig/CGroup, so we give PrimProc a small cgroup limit and a 1% threshold, which makes the guard fire right at startup.
# Back up the config first.
|
sudo cp -p /etc/columnstore/Columnstore.xml /etc/columnstore/Columnstore.xml.pre-exittest |
sudo systemctl stop mcs-primproc |
F=/etc/columnstore/Columnstore.xml |
|
|
# 1. ExeMgr guard threshold to 1%. The section is ExeMgr1 (resourcemanager.cpp:59), not ExeMgr.
|
sudo sed -i "0,/<ExeMgr1>/s|<ExeMgr1>|<ExeMgr1>\n <MaxPct>1</MaxPct>|" $F |
|
|
# 2. Make CGroupConfigurator read the unit's cgroup, then cap it at 2 GB (runtime only).
|
sudo sed -i "0,/<SystemConfig>/s|<SystemConfig>|<SystemConfig>\n <CGroup>system.slice/mcs-primproc.service</CGroup>|" $F |
sudo systemctl set-property --runtime mcs-primproc MemoryLimit=2G |
|
|
# 3. Start PrimProc. Idle RSS (~300 MB) is ~14% of 2 GB, so the guard fires immediately.
|
sudo systemctl start mcs-primproc |
P=$(systemctl show -p MainPID --value mcs-primproc); echo "PID=$P" |
sudo journalctl -u mcs-primproc -f # wait for "Too much memory allocated!" + CAL0044, then Ctrl-C |
|
|
# 4. Observe: same PID minutes later and NRestarts=0. A real exit would have produced a new PID.
|
systemctl show -p MainPID -p NRestarts mcs-primproc
|
sudo eu-stack -p $P | grep -B3 -A8 __run_exit_handlers |
|
|
# 5. Clean up.
|
sudo systemctl stop mcs-primproc |
sudo systemctl revert mcs-primproc && sudo systemctl daemon-reload |
sudo cp -p /etc/columnstore/Columnstore.xml.pre-exittest $F |
Expected: PrimProc exits after CAL0044, and systemd restarts it with a new PID.
Actual: CAL0044 was logged at 23:00:46. At 23:05:45 the same PID was still alive with NRestarts=0. The exiting thread is parked here:
#3 threadpool::ThreadPool::stop()
|
#4 threadpool::ThreadPool::~ThreadPool()
|
#5 __run_exit_handlers
|
#6 exit
|
#7 exemgr::RssMonFcn::operator()() const
|
#8 std::thread::_State_impl<std::thread::_Invoker<std::tuple<exemgr::RssMonFcn> > >::_M_run()
|
Full stack of every thread in the hung process: live-exittest-eustack.txt![]()
Confirmed under a real workload too
We hit the same thing on a 3-node cluster (m5.4xlarge, 62 GiB, Rocky 8, CMAPI failover on, EFS shared storage) running a large daily aggregation with TotalUmMemory 60% and NumBlocksPct 30. About 27-30 minutes in, one node's swap fills, the ExeMgr guard logs CAL0044, and PrimProc fails to exit. We captured a full vmcore with sysrq-c 60 s after CAL0044 and extracted a PrimProc core from it (gdb excerpt: vmcore-run3-exit-thread-bt.txt
):
- The exiting LWP is in RssMonFcn → exit(1) → __run_exit_handlers → ~ThreadPool(this=FEMsgHandler::threadPool) → stop() → boost::thread::join (futex wait), with fStop == true.
- The prune thread's boost thread_data has done == 0. Its pthread handle now belongs to a different live thread (the ExeMgr MessageQueueServer::accept loop): glibc reclaimed the pre-fork thread's stack and reused it. No thread runs pruneThread() for this pool.
Related but separate: DistributedEngineComm::read has no timeout, so one hung PM stalls queries cluster-wide.
Possible resolution (for the assigned engineer to decide)
These are suggestions from the investigation. The right approach is up to the engineer who picks this up.
- Narrow hotfix: call _exit() (or quick_exit() with no handlers registered) instead of exit() at the fatal sites listed above, so static destructors never run in the child. That avoids both pools and the join_all. Logging at those sites is a synchronous syslog call and cerr is unbuffered, so nothing should be lost.
- Structural hardening: don't start threads in a static object's constructor. For example, start the prune thread lazily on the first invoke() (which would also cover jobstepThreadPool, shared by DMLProc/DDLProc), or construct these pools after the fork.
- Probably not enough on its own: a pid check inside ThreadPool::stop(). It would fix the prune-thread join but not fThreads.join_all().
Attachments
Issue Links
- relates to
-
MCOL-6614 messageqcpp clients (DBRM client, DEC, WEClients) have no read timeout; one hung or vanished peer stalls callers forever
-
- Open
-