From 330c784b87abed8de992445cef85590149ecf046 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Wed, 18 Feb 2026 17:54:27 -0800 Subject: [PATCH 1/5] Fix some string index problems --- .../labkey/study/query/StudyQuerySchema.java | 22 +++++++++++-------- .../query/VisualizationVisitTagTable.java | 12 +++++----- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/study/src/org/labkey/study/query/StudyQuerySchema.java b/study/src/org/labkey/study/query/StudyQuerySchema.java index 3a1ca6143e4..6e1630eee23 100644 --- a/study/src/org/labkey/study/query/StudyQuerySchema.java +++ b/study/src/org/labkey/study/query/StudyQuerySchema.java @@ -646,22 +646,26 @@ public TableInfo createTable(String name, ContainerFilter cf) { // Name is encoded with useProtocolDay boolean, tagName, and altQueryName String params = name.substring(VISUALIZATION_VISIT_TAG_TABLE_NAME.length()); - boolean useProtocolDay; + Boolean useProtocolDay = null; if (params.startsWith("-true")) { - params = params.substring(params.indexOf("-true") + 6); + // Trim the prefix (which may or may not include a trailing hyphen) + params = params.substring(Math.min(params.length(), 6)); useProtocolDay = true; } - else + else if (params.startsWith("-false")) { - params = params.substring(params.indexOf("-false") + 7); + // Trim the prefix (which may or may not include a trailing hyphen) + params = params.substring(Math.min(params.length(), 7)); useProtocolDay = false; } - int hyphenIndex = params.indexOf("-"); - String tagName = hyphenIndex > -1 ? params.substring(0, hyphenIndex) : params; - String altQueryName = hyphenIndex > -1 ? params.substring(hyphenIndex + 1) : null; - - return new VisualizationVisitTagTable(this, cf, getStudy(), getUser(), tagName, useProtocolDay, altQueryName); + if (useProtocolDay != null) + { + int hyphenIndex = params.indexOf("-"); + String tagName = hyphenIndex > -1 ? params.substring(0, hyphenIndex) : params; + String altQueryName = hyphenIndex > -1 ? params.substring(hyphenIndex + 1) : null; + return new VisualizationVisitTagTable(this, cf, getStudy(), getUser(), tagName, useProtocolDay, altQueryName); + } } // Might be a dataset diff --git a/study/src/org/labkey/study/query/VisualizationVisitTagTable.java b/study/src/org/labkey/study/query/VisualizationVisitTagTable.java index f1c277ec7e8..cd54ad492b0 100644 --- a/study/src/org/labkey/study/query/VisualizationVisitTagTable.java +++ b/study/src/org/labkey/study/query/VisualizationVisitTagTable.java @@ -25,7 +25,7 @@ import org.labkey.api.study.StudyService; import org.labkey.study.model.StudyImpl; -public class VisualizationVisitTagTable extends VirtualTable +public class VisualizationVisitTagTable extends VirtualTable { private final StudyImpl _study; private final User _user; @@ -78,11 +78,11 @@ public SQLFragment getFromSQL(String alias) SQLFragment from = new SQLFragment("(SELECT VisitId, " + _dayString + ", " + innerAlias + ".ParticipantId, Container " + " FROM (SELECT ParticipantId, "); from.append("COALESCE(CohortVisitTag.VisitId, NoCohortTag.VisitId) As VisitId, CurrentCohortId FROM study.Participant\n") - .append("LEFT OUTER JOIN study.VisitTagMap CohortVisitTag ON study.Participant.CurrentCohortId = CohortVisitTag.CohortID AND CohortVisitTag.VisitTag=") - .append("'" + _visitTagName + "'\n") + .append("LEFT OUTER JOIN study.VisitTagMap CohortVisitTag ON study.Participant.CurrentCohortId = CohortVisitTag.CohortID AND CohortVisitTag.VisitTag=?\n") + .add(_visitTagName) .append("AND CohortVisitTag.Container = Participant.Container"); - from.append("\nLEFT OUTER JOIN study.VisitTagMap NoCohortTag ON NoCohortTag.VisitTag =") - .append("'" + _visitTagName + "' AND NoCohortTag.CohortID IS NULL\n") + from.append("\nLEFT OUTER JOIN study.VisitTagMap NoCohortTag ON NoCohortTag.VisitTag = ? AND NoCohortTag.CohortID IS NULL\n") + .add(_visitTagName) .append("AND NoCohortTag.Container = Participant.Container"); from.append("\nWHERE "); @@ -98,7 +98,7 @@ public SQLFragment getFromSQL(String alias) { // allow caller to pass in their own query to use for the SQL to get the participant-to-zero day map (used for CDS study axis alignment by visit tag) // NOTE: it is assumed that the query will have the expected columns plus a column for VisitTagMap to filter on - return new SQLFragment("(SELECT * FROM " + _altQueryName + " WHERE VisitTagName = '" + _visitTagName + "')").appendIdentifier(alias); + return new SQLFragment("(SELECT * FROM ").appendIdentifier(_altQueryName).append(" WHERE VisitTagName = ?)").add(_visitTagName).appendIdentifier(alias); } } } From 64eae9d8c74baae3096493045d71519bfa6f749a Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Wed, 18 Feb 2026 17:56:35 -0800 Subject: [PATCH 2/5] Delete unused code instead --- .../labkey/study/query/StudyQuerySchema.java | 26 ----- .../query/VisualizationVisitTagTable.java | 104 ------------------ 2 files changed, 130 deletions(-) delete mode 100644 study/src/org/labkey/study/query/VisualizationVisitTagTable.java diff --git a/study/src/org/labkey/study/query/StudyQuerySchema.java b/study/src/org/labkey/study/query/StudyQuerySchema.java index 6e1630eee23..9c31108a5e3 100644 --- a/study/src/org/labkey/study/query/StudyQuerySchema.java +++ b/study/src/org/labkey/study/query/StudyQuerySchema.java @@ -124,7 +124,6 @@ public class StudyQuerySchema extends UserSchema implements UserSchema.HasContex public static final String VISIT_TAG_TABLE_NAME = "VisitTag"; public static final String VISIT_TAG_MAP_TABLE_NAME = "VisitTagMap"; public static final String VISIT_ALIASES = "VisitAliases"; - public static final String VISUALIZATION_VISIT_TAG_TABLE_NAME = "VisualizationVisitTag"; public static final String VISIT_MAP_TABLE_NAME = "VisitMap"; public static final String STUDY_DATA_TABLE_NAME = "StudyData"; @@ -642,31 +641,6 @@ public TableInfo createTable(String name, ContainerFilter cf) { return new VisitMapTable(this, cf); } - if (name.startsWith(VISUALIZATION_VISIT_TAG_TABLE_NAME)) - { - // Name is encoded with useProtocolDay boolean, tagName, and altQueryName - String params = name.substring(VISUALIZATION_VISIT_TAG_TABLE_NAME.length()); - Boolean useProtocolDay = null; - if (params.startsWith("-true")) - { - // Trim the prefix (which may or may not include a trailing hyphen) - params = params.substring(Math.min(params.length(), 6)); - useProtocolDay = true; - } - else if (params.startsWith("-false")) - { - // Trim the prefix (which may or may not include a trailing hyphen) - params = params.substring(Math.min(params.length(), 7)); - useProtocolDay = false; - } - if (useProtocolDay != null) - { - int hyphenIndex = params.indexOf("-"); - String tagName = hyphenIndex > -1 ? params.substring(0, hyphenIndex) : params; - String altQueryName = hyphenIndex > -1 ? params.substring(hyphenIndex + 1) : null; - return new VisualizationVisitTagTable(this, cf, getStudy(), getUser(), tagName, useProtocolDay, altQueryName); - } - } // Might be a dataset DatasetDefinition dsd = getDatasetDefinitionByQueryName(name); diff --git a/study/src/org/labkey/study/query/VisualizationVisitTagTable.java b/study/src/org/labkey/study/query/VisualizationVisitTagTable.java deleted file mode 100644 index cd54ad492b0..00000000000 --- a/study/src/org/labkey/study/query/VisualizationVisitTagTable.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2014-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.labkey.study.query; - -import org.jetbrains.annotations.NotNull; -import org.labkey.api.data.ContainerFilter; -import org.labkey.api.data.JdbcType; -import org.labkey.api.data.SQLFragment; -import org.labkey.api.data.VirtualTable; -import org.labkey.api.query.ExprColumn; -import org.labkey.api.security.User; -import org.labkey.api.study.StudyService; -import org.labkey.study.model.StudyImpl; - -public class VisualizationVisitTagTable extends VirtualTable -{ - private final StudyImpl _study; - private final User _user; - private final boolean _useProtocolDay; - private final String _visitTagName; - private final String _dayString; - private final String _altQueryName; - - public VisualizationVisitTagTable(StudyQuerySchema schema, ContainerFilter cf, StudyImpl study, User user, String visitTagName, boolean useProtocolDay, String altQueryName) - { - super(schema.getDbSchema(), "VizVisitTag", schema, cf); - _study = study; - _user = user; - _visitTagName = visitTagName; - _useProtocolDay = useProtocolDay; - if (_useProtocolDay) - _dayString = "ProtocolDay"; - else - _dayString = "Day"; - _altQueryName = altQueryName; - - addColumn(new ExprColumn(this, StudyService.get().getSubjectColumnName(_study.getContainer()), - new SQLFragment(ExprColumn.STR_TABLE_ALIAS + "." + "ParticipantId"), JdbcType.VARCHAR)); - - // 20546: need to expose Container for use in StudyVisualizationProvider.getJoinColumns - addColumn(new ExprColumn(this, "Container", new SQLFragment(ExprColumn.STR_TABLE_ALIAS + "." + "Container"), JdbcType.VARCHAR)); - - addColumn(new ExprColumn(this, "ZeroDay", new SQLFragment(ExprColumn.STR_TABLE_ALIAS + "." + _dayString), JdbcType.INTEGER)); - } - - @NotNull - @Override - public SQLFragment getFromSQL(String alias) - { - checkReadBeforeExecute(); - if (_altQueryName == null) - { - String innerAlias = getSqlDialect().truncate(alias + "_SP", 0); - String joinString; - if (_useProtocolDay) - { - joinString = "\nJOIN study.Visit ON " + innerAlias + ".VisitId = study.Visit.RowId AND " + - innerAlias + ".VisitId IS NOT NULL"; - } - else - { - joinString = "\nJOIN study.ParticipantVisit ON " + innerAlias + ".VisitId = study.ParticipantVisit.VisitRowId AND " + - innerAlias + ".ParticipantId = study.ParticipantVisit.ParticipantId"; - } - - SQLFragment from = new SQLFragment("(SELECT VisitId, " + _dayString + ", " + innerAlias + ".ParticipantId, Container " + " FROM (SELECT ParticipantId, "); - from.append("COALESCE(CohortVisitTag.VisitId, NoCohortTag.VisitId) As VisitId, CurrentCohortId FROM study.Participant\n") - .append("LEFT OUTER JOIN study.VisitTagMap CohortVisitTag ON study.Participant.CurrentCohortId = CohortVisitTag.CohortID AND CohortVisitTag.VisitTag=?\n") - .add(_visitTagName) - .append("AND CohortVisitTag.Container = Participant.Container"); - from.append("\nLEFT OUTER JOIN study.VisitTagMap NoCohortTag ON NoCohortTag.VisitTag = ? AND NoCohortTag.CohortID IS NULL\n") - .add(_visitTagName) - .append("AND NoCohortTag.Container = Participant.Container"); - from.append("\nWHERE "); - - // TODO: this is a temp fix for the Dataspace usecase - from.append(ContainerFilter.Type.AllInProject.create(_study.getContainer(), _user).getSQLFragment(getSchema(), new SQLFragment("study.Participant.Container"))); - - from.append(") ").appendIdentifier(innerAlias); - from.append(joinString); - from.append("\n) ").appendIdentifier(alias); - return from; - } - else - { - // allow caller to pass in their own query to use for the SQL to get the participant-to-zero day map (used for CDS study axis alignment by visit tag) - // NOTE: it is assumed that the query will have the expected columns plus a column for VisitTagMap to filter on - return new SQLFragment("(SELECT * FROM ").appendIdentifier(_altQueryName).append(" WHERE VisitTagName = ?)").add(_visitTagName).appendIdentifier(alias); - } - } -} From 72f790da7bcfff7b70153ed0a1d9f569bb9e1294 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Wed, 18 Feb 2026 18:03:02 -0800 Subject: [PATCH 3/5] Delete more unused code --- .../StudyVisualizationProvider.java | 41 +++++++++---------- .../sql/OuterJoinSourceQuery.java | 6 --- .../sql/VisualizationSourceQuery.java | 17 +++----- 3 files changed, 24 insertions(+), 40 deletions(-) diff --git a/study/src/org/labkey/study/visualization/StudyVisualizationProvider.java b/study/src/org/labkey/study/visualization/StudyVisualizationProvider.java index 445a8eb9fff..353feb1e7dc 100644 --- a/study/src/org/labkey/study/visualization/StudyVisualizationProvider.java +++ b/study/src/org/labkey/study/visualization/StudyVisualizationProvider.java @@ -177,31 +177,28 @@ public List> getJoinC joinCols.add(new Pair<>(firstSubjectCol, secondSubjectCol)); - if (!first.isVisitTagQuery() && ! second.isVisitTagQuery()) - { - // attempt to lookup the dataset using the queryName by label and then by name - Dataset firstDataset = StudyService.get().resolveDataset(first.getContainer(), first.getQueryName()); - Dataset secondDataset = StudyService.get().resolveDataset(second.getContainer(), second.getQueryName()); + // attempt to lookup the dataset using the queryName by label and then by name + Dataset firstDataset = StudyService.get().resolveDataset(first.getContainer(), first.getQueryName()); + Dataset secondDataset = StudyService.get().resolveDataset(second.getContainer(), second.getQueryName()); - boolean subjectJoinOnly = isGroupByQuery || first.isSkipVisitJoin() || second.isSkipVisitJoin(); + boolean subjectJoinOnly = isGroupByQuery || first.isSkipVisitJoin() || second.isSkipVisitJoin(); - // if either query is a demographic dataset, it's sufficient to join on subject only: - if (!subjectJoinOnly && (firstDataset == null || firstDataset.getKeyType() != Dataset.KeyType.SUBJECT) && - (secondDataset == null || secondDataset.getKeyType() != Dataset.KeyType.SUBJECT)) + // if either query is a demographic dataset, it's sufficient to join on subject only: + if (!subjectJoinOnly && (firstDataset == null || firstDataset.getKeyType() != Dataset.KeyType.SUBJECT) && + (secondDataset == null || secondDataset.getKeyType() != Dataset.KeyType.SUBJECT)) + { + VisualizationSourceColumn firstSequenceCol = getVisitJoinColumn(factory, first, firstSubjectNounSingular); + VisualizationSourceColumn secondSequenceCol = getVisitJoinColumn(factory, second, secondSubjectNounSingular); + joinCols.add(new Pair<>(firstSequenceCol, secondSequenceCol)); + + // for datasets with matching 3rd keys, join on subject/visit/key (if neither are pivoted), allowing null results for this column so as to follow the lead of the primary measure column for this query: + if (firstDataset != null && firstDataset.getKeyType() == Dataset.KeyType.SUBJECT_VISIT_OTHER && + secondDataset != null && secondDataset.getKeyType() == Dataset.KeyType.SUBJECT_VISIT_OTHER && + first.getPivot() == null && second.getPivot() == null && firstDataset.hasMatchingExtraKey(secondDataset)) { - VisualizationSourceColumn firstSequenceCol = getVisitJoinColumn(factory, first, firstSubjectNounSingular); - VisualizationSourceColumn secondSequenceCol = getVisitJoinColumn(factory, second, secondSubjectNounSingular); - joinCols.add(new Pair<>(firstSequenceCol, secondSequenceCol)); - - // for datasets with matching 3rd keys, join on subject/visit/key (if neither are pivoted), allowing null results for this column so as to follow the lead of the primary measure column for this query: - if (firstDataset != null && firstDataset.getKeyType() == Dataset.KeyType.SUBJECT_VISIT_OTHER && - secondDataset != null && secondDataset.getKeyType() == Dataset.KeyType.SUBJECT_VISIT_OTHER && - first.getPivot() == null && second.getPivot() == null && firstDataset.hasMatchingExtraKey(secondDataset)) - { - VisualizationSourceColumn firstKeyCol = factory.create(first.getSchema(), first.getQueryName(), firstDataset.getKeyPropertyName(), true); - VisualizationSourceColumn secondKeyCol = factory.create(second.getSchema(), second.getQueryName(), secondDataset.getKeyPropertyName(), true); - joinCols.add(new Pair<>(firstKeyCol, secondKeyCol)); - } + VisualizationSourceColumn firstKeyCol = factory.create(first.getSchema(), first.getQueryName(), firstDataset.getKeyPropertyName(), true); + VisualizationSourceColumn secondKeyCol = factory.create(second.getSchema(), second.getQueryName(), secondDataset.getKeyPropertyName(), true); + joinCols.add(new Pair<>(firstKeyCol, secondKeyCol)); } } diff --git a/visualization/src/org/labkey/visualization/sql/OuterJoinSourceQuery.java b/visualization/src/org/labkey/visualization/sql/OuterJoinSourceQuery.java index 5b4b214e07c..e955a3d1554 100644 --- a/visualization/src/org/labkey/visualization/sql/OuterJoinSourceQuery.java +++ b/visualization/src/org/labkey/visualization/sql/OuterJoinSourceQuery.java @@ -235,12 +235,6 @@ public boolean isSkipVisitJoin() return false; } - @Override - public boolean isVisitTagQuery() - { - return false; - } - @Override public boolean isRequireLeftJoin() { diff --git a/visualization/src/org/labkey/visualization/sql/VisualizationSourceQuery.java b/visualization/src/org/labkey/visualization/sql/VisualizationSourceQuery.java index ba0b0901c54..026663f1ac0 100644 --- a/visualization/src/org/labkey/visualization/sql/VisualizationSourceQuery.java +++ b/visualization/src/org/labkey/visualization/sql/VisualizationSourceQuery.java @@ -131,12 +131,6 @@ public boolean requireInnerJoin() return false; } - @Override - public boolean isVisitTagQuery() - { - return _queryName.startsWith("VisualizationVisitTag"); - } - @Override public boolean isRequireLeftJoin() { @@ -213,7 +207,7 @@ public String getSelectListName(Set selectAliases) private static void addToColMap(Map> colMap, String name, VisualizationSourceColumn alias) { - Set aliases = colMap.computeIfAbsent(name, k -> new LinkedHashSet<>()); + Set aliases = colMap.computeIfAbsent(name, _ -> new LinkedHashSet<>()); aliases.add(alias); } @@ -434,7 +428,7 @@ public String getGroupByClause() return ""; } - private void appendValueList(StringBuilder sql, VisualizationSourceColumn col) throws org.labkey.api.visualization.SQLGenerationException + private void appendValueList(StringBuilder sql, VisualizationSourceColumn col) { if (col.getValues() != null && !col.getValues().isEmpty()) { @@ -457,7 +451,7 @@ private void appendValueList(StringBuilder sql, VisualizationSourceColumn col) t } } - public String getPivotClause() throws org.labkey.api.visualization.SQLGenerationException + public String getPivotClause() { if (_pivot != null) { @@ -560,7 +554,7 @@ private String appendSimpleFilter(StringBuilder where, SimpleFilter filter, Stri return separator; } - public String getWhereClause() throws org.labkey.api.visualization.SQLGenerationException + public String getWhereClause() { StringBuilder where = new StringBuilder(); String sep = "WHERE "; @@ -598,12 +592,11 @@ public String getWhereClause() throws org.labkey.api.visualization.SQLGeneration @Override public String getSQL(VisualizationSourceColumn.Factory factory) throws SQLGenerationException { - String sql = getSelectClause(factory) + "\n" + + return getSelectClause(factory) + "\n" + getFromClause() + "\n" + getWhereClause() + "\n" + getGroupByClause() + "\n" + getPivotClause() + "\n"; - return sql; } @Override From 996ab956af0a6b4da8ba8acc479778e20518c80a Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Wed, 18 Feb 2026 20:44:41 -0800 Subject: [PATCH 4/5] Missed file --- .../org/labkey/api/visualization/IVisualizationSourceQuery.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/src/org/labkey/api/visualization/IVisualizationSourceQuery.java b/api/src/org/labkey/api/visualization/IVisualizationSourceQuery.java index 0fc293bb331..9c017057af5 100644 --- a/api/src/org/labkey/api/visualization/IVisualizationSourceQuery.java +++ b/api/src/org/labkey/api/visualization/IVisualizationSourceQuery.java @@ -63,8 +63,6 @@ public interface IVisualizationSourceQuery boolean isSkipVisitJoin(); - boolean isVisitTagQuery(); - /** * True if any select or aggregate requires a left join explicitly. This is an override for any columns * that might require some form of an INNER JOIN. From 0b96c5529a987430b7dadb23bd6b3dd239b1660b Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Thu, 19 Feb 2026 09:28:21 -0800 Subject: [PATCH 5/5] Avoid reentrancy and infinite recursion --- .../org/labkey/api/util/DebugInfoDumper.java | 179 ++++++++++-------- 1 file changed, 98 insertions(+), 81 deletions(-) diff --git a/api/src/org/labkey/api/util/DebugInfoDumper.java b/api/src/org/labkey/api/util/DebugInfoDumper.java index 14fa7f9bbab..93aebcab63e 100644 --- a/api/src/org/labkey/api/util/DebugInfoDumper.java +++ b/api/src/org/labkey/api/util/DebugInfoDumper.java @@ -316,119 +316,136 @@ static boolean justWaiting(StackTraceElement[] stack) // OK probably just waiting for work. We could check for common tomcat/labkey patterns here to be more conservative. } + /** Prevent reentrancy when we get SQLException trying to grab locks from Postgres */ + private static final ThreadLocal DUMPING_THREADS = ThreadLocal.withInitial(() -> false); + /** * Writes the thread dump into threads.txt * */ public static synchronized void dumpThreads(LoggerWriter logWriter) { - logWriter.debug("*********************************************"); - logWriter.debug("Starting thread dump - " + LocalDateTime.now()); - long used = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed(); - long max = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax(); - logWriter.debug("Heap usage at " + DecimalFormat.getPercentInstance().format(((double)used / (double)max)) + " - " + - FileUtils.byteCountToDisplaySize(used) + " from a max of " + - FileUtils.byteCountToDisplaySize(max) + " (" + DecimalFormat.getInstance().format(used) + " / " + DecimalFormat.getInstance().format(max) + " bytes)"); - - OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); - if (osBean != null) + if (DUMPING_THREADS.get() == true) { - DecimalFormat f3 = new DecimalFormat("0.000"); - - if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) - { - logWriter.debug("Total OS memory (bytes): " + DecimalFormat.getInstance().format(sunOsBean.getTotalMemorySize())); - logWriter.debug("Free OS memory (bytes): " + DecimalFormat.getInstance().format(sunOsBean.getFreeMemorySize())); - logWriter.debug("OS CPU load: " + f3.format(sunOsBean.getCpuLoad())); - logWriter.debug("JVM CPU load: " + f3.format(sunOsBean.getProcessCpuLoad())); - } - logWriter.debug("CPU count: " + osBean.getAvailableProcessors()); + return; } - logWriter.debug("*********************************************"); + try + { + DUMPING_THREADS.set(true); - Map stackTraces = Thread.getAllStackTraces(); - var spidsByThread = ConnectionWrapper.getSPIDsForThreads(); + logWriter.debug("*********************************************"); + logWriter.debug("Starting thread dump - " + LocalDateTime.now()); + long used = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed(); + long max = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax(); + logWriter.debug("Heap usage at " + DecimalFormat.getPercentInstance().format(((double) used / (double) max)) + " - " + + FileUtils.byteCountToDisplaySize(used) + " from a max of " + + FileUtils.byteCountToDisplaySize(max) + " (" + DecimalFormat.getInstance().format(used) + " / " + DecimalFormat.getInstance().format(max) + " bytes)"); + + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + if (osBean != null) + { + DecimalFormat f3 = new DecimalFormat("0.000"); - ArrayList threadsToDump = new ArrayList<>(); - ArrayList boringThreads = new ArrayList<>(); + if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) + { + logWriter.debug("Total OS memory (bytes): " + DecimalFormat.getInstance().format(sunOsBean.getTotalMemorySize())); + logWriter.debug("Free OS memory (bytes): " + DecimalFormat.getInstance().format(sunOsBean.getFreeMemorySize())); + logWriter.debug("OS CPU load: " + f3.format(sunOsBean.getCpuLoad())); + logWriter.debug("JVM CPU load: " + f3.format(sunOsBean.getProcessCpuLoad())); + } + logWriter.debug("CPU count: " + osBean.getAvailableProcessors()); + } - for (Thread thread : stackTraces.keySet()) - { - Set spids = Objects.requireNonNullElse(spidsByThread.get(thread), Set.of()); - var stack = stackTraces.get(thread); + logWriter.debug("*********************************************"); - if (spids.isEmpty() && justWaiting(stack)) - boringThreads.add(thread); - else - threadsToDump.add(thread); - } + Map stackTraces = Thread.getAllStackTraces(); + var spidsByThread = ConnectionWrapper.getSPIDsForThreads(); - if (!threadsToDump.isEmpty()) - { - logWriter.debug(""); - logWriter.debug(" ----- active threads -----"); + ArrayList threadsToDump = new ArrayList<>(); + ArrayList boringThreads = new ArrayList<>(); - threadsToDump.sort(Comparator.comparing(Thread::getName, String.CASE_INSENSITIVE_ORDER)); - for (Thread thread : threadsToDump) + for (Thread thread : stackTraces.keySet()) { - dumpOneThread(thread, logWriter, stackTraces, spidsByThread); - } - } + Set spids = Objects.requireNonNullElse(spidsByThread.get(thread), Set.of()); + var stack = stackTraces.get(thread); - if (!boringThreads.isEmpty()) - { - logWriter.debug(""); - logWriter.debug(" ----- waiting threads -----"); + if (spids.isEmpty() && justWaiting(stack)) + boringThreads.add(thread); + else + threadsToDump.add(thread); + } - boringThreads.sort(Comparator.comparing(Thread::getName, String.CASE_INSENSITIVE_ORDER)); - for (Thread thread : boringThreads) + if (!threadsToDump.isEmpty()) { - dumpOneThread(thread, logWriter, stackTraces, spidsByThread); + logWriter.debug(""); + logWriter.debug(" ----- active threads -----"); + + threadsToDump.sort(Comparator.comparing(Thread::getName, String.CASE_INSENSITIVE_ORDER)); + for (Thread thread : threadsToDump) + { + dumpOneThread(thread, logWriter, stackTraces, spidsByThread); + } } - } - logWriter.debug("*********************************************"); - logWriter.debug("Completed thread dump"); - logWriter.debug("*********************************************"); + if (!boringThreads.isEmpty()) + { + logWriter.debug(""); + logWriter.debug(" ----- waiting threads -----"); - for (DbScope dbScope : DbScope.getDbScopes()) - { - dbScope.logCurrentConnectionState(logWriter); - } + boringThreads.sort(Comparator.comparing(Thread::getName, String.CASE_INSENSITIVE_ORDER)); + for (Thread thread : boringThreads) + { + dumpOneThread(thread, logWriter, stackTraces, spidsByThread); + } + } - if (ConnectionWrapper.getActiveConnectionCount() > 0) - { logWriter.debug("*********************************************"); - logWriter.debug("Start dump of all open connections"); + logWriter.debug("Completed thread dump"); logWriter.debug("*********************************************"); - ConnectionWrapper.dumpOpenConnections(logWriter, null); - logWriter.debug("*********************************************"); - logWriter.debug("Completed dump of all open connections"); - logWriter.debug("*********************************************"); - } - // GitHub Issue 713: Automatically include PG locks and active queries in thread dumps - UserSchema schema = QueryService.get().getUserSchema(User.getAdminServiceUser(), ContainerManager.getRoot(), BasePostgreSqlDialect.POSTGRES_SCHEMA_NAME); - // Schema won't exist on SQLServer - if (schema != null) - { - try - { - writeTable(logWriter, schema, BasePostgreSqlDialect.POSTGRES_LOCKS_TABLE_NAME, "Postgres locks"); - } - catch (RuntimeException e) + for (DbScope dbScope : DbScope.getDbScopes()) { - logWriter.debug("Failed to write Postgres locks table:" + e); + dbScope.logCurrentConnectionState(logWriter); } - try + + if (ConnectionWrapper.getActiveConnectionCount() > 0) { - writeTable(logWriter, schema, BasePostgreSqlDialect.POSTGRES_STAT_ACTIVITY_TABLE_NAME, "Postgres activity"); + logWriter.debug("*********************************************"); + logWriter.debug("Start dump of all open connections"); + logWriter.debug("*********************************************"); + ConnectionWrapper.dumpOpenConnections(logWriter, null); + logWriter.debug("*********************************************"); + logWriter.debug("Completed dump of all open connections"); + logWriter.debug("*********************************************"); } - catch (RuntimeException e) + + // GitHub Issue 713: Automatically include PG locks and active queries in thread dumps + UserSchema schema = QueryService.get().getUserSchema(User.getAdminServiceUser(), ContainerManager.getRoot(), BasePostgreSqlDialect.POSTGRES_SCHEMA_NAME); + // Schema won't exist on SQLServer + if (schema != null) { - logWriter.debug("Failed to write Postgres activity table:" + e); + try + { + writeTable(logWriter, schema, BasePostgreSqlDialect.POSTGRES_LOCKS_TABLE_NAME, "Postgres locks"); + } + catch (RuntimeException e) + { + logWriter.debug("Failed to write Postgres locks table:" + e); + } + try + { + writeTable(logWriter, schema, BasePostgreSqlDialect.POSTGRES_STAT_ACTIVITY_TABLE_NAME, "Postgres activity"); + } + catch (RuntimeException e) + { + logWriter.debug("Failed to write Postgres activity table:" + e); + } } } + finally + { + DUMPING_THREADS.set(false); + } } private static void writeTable(LoggerWriter logWriter, UserSchema schema, String tableName, String header)