From 92e60135fe566dafc41a59d67119c362200b5405 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sun, 1 May 2022 15:40:42 +0200 Subject: [PATCH] Added better error handling, and prepared for arena ship management. --- .../starship_arena/server/SocketGateway.java | 8 ++- .../server/api/ApiExceptionHandler.java | 15 +++++ .../server/api/dto/ErrorResponse.java | 6 ++ .../server/data/ArenaStore.java | 32 +++++++--- .../starship_arena/server/model/Arena.java | 4 ++ .../src/main/resources/application.properties | 2 + server/src/main/resources/ships/corvette.json | 62 +++++++++++++++++++ 7 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 server/src/main/java/nl/andrewl/starship_arena/server/api/ApiExceptionHandler.java create mode 100644 server/src/main/java/nl/andrewl/starship_arena/server/api/dto/ErrorResponse.java create mode 100644 server/src/main/resources/ships/corvette.json diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/SocketGateway.java b/server/src/main/java/nl/andrewl/starship_arena/server/SocketGateway.java index 96cec36..7c007d7 100644 --- a/server/src/main/java/nl/andrewl/starship_arena/server/SocketGateway.java +++ b/server/src/main/java/nl/andrewl/starship_arena/server/SocketGateway.java @@ -29,7 +29,9 @@ import java.util.concurrent.Executors; @Service @Slf4j public class SocketGateway implements Runnable { - public static final int INITIALIZATION_TIMEOUT = 1000; + /** + * The serializer that's used for communication between arenas and clients. + */ public static final Serializer SERIALIZER = new Serializer(); static { SERIALIZER.registerType(1, ClientConnectRequest.class); @@ -50,6 +52,8 @@ public class SocketGateway implements Runnable { private short tcpPort; @Value("${starship-arena.gateway.udp-port}") @Getter private short udpPort; + @Value("${starship-arena.gateway.init-timeout-ms}") + private int initTimeout; public SocketGateway(ArenaStore arenaStore) throws IOException { this.serverSocket = new ServerSocket(); @@ -104,7 +108,7 @@ public class SocketGateway implements Runnable { */ private void processIncomingConnection(Socket clientSocket) { try { - clientSocket.setSoTimeout(INITIALIZATION_TIMEOUT); // Set limited timeout so new connections don't waste resources. + clientSocket.setSoTimeout(initTimeout); // Set limited timeout so new connections don't waste resources. Message msg = SERIALIZER.readMessage(clientSocket.getInputStream()); if (msg instanceof ClientConnectRequest cm) { UUID arenaId = cm.arenaId(); diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/api/ApiExceptionHandler.java b/server/src/main/java/nl/andrewl/starship_arena/server/api/ApiExceptionHandler.java new file mode 100644 index 0000000..122880c --- /dev/null +++ b/server/src/main/java/nl/andrewl/starship_arena/server/api/ApiExceptionHandler.java @@ -0,0 +1,15 @@ +package nl.andrewl.starship_arena.server.api; + +import nl.andrewl.starship_arena.server.api.dto.ErrorResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.server.ResponseStatusException; + +@RestControllerAdvice +public class ApiExceptionHandler { + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity handleRSE(ResponseStatusException e) { + return new ResponseEntity<>(new ErrorResponse(e.getRawStatusCode(), e.getReason()), e.getStatus()); + } +} diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/api/dto/ErrorResponse.java b/server/src/main/java/nl/andrewl/starship_arena/server/api/dto/ErrorResponse.java new file mode 100644 index 0000000..6f81424 --- /dev/null +++ b/server/src/main/java/nl/andrewl/starship_arena/server/api/dto/ErrorResponse.java @@ -0,0 +1,6 @@ +package nl.andrewl.starship_arena.server.api.dto; + +public record ErrorResponse( + int status, + String message +) {} diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/data/ArenaStore.java b/server/src/main/java/nl/andrewl/starship_arena/server/data/ArenaStore.java index c9c1c51..767641c 100644 --- a/server/src/main/java/nl/andrewl/starship_arena/server/data/ArenaStore.java +++ b/server/src/main/java/nl/andrewl/starship_arena/server/data/ArenaStore.java @@ -6,6 +6,7 @@ import nl.andrewl.starship_arena.server.api.dto.ArenaResponse; import nl.andrewl.starship_arena.server.control.ArenaUpdater; import nl.andrewl.starship_arena.server.model.Arena; import nl.andrewl.starship_arena.server.model.ArenaStage; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; @@ -25,10 +26,14 @@ import java.util.concurrent.TimeUnit; @EnableScheduling @Slf4j public class ArenaStore { + @Value("${starship-arena.max-arenas : 100}") + private int maxArenas; + private final Map arenas = new ConcurrentHashMap<>(); public List getArenas() { return arenas.values().stream() + .filter(Arena::isActive) .sorted(Comparator.comparing(Arena::getCreatedAt)) .map(ArenaResponse::new) .toList(); @@ -38,25 +43,30 @@ public class ArenaStore { return Optional.ofNullable(arenas.get(id)); } + private Arena getOrThrow(String id) { + try { + UUID uuid = UUID.fromString(id); + Arena arena = arenas.get(uuid); + if (arena == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Arena with id \"" + id + "\" not found."); + return arena; + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Malformed id. Should be a UUID."); + } + } + public ArenaResponse registerArena(ArenaCreationPayload payload) { + if (arenas.size() >= maxArenas) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Too many arenas."); Arena arena = new Arena(payload.name()); this.arenas.put(arena.getId(), arena); return new ArenaResponse(arena); } public ArenaResponse getArena(String arenaId) { - try { - Arena arena = arenas.get(UUID.fromString(arenaId)); - if (arena == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND); - return new ArenaResponse(arena); - } catch (IllegalArgumentException e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid arena id."); - } + return new ArenaResponse(getOrThrow(arenaId)); } public void startArena(String arenaId) { - Arena arena = arenas.get(UUID.fromString(arenaId)); - if (arena == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND); + Arena arena = getOrThrow(arenaId); if (arena.getCurrentStage() != ArenaStage.PRE_STAGING) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Arena is already started."); } @@ -67,10 +77,12 @@ public class ArenaStore { public void cleanArenas() { Set removalSet = new HashSet<>(); final Instant cutoff = Instant.now().minus(5, ChronoUnit.MINUTES); + final Instant hardCutoff = Instant.now().minus(1, ChronoUnit.HOURS); for (var arena : arenas.values()) { if ( (arena.getCurrentStage() == ArenaStage.CLOSED) || - (arena.getCurrentStage() == ArenaStage.PRE_STAGING && arena.getCreatedAt().isBefore(cutoff)) + (arena.getCurrentStage() == ArenaStage.PRE_STAGING && arena.getCreatedAt().isBefore(cutoff)) || + (arena.getCreatedAt().isBefore(hardCutoff)) ) { removalSet.add(arena.getId()); } diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/model/Arena.java b/server/src/main/java/nl/andrewl/starship_arena/server/model/Arena.java index e74519b..34f8389 100644 --- a/server/src/main/java/nl/andrewl/starship_arena/server/model/Arena.java +++ b/server/src/main/java/nl/andrewl/starship_arena/server/model/Arena.java @@ -73,6 +73,10 @@ public class Arena { this.closesAt = battleEndsAt.plus(1, ChronoUnit.MINUTES); } + public boolean isActive() { + return currentStage != ArenaStage.PRE_STAGING && currentStage != ArenaStage.CLOSED; + } + private void close() { for (var client : clients.values()) { client.getNetManager().shutdown(); diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 28cce26..1841ad1 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -8,3 +8,5 @@ server.tomcat.processor-cache=50 starship-arena.gateway.host=127.0.0.1 starship-arena.gateway.tcp-port=8081 starship-arena.gateway.udp-port=8082 +starship-arena.gateway.init-timeout-ms=1000 +starship-arena.max-arenas=100 diff --git a/server/src/main/resources/ships/corvette.json b/server/src/main/resources/ships/corvette.json new file mode 100644 index 0000000..bc181c8 --- /dev/null +++ b/server/src/main/resources/ships/corvette.json @@ -0,0 +1,62 @@ +{ + "name": "Corvette", + "components": [ + { + "type": "panel", + "name": "Main Fuselage", + "mass": 5000, + "points": [ + {"x": 0.3, "y": 0.6}, + {"x": 0.2, "y": 0.1}, + {"x": 0.1, "y": 0.5}, + {"x": 0.2, "y": 0.8}, + {"x": 0.8, "y": 0.8}, + {"x": 0.9, "y": 0.5}, + {"x": 0.8, "y": 0.1}, + {"x": 0.7, "y": 0.6} + ] + }, + { + "type": "panel", + "name": "Front Cargo Bay", + "mass": 1000, + "points": [ + {"x": 0.4, "y": 0.2}, + {"x": 0.35, "y": 0.6}, + {"x": 0.65, "y": 0.6}, + {"x": 0.6, "y": 0.2} + ] + }, + { + "type": "cockpit", + "mass": 800, + "points": [ + {"x": 0.5, "y": 0.0}, + {"x": 0.4, "y": 0.2}, + {"x": 0.6, "y": 0.2} + ] + }, + { + "type": "gun", + "name": "Port-Side Machine Gun", + "mass": 500, + "location": {"x": 0.15, "y": 0.35}, + "rotation": 0, + "minRotation": -160, + "maxRotation": 5, + "barrelWidth": 0.02, + "barrelLength": 0.2 + }, + { + "type": "gun", + "name": "Starboard-Side Machine Gun", + "mass": 500, + "location": {"x": 0.85, "y": 0.35}, + "rotation": 0, + "minRotation": -5, + "maxRotation": 160, + "barrelWidth": 0.02, + "barrelLength": 0.2 + } + ] +} \ No newline at end of file