Skip to content

Commit f81bd0a

Browse files
authored
Merge from feature/reconnection
- Improve connection loss detection through ping checks in MetricsService - Properly trigger retry mechanism when connection is lost - Ensure coordinated reconnection between DatabaseService and MetricsService - Add public access to startRetryTaskIfNeeded for cross-service retry handling
2 parents 562874b + ee1a4b6 commit f81bd0a

File tree

6 files changed

+269
-27
lines changed

6 files changed

+269
-27
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
group = "it.renvins"
2-
version = "0.1.0-SNAPSHOT"
2+
version = "0.1.1-SNAPSHOT"
33

44
repositories {
55
mavenCentral()

plugin/src/main/java/it/renvins/serverpulse/ServerPulseLoader.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public void load() {
4141
return;
4242
}
4343
databaseService.load();
44+
if (!plugin.isEnabled()) {
45+
return;
46+
}
4447
metricsService.load();
4548
}
4649

plugin/src/main/java/it/renvins/serverpulse/service/IDatabaseService.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,32 @@
55

66
public interface IDatabaseService extends Service {
77

8+
/**
9+
* Performs a health check ping to the InfluxDB instance.
10+
* Should only be called internally by connect() or dedicated health checks.
11+
* @return true if ping is successful (HTTP 204), false otherwise.
12+
*/
13+
boolean ping();
14+
15+
/**
16+
* Returns the last known connection status. Does not perform a live check.
17+
* @return true if the service believes it's connected, false otherwise.
18+
*/
19+
boolean isConnected();
20+
21+
/**
22+
* Connects to the InfluxDB instance using the configured settings.
23+
* This method should be called before performing any database operations.
24+
*/
25+
void disconnect();
26+
27+
/**
28+
* Disconnects from the InfluxDB instance and cleans up resources.
29+
* This method should be called when the application is shutting down
30+
* or when the connection is no longer needed.
31+
*/
32+
void startRetryTaskIfNeeded();
33+
834
/**
935
* Gets the configured InfluxDB client instance.
1036
*
Lines changed: 231 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package it.renvins.serverpulse.service.impl;
22

3+
import java.net.SocketTimeoutException;
4+
import java.net.URI;
5+
import java.net.http.HttpClient;
6+
import java.net.http.HttpRequest;
7+
import java.net.http.HttpResponse;
8+
import java.time.Duration;
39
import java.util.logging.Level;
410

511
import com.influxdb.client.InfluxDBClient;
@@ -11,6 +17,7 @@
1117
import it.renvins.serverpulse.service.IDatabaseService;
1218
import lombok.Getter;
1319
import org.bukkit.configuration.ConfigurationSection;
20+
import org.bukkit.scheduler.BukkitTask;
1421

1522
public class DatabaseService implements IDatabaseService {
1623

@@ -20,68 +27,269 @@ public class DatabaseService implements IDatabaseService {
2027
@Getter private InfluxDBClient client;
2128
@Getter private WriteApi writeApi;
2229

30+
private HttpClient httpClient; // Keep for ping
31+
private volatile BukkitTask retryTask; // volatile for visibility across threads
32+
33+
private final int MAX_RETRIES = 5;
34+
private final long RETRY_DELAY_TICKS = 20L * 30L; // 30 seconds
35+
36+
// Use volatile as this is read/written by different threads
37+
private volatile boolean isConnected = false;
38+
private volatile int retryCount = 0;
39+
2340
public DatabaseService(ServerPulsePlugin plugin, CustomConfig customConfig) {
2441
this.plugin = plugin;
2542
this.customConfig = customConfig;
43+
this.httpClient = HttpClient.newBuilder()
44+
.connectTimeout(Duration.ofSeconds(10))
45+
.build();
2646
}
2747

2848
@Override
2949
public void load() {
3050
if (!checkConnectionData()) {
31-
ServerPulseLoader.LOGGER.severe("InfluxDB connection data is missing or wrong, shutting down the plugin...");
51+
ServerPulseLoader.LOGGER.severe("InfluxDB connection data is missing or invalid. Disabling plugin...");
3252
plugin.getServer().getPluginManager().disablePlugin(plugin);
3353
return;
3454
}
35-
connect();
55+
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, this::connect);
3656
}
3757

3858
@Override
3959
public void unload() {
40-
if (writeApi != null) {
41-
writeApi.close();
42-
ServerPulseLoader.LOGGER.info("Write API closed successfully...");
60+
stopRetryTask(); // Stop retries before disconnecting
61+
disconnect();
62+
if (httpClient != null) {
63+
httpClient.close();
4364
}
44-
if (client != null) {
45-
client.close();
46-
ServerPulseLoader.LOGGER.info("InfluxDB client closed successfully...");
65+
}
66+
67+
@Override
68+
public boolean ping() {
69+
String url = customConfig.getConfig().getString("metrics.influxdb.url");
70+
if (url == null || url.isEmpty()) {
71+
ServerPulseLoader.LOGGER.severe("InfluxDB URL is missing for ping...");
72+
return false;
73+
}
74+
75+
// Ensure httpClient is initialized
76+
if (this.httpClient == null) {
77+
ServerPulseLoader.LOGGER.severe("HttpClient not initialized for ping...");
78+
this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
79+
}
80+
81+
HttpRequest request;
82+
try {
83+
String pingUrl = url.endsWith("/") ? url + "ping" : url + "/ping";
84+
request = HttpRequest.newBuilder()
85+
.uri(URI.create(pingUrl))
86+
.GET()
87+
.timeout(Duration.ofSeconds(5)) // Add timeout specific to ping
88+
.build();
89+
} catch (IllegalArgumentException e) {
90+
ServerPulseLoader.LOGGER.log(Level.SEVERE, "Invalid InfluxDB URL format for ping: " + url, e);
91+
return false;
92+
}
93+
94+
try {
95+
HttpResponse<Void> response = this.httpClient.send(request, HttpResponse.BodyHandlers.discarding());
96+
return response.statusCode() == 204;
97+
} catch (java.net.ConnectException | java.net.UnknownHostException e) {
98+
ServerPulseLoader.LOGGER.warning("InfluxDB service is offline...");
99+
return false;
100+
} catch (
101+
SocketTimeoutException e) {
102+
ServerPulseLoader.LOGGER.warning("InfluxDB ping timed out: " + e.getMessage());
103+
return false;
104+
} catch (Exception e) {
105+
ServerPulseLoader.LOGGER.log(Level.SEVERE, "Error during InfluxDB ping: " + e.getMessage(), e);
106+
return false;
47107
}
48108
}
49109

110+
@Override
111+
public boolean isConnected() {
112+
// Return the flag, don't ping here!
113+
return this.isConnected;
114+
}
115+
50116
/**
51-
* Establishes the connection to the InfluxDB instance using details
52-
* from the configuration. Disables the plugin if the connection fails.
117+
* Attempts to connect to InfluxDB. Updates the internal connection status
118+
* and starts the retry task if connection fails.
119+
* Should be run asynchronously.
53120
*/
54121
private void connect() {
122+
// If already connected, don't try again unless forced (e.g., by retry task)
123+
// Note: This check might prevent the retry task from running if isConnected is true
124+
// but the connection actually dropped without detection. We rely on ping() inside here.
125+
126+
// Ensure previous resources are closed if attempting a new connection
127+
// This might be needed if connect() is called manually or by retry.
128+
disconnect();
129+
55130
ConfigurationSection section = customConfig.getConfig().getConfigurationSection("metrics.influxdb");
131+
if (section == null) {
132+
ServerPulseLoader.LOGGER.severe("InfluxDB config section missing during connect attempt...");
133+
return;
134+
}
135+
String url = section.getString("url");
136+
String token = section.getString("token");
137+
String org = section.getString("org");
138+
String bucket = section.getString("bucket");
139+
56140
try {
57-
client = InfluxDBClientFactory.create(section.getString("url"), section.getString("token").toCharArray(),
58-
section.getString("org"), section.getString("bucket"));
59-
writeApi = client.makeWriteApi();
141+
ServerPulseLoader.LOGGER.info("Attempting to connect to InfluxDB at " + url);
142+
client = InfluxDBClientFactory.create(url, token.toCharArray(), org, bucket);
60143

61-
ServerPulseLoader.LOGGER.info("Connected successfully to InfluxDB...");
144+
// Ping immediately after creating client to verify reachability & auth
145+
boolean isPingSuccessful = ping(); // Use the internal ping method
146+
147+
if (isPingSuccessful) {
148+
writeApi = client.makeWriteApi(); // Initialize Write API
149+
150+
this.isConnected = true;
151+
this.retryCount = 0; // Reset retry count on successful connection
152+
153+
stopRetryTask(); // Stop retrying if we just connected
154+
ServerPulseLoader.LOGGER.info("Successfully connected to InfluxDB and ping successful...");
155+
} else {
156+
// Ping failed after client creation
157+
ServerPulseLoader.LOGGER.warning("Created InfluxDB instance, but ping failed. Will retry...");
158+
this.isConnected = false; // Ensure status is false
159+
160+
if (client != null) {
161+
client.close(); // Close the client if ping failed
162+
client = null;
163+
}
164+
startRetryTaskIfNeeded(); // Start retry task
165+
}
62166
} catch (Exception e) {
63-
ServerPulseLoader.LOGGER.log(Level.SEVERE, "Could not connect to InfluxDB, shutting down the plugin...", e);
64-
plugin.getServer().getPluginManager().disablePlugin(plugin);
167+
// Handle exceptions during InfluxDBClientFactory.create() or ping()
168+
ServerPulseLoader.LOGGER.log(Level.SEVERE, "Failed to connect or ping InfluxDB: " + e.getMessage());
169+
this.isConnected = false;
170+
if (client != null) { // Ensure client is closed on exception
171+
client.close();
172+
client = null;
173+
}
174+
startRetryTaskIfNeeded(); // Start retry task
175+
}
176+
}
177+
178+
@Override
179+
public void disconnect() {
180+
if (writeApi != null) {
181+
try {
182+
writeApi.close();
183+
} catch (Exception e) {
184+
ServerPulseLoader.LOGGER.log(Level.WARNING, "Error closing InfluxDB WriteApi...", e);
185+
}
186+
writeApi = null;
187+
}
188+
if (client != null) {
189+
try {
190+
client.close();
191+
} catch (Exception e) {
192+
ServerPulseLoader.LOGGER.log(Level.WARNING, "Error closing InfluxDB Client...", e);
193+
}
194+
client = null;
195+
}
196+
this.isConnected = false;
197+
}
198+
199+
200+
@Override
201+
public synchronized void startRetryTaskIfNeeded() {
202+
if (retryTask != null && !retryTask.isCancelled()) {
203+
return;
204+
}
205+
if (!plugin.isEnabled()) {
206+
ServerPulseLoader.LOGGER.warning("Plugin disabling, not starting retry task...");
207+
return;
65208
}
209+
210+
// Reset retry count ONLY when starting the task sequence
211+
this.retryCount = 0;
212+
ServerPulseLoader.LOGGER.warning("Connection failed. Starting connection retry task (Max " + MAX_RETRIES + " attempts)...");
213+
214+
retryTask = plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, () -> {
215+
// Check connection status *first* using the flag
216+
if (this.isConnected) {
217+
ServerPulseLoader.LOGGER.info("Connection successful, stopping retry task...");
218+
stopRetryTask();
219+
return;
220+
}
221+
222+
// Check if plugin got disabled externally
223+
if (!plugin.isEnabled()) {
224+
ServerPulseLoader.LOGGER.warning("Plugin disabled during retry task execution...");
225+
stopRetryTask();
226+
return;
227+
}
228+
229+
// Check retries *before* attempting connection
230+
if (retryCount >= MAX_RETRIES) {
231+
ServerPulseLoader.LOGGER.severe("Max connection retries (" + MAX_RETRIES + ") reached. Disabling ServerPulse metrics...");
232+
stopRetryTask();
233+
disconnect(); // Clean up any partial connection
234+
// Schedule plugin disable on main thread
235+
plugin.getServer().getScheduler().runTask(plugin, () -> plugin.getServer().getPluginManager().disablePlugin(plugin));
236+
return;
237+
}
238+
retryCount++;
239+
240+
ServerPulseLoader.LOGGER.info("Retrying InfluxDB connection... Attempt " + retryCount + "/" + MAX_RETRIES);
241+
connect(); // Note: connect() will handle setting isConnected flag and potentially stopping the task if successful.
242+
}, RETRY_DELAY_TICKS, RETRY_DELAY_TICKS); // Start after delay, repeat at delay
66243
}
67244

