Tracks how many times the consumer actually ran ({@link #getExecutionCount()}) versus was throttled
+ * ({@link #getThrottledCount()}), useful for metrics and for tests asserting the throttling behavior.
*/
public class Throttle
{
private final BlockingCache _cache;
+ private final AtomicLong _executionCount = new AtomicLong();
+ private final AtomicLong _requestCount = new AtomicLong();
public Throttle(String name, int limit, long timeToLive, Consumer consumer)
{
_cache = CacheManager.getBlockingCache(limit, timeToLive, "Throttle for " + name, (key, argument) ->
{
+ _executionCount.incrementAndGet();
consumer.accept(key);
return key;
});
@@ -52,6 +59,19 @@ public Throttle(String name, int limit, long timeToLive, Consumer consumer)
public void execute(K key)
{
+ _requestCount.incrementAndGet();
_cache.get(key);
}
+
+ /** @return the number of times the consumer actually ran (i.e., calls that were not throttled) */
+ public long getExecutionCount()
+ {
+ return _executionCount.get();
+ }
+
+ /** @return the number of {@link #execute} calls that were throttled (the consumer did not run) */
+ public long getThrottledCount()
+ {
+ return _requestCount.get() - _executionCount.get();
+ }
}
diff --git a/api/src/org/labkey/api/data/DisplayColumn.java b/api/src/org/labkey/api/data/DisplayColumn.java
index 659a1b69dda..c46fbd12b20 100644
--- a/api/src/org/labkey/api/data/DisplayColumn.java
+++ b/api/src/org/labkey/api/data/DisplayColumn.java
@@ -24,7 +24,11 @@
import org.apache.poi.ss.usermodel.Workbook;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import org.junit.Assert;
+import org.junit.Test;
import org.labkey.api.action.HasViewContext;
+import org.labkey.api.cache.CacheManager;
+import org.labkey.api.cache.Throttle;
import org.labkey.api.collections.NullPreventingSet;
import org.labkey.api.compliance.PhiTransformedColumnInfo;
import org.labkey.api.ontology.Concept;
@@ -64,6 +68,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
@@ -112,6 +117,10 @@ public abstract class DisplayColumn extends RenderColumn
private String _description = null;
private String _displayClass;
+ // GH Issue 1332: Throttle to one warning per column + value type per hour, across all renders.
+ private static final Throttle FORMAT_MISMATCH_THROTTLE = new Throttle<>("DisplayColumn format mismatch", 1000, CacheManager.HOUR, key ->
+ LOG.warn("Unable to apply format to {}, likely a SQL type mismatch between XML metadata and actual ResultSet", key));
+
private final List _analyticsProviders = new ArrayList<>();
/** 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)
}
catch (IllegalArgumentException e)
{
- 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());
+ warnFormatMismatch(value);
return value.toString();
}
}
@@ -497,6 +506,13 @@ public String getFormattedText(RenderContext ctx)
}
+ private void warnFormatMismatch(Object value)
+ {
+ ColumnInfo col = getColumnInfo();
+ String key = "column \"" + (col != null ? col.getFieldKey() : getName()) + "\" (value type " + value.getClass().getName() + ")";
+ FORMAT_MISMATCH_THROTTLE.execute(key);
+ }
+
/**
* Render the value as text using the expr or format if provided without
* any html encoding.
@@ -540,7 +556,7 @@ else if (null != format)
}
catch (IllegalArgumentException e)
{
- 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());
+ warnFormatMismatch(value);
formattedString = ConvertUtils.convert(value);
}
}
@@ -1344,4 +1360,61 @@ else if (null == formatString && col.isNumericType())
}
}
}
+
+ // GH Issue 1332: a type-mismatched column logs in format() on every cell; verify we warn once per column, not once per row
+ public static class TestCase extends Assert
+ {
+ // Unique per column so each test gets its own throttle key
+ private static final AtomicInteger UNIQUE = new AtomicInteger();
+ public static final String NOT_A_NUMBER_VALUE = "not-a-number";
+
+ // A column whose configured format rejects the value type, mimicking XML metadata that disagrees with the ResultSet.
+ private DisplayColumn columnThatFailsFormatting()
+ {
+ String name = "formatMismatchTestColumn-" + UNIQUE.incrementAndGet();
+ Format numberFormat = new DecimalFormat("0.00"); // format() throws IllegalArgumentException on a non-Number
+ return new SimpleDisplayColumn()
+ {
+ @Override
+ public Object getValue(RenderContext ctx)
+ {
+ return NOT_A_NUMBER_VALUE;
+ }
+
+ @Override
+ public Format getFormat()
+ {
+ return numberFormat;
+ }
+
+ @Override
+ public String getName()
+ {
+ return name;
+ }
+ };
+ }
+
+ @Test
+ public void getFormattedTextWarnsOncePerColumn()
+ {
+ RenderContext ctx = new RenderContext(new ViewContext());
+ DisplayColumn dc = columnThatFailsFormatting();
+ long before = FORMAT_MISMATCH_THROTTLE.getExecutionCount();
+ for (int row = 0; row < 1000; row++)
+ assertEquals("Fallback must be the unformatted value", NOT_A_NUMBER_VALUE, dc.getFormattedText(ctx));
+ assertEquals("A format mismatch must warn once per column, not once per row", 1, FORMAT_MISMATCH_THROTTLE.getExecutionCount() - before);
+ }
+
+ @Test
+ public void formatValueWarnsOncePerColumn()
+ {
+ RenderContext ctx = new RenderContext(new ViewContext());
+ DisplayColumn dc = columnThatFailsFormatting();
+ long before = FORMAT_MISMATCH_THROTTLE.getExecutionCount();
+ for (int row = 0; row < 1000; row++)
+ assertEquals("Fallback must be the converted value", NOT_A_NUMBER_VALUE, dc.formatValue(ctx, NOT_A_NUMBER_VALUE, null, dc.getFormat(), null));
+ assertEquals("A format mismatch must warn once per column, not once per row", 1, FORMAT_MISMATCH_THROTTLE.getExecutionCount() - before);
+ }
+ }
}
diff --git a/api/src/org/labkey/api/dataiterator/SimpleTranslator.java b/api/src/org/labkey/api/dataiterator/SimpleTranslator.java
index 3dcad2bc631..3d3fe1528bf 100644
--- a/api/src/org/labkey/api/dataiterator/SimpleTranslator.java
+++ b/api/src/org/labkey/api/dataiterator/SimpleTranslator.java
@@ -692,6 +692,10 @@ public class ContainerColumn implements Supplier
final Set