Skip to content

RemoteConsoleEndpoint leaks one non-daemon thread per WebSocket connection (monex 4.x / eXist 6.x) #425

Description

@luckydem

Summary

org.exist.remoteconsole.RemoteConsoleEndpoint (monex 4.x) leaks exactly one
non-daemon JVM thread per WebSocket connection to /exist/rconsole. On a
production eXist 6.4.0 system where an application opens an rconsole socket on
every page load, this exhausted the JVM's native thread limit after ~4 days
(946 of 1003 JVM threads were leaked global.rconsole threads), crashing the
server.

Root cause

@ServerEndpoint classes are instantiated once per connection (JSR-356
default lifecycle). The constructor creates a single-thread
ScheduledExecutorService whose thread (named global.rconsole, non-daemon)
pings all sessions every 500 ms:

public RemoteConsoleEndpoint() {
    ConsoleModule.setAdapter(new RemoteConsoleAdapter(this::sendAll, this::sendAll));
    final ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(
            runnable -> ThreadUtils.newGlobalThread("rconsole", runnable));
    service.scheduleAtFixedRate(this::pingAll, 500, 500, TimeUnit.MILLISECONDS);
}

@OnClose removes the session from the static map but never calls
service.shutdown(), so the executor thread survives forever. One leaked
thread per open/close cycle; the reference is not even kept, so it can never
be shut down later.

Reproduction

Against a stock eXist 6.4.0 + monex 4.2.x install, open and cleanly close N
WebSocket connections to /exist/rconsole, then count threads:

sort /proc/<exist-pid>/task/*/comm | uniq -c | grep rconsole

50 open/close cycles → exactly 50 global.rconsole threads (verified
2026-07-08 on eXist 6.4.0 / monex 4.2.2 and 4.2.4).

Fix (verified)

Store the executor as an instance field and shut it down on close:

private final ScheduledExecutorService service;
// in constructor: this.service = ...
@OnClose
public void closeSession(final Session session, final CloseReason closeReason) {
    sessions.remove(session);
    service.shutdownNow();
}

With this patch, 500 open/close cycles leak 0 threads and
console:send() channel delivery still works.

Why an issue and not a PR

We understand #367 (merged) removed monex's Java for the 5.x line (eXist
7.0+), so the 4.x branch appears frozen. Filing this for disclosure so other
eXist 6.x users hitting mysterious global.rconsole thread growth can find
it — happy to turn the patch above into a PR if a 4.x maintenance release is
ever considered.

A working patched branch (off v4.2.4) is available here for reference:
https://github.com/luckydem/monex/tree/epod/4.2.4-rconsole-thread-fix

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions