Added better error handling, and prepared for arena ship management.

This commit is contained in:
Andrew Lalis 2022-05-01 15:40:42 +02:00
parent 102f1c7b35
commit 92e60135fe
7 changed files with 117 additions and 12 deletions

View File

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

View File

@ -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<ErrorResponse> handleRSE(ResponseStatusException e) {
return new ResponseEntity<>(new ErrorResponse(e.getRawStatusCode(), e.getReason()), e.getStatus());
}
}

View File

@ -0,0 +1,6 @@
package nl.andrewl.starship_arena.server.api.dto;
public record ErrorResponse(
int status,
String message
) {}

View File

@ -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<UUID, Arena> arenas = new ConcurrentHashMap<>();
public List<ArenaResponse> 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<UUID> 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());
}

View File

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

View File

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

View File

@ -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
}
]
}