Skip to content

Commit 51b8dbf

Browse files
GitHub Issue 1332: Reduce per-row logging
1 parent ec8642a commit 51b8dbf

8 files changed

Lines changed: 137 additions & 17 deletions

File tree

api/src/org/labkey/api/ApiModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.labkey.api.data.DbSchema;
5858
import org.labkey.api.data.DbScope;
5959
import org.labkey.api.data.DbSequenceManager;
60+
import org.labkey.api.data.DisplayColumn;
6061
import org.labkey.api.data.ExcelColumn;
6162
import org.labkey.api.data.ExcelWriter;
6263
import org.labkey.api.data.InlineInClauseGenerator;
@@ -522,6 +523,7 @@ public void registerServlets(ServletContext servletCtx)
522523
DbScope.SchemaNameTestCase.class,
523524
DbScope.TransactionTestCase.class,
524525
DbSequenceManager.TestCase.class,
526+
DisplayColumn.TestCase.class,
525527
DomTestCase.class,
526528
DomainTemplateGroup.TestCase.class,
527529
Encryption.TestCase.class,

api/src/org/labkey/api/cache/Throttle.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.labkey.api.cache;
1717

18+
import java.util.concurrent.atomic.AtomicLong;
1819
import java.util.function.Consumer;
1920

2021
/**
@@ -36,22 +37,41 @@
3637
* THROTTLE.execute(user);
3738
* }
3839
* </pre>
40+
*
41+
* <p>Tracks how many times the consumer actually ran ({@link #getExecutionCount()}) versus was throttled
42+
* ({@link #getThrottledCount()}), useful for metrics and for tests asserting the throttling behavior.</p>
3943
*/
4044
public class Throttle<K>
4145
{
4246
private final BlockingCache<K, K> _cache;
47+
private final AtomicLong _executionCount = new AtomicLong();
48+
private final AtomicLong _requestCount = new AtomicLong();
4349

4450
public Throttle(String name, int limit, long timeToLive, Consumer<K> consumer)
4551
{
4652
_cache = CacheManager.getBlockingCache(limit, timeToLive, "Throttle for " + name, (key, argument) ->
4753
{
54+
_executionCount.incrementAndGet();
4855
consumer.accept(key);
4956
return key;
5057
});
5158
}
5259

5360
public void execute(K key)
5461
{
62+
_requestCount.incrementAndGet();
5563
_cache.get(key);
5664
}
65+
66+
/** @return the number of times the consumer actually ran (i.e., calls that were not throttled) */
67+
public long getExecutionCount()
68+
{
69+
return _executionCount.get();
70+
}
71+
72+
/** @return the number of {@link #execute} calls that were throttled (the consumer did not run) */
73+
public long getThrottledCount()
74+
{
75+
return _requestCount.get() - _executionCount.get();
76+
}
5777
}

api/src/org/labkey/api/data/DisplayColumn.java

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
import org.apache.poi.ss.usermodel.Workbook;
2525
import org.jetbrains.annotations.NotNull;
2626
import org.jetbrains.annotations.Nullable;
27+
import org.junit.Assert;
28+
import org.junit.Test;
2729
import org.labkey.api.action.HasViewContext;
30+
import org.labkey.api.cache.CacheManager;
31+
import org.labkey.api.cache.Throttle;
2832
import org.labkey.api.collections.NullPreventingSet;
2933
import org.labkey.api.compliance.PhiTransformedColumnInfo;
3034
import org.labkey.api.ontology.Concept;
@@ -64,6 +68,7 @@
6468
import java.util.List;
6569
import java.util.Map;
6670
import java.util.Set;
71+
import java.util.concurrent.atomic.AtomicInteger;
6772

6873
import static org.apache.commons.lang3.StringUtils.isBlank;
6974
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
@@ -112,6 +117,10 @@ public abstract class DisplayColumn extends RenderColumn
112117
private String _description = null;
113118
private String _displayClass;
114119

120+
// GH Issue 1332: Throttle to one warning per column + value type per hour, across all renders.
121+
private static final Throttle<String> FORMAT_MISMATCH_THROTTLE = new Throttle<>("DisplayColumn format mismatch", 1000, CacheManager.HOUR, key ->
122+
LOG.warn("Unable to apply format to {}, likely a SQL type mismatch between XML metadata and actual ResultSet", key));
123+
115124
private final List<ColumnAnalyticsProvider> _analyticsProviders = new ArrayList<>();
116125

117126
/** Handles spanning multiple rows in a grid. A separate interface to allow for easier mixing and matching with DisplayColumn implementations. */
@@ -488,7 +497,7 @@ public String getFormattedText(RenderContext ctx)
488497
}
489498
catch (IllegalArgumentException e)
490499
{
491-
LOG.warn("Unable to apply format to {} value \"{}\" for column \"{}\", likely a SQL type mismatch between XML metadata and actual ResultSet", value.getClass().getName(), value, getName());
500+
warnFormatMismatch(value);
492501
return value.toString();
493502
}
494503
}
@@ -497,6 +506,13 @@ public String getFormattedText(RenderContext ctx)
497506
}
498507

499508

509+
private void warnFormatMismatch(Object value)
510+
{
511+
ColumnInfo col = getColumnInfo();
512+
String key = "column \"" + (col != null ? col.getFieldKey() : getName()) + "\" (value type " + value.getClass().getName() + ")";
513+
FORMAT_MISMATCH_THROTTLE.execute(key);
514+
}
515+
500516
/**
501517
* Render the value as text using the <code>expr</code> or <code>format</code> if provided without
502518
* any html encoding.
@@ -540,7 +556,7 @@ else if (null != format)
540556
}
541557
catch (IllegalArgumentException e)
542558
{
543-
LOG.warn("Unable to apply format to {} value \"{}\" for column \"{}\", likely a SQL type mismatch between XML metadata and actual ResultSet", value.getClass().getName(), value, getName());
559+
warnFormatMismatch(value);
544560
formattedString = ConvertUtils.convert(value);
545561
}
546562
}
@@ -1344,4 +1360,61 @@ else if (null == formatString && col.isNumericType())
13441360
}
13451361
}
13461362
}
1363+
1364+
// GH Issue 1332: a type-mismatched column logs in format() on every cell; verify we warn once per column, not once per row
1365+
public static class TestCase extends Assert
1366+
{
1367+
// Unique per column so each test gets its own throttle key
1368+
private static final AtomicInteger UNIQUE = new AtomicInteger();
1369+
public static final String NOT_A_NUMBER_VALUE = "not-a-number";
1370+
1371+
// A column whose configured format rejects the value type, mimicking XML metadata that disagrees with the ResultSet.
1372+
private DisplayColumn columnThatFailsFormatting()
1373+
{
1374+
String name = "formatMismatchTestColumn-" + UNIQUE.incrementAndGet();
1375+
Format numberFormat = new DecimalFormat("0.00"); // format() throws IllegalArgumentException on a non-Number
1376+
return new SimpleDisplayColumn()
1377+
{
1378+
@Override
1379+
public Object getValue(RenderContext ctx)
1380+
{
1381+
return NOT_A_NUMBER_VALUE;
1382+
}
1383+
1384+
@Override
1385+
public Format getFormat()
1386+
{
1387+
return numberFormat;
1388+
}
1389+
1390+
@Override
1391+
public String getName()
1392+
{
1393+
return name;
1394+
}
1395+
};
1396+
}
1397+
1398+
@Test
1399+
public void getFormattedTextWarnsOncePerColumn()
1400+
{
1401+
RenderContext ctx = new RenderContext(new ViewContext());
1402+
DisplayColumn dc = columnThatFailsFormatting();
1403+
long before = FORMAT_MISMATCH_THROTTLE.getExecutionCount();
1404+
for (int row = 0; row < 1000; row++)
1405+
assertEquals("Fallback must be the unformatted value", NOT_A_NUMBER_VALUE, dc.getFormattedText(ctx));
1406+
assertEquals("A format mismatch must warn once per column, not once per row", 1, FORMAT_MISMATCH_THROTTLE.getExecutionCount() - before);
1407+
}
1408+
1409+
@Test
1410+
public void formatValueWarnsOncePerColumn()
1411+
{
1412+
RenderContext ctx = new RenderContext(new ViewContext());
1413+
DisplayColumn dc = columnThatFailsFormatting();
1414+
long before = FORMAT_MISMATCH_THROTTLE.getExecutionCount();
1415+
for (int row = 0; row < 1000; row++)
1416+
assertEquals("Fallback must be the converted value", NOT_A_NUMBER_VALUE, dc.formatValue(ctx, NOT_A_NUMBER_VALUE, null, dc.getFormat(), null));
1417+
assertEquals("A format mismatch must warn once per column, not once per row", 1, FORMAT_MISMATCH_THROTTLE.getExecutionCount() - before);
1418+
}
1419+
}
13471420
}

api/src/org/labkey/api/dataiterator/SimpleTranslator.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,10 @@ public class ContainerColumn implements Supplier
692692

693693
final Set<Object> allowableContainers = new HashSet<>();
694694

695+
// GH Issue 1332: a bad container value recurs on every row; warn once per distinct value, not per row
696+
final Set<Object> loggedUnresolvedContainers = new HashSet<>();
697+
final Set<Object> loggedRejectedContainers = new HashSet<>();
698+
695699
public ContainerColumn(UserSchema us, TableInfo tableInfo, String containerId, int idx)
696700
{
697701
this.us = us;
@@ -723,7 +727,8 @@ public Object get()
723727
if (!this.us.getContainer().allowRowMutationForContainer(rowContainer))
724728
{
725729
getRowError().addError(new SimpleValidationError("Row supplied container value: " + rowContainerVal + " cannot be used for actions against the container: " + us.getContainer().getPath()));
726-
LOG.warn("Resolved container to {} but rejected as valid location for import into {} in {}.{}", rowContainer.getPath(), us.getContainer().getPath(), us.getSchemaName(), tableInfo.getPublicSchemaName());
730+
if (loggedRejectedContainers.add(rowContainerVal))
731+
LOG.warn("Resolved container to {} but rejected as valid location for import into {} in {}.{}", rowContainer.getPath(), us.getContainer().getPath(), us.getSchemaName(), tableInfo.getPublicSchemaName());
727732
}
728733
else
729734
{
@@ -734,8 +739,8 @@ public Object get()
734739
}
735740
else
736741
{
737-
// only log if the incoming value is GUID-like
738-
if (rowContainerVal instanceof String && GUID.isGUID((String)rowContainerVal))
742+
// only log if the incoming value is GUID-like, and only once per distinct value
743+
if (rowContainerVal instanceof String s && GUID.isGUID(s) && loggedUnresolvedContainers.add(rowContainerVal))
739744
{
740745
LOG.warn("Failed to resolve container value '{}' to container for import into {}.{}, defaulting to original target container of {}", rowContainerVal, us.getSchemaName(), tableInfo.getPublicSchemaName(), us.getContainer().getPath());
741746
}

api/src/org/labkey/api/study/query/PublishResultsQueryView.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,22 +94,26 @@
9494
import java.util.Map;
9595
import java.util.Objects;
9696
import java.util.Set;
97+
import java.util.concurrent.ConcurrentHashMap;
9798
import java.util.stream.Collectors;
9899
import java.util.stream.Stream;
99100

100-
import static org.labkey.api.util.IntegerUtils.asInteger;
101-
import static org.labkey.api.util.IntegerUtils.asLong;
102101
import static org.labkey.api.study.publish.StudyPublishService.LinkToStudyKeys;
103102
import static org.labkey.api.util.DOM.Attribute.id;
104103
import static org.labkey.api.util.DOM.DIV;
105104
import static org.labkey.api.util.DOM.SCRIPT;
106105
import static org.labkey.api.util.DOM.at;
106+
import static org.labkey.api.util.IntegerUtils.asInteger;
107+
import static org.labkey.api.util.IntegerUtils.asLong;
107108
import static org.labkey.api.util.IntegerUtils.asLongElseNull;
108109

109110
public class PublishResultsQueryView extends QueryView
110111
{
111112
private static final Logger LOG = LogManager.getLogger(PublishResultsQueryView.class);
112113

114+
// GH Issue 1332: Only warn once per distinct column
115+
private static final Set<String> LOGGED_MULTIVALUE_COLUMNS = ConcurrentHashMap.newKeySet();
116+
113117
private final SimpleFilter _filter;
114118
private final Container _targetStudyContainer;
115119
private final boolean _mismatched;
@@ -303,7 +307,7 @@ public static Object getColumnValue(ColumnInfo col, RenderContext ctx)
303307
List<Object> values = ((IMultiValuedDisplayColumn)dc).getDisplayValues(ctx);
304308
if (values.size() == 1)
305309
return values.getFirst();
306-
else
310+
else if (LOGGED_MULTIVALUE_COLUMNS.add(col.getFieldKey().toString()))
307311
LOG.warn("Unable to use the value returned from column : {} because this multi-value column returned more than a single value.", col.getName());
308312
}
309313
return col.getValue(ctx);

assay/src/org/labkey/assay/query/TypeDisplayColumn.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public class TypeDisplayColumn extends DataColumn
4242

4343
private static final FieldKey LSID_FIELD_KEY = new FieldKey(null, "LSID");
4444

45+
// GH Issue 1332: a broken provider pattern misses on every row; warn once per instance, not once per cell
46+
private boolean _loggedProviderMismatch = false;
47+
4548
public TypeDisplayColumn(ColumnInfo colInfo)
4649
{
4750
super(colInfo);
@@ -74,7 +77,11 @@ public void renderGridCellContents(RenderContext ctx, HtmlWriter out)
7477
AssayProvider provider = AssayService.get().getProvider(protocol);
7578
if (provider != null)
7679
{
77-
LOG.warn("Failed to match AssayProvider '{}' using pattern '{}' for LSID: {}", provider.getName(), provider.getProtocolPattern(), lsid);
80+
if (!_loggedProviderMismatch)
81+
{
82+
_loggedProviderMismatch = true;
83+
LOG.warn("Failed to match AssayProvider '{}' using pattern '{}' for LSID: {}", provider.getName(), provider.getProtocolPattern(), lsid);
84+
}
7885
out.write(provider.getName());
7986
return;
8087
}

issues/src/org/labkey/issue/query/IssuesTable.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
import java.util.List;
115115
import java.util.Map;
116116
import java.util.Set;
117+
import java.util.concurrent.ConcurrentHashMap;
117118
import java.util.regex.Matcher;
118119
import java.util.regex.Pattern;
119120
import java.util.stream.Collectors;
@@ -906,6 +907,9 @@ class PullRequestsDisplayColumn extends DataColumn
906907
{
907908
private static final Pattern GITHUB_HTTP_PR_URL = Pattern.compile("https://github.com/(?<org>[^/]*)/(?<project>[^/]*)/pull/(?<pullId>\\d*)");
908909

910+
// GH Issue 1332: parseGithubUrl is a per-cell static call; warn once per distinct unparseable url, not once per row
911+
private static final Set<String> LOGGED_BAD_PR_URLS = ConcurrentHashMap.newKeySet();
912+
909913
public PullRequestsDisplayColumn(ColumnInfo col)
910914
{
911915
super(col);
@@ -976,13 +980,16 @@ public void renderGridCellContents(RenderContext ctx, HtmlWriter out)
976980
}
977981
catch (NumberFormatException e)
978982
{
979-
// The issueId value can be null if it the column isn't included in the select for a custom query
980-
@Nullable Integer issueId = ctx.get(FieldKey.fromParts("IssueId"), Integer.class);
983+
if (LOGGED_BAD_PR_URLS.add(url))
984+
{
985+
// The issueId value can be null if it the column isn't included in the select for a custom query
986+
@Nullable Integer issueId = ctx.get(FieldKey.fromParts("IssueId"), Integer.class);
981987

982-
StringBuilder sb = new StringBuilder("Failed to parse pull request url '" + url + "'");
983-
if (issueId != null)
984-
sb.append(" for issue ").append(issueId);
985-
IssuesTable.LOG.warn(sb.toString());
988+
StringBuilder sb = new StringBuilder("Failed to parse pull request url '" + url + "'");
989+
if (issueId != null)
990+
sb.append(" for issue ").append(issueId);
991+
IssuesTable.LOG.warn(sb.toString());
992+
}
986993
}
987994
}
988995

query/src/org/labkey/query/controllers/QueryController.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4615,6 +4615,7 @@ protected JSONObject executeJson(JSONObject json, CommandType commandType, boole
46154615
if (commandType == CommandType.insert || commandType == CommandType.insertWithKeys || commandType == CommandType.delete)
46164616
f = new RowMapFactory<>();
46174617
CaseInsensitiveHashMap<Object> referenceCasing = new CaseInsensitiveHashMap<>();
4618+
boolean loggedConflictingCasing = false;
46184619

46194620
for (int idx = 0; idx < rows.length(); ++idx)
46204621
{
@@ -4632,9 +4633,10 @@ protected JSONObject executeJson(JSONObject json, CommandType commandType, boole
46324633
Map<String, Object> rowMap = null == f ? new CaseInsensitiveHashMap<>(new HashMap<>(), referenceCasing) : f.getRowMap();
46334634
// Use shallow copy since jsonObj.toMap() will translate contained JSONObjects into Maps, which we don't want
46344635
boolean conflictingCasing = JsonUtil.fillMapShallow(jsonObj, rowMap);
4635-
if (conflictingCasing)
4636+
if (conflictingCasing && !loggedConflictingCasing)
46364637
{
4637-
// Issue 52616
4638+
loggedConflictingCasing = true;
4639+
// Issue 52616; GH Issue 1332: log once per request, not once per conflicting row
46384640
LOG.error("Row contained conflicting casing for key names in the incoming row: {}", jsonObj);
46394641
}
46404642
if (allowRowAttachments())

0 commit comments

Comments
 (0)