Improved registry with more customization.

This commit is contained in:
Andrew Lalis 2021-07-07 17:03:05 +02:00
parent fa8c553041
commit 4481f1c028
5 changed files with 83 additions and 24 deletions

View File

@ -10,22 +10,44 @@ import nl.andrewlalis.aos_server_registry.data.ServerDataPruner;
import nl.andrewlalis.aos_server_registry.servlet.ServerInfoServlet; import nl.andrewlalis.aos_server_registry.servlet.ServerInfoServlet;
import javax.servlet.ServletException; 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.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class ServerRegistry { 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 final ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) throws ServletException { public static void main(String[] args) throws ServletException, IOException {
startServer(); 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. // 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); 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() DeploymentInfo servletBuilder = Servlets.deployment()
.setClassLoader(ServerRegistry.class.getClassLoader()) .setClassLoader(ServerRegistry.class.getClassLoader())
.setContextPath("/") .setContextPath("/")
@ -38,7 +60,7 @@ public class ServerRegistry {
manager.deploy(); manager.deploy();
HttpHandler servletHandler = manager.start(); HttpHandler servletHandler = manager.start();
Undertow server = Undertow.builder() Undertow server = Undertow.builder()
.addHttpListener(PORT, "localhost") .addHttpListener(port, "localhost")
.setHandler(servletHandler) .setHandler(servletHandler)
.build(); .build();
server.start(); server.start();

View File

@ -9,9 +9,14 @@ import java.util.logging.Logger;
* registry which have not been updated in a while. * registry which have not been updated in a while.
*/ */
public class ServerDataPruner implements Runnable { public class ServerDataPruner implements Runnable {
public static final int INTERVAL_MINUTES = 5;
private static final Logger log = Logger.getLogger(ServerDataPruner.class.getName()); private static final Logger log = Logger.getLogger(ServerDataPruner.class.getName());
private final long intervalMinutes;
public ServerDataPruner(long intervalMinutes) {
this.intervalMinutes = intervalMinutes;
}
@Override @Override
public void run() { public void run() {
try { try {
@ -21,7 +26,7 @@ public class ServerDataPruner implements Runnable {
WHERE DATEDIFF('MINUTE', servers.updated_at, CURRENT_TIMESTAMP(0)) > ? WHERE DATEDIFF('MINUTE', servers.updated_at, CURRENT_TIMESTAMP(0)) > ?
"""; """;
PreparedStatement stmt = con.prepareStatement(sql); PreparedStatement stmt = con.prepareStatement(sql);
stmt.setInt(1, INTERVAL_MINUTES); stmt.setLong(1, this.intervalMinutes);
int rowCount = stmt.executeUpdate(); int rowCount = stmt.executeUpdate();
stmt.close(); stmt.close();
if (rowCount > 0) { if (rowCount > 0) {

View File

@ -6,6 +6,9 @@ import java.util.function.Function;
import static nl.andrewlalis.aos_server_registry.ServerRegistry.mapper; import static nl.andrewlalis.aos_server_registry.ServerRegistry.mapper;
/**
* Helper methods for working with HTTP requests.
*/
public class Requests { public class Requests {
public static <T> T getBody(HttpServletRequest req, Class<T> bodyClass) throws IOException { public static <T> T getBody(HttpServletRequest req, Class<T> bodyClass) throws IOException {
return mapper.readValue(req.getInputStream(), bodyClass); return mapper.readValue(req.getInputStream(), bodyClass);

View File

@ -0,0 +1,5 @@
# Default properties for the AOS Server Registry
port=8567
prune-delay=60
prune-threshold-minutes=5

View File

@ -16,9 +16,7 @@ import java.time.Duration;
import java.util.Base64; import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executors; import java.util.concurrent.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/** /**
* The registry manager is responsible for keeping the server registry up to * 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}; public static final long[] RETRY_TIMINGS = new long[]{5, 10, 30, 60, 120, 300};
private int retryTimingIndex = 0; 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 ScheduledExecutorService executorService;
private final Server server; private final Server server;
private final ObjectMapper mapper; private final ObjectMapper mapper;
private final HttpClient httpClient; 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) { public RegistryManager(Server server) {
this.server = server; this.server = server;
this.mapper = new ObjectMapper(); this.mapper = new ObjectMapper();
@ -73,14 +81,14 @@ public class RegistryManager {
var response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString()); var response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) { if (response.statusCode() != 200) {
System.err.println("Non-OK status when sending registry info:\n" + response.body() + "\nAttempting to send again in 10 seconds..."); 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) { } else if (this.retryTimingIndex > 0) {
this.retryTimingIndex = 0; // Reset the retry timing index if we successfully sent our server info. this.retryTimingIndex = 0; // Reset the retry timing index if we successfully sent our server info.
} }
} catch (IOException e) { } catch (IOException e) {
long retryTiming = RETRY_TIMINGS[this.retryTimingIndex]; 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..."); 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) { if (this.retryTimingIndex < RETRY_TIMINGS.length - 1) {
this.retryTimingIndex++; this.retryTimingIndex++;
} }
@ -103,8 +111,15 @@ public class RegistryManager {
.build(); .build();
try { try {
var response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString()); var response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) { if (response.statusCode() == 404) {
System.err.println("Received non-OK status when sending registry update:\n" + response.body()); 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) { } catch (IOException e) {
System.err.println("Error sending update to registry server: " + 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)) { if (Files.exists(iconFile)) {
byte[] imageBytes = Files.readAllBytes(iconFile); try {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes)); byte[] imageBytes = Files.readAllBytes(iconFile);
if (image.getWidth() == 64 && image.getHeight() == 64) { BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes));
return Base64.getUrlEncoder().encodeToString(imageBytes); if (image.getWidth() == ICON_SIZE && image.getHeight() == ICON_SIZE) {
} else { return Base64.getUrlEncoder().encodeToString(imageBytes);
System.err.println("icon.png must be 64 x 64."); } else {
System.err.printf("%s must be %d x %d.\n", ICON_FILE, ICON_SIZE, ICON_SIZE);
}
} catch (IOException e) {
e.printStackTrace();
} }
} }
return null; return null;