Skip to content

Commit 2b7a49e

Browse files
GH Issue 1574: Report connection counts in thread dumps and over JMX (#8062)
## Rationale A connector that hits maxConnections stops accepting while its threads sit idle, meaning there's no clue of the problem in thread dumps for asynchronous requests or websockets. ## Related Pull Requests - LabKey/premiumModules#752 ## Changes - Capture connection counts in thread dumps - Track websockets
1 parent d1003ee commit 2b7a49e

7 files changed

Lines changed: 208 additions & 2 deletions

File tree

‎api/src/org/labkey/api/util/DebugInfoDumper.java‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
1919
import org.apache.commons.io.FileUtils;
2020
import org.apache.logging.log4j.Logger;
21+
import org.jetbrains.annotations.Nullable;
2122
import org.labkey.api.action.NullSafeBindException;
2223
import org.labkey.api.data.ConnectionWrapper;
2324
import org.labkey.api.data.ContainerManager;
@@ -27,6 +28,7 @@
2728
import org.labkey.api.data.dialect.BasePostgreSqlDialect;
2829
import org.labkey.api.files.FileSystemDirectoryListener;
2930
import org.labkey.api.files.FileSystemWatchers;
31+
import org.labkey.api.mbean.LabKeyManagement;
3032
import org.labkey.api.miniprofiler.MiniProfiler;
3133
import org.labkey.api.module.ModuleLoader;
3234
import org.labkey.api.query.QueryForm;
@@ -38,9 +40,13 @@
3840
import org.labkey.api.view.ActionURL;
3941
import org.labkey.api.view.HttpView;
4042
import org.labkey.api.view.ViewContext;
43+
import org.labkey.api.websocket.WebSocketTracker;
4144
import org.labkey.api.writer.PrintWriters;
4245
import org.labkey.vfs.FileLike;
4346

47+
import javax.management.MBeanServer;
48+
import javax.management.ObjectName;
49+
4450
import java.io.File;
4551
import java.io.IOException;
4652
import java.io.PrintWriter;
@@ -356,6 +362,8 @@ public static synchronized void dumpThreads(LoggerWriter logWriter)
356362
logWriter.debug("CPU count: " + osBean.getAvailableProcessors());
357363
}
358364

365+
dumpConnectionCounts(logWriter);
366+
359367
logWriter.debug("*********************************************");
360368

361369
Map<Thread, StackTraceElement[]> stackTraces = Thread.getAllStackTraces();
@@ -448,6 +456,82 @@ public static synchronized void dumpThreads(LoggerWriter logWriter)
448456
}
449457
}
450458

