Skip to content

Commit d0059b9

Browse files
authored
Add robustness to some of the timing-susceptible metrics tests; add util matcher with retry (helidon-io#5047)
Signed-off-by: tim.quinn@oracle.com <tim.quinn@oracle.com> Signed-off-by: tim.quinn@oracle.com <tim.quinn@oracle.com>
1 parent e66e900 commit d0059b9

7 files changed

Lines changed: 148 additions & 36 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (c) 2022 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.config.testing;
17+
18+
import java.util.concurrent.Callable;
19+
import java.util.function.Supplier;
20+
21+
import org.hamcrest.Description;
22+
import org.hamcrest.Matcher;
23+
import org.hamcrest.StringDescription;
24+
25+
/**
26+
* Hamcrest matcher capable of configured retries before failing the assertion, plus more generic retry processing.
27+
*/
28+
public class MatcherWithRetry {
29+
30+
private static final int RETRY_COUNT = Integer.getInteger("io.helidon.test.retryCount", 10);
31+
private static final int RETRY_DELAY_MS = Integer.getInteger("io.helidon.test.retryDelayMs", 500);
32+
33+
private MatcherWithRetry() {
34+
}
35+
36+
/**
37+
* Checks the matcher, possibly multiple times after configured delays, invoking the supplier of the matched value each time,
38+
* until either the matcher passes or the maximum retry expires.
39+
* @param reason explanation of the assertion
40+
* @param actualSupplier {@code Supplier} that furnishes the value to submit to the matcher
41+
* @param matcher Hamcrest matcher which evaluates the supplied value
42+
* @return the supplied value
43+
* @param <T> type of the supplied value
44+
* @throws InterruptedException if the sleep is interrupted
45+
*/
46+
public static <T> T assertThatWithRetry(String reason, Supplier<T> actualSupplier, Matcher<? super T> matcher)
47+
throws InterruptedException {
48+
49+
T actual = null;
50+
for (int i = 0; i < RETRY_COUNT; i++) {
51+
actual = actualSupplier.get();
52+
if (matcher.matches(actual)) {
53+
return actual;
54+
}
55+
Thread.sleep(RETRY_DELAY_MS);
56+
}
57+
58+
Description description = new StringDescription();
59+
description.appendText(reason)
60+
.appendText("\nExpected: ")
61+
.appendDescriptionOf(matcher)
62+
.appendText("\n but: ");
63+
matcher.describeMismatch(actual, description);
64+
65+
throw new AssertionError(description.toString());
66+
}
67+
68+
/**
69+
* Retries the specified work until successful or retry limit is exceeded.
70+
*
71+
* @param work the work to perform; returned boolean indicates if the work was successful
72+
* @throws Exception exception from the work or from the sleep being interrupted
73+
*/
74+
public static void retry(Callable<Boolean> work) throws Exception {
75+
for (int i = 0; i < RETRY_COUNT; i++) {
76+
if (work.call()) {
77+
return;
78+
}
79+
Thread.sleep(RETRY_DELAY_MS);
80+
}
81+
}
82+
}

metrics/metrics/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@
8787
<artifactId>helidon-webclient</artifactId>
8888
<scope>test</scope>
8989
</dependency>
90+
<dependency>
91+
<groupId>io.helidon.config</groupId>
92+
<artifactId>helidon-config-testing</artifactId>
93+
<scope>test</scope>
94+
</dependency>
9095
<dependency>
9196
<groupId>org.junit.jupiter</groupId>
9297
<artifactId>junit-jupiter-api</artifactId>

metrics/metrics/src/test/java/io/helidon/metrics/TestServer.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import io.helidon.common.http.Http;
3030
import io.helidon.common.http.MediaType;
31+
import io.helidon.config.testing.MatcherWithRetry;
3132
import io.helidon.media.jsonp.JsonpSupport;
3233
import io.helidon.webclient.WebClient;
3334
import io.helidon.webclient.WebClientRequestBuilder;
@@ -187,7 +188,7 @@ void checkKPIDisabledByDefault() {
187188
}
188189

189190
@Test
190-
void checkMetricsForExecutorService() {
191+
void checkMetricsForExecutorService() throws Exception {
191192

192193
String jsonKeyForCompleteTaskCountInThreadPool =
193194
"executor-service.completed-task-count;poolIndex=0;supplierCategory=my-thread-thread-pool-1;supplierIndex=0";
@@ -223,22 +224,25 @@ void checkMetricsForExecutorService() {
223224

224225
assertThat("Slow greet access response status", slowGreetResponse.status().code(), is(200));
225226

226-
WebClientResponse secondMetricsResponse = metricsRequestBuilder
227-
.submit()
228-
.await(CLIENT_TIMEOUT);
227+
MatcherWithRetry.retry(() -> {
229228

230-
assertThat("Second access to metrics", secondMetricsResponse.status().code(), is(200));
229+
WebClientResponse secondMetricsResponse = metricsRequestBuilder
230+
.submit()
231+
.await(CLIENT_TIMEOUT);
231232

232-
JsonObject secondMetrics = secondMetricsResponse.content().as(JsonObject.class).await(CLIENT_TIMEOUT);
233+
assertThat("Second access to metrics", secondMetricsResponse.status().code(), is(200));
233234

234-
assertThat("JSON metrics results after accessing slow endpoint",
235-
secondMetrics,
236-
hasKey(jsonKeyForCompleteTaskCountInThreadPool));
235+
JsonObject secondMetrics = secondMetricsResponse.content().as(JsonObject.class).await(CLIENT_TIMEOUT);
237236

237+
assertThat("JSON metrics results after accessing slow endpoint",
238+
secondMetrics,
239+
hasKey(jsonKeyForCompleteTaskCountInThreadPool));
238240

239-
int secondCompletedTaskCount = secondMetrics.getInt(jsonKeyForCompleteTaskCountInThreadPool);
241+
int secondCompletedTaskCount = secondMetrics.getInt(jsonKeyForCompleteTaskCountInThreadPool);
240242

241-
assertThat("Completed task count after accessing slow endpoint", secondCompletedTaskCount, is(1));
243+
assertThat("Completed task count after accessing slow endpoint", secondCompletedTaskCount, is(1));
244+
return true;
245+
});
242246
}
243247

244248
@ParameterizedTest

microprofile/metrics/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@
9797
<artifactId>helidon-microprofile-tests-junit5</artifactId>
9898
<scope>test</scope>
9999
</dependency>
100+
<dependency>
101+
<groupId>io.helidon.config</groupId>
102+
<artifactId>helidon-config-testing</artifactId>
103+
<scope>test</scope>
104+
</dependency>
100105
</dependencies>
101106

102107
<build>

microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldAsyncResponseTest.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.eclipse.microprofile.metrics.annotation.RegistryType;
3939
import org.junit.jupiter.api.Test;
4040

41+
import static io.helidon.config.testing.MatcherWithRetry.assertThatWithRetry;
4142
import static io.helidon.metrics.api.KeyPerformanceIndicatorMetricsSettings.Builder.KEY_PERFORMANCE_INDICATORS_CONFIG_KEY;
4243
import static io.helidon.metrics.api.KeyPerformanceIndicatorMetricsSettings.Builder.KEY_PERFORMANCE_INDICATORS_EXTENDED_CONFIG_KEY;
4344
import static io.helidon.microprofile.metrics.HelloWorldResource.SLOW_MESSAGE_SIMPLE_TIMER;
@@ -117,9 +118,9 @@ public void test() throws Exception {
117118

118119
Duration minDuration = Duration.ofMillis(HelloWorldResource.SLOW_DELAY_MS);
119120

120-
assertThat("Change in count for explicit SimpleTimer",
121-
explicitSimpleTimer.getCount() - explicitSimpleTimerCountBefore, is(1L));
122-
long explicitDiffNanos = explicitSimpleTimer.getElapsedTime().toNanos() - explicitSimpleTimerDurationBefore.toNanos();
121+
assertThatWithRetry("Change in count for explicit SimpleTimer",
122+
() -> explicitSimpleTimer.getCount() - explicitSimpleTimerCountBefore,
123+
is(1L));long explicitDiffNanos = explicitSimpleTimer.getElapsedTime().toNanos() - explicitSimpleTimerDurationBefore.toNanos();
123124
assertThat("Change in elapsed time for explicit SimpleTimer", explicitDiffNanos, is(greaterThan(minDuration.toNanos())));
124125

125126
long syntheticDiffNanos = simpleTimer.getElapsedTime().toNanos() - syntheticaSimpleTimerDurationBefore.toNanos();

microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldAsyncResponseWithRestRequestTest.java

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package io.helidon.microprofile.metrics;
1717

1818
import java.util.Map;
19-
import java.util.concurrent.TimeUnit;
2019

2120
import io.helidon.microprofile.tests.junit5.AddConfig;
2221
import io.helidon.microprofile.tests.junit5.HelidonTest;
@@ -31,6 +30,7 @@
3130
import jakarta.ws.rs.core.Response;
3231
import org.junit.jupiter.api.Test;
3332

33+
import static io.helidon.config.testing.MatcherWithRetry.assertThatWithRetry;
3434
import static org.hamcrest.MatcherAssert.assertThat;
3535
import static org.hamcrest.Matchers.containsString;
3636
import static org.hamcrest.Matchers.greaterThan;
@@ -53,10 +53,11 @@ void checkForAsyncMethodRESTRequestMetric() throws NoSuchMethodException {
5353
JsonObject restRequest = getRESTRequestJSON();
5454

5555
// Make sure count is 0 before invoking.
56-
JsonNumber getAsyncCount = JsonNumber.class.cast(getRESTRequestValueForGetAsyncMethod(restRequest,
57-
"count",
58-
false));
59-
assertThat("getAsync count value before invocation", getAsyncCount.longValue(), is(0L));
56+
long getAsyncCount = JsonNumber.class.cast(getRESTRequestValueForGetAsyncMethod(restRequest,
57+
"count",
58+
false))
59+
.longValue();
60+
assertThat("getAsync count value before invocation", getAsyncCount, is(0L));
6061

6162
JsonNumber getAsyncTime = JsonNumber.class.cast(getRESTRequestValueForGetAsyncMethod(restRequest,
6263
"elapsedTime",
@@ -85,15 +86,20 @@ void checkForAsyncMethodRESTRequestMetric() throws NoSuchMethodException {
8586
// Retrieve metrics again and make sure we see an additional count and added time. Don't bother checking the min and
8687
// max because we'd have to wait up to a minute for those values to change.
8788

88-
restRequest = getRESTRequestJSON();
89+
JsonObject nextRestRequest = getRESTRequestJSON();
8990

9091
try {
91-
getAsyncCount = JsonNumber.class.cast(getRESTRequestValueForGetAsyncMethod(restRequest,
92-
"count",
93-
false));
94-
assertThat("getAsync count value after invocation", getAsyncCount.longValue(), is(1L));
92+
// With async endpoints, metrics updates can occur after the server sends the response.
93+
// Retry as needed for a little while for the count to change.
94+
assertThatWithRetry("getAsync count value after invocation",
95+
() -> JsonNumber.class.cast(getRESTRequestValueForGetAsyncMethod(nextRestRequest,
96+
"count",
97+
false))
98+
.longValue(),
99+
is(1L));
95100

96-
getAsyncTime = JsonNumber.class.cast(getRESTRequestValueForGetAsyncMethod(restRequest,
101+
102+
getAsyncTime = JsonNumber.class.cast(getRESTRequestValueForGetAsyncMethod(nextRestRequest,
97103
"elapsedTime",
98104
false));
99105
assertThat("getAsync elapsedTime value after invocation", getAsyncTime.longValue(), is(greaterThan(0L)));
@@ -124,20 +130,24 @@ private JsonObject getRESTRequestJSON() {
124130

125131
private JsonValue getRESTRequestValueForGetAsyncMethod(JsonObject restRequestJson,
126132
String valueName,
127-
boolean nullOK) throws NoSuchMethodException {
133+
boolean nullOK) {
128134
JsonValue getAsyncValue = null;
129135

130136
for (Map.Entry<String, JsonValue> entry : restRequestJson.entrySet()) {
131137
// Conceivably there could be multiple tags in the metric ID besides the class and method ones, so do not assume
132138
// the key value in the JSON object has only the class and method tags and only in that order.
133-
if (entry.getKey().startsWith(valueName + ";")
134-
&& entry.getKey().contains("class=" + HelloWorldResource.class.getName())
135-
&& entry.getKey().contains(
136-
"method="
137-
+ HelloWorldResource.class.getMethod("getAsync", AsyncResponse.class).getName()
138-
+ "_" + AsyncResponse.class.getName())) {
139-
getAsyncValue = entry.getValue();
140-
break;
139+
try {
140+
if (entry.getKey().startsWith(valueName + ";")
141+
&& entry.getKey().contains("class=" + HelloWorldResource.class.getName())
142+
&& entry.getKey().contains(
143+
"method="
144+
+ HelloWorldResource.class.getMethod("getAsync", AsyncResponse.class).getName()
145+
+ "_" + AsyncResponse.class.getName())) {
146+
getAsyncValue = entry.getValue();
147+
break;
148+
}
149+
} catch (NoSuchMethodException e) {
150+
throw new RuntimeException(e);
141151
}
142152
}
143153

microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldTest.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.junit.jupiter.api.BeforeEach;
4242
import org.junit.jupiter.api.Test;
4343

44+
import static io.helidon.config.testing.MatcherWithRetry.assertThatWithRetry;
4445
import static io.helidon.microprofile.metrics.HelloWorldResource.MESSAGE_SIMPLE_TIMER;
4546
import static org.hamcrest.MatcherAssert.assertThat;
4647
import static org.hamcrest.Matchers.is;
@@ -151,7 +152,9 @@ public void testMappedException() throws Exception {
151152
);
152153

153154
assertThat("Response code from mapped exception endpoint", response.getStatus(), is(500));
154-
assertThat("Change in successful count", simpleTimer.getCount() - successfulBeforeRequest, is(1L));
155+
assertThatWithRetry("Change in successful count",
156+
() -> simpleTimer.getCount() - successfulBeforeRequest,
157+
is(1L));
155158
assertThat("Change in unsuccessful count", counter.getCount() - unsuccessfulBeforeRequest, is(0L));
156159
}
157160

@@ -172,7 +175,9 @@ void testUnmappedException() throws Exception {
172175
);
173176

174177
assertThat("Response code from unmapped exception endpoint", response.getStatus(), is(500));
175-
assertThat("Change in successful count", simpleTimer.getCount() - successfulBeforeRequest, is(0L));
178+
assertThatWithRetry("Change in successful count",
179+
() -> simpleTimer.getCount() - successfulBeforeRequest,
180+
is(0L));
176181
assertThat("Change in unsuccessful count", counter.getCount() - unsuccessfulBeforeRequest, is(1L));
177182
}
178183

0 commit comments

Comments
 (0)