Add Redis caching for Minecraft servers

This commit is contained in:
Braydon 2024-04-06 22:54:31 -04:00
parent f99ffbf1e6
commit 3183c56d78
6 changed files with 80 additions and 8 deletions

@ -3,6 +3,7 @@ package me.braydon.mc.controller;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import me.braydon.mc.model.MinecraftServer; import me.braydon.mc.model.MinecraftServer;
import me.braydon.mc.model.cache.CachedMinecraftServer;
import me.braydon.mc.service.MojangService; import me.braydon.mc.service.MojangService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -38,8 +39,8 @@ public final class ServerController {
*/ */
@GetMapping("/{platform}/{hostname}") @GetMapping("/{platform}/{hostname}")
@ResponseBody @ResponseBody
public ResponseEntity<MinecraftServer> getServer(@PathVariable @NonNull String platform, public ResponseEntity<CachedMinecraftServer> getServer(@PathVariable @NonNull String platform,
@PathVariable @NonNull String hostname @PathVariable @NonNull String hostname
) { ) {
return ResponseEntity.ofNullable(mojangService.getMinecraftServer(platform, hostname)); return ResponseEntity.ofNullable(mojangService.getMinecraftServer(platform, hostname));
} }

@ -62,9 +62,9 @@ public class MinecraftServer {
private final int max; private final int max;
/** /**
* A sample of players on this server. * A sample of players on this server, null or empty if no sample.
*/ */
@NonNull private final Sample[] sample; private final Sample[] sample;
/** /**
* A sample player. * A sample player.

@ -0,0 +1,31 @@
package me.braydon.mc.model.cache;
import lombok.*;
import me.braydon.mc.model.MinecraftServer;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import java.io.Serializable;
/**
* @author Braydon
*/
@AllArgsConstructor @Setter @Getter @ToString
@RedisHash(value = "server", timeToLive = 60L) // 1 minute (in seconds)
public final class CachedMinecraftServer implements Serializable {
/**
* The hostname of the cached server.
*/
@Id @NonNull private transient final String hostname;
/**
* The cached server.
*/
@NonNull private final MinecraftServer value;
/**
* The unix timestamp of when this
* server was cached, -1 if not cached.
*/
private long cached;
}

@ -1,6 +1,8 @@
package me.braydon.mc.model.response; package me.braydon.mc.model.response;
import lombok.*; import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import java.util.Date; import java.util.Date;

@ -0,0 +1,11 @@
package me.braydon.mc.repository;
import me.braydon.mc.model.cache.CachedMinecraftServer;
import org.springframework.data.repository.CrudRepository;
/**
* A cache repository for {@link CachedMinecraftServer}'s.
*
* @author Braydon
*/
public interface MinecraftServerCacheRepository extends CrudRepository<CachedMinecraftServer, String> { }

@ -12,10 +12,12 @@ import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.exception.impl.InvalidMinecraftServerPlatform; import me.braydon.mc.exception.impl.InvalidMinecraftServerPlatform;
import me.braydon.mc.exception.impl.ResourceNotFoundException; import me.braydon.mc.exception.impl.ResourceNotFoundException;
import me.braydon.mc.model.*; import me.braydon.mc.model.*;
import me.braydon.mc.model.cache.CachedMinecraftServer;
import me.braydon.mc.model.cache.CachedPlayer; import me.braydon.mc.model.cache.CachedPlayer;
import me.braydon.mc.model.cache.CachedPlayerName; import me.braydon.mc.model.cache.CachedPlayerName;
import me.braydon.mc.model.token.MojangProfileToken; import me.braydon.mc.model.token.MojangProfileToken;
import me.braydon.mc.model.token.MojangUsernameToUUIDToken; import me.braydon.mc.model.token.MojangUsernameToUUIDToken;
import me.braydon.mc.repository.MinecraftServerCacheRepository;
import me.braydon.mc.repository.PlayerCacheRepository; import me.braydon.mc.repository.PlayerCacheRepository;
import me.braydon.mc.repository.PlayerNameCacheRepository; import me.braydon.mc.repository.PlayerNameCacheRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -49,10 +51,17 @@ public final class MojangService {
*/ */
@NonNull private final PlayerCacheRepository playerCache; @NonNull private final PlayerCacheRepository playerCache;
/**
* The cache repository for {@link MinecraftServer}'s.
*/
@NonNull private final MinecraftServerCacheRepository minecraftServerCache;
@Autowired @Autowired
public MojangService(@NonNull PlayerNameCacheRepository playerNameCache, @NonNull PlayerCacheRepository playerCache) { public MojangService(@NonNull PlayerNameCacheRepository playerNameCache, @NonNull PlayerCacheRepository playerCache,
@NonNull MinecraftServerCacheRepository minecraftServerCache) {
this.playerNameCache = playerNameCache; this.playerNameCache = playerNameCache;
this.playerCache = playerCache; this.playerCache = playerCache;
this.minecraftServerCache = minecraftServerCache;
} }
/** /**
@ -143,19 +152,37 @@ public final class MojangService {
* @throws ResourceNotFoundException if the server isn't found * @throws ResourceNotFoundException if the server isn't found
*/ */
@NonNull @NonNull
public MinecraftServer getMinecraftServer(@NonNull String platformName, @NonNull String hostname) public CachedMinecraftServer getMinecraftServer(@NonNull String platformName, @NonNull String hostname)
throws BadRequestException, InvalidMinecraftServerPlatform, ResourceNotFoundException { throws BadRequestException, InvalidMinecraftServerPlatform, ResourceNotFoundException {
MinecraftServer.Platform platform = EnumUtils.getEnumConstant(MinecraftServer.Platform.class, platformName.toUpperCase()); MinecraftServer.Platform platform = EnumUtils.getEnumConstant(MinecraftServer.Platform.class, platformName.toUpperCase());
if (platform == null) { // Invalid platform if (platform == null) { // Invalid platform
throw new InvalidMinecraftServerPlatform(); throw new InvalidMinecraftServerPlatform();
} }
String lookupHostname = hostname; // The hostname used to lookup the server
// Check the cache for the server
Optional<CachedMinecraftServer> cached = minecraftServerCache.findById(hostname);
if (cached.isPresent()) { // Respond with the cache if present
log.info("Found server in cache: {}", hostname);
return cached.get();
}
InetSocketAddress address = DNSUtils.resolveSRV(hostname); // Resolve the SRV record InetSocketAddress address = DNSUtils.resolveSRV(hostname); // Resolve the SRV record
int port = platform.getDefaultPort(); // Port to ping int port = platform.getDefaultPort(); // Port to ping
if (address != null) { // SRV was resolved, use the hostname and port if (address != null) { // SRV was resolved, use the hostname and port
hostname = address.getHostName(); hostname = address.getHostName();
port = address.getPort(); port = address.getPort();
} }
return platform.getPinger().ping(hostname, port); // Ping the server and return with the response // Build our server model, cache it, and then return it
CachedMinecraftServer minecraftServer = new CachedMinecraftServer(
lookupHostname,
platform.getPinger().ping(hostname, port),
System.currentTimeMillis()
);
minecraftServerCache.save(minecraftServer);
log.info("Cached server: {}", hostname);
minecraftServer.setCached(-1L); // Set to -1 to indicate it's not cached in the response
return minecraftServer;
} }
/** /**