245+
246+
/** Stops and nullifies the retry task if it's running. */
247+
private synchronized void stopRetryTask() {
248+
if (retryTask != null) {
249+
if (!retryTask.isCancelled()) {
250+
try {
251+
retryTask.cancel();
252+
} catch (Exception e) {
253+
// Ignore potential errors during cancellation
254+
}
255+
}
256+
retryTask = null;
257+
}
258+
}
259+
260+
68261
/**
69-
* Validates the presence and basic correctness of InfluxDB connection details
70-
* in the configuration file.
71-
*
72-
* @return {@code true} if connection data seems valid, {@code false} otherwise.
262+
* Checks if the essential connection data is present in the config.
263+
* @return true if data seems present, false otherwise.
73264
*/
74265
private boolean checkConnectionData() {
75266
ConfigurationSection section = customConfig.getConfig().getConfigurationSection("metrics.influxdb");
76267
if (section == null) {
268+
ServerPulseLoader.LOGGER.severe("Missing 'metrics.influxdb' section in config.");
77269
return false;
78270
}
79271
String url = section.getString("url");
80272
String bucket = section.getString("bucket");
81273
String org = section.getString("org");
82274
String token = section.getString("token");
83275

84-
return url != null && !url.isEmpty() && bucket != null && !bucket.isEmpty() &&
85-
org != null && !org.isEmpty() && token != null && !token.isEmpty() && !token.equals("my-token");
276+
boolean valid = true;
277+
if (url == null || url.isEmpty()) {
278+
ServerPulseLoader.LOGGER.severe("Missing or empty 'metrics.influxdb.url' in config...");
279+
valid = false;
280+
}
281+
if (bucket == null || bucket.isEmpty()) {
282+
ServerPulseLoader.LOGGER.severe("Missing or empty 'metrics.influxdb.bucket' in config...");
283+
valid = false;
284+
}
285+
if (org == null || org.isEmpty()) {
286+
ServerPulseLoader.LOGGER.severe("Missing or empty 'metrics.influxdb.org' in config...");
287+
valid = false;
288+
}
289+
if (token == null || token.isEmpty() || token.equals("my-token")) {
290+
ServerPulseLoader.LOGGER.severe("Missing, empty, or default 'metrics.influxdb.token' in config...");
291+
valid = false;
292+
}
293+
return valid;
86294
}
87-
}
295+
}

plugin/src/main/java/it/renvins/serverpulse/service/impl/MetricsService.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,12 @@ public void load() {
5555

5656
@Override
5757
public void collectAndSendMetrics() {
58-
if (databaseService.getWriteApi() == null) {
59-
ServerPulseLoader.LOGGER.warning("Database Write API not available. Skipping metrics send...");
58+
if (!databaseService.isConnected() || databaseService.getWriteApi() == null) {
59+
return;
60+
}
61+
if (!databaseService.ping()) {
62+
databaseService.disconnect();
63+
databaseService.startRetryTaskIfNeeded();
6064
return;
6165
}
6266
CompletableFuture.supplyAsync(this::collectSnapshot, Bukkit.getScheduler().getMainThreadExecutor(plugin))

plugin/src/main/resources/plugin.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ name: ServerPulse
22
version: ${rootProject.version}
33
main: it.renvins.serverpulse.ServerPulsePlugin
44
author: renvins
5-
description: ServerPulse plugin for monitoring server metrics
5+
description: ServerPulse plugin for monitoring server metrics
6+
api-version: 1.21.4

0 commit comments

Comments
 (0)