Skip to content

Commit a208e22

Browse files
authored
Avoid return negative value in CounterMetric (#71446)
A CounterMetric is used to track the number of completed and outstanding items, for example, the number of executed refreshes, the currently used memory by indexing, the current pending search requests. In all cases, the current count of CounterMetric is always non-negative. However, as this metric is implemented using a LongAdder, the returned count is NOT an atomic snapshot; invocation in the absence of concurrent updates returns an accurate result, but concurrent updates that occur while the sum is being calculated might not be incorporated. We can replace LongAdder with AtomicLong, but this commit chooses to continue using LongAdder but returns 0 when the sum value is negative. Relates #52411 Closes #70968
1 parent 07de28e commit a208e22

File tree

5 files changed

+54
-39
lines changed

5 files changed

+54
-39
lines changed

server/src/main/java/org/elasticsearch/common/metrics/CounterMetric.java

+38-2
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,65 @@
88

99
package org.elasticsearch.common.metrics;
1010

11+
import org.elasticsearch.Assertions;
12+
13+
import java.util.concurrent.atomic.AtomicLong;
1114
import java.util.concurrent.atomic.LongAdder;
1215

13-
public class CounterMetric implements Metric {
1416

17+
/**
18+
* A {@link CounterMetric} is used to track the number of completed and outstanding items, for example, the number of executed refreshes,
19+
* the currently used memory by indexing, the current pending search requests. In both cases, the current {@link CounterMetric#count} is
20+
* always non-negative.
21+
*/
22+
public final class CounterMetric {
1523
private final LongAdder counter = new LongAdder();
24+
private final AtomicLong assertingCounter = Assertions.ENABLED ? new AtomicLong() : null;
25+
26+
private boolean assertNonNegative(long n) {
27+
assert n >= 0 : "CounterMetric value must always be non-negative; got: " + n;
28+
return true;
29+
}
1630

1731
public void inc() {
1832
counter.increment();
33+
assert assertNonNegative(assertingCounter.incrementAndGet());
1934
}
2035

2136
public void inc(long n) {
2237
counter.add(n);
38+
assert assertNonNegative(assertingCounter.addAndGet(n));
2339
}
2440

2541
public void dec() {
2642
counter.decrement();
43+
assert assertNonNegative(assertingCounter.decrementAndGet());
2744
}
2845

2946
public void dec(long n) {
3047
counter.add(-n);
48+
assert assertNonNegative(assertingCounter.addAndGet(-n));
3149
}
3250

51+
/**
52+
* Returns the current count of this metric. The returned value is always non-negative.
53+
* <p>
54+
* As this metric is implemented using a {@link LongAdder}, the returned value is NOT an atomic snapshot;
55+
* invocation in the absence of concurrent updates returns an accurate result, but concurrent updates that
56+
* occur while the sum is being calculated might not be incorporated.
57+
*
58+
* @see LongAdder#sum()
59+
*/
3360
public long count() {
34-
return counter.sum();
61+
// The `counter.sum()` value is expected to always be non-negative. And if it's negative, then some concurrent updates
62+
// aren't incorporated yet. In this case, we can immediately return 0L; but here we choose to retry several times
63+
// to hopefully have a more accurate value than 0L.
64+
for (int i = 0; i < 5; i++) {
65+
final long count = counter.sum();
66+
if (count >= 0L) {
67+
return count;
68+
}
69+
}
70+
return 0L;
3571
}
3672
}

server/src/main/java/org/elasticsearch/common/metrics/MeanMetric.java

+8-12
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import java.util.concurrent.atomic.LongAdder;
1212

13-
public class MeanMetric implements Metric {
13+
public final class MeanMetric {
1414

1515
private final LongAdder counter = new LongAdder();
1616
private final LongAdder sum = new LongAdder();
@@ -20,13 +20,14 @@ public void inc(long n) {
2020
sum.add(n);
2121
}
2222

23-
public void dec(long n) {
24-
counter.decrement();
25-
sum.add(-n);
26-
}
27-
23+
/**
24+
* Returns the current count of this metric. This metric supports only {@link #inc(long)} that increases the counter
25+
* whenever it's invoked; hence, the returned count is always non-negative.
26+
*/
2827
public long count() {
29-
return counter.sum();
28+
final long count = counter.sum();
29+
assert count >= 0 : "Count of MeanMetric must always be non-negative; got " + count;
30+
return count;
3031
}
3132

3233
public long sum() {
@@ -40,9 +41,4 @@ public double mean() {
4041
}
4142
return 0.0;
4243
}
43-
44-
public void clear() {
45-
counter.reset();
46-
sum.reset();
47-
}
4844
}

server/src/main/java/org/elasticsearch/common/metrics/Metric.java

-12
This file was deleted.

server/src/main/java/org/elasticsearch/index/search/stats/ShardSearchStats.java

-5
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,8 @@ public void onFailedQueryPhase(SearchContext searchContext) {
7171
computeStats(searchContext, statsHolder -> {
7272
if (searchContext.hasOnlySuggest()) {
7373
statsHolder.suggestCurrent.dec();
74-
assert statsHolder.suggestCurrent.count() >= 0;
7574
} else {
7675
statsHolder.queryCurrent.dec();
77-
assert statsHolder.queryCurrent.count() >= 0;
7876
}
7977
});
8078
}
@@ -85,11 +83,9 @@ public void onQueryPhase(SearchContext searchContext, long tookInNanos) {
8583
if (searchContext.hasOnlySuggest()) {
8684
statsHolder.suggestMetric.inc(tookInNanos);
8785
statsHolder.suggestCurrent.dec();
88-
assert statsHolder.suggestCurrent.count() >= 0;
8986
} else {
9087
statsHolder.queryMetric.inc(tookInNanos);
9188
statsHolder.queryCurrent.dec();
92-
assert statsHolder.queryCurrent.count() >= 0;
9389
}
9490
});
9591
}
@@ -109,7 +105,6 @@ public void onFetchPhase(SearchContext searchContext, long tookInNanos) {
109105
computeStats(searchContext, statsHolder -> {
110106
statsHolder.fetchMetric.inc(tookInNanos);
111107
statsHolder.fetchCurrent.dec();
112-
assert statsHolder.fetchCurrent.count() >= 0;
113108
});
114109
}
115110

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/slm/SnapshotLifecycleStatsTests.java

+8-8
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ protected SnapshotLifecycleStats doParseInstance(XContentParser parser) throws I
2323

2424
public static SnapshotLifecycleStats.SnapshotPolicyStats randomPolicyStats(String policyId) {
2525
return new SnapshotLifecycleStats.SnapshotPolicyStats(policyId,
26-
randomBoolean() ? 0 : randomNonNegativeLong(),
27-
randomBoolean() ? 0 : randomNonNegativeLong(),
28-
randomBoolean() ? 0 : randomNonNegativeLong(),
29-
randomBoolean() ? 0 : randomNonNegativeLong());
26+
randomBoolean() ? 0 : randomIntBetween(0, Integer.MAX_VALUE),
27+
randomBoolean() ? 0 : randomIntBetween(0, Integer.MAX_VALUE),
28+
randomBoolean() ? 0 : randomIntBetween(0, Integer.MAX_VALUE),
29+
randomBoolean() ? 0 : randomIntBetween(0, Integer.MAX_VALUE));
3030
}
3131

3232
public static SnapshotLifecycleStats randomLifecycleStats() {
@@ -37,10 +37,10 @@ public static SnapshotLifecycleStats randomLifecycleStats() {
3737
policyStats.put(policy, randomPolicyStats(policy));
3838
}
3939
return new SnapshotLifecycleStats(
40-
randomBoolean() ? 0 : randomNonNegativeLong(),
41-
randomBoolean() ? 0 : randomNonNegativeLong(),
42-
randomBoolean() ? 0 : randomNonNegativeLong(),
43-
randomBoolean() ? 0 : randomNonNegativeLong(),
40+
randomBoolean() ? 0 : randomIntBetween(0, Integer.MAX_VALUE),
41+
randomBoolean() ? 0 : randomIntBetween(0, Integer.MAX_VALUE),
42+
randomBoolean() ? 0 : randomIntBetween(0, Integer.MAX_VALUE),
43+
randomBoolean() ? 0 : randomIntBetween(0, Integer.MAX_VALUE),
4444
policyStats);
4545
}
4646

0 commit comments

Comments
 (0)