Skip to content

Commit e4eb247

Browse files
committed
Fix websockets, add test that starts, restarts and stops server
1 parent 59e3e5c commit e4eb247

File tree

6 files changed

+88
-108
lines changed

6 files changed

+88
-108
lines changed

src/main/java/com/exaroton/api/server/Server.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
import java.util.Objects;
1717
import java.util.Optional;
1818
import java.util.Set;
19-
import java.util.concurrent.*;
19+
import java.util.concurrent.CompletableFuture;
20+
import java.util.concurrent.Future;
2021

2122
public final class Server implements Initializable {
2223

@@ -379,10 +380,7 @@ public CompletableFuture<Void> restart() throws IOException {
379380
* @return a future that completes when the server has reached one of the given statuses
380381
*/
381382
public Future<Server> waitForStatus(Set<ServerStatus> statuses) {
382-
if (this.webSocket == null) {
383-
this.subscribe();
384-
}
385-
return webSocket.waitForStatus(statuses);
383+
return this.subscribe().waitForStatus(statuses);
386384
}
387385

388386
/**

src/main/java/com/exaroton/api/ws/WaitForStatusSubscriber.java

Lines changed: 11 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,15 @@
66
import com.exaroton.api.ws.subscriber.ServerStatusSubscriber;
77
import org.jetbrains.annotations.ApiStatus;
88
import org.jetbrains.annotations.NotNull;
9-
import org.jetbrains.annotations.Nullable;
109

1110
import java.util.Objects;
12-
import java.util.Optional;
1311
import java.util.Set;
1412
import java.util.concurrent.*;
1513

1614
@ApiStatus.Internal
1715
public final class WaitForStatusSubscriber implements ServerStatusSubscriber, Future<Server> {
1816
private final Set<ServerStatus> statuses;
19-
private final ServerStatusStream stream;
20-
@Nullable
21-
private Server result;
22-
private boolean cancelled;
23-
@Nullable
24-
private Throwable throwable;
25-
@Nullable
26-
private CountDownLatch latch;
17+
private final CompletableFuture<Server> future;
2718

2819
/**
2920
* Create a new future that will complete when the server has the specified status. This class will register itself
@@ -33,7 +24,9 @@ public final class WaitForStatusSubscriber implements ServerStatusSubscriber, Fu
3324
*/
3425
public WaitForStatusSubscriber(Set<ServerStatus> statuses, ServerStatusStream stream) {
3526
this.statuses = Objects.requireNonNull(statuses);
36-
this.stream = Objects.requireNonNull(stream);
27+
ServerStatusStream stream1 = Objects.requireNonNull(stream);
28+
this.future = new CompletableFuture<Server>()
29+
.whenComplete((x, y) -> stream.removeSubscriber(this));
3730
stream.addSubscriber(this);
3831
}
3932

@@ -42,94 +35,40 @@ public void handleStatusUpdate(Server oldServer, Server newServer) {
4235
try {
4336
if (newServer.hasStatus(statuses)) {
4437
synchronized (this) {
45-
this.result = newServer;
38+
future.complete(newServer);
4639
}
4740
}
4841
} catch (Throwable t) {
4942
synchronized (this) {
50-
throwable = t;
43+
future.completeExceptionally(t);
5144
}
5245
}
5346
}
5447

5548
@Override
5649
public boolean cancel(boolean mayInterruptIfRunning) {
5750
synchronized (this) {
58-
if (isDone()) {
59-
return false;
60-
}
61-
62-
cancelled = true;
51+
return future.cancel(mayInterruptIfRunning);
6352
}
64-
return true;
6553
}
6654

6755
@Override
6856
public boolean isCancelled() {
69-
return cancelled;
57+
return future.isCancelled();
7058
}
7159

7260
@Override
7361
public boolean isDone() {
74-
return cancelled || throwable != null || result != null;
62+
return future.isDone();
7563
}
7664

7765
@Override
7866
public Server get() throws InterruptedException, ExecutionException {
79-
var latch = getWaitingLatch();
80-
81-
if (latch.isPresent()) {
82-
latch.get().await();
83-
}
84-
85-
return getResult();
67+
return future.get();
8668
}
8769

8870
@Override
8971
public Server get(long timeout, @NotNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
90-
var latch = getWaitingLatch();
91-
92-
if (latch.isPresent()) {
93-
if (!latch.get().await(timeout, unit)) {
94-
throw new TimeoutException();
95-
}
96-
}
97-
98-
stream.removeSubscriber(this);
99-
100-
return getResult();
101-
}
102-
103-
/**
104-
* Get the result or throw the corresponding exception. Future must have been completed before this is called.
105-
* @return result
106-
*/
107-
private Server getResult() throws ExecutionException {
108-
if (cancelled) {
109-
throw new CancellationException();
110-
}
111-
112-
if (throwable != null) {
113-
throw new ExecutionException(throwable);
114-
}
115-
116-
return result;
117-
}
118-
119-
/**
120-
* Get the latch used to wait until the future is done
121-
* @return latch or empty if the future is already done
122-
*/
123-
private Optional<CountDownLatch> getWaitingLatch() {
124-
synchronized (this) {
125-
if (isDone()) {
126-
return Optional.empty();
127-
}
128-
129-
if (latch == null) {
130-
latch = new CountDownLatch(1);
131-
}
132-
return Optional.of(latch);
133-
}
72+
return future.get(timeout, unit);
13473
}
13574
}

src/main/java/com/exaroton/api/ws/WebSocketConnection.java

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import com.exaroton.api.server.Server;
44
import com.exaroton.api.server.ServerStatus;
5-
import com.exaroton.api.ws.stream.*;
6-
import com.exaroton.api.ws.subscriber.*;
5+
import com.exaroton.api.ws.stream.ConsoleStream;
6+
import com.exaroton.api.ws.stream.ServerStatusStream;
7+
import com.exaroton.api.ws.stream.Stream;
8+
import com.exaroton.api.ws.stream.StreamType;
9+
import com.exaroton.api.ws.subscriber.ServerStatusSubscriber;
710
import com.google.gson.Gson;
811
import com.google.gson.JsonParser;
912
import org.jetbrains.annotations.ApiStatus;
@@ -17,7 +20,9 @@
1720
import java.net.http.HttpClient;
1821
import java.net.http.WebSocket;
1922
import java.util.*;
20-
import java.util.concurrent.*;
23+
import java.util.concurrent.CompletableFuture;
24+
import java.util.concurrent.CompletionStage;
25+
import java.util.concurrent.Future;
2126

2227
public final class WebSocketConnection implements WebSocket.Listener {
2328
/**
@@ -90,23 +95,6 @@ public WebSocketConnection(
9095
connect();
9196
}
9297

93-
/**
94-
* subscribe to a stream if it is not already active
95-
*
96-
* @param name stream name
97-
*/
98-
public void subscribe(@NotNull StreamType name) {
99-
Objects.requireNonNull(name);
100-
101-
if (streams.containsKey(name.getStreamClass())) {
102-
return;
103-
}
104-
105-
Stream<?> stream = name.construct(this, gson);
106-
this.streams.put(name.getStreamClass(), stream);
107-
stream.start();
108-
}
109-
11098
/**
11199
* unsubscribe from a stream
112100
*
@@ -252,13 +240,13 @@ public <T> void removeStreamSubscriber(Class<? extends Stream<T>> c, T subscribe
252240
@ApiStatus.Internal
253241
public void unsubscribeFromEmptyStreams() {
254242
for (Stream<?> stream : streams.values()) {
255-
if (!stream.hasSubscribers()) {
243+
if (stream.hasNoSubscribers()) {
256244
this.unsubscribe(stream.getType());
257245
}
258246
}
259247

260248
// server status stream is always active
261-
if (streams.size() == 1 && !getStream(ServerStatusStream.class).hasSubscribers()) {
249+
if (streams.size() == 1 && getStream(ServerStatusStream.class).hasNoSubscribers()) {
262250
this.server.unsubscribe();
263251
}
264252
}
@@ -294,6 +282,7 @@ public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean
294282
webSocket.sendText(x, true);
295283
}
296284
this.messages.clear();
285+
break;
297286

298287
default:
299288
final StreamType name = StreamType.get(message.get("stream").getAsString());
@@ -336,6 +325,12 @@ public void run() {
336325
return null;
337326
}
338327

328+
@Override
329+
@ApiStatus.Internal
330+
public void onError(WebSocket webSocket, Throwable error) {
331+
logger.error("Websocket connection to {} failed", uri, error);
332+
}
333+
339334
/**
340335
* send data once connection is ready
341336
*

src/main/java/com/exaroton/api/ws/stream/Stream.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.exaroton.api.ws.stream;
22

33
import com.exaroton.api.server.ServerStatus;
4-
import com.exaroton.api.ws.WaitForStatusSubscriber;
54
import com.exaroton.api.ws.WebSocketConnection;
65
import com.exaroton.api.ws.data.StreamData;
76
import com.google.gson.Gson;
@@ -73,8 +72,8 @@ public void removeSubscriber(T subscriber) {
7372
* Check if this stream has subscribers
7473
* @return has subscribers?
7574
*/
76-
public boolean hasSubscribers() {
77-
return !this.subscribers.isEmpty();
75+
public boolean hasNoSubscribers() {
76+
return this.subscribers.isEmpty();
7877
}
7978

8079
/**

src/test/java/APIClientTest.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,46 @@
11
import com.exaroton.api.ExarotonClient;
22
import com.exaroton.api.server.Server;
3+
import com.exaroton.api.server.ServerStatus;
34
import org.jetbrains.annotations.NotNull;
5+
import org.junit.jupiter.api.AfterEach;
6+
import org.junit.jupiter.api.BeforeEach;
7+
8+
import java.io.IOException;
9+
import java.util.concurrent.ExecutionException;
10+
import java.util.concurrent.TimeUnit;
11+
import java.util.concurrent.TimeoutException;
412

513
public abstract class APIClientTest {
614
protected static final @NotNull String TEST_SERVER_ID = System.getenv("EXAROTON_TEST_SERVER");
7-
protected final @NotNull ExarotonClient client = new ExarotonClient(System.getenv("EXAROTON_API_TOKEN"));
8-
protected final @NotNull Server server = client.getServer(TEST_SERVER_ID);
15+
protected ExarotonClient client;
16+
protected Server server;
17+
18+
@BeforeEach
19+
void setUp() {
20+
client = new ExarotonClient(System.getenv("EXAROTON_API_TOKEN"));
21+
server = client.getServer(TEST_SERVER_ID);
922

10-
protected APIClientTest() {
1123
try {
1224
Thread.sleep(100);
1325
} catch (InterruptedException e) {
1426
throw new RuntimeException(e);
1527
}
1628
}
29+
30+
@AfterEach
31+
void tearDown() throws IOException, ExecutionException, InterruptedException, TimeoutException {
32+
server.fetch().join();
33+
34+
if (server.hasStatus(ServerStatus.RESTARTING, ServerStatus.LOADING, ServerStatus.PREPARING)) {
35+
server.waitForStatus(ServerStatus.STARTING, ServerStatus.ONLINE, ServerStatus.OFFLINE, ServerStatus.CRASHED)
36+
.get(1, TimeUnit.MINUTES);
37+
}
38+
39+
if (server.hasStatus(ServerStatus.ONLINE, ServerStatus.STARTING)) {
40+
server.stop().join();
41+
server.waitForStatus(ServerStatus.OFFLINE, ServerStatus.CRASHED).get(1, TimeUnit.MINUTES);
42+
}
43+
44+
server.unsubscribe();
45+
}
1746
}

src/test/java/ServerTest.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import com.exaroton.api.server.*;
2-
import org.junit.jupiter.api.Assertions;
32
import org.junit.jupiter.api.Test;
43

54
import java.io.IOException;
65
import java.util.List;
76
import java.util.concurrent.CompletableFuture;
7+
import java.util.concurrent.ExecutionException;
8+
import java.util.concurrent.TimeUnit;
9+
import java.util.concurrent.TimeoutException;
810

911
import static org.junit.jupiter.api.Assertions.*;
1012

@@ -133,8 +135,26 @@ void testGetPlayerLists() throws IOException {
133135
}
134136

135137
@Test
136-
void testStartServer() throws IOException {
137-
assertEquals(ServerStatus.OFFLINE, server.getStatus());
138+
void testStartRestartStopServer() throws IOException, ExecutionException, InterruptedException, TimeoutException {
139+
server.fetch().join();
140+
assertTrue(server.hasStatus(ServerStatus.OFFLINE, ServerStatus.CRASHED));
141+
142+
// Start server
138143
server.start().join();
144+
server.waitForStatus(ServerStatus.ONLINE).get(3, TimeUnit.MINUTES);
145+
assertEquals(ServerStatus.ONLINE, server.getStatus());
146+
147+
// Restart Server
148+
server.restart().join();
149+
server.waitForStatus(ServerStatus.RESTARTING).get(1, TimeUnit.MINUTES);
150+
assertEquals(ServerStatus.RESTARTING, server.getStatus());
151+
152+
server.waitForStatus(ServerStatus.ONLINE).get(3, TimeUnit.MINUTES);
153+
assertEquals(ServerStatus.ONLINE, server.getStatus());
154+
155+
// Stop Server
156+
server.stop().join();
157+
server.waitForStatus(ServerStatus.OFFLINE, ServerStatus.CRASHED).get(3, TimeUnit.MINUTES);
158+
assertTrue(server.hasStatus(ServerStatus.OFFLINE, ServerStatus.CRASHED), "Expected server to be offline or crashed");
139159
}
140160
}

0 commit comments

Comments
 (0)