459+
/**
460+
* Connection counts per connector, plus the long-lived populations that fill them. A connector that reaches
461+
* maxConnections stops accepting while its threads sit idle, so the stacks below show an untroubled server.
462+
* See GH Issue 1574.
463+
*/
464+
private static void dumpConnectionCounts(LoggerWriter logWriter)
465+
{
466+
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
467+
468+
List<ObjectName> connectors;
469+
470+
try
471+
{
472+
// Wildcard domain: embedded Tomcat registers these under "Tomcat" (the engine name Tomcat.getEngine()
473+
// assigns), a standalone install under "Catalina"
474+
connectors = new ArrayList<>(mbs.queryNames(new ObjectName("*:type=ThreadPool,name=*"), null));
475+
connectors.sort(Comparator.comparing(ObjectName::getCanonicalName));
476+
}
477+
catch (Exception e)
478+
{
479+
logWriter.debug("Failed to find connector ThreadPool MBeans: " + e);
480+
connectors = List.of();
481+
}
482+
483+
if (connectors.isEmpty())
484+
logWriter.debug("No connector ThreadPool MBeans found; connection counts are unavailable");
485+
486+
// The saturated connector is the one worth reading, so don't let an earlier one's failure hide it
487+
for (ObjectName connector : connectors)
488+
{
489+
try
490+
{
491+
logWriter.debug("Connector " + unquote(connector.getKeyProperty("name")) +
492+
": connections " + getMBeanAttribute(mbs, connector, "connectionCount") +
493+
"/" + getMBeanLimit(mbs, connector, "maxConnections") +
494+
", threads busy " + getMBeanAttribute(mbs, connector, "currentThreadsBusy") +
495+
"/" + getMBeanLimit(mbs, connector, "maxThreads"));
496+
}
497+
catch (Exception e)
498+
{
499+
logWriter.debug("Failed to read connection counts for " + connector.getCanonicalName() + ": " + e);
500+
}
501+
}
502+
503+
logWriter.debug("Open WebSocket connections: " + WebSocketTracker.getOpenCount());
504+
505+
// MCP ships in a premium module, so read its session count over JMX rather than depending on it
506+
Object mcpSessions = getMBeanAttribute(mbs, LabKeyManagement.createName("MCP", null), "SessionCount");
507+
if (null != mcpSessions)
508+
logWriter.debug("Open MCP sessions: " + mcpSessions);
509+
}
510+
511+
private static String unquote(@Nullable String name)
512+
{
513+
return null != name && name.startsWith("\"") ? ObjectName.unquote(name) : String.valueOf(name);
514+
}
515+
516+
private static @Nullable Object getMBeanAttribute(MBeanServer mbs, ObjectName name, String attribute)
517+
{
518+
try
519+
{
520+
return mbs.getAttribute(name, attribute);
521+
}
522+
catch (Exception e)
523+
{
524+
return null;
525+
}
526+
}
527+
528+
/** Tomcat reports an unbounded maximum as -1 */
529+
private static String getMBeanLimit(MBeanServer mbs, ObjectName name, String attribute)
530+
{
531+
Object value = getMBeanAttribute(mbs, name, attribute);
532+
return value instanceof Number number && number.longValue() < 0 ? "unlimited" : String.valueOf(value);
533+
}
534+
451535
private static void writeTable(LoggerWriter logWriter, UserSchema schema, String tableName, String header)
452536
{
453537
QueryForm form = new QueryForm();

‎api/src/org/labkey/api/websocket/BrowserEndpoint.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.labkey.api.security.AuthenticationManager;
3636
import org.labkey.api.security.SecurityManager;
3737
import org.labkey.api.security.User;
38+
import org.labkey.api.util.QuietCloser;
3839
import org.labkey.api.util.UnexpectedException;
3940
import org.labkey.api.util.logging.LogHelper;
4041

@@ -54,6 +55,7 @@ public abstract class BrowserEndpoint extends Endpoint
5455

5556
protected Session browserSession;
5657
ServerEndpoint serverEndpoint = null;
58+
private QuietCloser tracked;
5759

5860
private void close()
5961
{
@@ -90,6 +92,9 @@ public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest reques
9092
@Override
9193
public void onOpen(Session session, EndpointConfig endpointConfig)
9294
{
95+
// Only the inbound browser socket counts against the connector; serverEndpoint's socket is outbound
96+
tracked = WebSocketTracker.opened();
97+
9398
String uri = null;
9499
try
95100
{
@@ -113,6 +118,8 @@ public void onOpen(Session session, EndpointConfig endpointConfig)
113118
public void onClose(Session session, CloseReason closeReason)
114119
{
115120
LOG.debug("BrowserEndpoint.onClose()");
121+
if (null != tracked)
122+
tracked.close();
116123
if (null != serverEndpoint)
117124
serverEndpoint.close();
118125
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (c) 2026 LabKey Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.labkey.api.websocket;
17+
18+
import org.labkey.api.util.QuietCloser;
19+
20+
import java.util.concurrent.atomic.AtomicBoolean;
21+
import java.util.concurrent.atomic.AtomicInteger;
22+
23+
/**
24+
* Live count of inbound WebSocket connections, guests included. Each holds a Tomcat connector connection slot for as
25+
* long as the client keeps the socket open, so it competes with every other request against maxConnections.
26+
* See GH Issue 1574.
27+
*/
28+
public class WebSocketTracker
29+
{
30+
private static final AtomicInteger OPEN_COUNT = new AtomicInteger();
31+
32+
private WebSocketTracker()
33+
{
34+
}
35+
36+
/** Call from onOpen(); close the result from onClose(). Closing more than once is a no-op. */
37+
public static QuietCloser opened()
38+
{
39+
OPEN_COUNT.incrementAndGet();
40+
AtomicBoolean closed = new AtomicBoolean();
41+
42+
return () -> {
43+
if (closed.compareAndSet(false, true))
44+
OPEN_COUNT.decrementAndGet();
45+
};
46+
}
47+
48+
public static int getOpenCount()
49+
{
50+
return OPEN_COUNT.get();
51+
}
52+
}

‎core/src/org/labkey/core/CoreModule.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
import org.labkey.api.files.FileBrowserConfigWriter;
9191
import org.labkey.api.files.FileContentService;
9292
import org.labkey.api.markdown.MarkdownService;
93+
import org.labkey.api.mbean.LabKeyManagement;
9394
import org.labkey.api.mcp.McpService;
9495
import org.labkey.api.message.settings.MessageConfigService;
9596
import org.labkey.api.migration.DatabaseMigrationService;
@@ -261,6 +262,7 @@
261262
import org.labkey.core.login.LoginController;
262263
import org.labkey.core.metrics.SimpleMetricsServiceImpl;
263264
import org.labkey.core.metrics.WebSocketConnectionManager;
265+
import org.labkey.core.metrics.WebSocketMXBean;
264266
import org.labkey.core.notification.EmailPreferenceConfigServiceImpl;
265267
import org.labkey.core.notification.EmailPreferenceContainerListener;
266268
import org.labkey.core.notification.EmailPreferenceUserListener;
@@ -346,6 +348,8 @@
346348
import org.radeox.test.macro.list.ExampleListFormatterTest;
347349
import org.radeox.test.macro.list.SimpleListTest;
348350

351+
import javax.management.StandardMBean;
352+
349353
import java.io.IOException;
350354
import java.lang.reflect.InvocationTargetException;
351355
import java.lang.reflect.Method;
@@ -1292,6 +1296,7 @@ public void moduleStartupComplete(ServletContext servletContext)
12921296
});
12931297

12941298
UsageMetricsService.get().registerUsageMetrics(getName(), WebSocketConnectionManager.getInstance());
1299+
LabKeyManagement.register(new StandardMBean(WebSocketConnectionManager.getInstance(), WebSocketMXBean.class, true), "WebSockets");
12951300
UsageMetricsService.get().registerUsageMetrics(getName(), DbLoginManager.getMetricsProvider());
12961301
UsageMetricsService.get().registerUsageMetrics(getName(), SecurityManager.getMetricsProvider());
12971302
UsageMetricsService.get().registerUsageMetrics(getName(), DisplayFormatAnalyzer.getMetricsProvider());

‎core/src/org/labkey/core/metrics/WebSocketConnectionManager.java‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
package org.labkey.core.metrics;
1717

1818
import org.labkey.api.usageMetrics.UsageMetricsProvider;
19+
import org.labkey.api.websocket.WebSocketTracker;
1920

2021
import java.util.Map;
2122
import java.util.concurrent.atomic.AtomicInteger;
2223

23-
public class WebSocketConnectionManager implements UsageMetricsProvider
24+
public class WebSocketConnectionManager implements UsageMetricsProvider, WebSocketMXBean
2425
{
2526
private static final WebSocketConnectionManager _instance = new WebSocketConnectionManager();
2627

@@ -44,6 +45,24 @@ public void incrementCounter(boolean success)
4445
failureCounter.incrementAndGet();
4546
}
4647

48+
@Override
49+
public int getOpenConnectionCount()
50+
{
51+
return WebSocketTracker.getOpenCount();
52+
}
53+
54+
@Override
55+
public int getSuccessCount()
56+
{
57+
return successCounter.get();
58+
}
59+
60+
@Override
61+
public int getFailureCount()
62+
{
63+
return failureCounter.get();
64+
}
65+
4766
public boolean showWarning()
4867
{
4968
return successCounter.get() == 0 && failureCounter.get() > 0;
@@ -55,7 +74,8 @@ public Map<String, Object> getUsageMetrics()
5574
return Map.of(
5675
"webSocketConnections", Map.of(
5776
"success", successCounter.get(),
58-
"failure", failureCounter.get()
77+
"failure", failureCounter.get(),
78+
"open", WebSocketTracker.getOpenCount()
5979
)
6080
);
6181
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) 2026 LabKey Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.labkey.core.metrics;
17+
18+
/** WebSocket connection counts, exported over JMX as LabKey:name=WebSockets. See GH Issue 1574. */
19+
public interface WebSocketMXBean
20+
{
21+
/** @return inbound WebSocket connections the server is currently holding, guests included */
22+
int getOpenConnectionCount();
23+
24+
/** @return connections opened since startup by signed-in users */
25+
int getSuccessCount();
26+
27+
/** @return connection attempts that browsers have reported back as failed */
28+
int getFailureCount();
29+
}

‎core/src/org/labkey/core/notification/NotificationEndpoint.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
import org.apache.logging.log4j.LogManager;
2222
import org.labkey.api.security.User;
2323
import org.labkey.api.util.MemTracker;
24+
import org.labkey.api.util.QuietCloser;
2425
import org.labkey.api.security.SecurityManager;
26+
import org.labkey.api.websocket.WebSocketTracker;
2527
import org.labkey.core.metrics.WebSocketConnectionManager;
2628

2729
import jakarta.servlet.http.HttpSession;
@@ -57,6 +59,7 @@ public class NotificationEndpoint extends Endpoint
5759
private Session session;
5860
private int userId;
5961
private boolean errored;
62+
private QuietCloser tracked;
6063

6164
public NotificationEndpoint()
6265
{
@@ -75,6 +78,10 @@ public void onOpen(Session session, EndpointConfig endpointConfig)
7578
this.userId = null==id ? 0 : id;
7679

7780
LOG.debug("{} onOpen", this);
81+
// endpointsMap holds only authenticated endpoints, but a guest's socket occupies a connection slot just the
82+
// same - on a public site they're nearly all guests. GH Issue 1574
83+
tracked = WebSocketTracker.opened();
84+
7885
synchronized (endpointsMap)
7986
{
8087
if (this.userId > 0)
@@ -101,6 +108,8 @@ public void onClose(Session session, CloseReason closeReason)
101108
{
102109
endpointsMap.removeMapping(this.userId, this);
103110
}
111+
if (null != tracked)
112+
tracked.close();
104113
super.onClose(session, closeReason);
105114
}
106115

0 commit comments

Comments
 (0)