From 4481f1c028e598659a97ec9081b8843e81a8404e Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Wed, 7 Jul 2021 17:03:05 +0200 Subject: [PATCH] Improved registry with more customization. --- .../aos_server_registry/ServerRegistry.java | 34 +++++++++-- .../data/ServerDataPruner.java | 9 ++- .../aos_server_registry/util/Requests.java | 3 + .../aos_server_registry/defaults.properties | 5 ++ .../aos_server/RegistryManager.java | 56 +++++++++++++------ 5 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/defaults.properties diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/ServerRegistry.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/ServerRegistry.java index 5c2171f..3fa5517 100644 --- a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/ServerRegistry.java +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/ServerRegistry.java @@ -10,22 +10,44 @@ import nl.andrewlalis.aos_server_registry.data.ServerDataPruner; import nl.andrewlalis.aos_server_registry.servlet.ServerInfoServlet; import javax.servlet.ServletException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ServerRegistry { - public static final int PORT = 8567; + public static final String SETTINGS_FILE = "settings.properties"; public static final ObjectMapper mapper = new ObjectMapper(); - public static void main(String[] args) throws ServletException { - startServer(); + public static void main(String[] args) throws ServletException, IOException { + Properties props = new Properties(); + props.load(ServerRegistry.class.getResourceAsStream("/nl/andrewlalis/aos_server_registry/defaults.properties")); + Path settingsPath = Path.of(SETTINGS_FILE); + if (Files.exists(settingsPath)) { + props.load(Files.newBufferedReader(settingsPath)); + } else { + System.out.println("Using built-in default settings. Create a settings.properties file to configure."); + } + + startServer(Integer.parseInt(props.getProperty("port"))); + // Every few minutes, prune all stale servers from the registry. + long pruneDelaySeconds = Long.parseLong(props.getProperty("prune-delay")); + long pruneThresholdMinutes = Long.parseLong(props.getProperty("prune-threshold-minutes")); + System.out.printf("Will prune servers inactive for more than %d minutes, checking every %d seconds.\n", pruneThresholdMinutes, pruneDelaySeconds); ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3); - scheduler.scheduleAtFixedRate(new ServerDataPruner(), 1, 1, TimeUnit.MINUTES); + scheduler.scheduleAtFixedRate(new ServerDataPruner(pruneThresholdMinutes), pruneDelaySeconds, pruneDelaySeconds, TimeUnit.SECONDS); } - private static void startServer() throws ServletException { + /** + * Starts the Undertow HTTP servlet container. + * @param port The port to bind to. + * @throws ServletException If the server could not be started. + */ + private static void startServer(int port) throws ServletException { DeploymentInfo servletBuilder = Servlets.deployment() .setClassLoader(ServerRegistry.class.getClassLoader()) .setContextPath("/") @@ -38,7 +60,7 @@ public class ServerRegistry { manager.deploy(); HttpHandler servletHandler = manager.start(); Undertow server = Undertow.builder() - .addHttpListener(PORT, "localhost") + .addHttpListener(port, "localhost") .setHandler(servletHandler) .build(); server.start(); diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ServerDataPruner.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ServerDataPruner.java index 371434c..47649e6 100644 --- a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ServerDataPruner.java +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ServerDataPruner.java @@ -9,9 +9,14 @@ import java.util.logging.Logger; * registry which have not been updated in a while. */ public class ServerDataPruner implements Runnable { - public static final int INTERVAL_MINUTES = 5; private static final Logger log = Logger.getLogger(ServerDataPruner.class.getName()); + private final long intervalMinutes; + + public ServerDataPruner(long intervalMinutes) { + this.intervalMinutes = intervalMinutes; + } + @Override public void run() { try { @@ -21,7 +26,7 @@ public class ServerDataPruner implements Runnable { WHERE DATEDIFF('MINUTE', servers.updated_at, CURRENT_TIMESTAMP(0)) > ? """; PreparedStatement stmt = con.prepareStatement(sql); - stmt.setInt(1, INTERVAL_MINUTES); + stmt.setLong(1, this.intervalMinutes); int rowCount = stmt.executeUpdate(); stmt.close(); if (rowCount > 0) { diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Requests.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Requests.java index ea186b9..a5621ab 100644 --- a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Requests.java +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Requests.java @@ -6,6 +6,9 @@ import java.util.function.Function; import static nl.andrewlalis.aos_server_registry.ServerRegistry.mapper; +/** + * Helper methods for working with HTTP requests. + */ public class Requests { public static T getBody(HttpServletRequest req, Class bodyClass) throws IOException { return mapper.readValue(req.getInputStream(), bodyClass); diff --git a/server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/defaults.properties b/server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/defaults.properties new file mode 100644 index 0000000..32fbaf4 --- /dev/null +++ b/server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/defaults.properties @@ -0,0 +1,5 @@ +# Default properties for the AOS Server Registry + +port=8567 +prune-delay=60 +prune-threshold-minutes=5 diff --git a/server/src/main/java/nl/andrewlalis/aos_server/RegistryManager.java b/server/src/main/java/nl/andrewlalis/aos_server/RegistryManager.java index d944db3..45ced45 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/RegistryManager.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/RegistryManager.java @@ -16,9 +16,7 @@ import java.time.Duration; import java.util.Base64; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; /** * The registry manager is responsible for keeping the server registry up to @@ -33,12 +31,22 @@ public class RegistryManager { public static final long[] RETRY_TIMINGS = new long[]{5, 10, 30, 60, 120, 300}; private int retryTimingIndex = 0; + private static final String ICON_FILE = "icon.png"; + private static final int ICON_SIZE = 64; + private final ScheduledExecutorService executorService; private final Server server; - private final ObjectMapper mapper; private final HttpClient httpClient; + /** + * Future that represents a planned attempt to send this server's info to + * the registry. This is tracked so that we can cancel this attempt if we + * discover during a more frequent update ping that the registry has been + * reset. + */ + private Future infoSendFuture; + public RegistryManager(Server server) { this.server = server; this.mapper = new ObjectMapper(); @@ -73,14 +81,14 @@ public class RegistryManager { var response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { System.err.println("Non-OK status when sending registry info:\n" + response.body() + "\nAttempting to send again in 10 seconds..."); - this.executorService.schedule(this::sendInfo, 10, TimeUnit.SECONDS); + this.infoSendFuture = this.executorService.schedule(this::sendInfo, 10, TimeUnit.SECONDS); } else if (this.retryTimingIndex > 0) { this.retryTimingIndex = 0; // Reset the retry timing index if we successfully sent our server info. } } catch (IOException e) { long retryTiming = RETRY_TIMINGS[this.retryTimingIndex]; System.err.println("Could not send info to registry server. Registry may be offline, or this server may not have internet access. Attempting to resend info in " + retryTiming + " seconds..."); - this.executorService.schedule(this::sendInfo, retryTiming, TimeUnit.SECONDS); + this. infoSendFuture = this.executorService.schedule(this::sendInfo, retryTiming, TimeUnit.SECONDS); if (this.retryTimingIndex < RETRY_TIMINGS.length - 1) { this.retryTimingIndex++; } @@ -103,8 +111,15 @@ public class RegistryManager { .build(); try { var response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() != 200) { - System.err.println("Received non-OK status when sending registry update:\n" + response.body()); + if (response.statusCode() == 404) { + System.out.println("Registry doesn't recognize this server yet. Sending info now..."); + // Cancel any existing plans to send info in the future, and send it now. + if (this.infoSendFuture != null && !this.infoSendFuture.isDone()) { + this.infoSendFuture.cancel(false); + } + this.infoSendFuture = this.executorService.submit(this::sendInfo); + } else if (response.statusCode() != 200) { + System.err.printf("Received status %d when sending registry update:\n%s\n", response.statusCode(), response.body()); } } catch (IOException e) { System.err.println("Error sending update to registry server: " + e); @@ -114,15 +129,24 @@ public class RegistryManager { } } - private String getIconData() throws IOException { - Path iconFile = Path.of("icon.png"); + /** + * Gets a Base64-URL-encoded string representing the bytes of this server's + * icon, if the server has an icon, or null otherwise. + * @return The Base64-encoded bytes of the server icon, or null. + */ + private String getIconData() { + Path iconFile = Path.of(ICON_FILE); if (Files.exists(iconFile)) { - byte[] imageBytes = Files.readAllBytes(iconFile); - BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes)); - if (image.getWidth() == 64 && image.getHeight() == 64) { - return Base64.getUrlEncoder().encodeToString(imageBytes); - } else { - System.err.println("icon.png must be 64 x 64."); + try { + byte[] imageBytes = Files.readAllBytes(iconFile); + BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes)); + if (image.getWidth() == ICON_SIZE && image.getHeight() == ICON_SIZE) { + return Base64.getUrlEncoder().encodeToString(imageBytes); + } else { + System.err.printf("%s must be %d x %d.\n", ICON_FILE, ICON_SIZE, ICON_SIZE); + } + } catch (IOException e) { + e.printStackTrace(); } } return null;