From 077453975f4c0ed48c687e749474ab0e737bfd1c Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sat, 26 Sep 2026 12:19:13 -0700 Subject: [PATCH 1/3] Mesure the count and time spent borrowing connections per request --- src/org/labkey/test/BaseWebDriverTest.java | 63 ++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/org/labkey/test/BaseWebDriverTest.java b/src/org/labkey/test/BaseWebDriverTest.java index 11e7412a89..850dbe4795 100644 --- a/src/org/labkey/test/BaseWebDriverTest.java +++ b/src/org/labkey/test/BaseWebDriverTest.java @@ -1261,7 +1261,10 @@ private void doPostamble() checkViews(); if (isTestRunningOnTeamCity()) + { checkActionCoverage(); + checkConnectionUsage(); + } CspLogUtil.checkNewCspWarnings(getArtifactCollector()); @@ -1658,6 +1661,66 @@ protected void checkActionCoverage() refresh(); // Chrome blocks sequential downloads from javascript } + @LogMethod + protected void checkConnectionUsage() + { + if (isGuestModeTest()) + return; + + SimpleHttpResponse response = WebTestHelper.getHttpResponse(WebTestHelper.buildURL("admin", "exportConnectionUsage")); + if (response.getResponseCode() != HttpStatus.SC_OK) + { + TestLogger.error("Failed to export connection usage: " + response.getResponseCode() + " " + response.getResponseMessage()); + return; + } + + String tsv = response.getResponseBody(); + try + { + Files.writeString(new File(TestFileUtils.getGradleReportDir(), "ConnectionUsage.tsv").toPath(), tsv); + } + catch (IOException e) + { + TestLogger.error("Failed to write connection usage file.", e); + } + + writeConnectionStatistics(tsv); + } + + private static final double BORROWS_PER_INVOCATION_THRESHOLD = 10; + + private void writeConnectionStatistics(String tsv) + { + List lines = tsv.lines().filter(line -> !line.isBlank()).toList(); + if (lines.isEmpty()) + return; + + List header = Arrays.asList(lines.getFirst().split("\t")); + int invocationsCol = header.indexOf("invocations"); + int borrowsCol = header.indexOf("borrows"); + int perInvocationCol = header.indexOf("borrowsPerInvocation"); + int unreturnedCol = header.indexOf("unreturned"); + + long invocations = 0; + long borrows = 0; + long unreturned = 0; + int actionsOverThreshold = 0; + for (String line : lines.subList(1, lines.size())) + { + String[] values = line.split("\t"); + invocations += Long.parseLong(values[invocationsCol]); + borrows += Long.parseLong(values[borrowsCol]); + unreturned += Long.parseLong(values[unreturnedCol]); + if (Double.parseDouble(values[perInvocationCol]) > BORROWS_PER_INVOCATION_THRESHOLD) + actionsOverThreshold++; + } + + TeamCityUtils.reportBuildStatisticValue("connectionBorrows", borrows); + TeamCityUtils.reportBuildStatisticValue("connectionBorrowsPerInvocation", invocations == 0 ? 0 : borrows / (double) invocations); + TeamCityUtils.reportBuildStatisticValue("actionsOverBorrowThreshold", actionsOverThreshold); + TeamCityUtils.reportBuildStatisticValue("unreturnedConnections", unreturned); + } + @LogMethod protected void checkLinks() { From c4bf0a46b3a28802b3323ee8b95951414463ac65 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sat, 26 Sep 2026 14:34:36 -0700 Subject: [PATCH 2/3] Track connection pool stats --- src/org/labkey/test/BaseWebDriverTest.java | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/org/labkey/test/BaseWebDriverTest.java b/src/org/labkey/test/BaseWebDriverTest.java index 850dbe4795..066af0d4a4 100644 --- a/src/org/labkey/test/BaseWebDriverTest.java +++ b/src/org/labkey/test/BaseWebDriverTest.java @@ -27,6 +27,7 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; import org.json.JSONObject; import org.junit.Assume; import org.junit.AssumptionViolatedException; @@ -147,6 +148,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import static org.junit.Assert.assertEquals; @@ -1685,6 +1687,43 @@ protected void checkConnectionUsage() } writeConnectionStatistics(tsv); + checkConnectionPool(); + } + + private void checkConnectionPool() + { + SimpleHttpResponse response = WebTestHelper.getHttpResponse(WebTestHelper.buildURL("admin", "getConnectionPoolStats")); + if (response.getResponseCode() != HttpStatus.SC_OK) + { + TestLogger.error("Failed to get connection pool statistics: " + response.getResponseCode() + " " + response.getResponseMessage()); + return; + } + + String json = response.getResponseBody(); + try + { + Files.writeString(new File(TestFileUtils.getGradleReportDir(), "ConnectionPool.json").toPath(), json); + } + catch (IOException e) + { + TestLogger.error("Failed to write connection pool file.", e); + } + + JSONArray dataSources = new JSONObject(json).getJSONArray("dataSources"); + JSONObject pool = IntStream.range(0, dataSources.length()) + .mapToObj(dataSources::getJSONObject) + .filter(ds -> ds.optBoolean("isLabKeyScope")) + .findFirst() + .orElse(null); + if (null == pool || !pool.has("createdCount")) + return; + + long created = pool.getLong("createdCount"); + long borrowed = pool.getLong("borrowedCount"); + TeamCityUtils.reportBuildStatisticValue("connectionsOpened", created); + TeamCityUtils.reportBuildStatisticValue("connectionsClosed", pool.getLong("destroyedCount")); + TeamCityUtils.reportBuildStatisticValue("connectionOpenPercent", borrowed == 0 ? 0 : created * 100.0 / borrowed); + TeamCityUtils.reportBuildStatisticValue("maxConnectionBorrowWaitMillis", pool.getLong("maxBorrowWaitMillis")); } private static final double BORROWS_PER_INVOCATION_THRESHOLD = 10; From 56b21e79a15f1efdf842240faf2bddedbfb12cb8 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sat, 3 Oct 2026 16:35:52 -0700 Subject: [PATCH 3/3] Skip connection usage stats when tracking is off and don't fail tests on parse errors --- src/org/labkey/test/BaseWebDriverTest.java | 57 +++++++++++++++------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/src/org/labkey/test/BaseWebDriverTest.java b/src/org/labkey/test/BaseWebDriverTest.java index 066af0d4a4..37f16dcafe 100644 --- a/src/org/labkey/test/BaseWebDriverTest.java +++ b/src/org/labkey/test/BaseWebDriverTest.java @@ -1669,47 +1669,68 @@ protected void checkConnectionUsage() if (isGuestModeTest()) return; - SimpleHttpResponse response = WebTestHelper.getHttpResponse(WebTestHelper.buildURL("admin", "exportConnectionUsage")); - if (response.getResponseCode() != HttpStatus.SC_OK) + String json = fetchReport("getConnectionPoolStats", "ConnectionPool.json"); + if (null == json) + return; + + boolean tracked; + try + { + JSONObject stats = new JSONObject(json); + tracked = stats.optBoolean("connectionUsageTracked"); + writePoolStatistics(stats); + } + catch (RuntimeException e) + { + TestLogger.error("Failed to parse connection pool statistics.", e); + return; + } + + if (!tracked) { - TestLogger.error("Failed to export connection usage: " + response.getResponseCode() + " " + response.getResponseMessage()); + log("Connection usage tracking is off; skipping connection usage statistics."); return; } - String tsv = response.getResponseBody(); + String tsv = fetchReport("exportConnectionUsage", "ConnectionUsage.tsv"); + if (null == tsv) + return; + try { - Files.writeString(new File(TestFileUtils.getGradleReportDir(), "ConnectionUsage.tsv").toPath(), tsv); + writeConnectionStatistics(tsv); } - catch (IOException e) + catch (RuntimeException e) { - TestLogger.error("Failed to write connection usage file.", e); + TestLogger.error("Failed to parse connection usage.", e); } - - writeConnectionStatistics(tsv); - checkConnectionPool(); } - private void checkConnectionPool() + /** Fetches an admin report and saves it as a build artifact; returns null on failure */ + private @Nullable String fetchReport(String action, String fileName) { - SimpleHttpResponse response = WebTestHelper.getHttpResponse(WebTestHelper.buildURL("admin", "getConnectionPoolStats")); + SimpleHttpResponse response = WebTestHelper.getHttpResponse(WebTestHelper.buildURL("admin", action)); if (response.getResponseCode() != HttpStatus.SC_OK) { - TestLogger.error("Failed to get connection pool statistics: " + response.getResponseCode() + " " + response.getResponseMessage()); - return; + TestLogger.error("Failed to get " + action + ": " + response.getResponseCode() + " " + response.getResponseMessage()); + return null; } - String json = response.getResponseBody(); + String body = response.getResponseBody(); try { - Files.writeString(new File(TestFileUtils.getGradleReportDir(), "ConnectionPool.json").toPath(), json); + Files.writeString(new File(TestFileUtils.getGradleReportDir(), fileName).toPath(), body); } catch (IOException e) { - TestLogger.error("Failed to write connection pool file.", e); + TestLogger.error("Failed to write " + fileName + ".", e); } + return body; + } - JSONArray dataSources = new JSONObject(json).getJSONArray("dataSources"); + private void writePoolStatistics(JSONObject stats) + { + JSONArray dataSources = stats.getJSONArray("dataSources"); JSONObject pool = IntStream.range(0, dataSources.length()) .mapToObj(dataSources::getJSONObject) .filter(ds -> ds.optBoolean("isLabKeyScope"))