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 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();

View File

@ -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) {

View File

@ -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> T getBody(HttpServletRequest req, Class<T> bodyClass) throws IOException {
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.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;