From d3aa517928bc494bdfbf0162235ec16aa590e67e Mon Sep 17 00:00:00 2001 From: Stuart Tettemer Date: Mon, 27 Sep 2021 09:32:24 -0500 Subject: [PATCH] Script: compile/cache eviction history metric placeholders (#78257) Adds 5m/15m/24h metrics to _nodes/stats when those metrics are non-zero. Those metrics are not yet populated. BWC: history metrics are only sent between v8.0.0+ nodes, v7.16.0 nodes will not send those metrics until they are populated. Refs: #62899 Backport: db75c4b --- .../script/ScriptContextStats.java | 96 ++++++++++- .../elasticsearch/script/ScriptMetrics.java | 4 +- .../cluster/node/stats/NodeStatsTests.java | 70 +++++++- .../script/ScriptStatsTests.java | 163 ++++++++++++++++++ 4 files changed, 326 insertions(+), 7 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/script/ScriptStatsTests.java diff --git a/server/src/main/java/org/elasticsearch/script/ScriptContextStats.java b/server/src/main/java/org/elasticsearch/script/ScriptContextStats.java index c0fc5d8b2493e..970151b8c05ec 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptContextStats.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptContextStats.java @@ -20,14 +20,19 @@ public class ScriptContextStats implements Writeable, ToXContentFragment, Comparable { private final String context; private final long compilations; + private final TimeSeries compilationsHistory; private final long cacheEvictions; + private final TimeSeries cacheEvictionsHistory; private final long compilationLimitTriggered; - public ScriptContextStats(String context, long compilations, long cacheEvictions, long compilationLimitTriggered) { + public ScriptContextStats(String context, long compilations, long cacheEvictions, long compilationLimitTriggered, + TimeSeries compilationsHistory, TimeSeries cacheEvictionsHistory) { this.context = Objects.requireNonNull(context); this.compilations = compilations; this.cacheEvictions = cacheEvictions; this.compilationLimitTriggered = compilationLimitTriggered; + this.compilationsHistory = compilationsHistory; + this.cacheEvictionsHistory = cacheEvictionsHistory; } public ScriptContextStats(StreamInput in) throws IOException { @@ -35,6 +40,8 @@ public ScriptContextStats(StreamInput in) throws IOException { compilations = in.readVLong(); cacheEvictions = in.readVLong(); compilationLimitTriggered = in.readVLong(); + compilationsHistory = null; + cacheEvictionsHistory = null; } @Override @@ -45,6 +52,65 @@ public void writeTo(StreamOutput out) throws IOException { out.writeVLong(compilationLimitTriggered); } + public static class TimeSeries implements Writeable, ToXContentFragment { + public final long fiveMinutes; + public final long fifteenMinutes; + public final long twentyFourHours; + + public TimeSeries() { + this.fiveMinutes = 0; + this.fifteenMinutes = 0; + this.twentyFourHours = 0; + } + + public TimeSeries(long fiveMinutes, long fifteenMinutes, long twentyFourHours) { + assert fiveMinutes >= 0; + this.fiveMinutes = fiveMinutes; + assert fifteenMinutes >= fiveMinutes; + this.fifteenMinutes = fifteenMinutes; + assert twentyFourHours >= fifteenMinutes; + this.twentyFourHours = twentyFourHours; + } + + public TimeSeries(StreamInput in) throws IOException { + fiveMinutes = in.readVLong(); + fifteenMinutes = in.readVLong(); + twentyFourHours = in.readVLong(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field(Fields.FIVE_MINUTES, fiveMinutes); + builder.field(Fields.FIFTEEN_MINUTES, fifteenMinutes); + builder.field(Fields.TWENTY_FOUR_HOURS, twentyFourHours); + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVLong(fiveMinutes); + out.writeVLong(fifteenMinutes); + out.writeVLong(twentyFourHours); + } + + public boolean isEmpty() { + return twentyFourHours == 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TimeSeries that = (TimeSeries) o; + return fiveMinutes == that.fiveMinutes && fifteenMinutes == that.fifteenMinutes && twentyFourHours == that.twentyFourHours; + } + + @Override + public int hashCode() { + return Objects.hash(fiveMinutes, fifteenMinutes, twentyFourHours); + } + } + public String getContext() { return context; } @@ -53,10 +119,18 @@ public long getCompilations() { return compilations; } + public TimeSeries getCompilationsHistory() { + return compilationsHistory; + } + public long getCacheEvictions() { return cacheEvictions; } + public TimeSeries getCacheEvictionsHistory() { + return cacheEvictionsHistory; + } + public long getCompilationLimitTriggered() { return compilationLimitTriggered; } @@ -66,7 +140,22 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.startObject(); builder.field(Fields.CONTEXT, getContext()); builder.field(Fields.COMPILATIONS, getCompilations()); + + TimeSeries series = getCompilationsHistory(); + if (series != null && series.isEmpty() == false) { + builder.startObject(Fields.COMPILATIONS_HISTORY); + series.toXContent(builder, params); + builder.endObject(); + } + builder.field(Fields.CACHE_EVICTIONS, getCacheEvictions()); + series = getCacheEvictionsHistory(); + if (series != null && series.isEmpty() == false) { + builder.startObject(Fields.CACHE_EVICTIONS_HISTORY); + series.toXContent(builder, params); + builder.endObject(); + } + builder.field(Fields.COMPILATION_LIMIT_TRIGGERED, getCompilationLimitTriggered()); builder.endObject(); return builder; @@ -80,7 +169,12 @@ public int compareTo(ScriptContextStats o) { static final class Fields { static final String CONTEXT = "context"; static final String COMPILATIONS = "compilations"; + static final String COMPILATIONS_HISTORY = "compilations_history"; static final String CACHE_EVICTIONS = "cache_evictions"; + static final String CACHE_EVICTIONS_HISTORY = "cache_evictions_history"; static final String COMPILATION_LIMIT_TRIGGERED = "compilation_limit_triggered"; + static final String FIVE_MINUTES = "5m"; + static final String FIFTEEN_MINUTES = "15m"; + static final String TWENTY_FOUR_HOURS = "24h"; } } diff --git a/server/src/main/java/org/elasticsearch/script/ScriptMetrics.java b/server/src/main/java/org/elasticsearch/script/ScriptMetrics.java index 0a47642381427..c4d26bc861c8c 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptMetrics.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptMetrics.java @@ -36,7 +36,9 @@ public ScriptContextStats stats(String context) { context, compilationsMetric.count(), cacheEvictionsMetric.count(), - compilationLimitTriggered.count() + compilationLimitTriggered.count(), + new ScriptContextStats.TimeSeries(), + new ScriptContextStats.TimeSeries() ); } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/stats/NodeStatsTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/stats/NodeStatsTests.java index ada364501049b..412fe21c55185 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/stats/NodeStatsTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/stats/NodeStatsTests.java @@ -26,6 +26,7 @@ import org.elasticsearch.node.AdaptiveSelectionStats; import org.elasticsearch.node.ResponseCollectorService; import org.elasticsearch.script.ScriptCacheStats; +import org.elasticsearch.script.ScriptContextStats; import org.elasticsearch.script.ScriptStats; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; @@ -36,9 +37,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; @@ -233,11 +236,38 @@ public void testSerialization() throws IOException { } } ScriptStats scriptStats = nodeStats.getScriptStats(); + ScriptStats deserializedScriptStats = deserializedNodeStats.getScriptStats(); if (scriptStats == null) { - assertNull(deserializedNodeStats.getScriptStats()); + assertNull(deserializedScriptStats); } else { - assertEquals(scriptStats.getCacheEvictions(), deserializedNodeStats.getScriptStats().getCacheEvictions()); - assertEquals(scriptStats.getCompilations(), deserializedNodeStats.getScriptStats().getCompilations()); + List deserialized = deserializedScriptStats.getContextStats(); + long evictions = 0; + long limited = 0; + long compilations = 0; + List stats = scriptStats.getContextStats(); + for (ScriptContextStats generatedStats: stats) { + List maybeDeserStats = deserialized.stream().filter( + s -> s.getContext().equals(generatedStats.getContext()) + ).collect(Collectors.toList()); + + assertEquals(1, maybeDeserStats.size()); + ScriptContextStats deserStats = maybeDeserStats.get(0); + + evictions += generatedStats.getCacheEvictions(); + assertEquals(generatedStats.getCacheEvictions(), deserStats.getCacheEvictions()); + + limited += generatedStats.getCompilationLimitTriggered(); + assertEquals(generatedStats.getCompilationLimitTriggered(), deserStats.getCompilationLimitTriggered()); + + compilations += generatedStats.getCompilations(); + assertEquals(generatedStats.getCompilations(), deserStats.getCompilations()); + + assertNull(deserStats.getCacheEvictionsHistory()); + assertNull(deserStats.getCompilationsHistory()); + } + assertEquals(evictions, scriptStats.getCacheEvictions()); + assertEquals(limited, scriptStats.getCompilationLimitTriggered()); + assertEquals(compilations, scriptStats.getCompilations()); } DiscoveryStats discoveryStats = nodeStats.getDiscoveryStats(); DiscoveryStats deserializedDiscoveryStats = deserializedNodeStats.getDiscoveryStats(); @@ -551,8 +581,27 @@ public static NodeStats createNodeStats() { } allCircuitBreakerStats = new AllCircuitBreakerStats(circuitBreakerStatsArray); } - ScriptStats scriptStats = frequently() ? - new ScriptStats(randomNonNegativeLong(), randomNonNegativeLong(), randomNonNegativeLong()) : null; + ScriptStats scriptStats = null; + if (frequently()) { + int numContents = randomIntBetween(0, 20); + List stats = new ArrayList<>(numContents); + HashSet contexts = new HashSet<>(); + for (int i = 0; i < numContents; i++) { + long compile = randomLongBetween(0, 1024); + long eviction = randomLongBetween(0, 1024); + String context = randomValueOtherThanMany(contexts::contains, () -> randomAlphaOfLength(12)); + contexts.add(context); + stats.add(new ScriptContextStats( + context, + compile, + eviction, + randomLongBetween(0, 1024), + randomTimeSeries(), + randomTimeSeries()) + ); + } + scriptStats = new ScriptStats(stats); + } DiscoveryStats discoveryStats = frequently() ? new DiscoveryStats( randomBoolean() @@ -653,6 +702,17 @@ public static NodeStats createNodeStats() { ingestStats, adaptiveSelectionStats, scriptCacheStats, null); } + private static ScriptContextStats.TimeSeries randomTimeSeries() { + if (randomBoolean()) { + long day = randomLongBetween(0, 1024); + long fifteen = day >= 1 ? randomLongBetween(0, day) : 0; + long five = fifteen >= 1 ? randomLongBetween(0, fifteen) : 0; + return new ScriptContextStats.TimeSeries(five, fifteen, day); + } else { + return new ScriptContextStats.TimeSeries(); + } + } + private IngestStats.Stats getPipelineStats(List pipelineStats, String id) { return pipelineStats.stream().filter(p1 -> p1.getPipelineId().equals(id)).findFirst().map(p2 -> p2.getStats()).orElse(null); } diff --git a/server/src/test/java/org/elasticsearch/script/ScriptStatsTests.java b/server/src/test/java/org/elasticsearch/script/ScriptStatsTests.java new file mode 100644 index 0000000000000..7d5a8df9d17d2 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/script/ScriptStatsTests.java @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.script; + +import org.elasticsearch.Version; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; + +import static org.hamcrest.Matchers.equalTo; + +public class ScriptStatsTests extends ESTestCase { + public void testXContent() throws IOException { + List contextStats = org.elasticsearch.core.List.of( + new ScriptContextStats("contextB", 100, 201, 302, + new ScriptContextStats.TimeSeries(1000, 1001, 1002), + new ScriptContextStats.TimeSeries(2000, 2001, 2002) + ), + new ScriptContextStats("contextA", 1000, 2010, 3020, null, new ScriptContextStats.TimeSeries(0, 0, 0)) + ); + ScriptStats stats = new ScriptStats(contextStats); + final XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint(); + builder.startObject(); + stats.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + + String expected = "{\n" + + " \"script\" : {\n" + + " \"compilations\" : 1100,\n" + + " \"cache_evictions\" : 2211,\n" + + " \"compilation_limit_triggered\" : 3322\n" + + " }\n" + + "}"; + assertThat(Strings.toString(builder), equalTo(expected)); + } + + public void testSerializeEmptyTimeSeries() throws IOException { + ScriptContextStats.TimeSeries empty = new ScriptContextStats.TimeSeries(); + ScriptContextStats stats = new ScriptContextStats("c", 1111, 2222, 3333, null, empty); + + XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint(); + stats.toXContent(builder, ToXContent.EMPTY_PARAMS); + + String expected = + "{\n" + + " \"context\" : \"c\",\n" + + " \"compilations\" : 1111,\n" + + " \"cache_evictions\" : 2222,\n" + + " \"compilation_limit_triggered\" : 3333\n" + + "}"; + + assertThat(Strings.toString(builder), equalTo(expected)); + } + + public void testSerializeTimeSeries() throws IOException { + Function mkContextStats = + (ts) -> new ScriptContextStats("c", 1111, 2222, 3333, null, ts); + + ScriptContextStats.TimeSeries series = new ScriptContextStats.TimeSeries(0, 0, 5); + String format = + "{\n" + + " \"context\" : \"c\",\n" + + " \"compilations\" : 1111,\n" + + " \"cache_evictions\" : 2222,\n" + + " \"cache_evictions_history\" : {\n" + + " \"5m\" : %d,\n" + + " \"15m\" : %d,\n" + + " \"24h\" : %d\n" + + " },\n" + + " \"compilation_limit_triggered\" : 3333\n" + + "}"; + + XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint(); + mkContextStats.apply(series).toXContent(builder, ToXContent.EMPTY_PARAMS); + + assertThat(Strings.toString(builder), equalTo(String.format(Locale.ROOT, format, 0, 0, 5))); + + series = new ScriptContextStats.TimeSeries(0, 7, 1234); + builder = XContentFactory.jsonBuilder().prettyPrint(); + mkContextStats.apply(series).toXContent(builder, ToXContent.EMPTY_PARAMS); + assertThat(Strings.toString(builder), equalTo(String.format(Locale.ROOT, format, 0, 7, 1234))); + + series = new ScriptContextStats.TimeSeries(123, 456, 789); + builder = XContentFactory.jsonBuilder().prettyPrint(); + mkContextStats.apply(series).toXContent(builder, ToXContent.EMPTY_PARAMS); + assertThat(Strings.toString(builder), equalTo(String.format(Locale.ROOT, format, 123, 456, 789))); + } + + public void testTimeSeriesAssertions() { + expectThrows(AssertionError.class, () -> new ScriptContextStats.TimeSeries(-1, 1, 2)); + expectThrows(AssertionError.class, () -> new ScriptContextStats.TimeSeries(1, 0, 2)); + expectThrows(AssertionError.class, () -> new ScriptContextStats.TimeSeries(1, 3, 2)); + } + + public void testTimeSeriesIsEmpty() { + assertTrue((new ScriptContextStats.TimeSeries(0, 0, 0)).isEmpty()); + long day = randomLongBetween(1, 1024); + long fifteen = day >= 1 ? randomLongBetween(0, day) : 0; + long five = fifteen >= 1 ? randomLongBetween(0, fifteen) : 0; + assertFalse((new ScriptContextStats.TimeSeries(five, fifteen, day)).isEmpty()); + } + + public void testTimeSeriesSerialization() throws IOException { + ScriptContextStats stats = randomStats(); + + ScriptContextStats deserStats = serDeser(Version.V_7_16_0, Version.V_7_16_0, stats); + assertEquals(stats.getCompilations(), deserStats.getCompilations()); + assertEquals(stats.getCacheEvictions(), deserStats.getCacheEvictions()); + assertEquals(stats.getCompilationLimitTriggered(), deserStats.getCompilationLimitTriggered()); + assertNull(deserStats.getCompilationsHistory()); + assertNull(deserStats.getCacheEvictionsHistory()); + } + + public ScriptContextStats serDeser(Version outVersion, Version inVersion, ScriptContextStats stats) throws IOException { + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.setVersion(outVersion); + stats.writeTo(out); + try (StreamInput in = out.bytes().streamInput()) { + in.setVersion(inVersion); + return new ScriptContextStats(in); + } + } + } + + public ScriptContextStats randomStats() { + long[] histStats = {randomLongBetween(0, 2048), randomLongBetween(0, 2048)}; + List timeSeries = new ArrayList<>(); + for (int j = 0; j < 2; j++) { + if (randomBoolean() && histStats[j] > 0) { + long day = randomLongBetween(0, histStats[j]); + long fifteen = day >= 1 ? randomLongBetween(0, day) : 0; + long five = fifteen >= 1 ? randomLongBetween(0, fifteen) : 0; + timeSeries.add(new ScriptContextStats.TimeSeries(five, fifteen, day)); + } else { + timeSeries.add(new ScriptContextStats.TimeSeries()); + } + } + return new ScriptContextStats( + randomAlphaOfLength(15), + histStats[0], + histStats[1], + randomLongBetween(0, 1024), + timeSeries.get(0), + timeSeries.get(1) + ); + } +}