|
18 | 18 | import org.apache.commons.collections4.multimap.HashSetValuedHashMap; |
19 | 19 | import org.apache.commons.io.FileUtils; |
20 | 20 | import org.apache.logging.log4j.Logger; |
| 21 | +import org.jetbrains.annotations.Nullable; |
21 | 22 | import org.labkey.api.action.NullSafeBindException; |
22 | 23 | import org.labkey.api.data.ConnectionWrapper; |
23 | 24 | import org.labkey.api.data.ContainerManager; |
|
27 | 28 | import org.labkey.api.data.dialect.BasePostgreSqlDialect; |
28 | 29 | import org.labkey.api.files.FileSystemDirectoryListener; |
29 | 30 | import org.labkey.api.files.FileSystemWatchers; |
| 31 | +import org.labkey.api.mbean.LabKeyManagement; |
30 | 32 | import org.labkey.api.miniprofiler.MiniProfiler; |
31 | 33 | import org.labkey.api.module.ModuleLoader; |
32 | 34 | import org.labkey.api.query.QueryForm; |
|
38 | 40 | import org.labkey.api.view.ActionURL; |
39 | 41 | import org.labkey.api.view.HttpView; |
40 | 42 | import org.labkey.api.view.ViewContext; |
| 43 | +import org.labkey.api.websocket.WebSocketTracker; |
41 | 44 | import org.labkey.api.writer.PrintWriters; |
42 | 45 | import org.labkey.vfs.FileLike; |
43 | 46 |
|
| 47 | +import javax.management.MBeanServer; |
| 48 | +import javax.management.ObjectName; |
| 49 | + |
44 | 50 | import java.io.File; |
45 | 51 | import java.io.IOException; |
46 | 52 | import java.io.PrintWriter; |
@@ -356,6 +362,8 @@ public static synchronized void dumpThreads(LoggerWriter logWriter) |
356 | 362 | logWriter.debug("CPU count: " + osBean.getAvailableProcessors()); |
357 | 363 | } |
358 | 364 |
|
| 365 | + dumpConnectionCounts(logWriter); |
| 366 | + |
359 | 367 | logWriter.debug("*********************************************"); |
360 | 368 |
|
361 | 369 | Map<Thread, StackTraceElement[]> stackTraces = Thread.getAllStackTraces(); |
@@ -448,6 +456,82 @@ public static synchronized void dumpThreads(LoggerWriter logWriter) |
448 | 456 | } |
449 | 457 | } |
450 | 458 |
|
| 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 | + |
451 | 535 | private static void writeTable(LoggerWriter logWriter, UserSchema schema, String tableName, String header) |
452 | 536 | { |
453 | 537 | QueryForm form = new QueryForm(); |
|
0 commit comments