Skip to content

Commit 525b06d

Browse files
authored
Add support for async metrics, make world size the first async metric (#220)
* add support for async metrics, make world size the first async metric * clean up execution logic a bit
1 parent 331885b commit 525b06d

File tree

5 files changed

+61
-40
lines changed

5 files changed

+61
-40
lines changed

src/main/java/de/sldk/mc/MetricRegistry.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java.util.ArrayList;
66
import java.util.List;
7+
import java.util.concurrent.CompletableFuture;
78

89
public class MetricRegistry {
910

@@ -23,8 +24,11 @@ public void register(Metric metric) {
2324
this.metrics.add(metric);
2425
}
2526

26-
void collectMetrics() {
27-
this.metrics.forEach(Metric::collect);
27+
CompletableFuture<Void> collectMetrics() {
28+
/* Combine all Completable futures into a single one */
29+
return CompletableFuture.allOf(this.metrics.stream()
30+
.map(Metric::collect)
31+
.toArray(CompletableFuture[]::new));
2832
}
2933

3034
}

src/main/java/de/sldk/mc/MetricsController.java

+1-11
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
import java.io.IOException;
1212
import java.util.concurrent.ExecutionException;
13-
import java.util.concurrent.Future;
1413
import java.util.logging.Level;
1514

1615
public class MetricsController extends AbstractHandler {
@@ -31,17 +30,8 @@ public void handle(String target, Request request, HttpServletRequest httpServle
3130
return;
3231
}
3332

34-
/*
35-
* Bukkit API calls have to be made from the main thread.
36-
* That's why we use the BukkitScheduler to retrieve the server stats.
37-
* */
38-
Future<Object> future = exporter.getServer().getScheduler().callSyncMethod(exporter, () -> {
39-
metricRegistry.collectMetrics();
40-
return null;
41-
});
42-
4333
try {
44-
future.get();
34+
metricRegistry.collectMetrics().get();
4535

4636
response.setStatus(HttpStatus.OK_200);
4737
response.setContentType(TextFormat.CONTENT_TYPE_004);

src/main/java/de/sldk/mc/metrics/Metric.java

+49-11
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import io.prometheus.client.Collector;
44
import io.prometheus.client.CollectorRegistry;
5+
import org.bukkit.Bukkit;
56
import org.bukkit.plugin.Plugin;
67

8+
import java.util.concurrent.CompletableFuture;
9+
import java.util.concurrent.ExecutionException;
710
import java.util.logging.Logger;
811

912
public abstract class Metric {
@@ -24,21 +27,56 @@ protected Plugin getPlugin() {
2427
return plugin;
2528
}
2629

27-
public void collect() {
28-
29-
if (!enabled) {
30-
return;
31-
}
32-
33-
try {
34-
doCollect();
35-
} catch (Exception e) {
36-
logException(e);
37-
}
30+
public CompletableFuture<Void> collect() {
31+
return CompletableFuture.runAsync(() -> {
32+
if (!enabled) {
33+
return;
34+
}
35+
36+
/* If metric is capable of async execution run it on a thread pool */
37+
if (isAsyncCapable()) {
38+
39+
try {
40+
doCollect();
41+
}
42+
catch (Exception e) {
43+
logException(e);
44+
}
45+
return;
46+
}
47+
48+
/*
49+
* Otherwise run the metric on the main thread and make the
50+
* thread on thread pool wait for completion
51+
*/
52+
try {
53+
Bukkit.getScheduler().callSyncMethod(plugin, () -> {
54+
try {
55+
doCollect();
56+
}
57+
catch (Exception e) {
58+
logException(e);
59+
}
60+
return null;
61+
}).get();
62+
} catch (InterruptedException | ExecutionException e) {
63+
logException(e);
64+
}
65+
});
3866
}
3967

4068
protected abstract void doCollect();
4169

70+
/**
71+
* This method is called during the Metric collection
72+
* to determine if it is safe to do it async.
73+
* By default, all Metrics are sync unless this method
74+
* is overridden.
75+
*/
76+
protected boolean isAsyncCapable() {
77+
return false;
78+
}
79+
4280
private void logException(Exception e) {
4381
final Logger log = plugin.getLogger();
4482
final String className = getClass().getSimpleName();

src/main/java/de/sldk/mc/metrics/WorldSize.java

+5
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,9 @@ public void collect(World world) {
3636
log.throwing(this.getClass().getSimpleName(), "collect", t);
3737
}
3838
}
39+
40+
@Override
41+
protected boolean isAsyncCapable() {
42+
return true;
43+
}
3944
}

src/test/java/de/sldk/mc/exporter/PrometheusExporterTest.java

-16
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@
22

33

44
import static org.assertj.core.api.Assertions.assertThat;
5-
import static org.mockito.ArgumentMatchers.any;
6-
import static org.mockito.Mockito.when;
75

86
import de.sldk.mc.MetricsServer;
97
import de.sldk.mc.PrometheusExporter;
108
import io.prometheus.client.CollectorRegistry;
119
import io.prometheus.client.Counter;
1210
import io.prometheus.client.exporter.common.TextFormat;
1311
import io.restassured.RestAssured;
14-
import org.bukkit.Server;
15-
import org.bukkit.scheduler.BukkitScheduler;
1612
import org.eclipse.jetty.http.HttpStatus;
1713
import org.eclipse.jetty.util.URIUtil;
1814
import org.junit.jupiter.api.AfterEach;
@@ -24,17 +20,12 @@
2420

2521
import java.io.IOException;
2622
import java.net.ServerSocket;
27-
import java.util.concurrent.CompletableFuture;
2823

2924
@ExtendWith(MockitoExtension.class)
3025
public class PrometheusExporterTest {
3126

3227
@Mock
3328
private PrometheusExporter exporterMock;
34-
@Mock
35-
private Server mockServer;
36-
@Mock
37-
private BukkitScheduler mockScheduler;
3829

3930
private int metricsServerPort;
4031
private MetricsServer metricsServer;
@@ -60,7 +51,6 @@ void cleanup() throws Exception {
6051

6152
@Test
6253
void metrics_server_should_return_valid_prometheus_response() {
63-
mockBukkitApis();
6454
mockPrometheusCounter("mc_mock_metric", "This is a mock metric", 419);
6555

6656
String requestPath = URIUtil.newURI("http", "localhost", metricsServerPort, "/metrics", null);
@@ -78,12 +68,6 @@ void metrics_server_should_return_valid_prometheus_response() {
7868
assertThat(lines[2]).isEqualTo("mc_mock_metric_total 419.0");
7969
}
8070

81-
private void mockBukkitApis() {
82-
when(exporterMock.getServer()).thenReturn(mockServer);
83-
when(mockServer.getScheduler()).thenReturn(mockScheduler);
84-
when(mockScheduler.callSyncMethod(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
85-
}
86-
8771
private void mockPrometheusCounter(String name, String help, int value) {
8872
Counter mockPrometheusCounter = Counter.build().name(name).help(help).register();
8973
mockPrometheusCounter.inc(value);

0 commit comments

Comments
 (0)