Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use SSE Events to push realtime data from the internal server to the webmap #8

Merged
merged 69 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
80303a6
make scheduler run on every tick
granny Oct 22, 2023
f727f0c
use delay/repeat values set in MarkerTask class
granny Oct 22, 2023
9013c3b
move parsePlayers method to PlayerRegistry
granny Oct 22, 2023
9da766b
working SSE prototype
granny Oct 22, 2023
f9f0246
add temporary config option
granny Oct 22, 2023
b158615
check PlayerManager for amount of players instead of settings.json
granny Oct 22, 2023
1affd7a
don't update url when following player
granny Oct 22, 2023
b8801a5
initialize SSE after everything else loads
granny Oct 22, 2023
77ff796
close all SSE connections when stopping web server
granny Dec 22, 2023
a9975c2
stop static abusing
granny Dec 23, 2023
9304234
continue returning interval as seconds
granny Dec 23, 2023
7394f2d
merge sse task into marker task
granny Dec 23, 2023
57f1ee8
updater marker and settings task every tick
granny Dec 23, 2023
e4dd532
stop requesting settings.json if using sse
granny Dec 23, 2023
6ec42fe
this check wasn't doing anything before
granny Dec 23, 2023
857e164
missed this
granny Dec 23, 2023
a29c2d9
cleaner tick handling
granny Dec 23, 2023
e619b18
Merge branch 'v3' into feat/sse-events
granny Jan 26, 2024
83f8823
need to update loom for some reason
granny Jan 26, 2024
f12a852
add get/set methods for update interval in ticks
granny Jan 26, 2024
4b342cc
clean up layer update logic
granny Jan 26, 2024
85fe0da
add update-interval-in-ticks option to layer configs
granny Jan 26, 2024
1bd6a37
Merge branch 'v3' into feat/sse-events
granny Jan 26, 2024
8ea5bd2
missed this
granny Jan 26, 2024
06f6f31
add methods for setting task delay as ticks in scheduler
granny Jan 26, 2024
7da3fb6
fix incorrect javadoc comments
granny Jan 26, 2024
d912505
make the marker files update by at least a second
granny Jan 26, 2024
9b92cc7
use json files if SSE takes longer than one second
granny Jan 29, 2024
3a29bf4
only send SSE events if the data has been modified
granny Jan 29, 2024
a4d92d0
Merge branch 'v3' into feat/sse-events
granny Jan 29, 2024
f8b8890
force update markers when loading worlds
granny Feb 2, 2024
f67b857
make marker cache key more unique
granny Feb 2, 2024
8e9de46
Merge branch 'v3' into feat/sse-events
granny Feb 8, 2024
fcc2a66
Merge branch 'v3' into feat/sse-events
granny Feb 13, 2024
de0a2e9
convert layer interval properly
granny Feb 16, 2024
4150784
Merge branch 'v3' into feat/sse-events
granny Feb 20, 2024
a79a8d8
Merge branch 'v3' into feat/sse-events
granny Feb 29, 2024
d11f85f
global and per-world sse endpoints
granny Feb 29, 2024
9dbf851
add live update toggle to layers
granny Feb 29, 2024
39314f8
filter disabled worlds from sse endpoint
granny Feb 29, 2024
be311b4
remove newline from custom http error message
granny Feb 29, 2024
31d88f3
Merge branch 'v3' into feat/sse-events
granny Feb 29, 2024
f08aef0
add `settings.layer.live-update` to built-in layers
granny Mar 1, 2024
2bcf638
remove `settings.layer.update-interval-in-ticks`
granny Mar 1, 2024
31c145b
[chore] remove duplicate calls
granny Mar 1, 2024
3ef690e
separate the live updating into it's own task
granny Mar 4, 2024
b7ede6e
flip enabled world check in sse endpoint
granny Mar 4, 2024
6f22b53
comment out a debug log
granny Mar 4, 2024
c37a45b
[chore] remove unused imports
granny Mar 4, 2024
dbc44e6
cache marker hashcodes instead of stringified json object
granny Mar 5, 2024
905170e
clean up sse route logic
granny Mar 5, 2024
37004de
[chore] remove unused imports & add newline at eof
granny Mar 5, 2024
42a15d6
specify buffering header for nginx users
granny Mar 9, 2024
fbfdad6
make codefactor happy
granny Mar 15, 2024
e7407d7
add temporary debug logs
granny Mar 15, 2024
3e82eb5
remove unused imports
granny Mar 15, 2024
bb5893c
Merge branch 'v3' into feat/sse-events
granny Mar 25, 2024
ef74cb8
Merge branch 'v3' into feat/sse-events
granny Mar 26, 2024
f28a6cb
Merge branch 'v3' into feat/sse-events
granny Mar 26, 2024
0d7b060
Merge branch 'v3' into feat/sse-events
granny Mar 30, 2024
2a71574
Merge branch 'v3' into feat/sse-events
granny Apr 2, 2024
83665d4
Merge branch 'v3' into feat/sse-events
granny Apr 5, 2024
d183c66
Make thread count configurable (#36)
jedrek0429 Apr 5, 2024
20d1ad9
cache hashcode instead of string
granny Apr 12, 2024
ade62c7
wrap ServerSentEventHandler class
granny Apr 12, 2024
8fcb620
Merge branch 'v3' into feat/sse-events
granny Apr 12, 2024
5e3ffcc
label SAM interfaces with annotations
granny Apr 12, 2024
06c8a68
throw away unused SpiFix class
granny Apr 12, 2024
803c5a2
Merge branch 'chore/1-20-6' into feat/sse-events
granny May 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public void onEnable() {
}

getServer().getScheduler().runTaskTimer(this, () ->
this.pl3xmap.getScheduler().tick(), 20, 20);
this.pl3xmap.getScheduler().tick(), 20, 1);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ How many scroll pixels (as reported by L.DomEvent.getWheelDelta) mean
for security reasons. But you do you, boo boo.""")
public static boolean HTTPD_FOLLOW_SYMLINKS = false;

@Key("settings.performance.live-update-threads")
@Comment("""
The number of process-threads to use for real-time marker updates on the map.
Value of -1 will use 50% of the available cpu-threads. (recommended)""")
public static int LIVE_UPDATE_THREADS = -1;

@Key("settings.performance.render-threads")
@Comment("""
The number of process-threads to use for loading and scanning chunks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ public final class PlayersLayerConfig extends AbstractConfig {

@Key("settings.layer.update-interval")
@Comment("""
How often (in seconds) to update the marker.
Setting to 0 is the same as setting it to 1.""")
How often (in seconds) to update the marker.""")
public static int UPDATE_INTERVAL = 0;
@Key("settings.layer.live-update")
@Comment("""
Whether to push this layer through SSE or not.""")
public static boolean LIVE_UPDATE = true;
@Key("settings.layer.show-controls")
@Comment("""
Whether the players layer control shows up in the layers list or not.""")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ public final class SpawnLayerConfig extends AbstractConfig {

@Key("settings.layer.update-interval")
@Comment("""
How often (in seconds) to update the marker.
Setting to 0 is the same as setting it to 1.""")
How often (in seconds) to update the marker.""")
public static int UPDATE_INTERVAL = 30;
@Key("settings.layer.live-update")
@Comment("""
Whether to push this layer through SSE or not.""")
public static boolean LIVE_UPDATE = true;
@Key("settings.layer.show-controls")
@Comment("""
Whether the spawn layer control shows up in the layers list or not.""")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@ public final class WorldBorderLayerConfig extends AbstractConfig {

@Key("settings.layer.update-interval")
@Comment("""
How often (in seconds) to update the marker.
Setting to 0 is the same as setting it to 1.""")
How often (in seconds) to update the marker.""")
public static int UPDATE_INTERVAL = 30;
@Key("settings.layer.live-update")
@Comment("""
Whether to push this layer through SSE or not.""")
public static boolean LIVE_UPDATE = true;
@Key("settings.layer.show-controls")
@Comment("""
Whether the vanilla world border layer control shows up in the layers list or not.""")
Expand Down
76 changes: 66 additions & 10 deletions core/src/main/java/net/pl3x/map/core/httpd/HttpdServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,40 @@
*/
package net.pl3x.map.core.httpd;

import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.UndertowLogger;
import io.undertow.UndertowOptions;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.resource.PathResourceManager;
import io.undertow.server.handlers.resource.ResourceHandler;
import io.undertow.server.handlers.resource.ResourceManager;
import io.undertow.util.ETag;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import io.undertow.util.StatusCodes;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.stream.Collectors;
import net.pl3x.map.core.Pl3xMap;
import net.pl3x.map.core.configuration.Config;
import net.pl3x.map.core.configuration.Lang;
import net.pl3x.map.core.log.LogFilter;
import net.pl3x.map.core.log.Logger;
import net.pl3x.map.core.registry.WorldRegistry;
import net.pl3x.map.core.util.FileUtil;
import net.pl3x.map.core.world.World;

public class HttpdServer {
private HttpString X_ACCEL_BUFFERING = new HttpString("X-Accel-Buffering");
private Undertow server;
private LiveDataHandler liveDataHandler = new LiveDataHandler();

public LiveDataHandler getLiveDataHandler() {
return liveDataHandler;
}

public void startServer() {
if (!Config.HTTPD_ENABLED) {
Expand Down Expand Up @@ -81,16 +95,48 @@ public void startServer() {
this.server = Undertow.builder()
.setServerOption(UndertowOptions.ENABLE_HTTP2, true)
.addHttpListener(Config.HTTPD_PORT, Config.HTTPD_BIND)
.setHandler(exchange -> {
if (exchange.getRelativePath().startsWith("/tiles")) {
exchange.getResponseHeaders().put(Headers.CACHE_CONTROL, "max-age=0, must-revalidate, no-cache");
}
if (exchange.getRelativePath().endsWith(".gz")) {
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json");
exchange.getResponseHeaders().put(Headers.CONTENT_ENCODING, "gzip");
}
resourceHandler.handleRequest(exchange);
})
.setHandler(
Handlers.path(exchange -> {
if (exchange.getRelativePath().startsWith("/tiles")) {
exchange.getResponseHeaders().put(Headers.CACHE_CONTROL, "max-age=0, must-revalidate, no-cache");
}
if (exchange.getRelativePath().endsWith(".gz")) {
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json");
exchange.getResponseHeaders().put(Headers.CONTENT_ENCODING, "gzip");
}
resourceHandler.handleRequest(exchange);
})
.addPrefixPath("/sse",
Handlers.pathTemplate()
.add("{world}", exchange -> {
String worldName = exchange.getQueryParameters().get("world").peek();
if (worldName == null || worldName.isEmpty()) {
exchange.getResponseHeaders().put(X_ACCEL_BUFFERING, "no");
liveDataHandler.handle(exchange);
return;
}

WorldRegistry worldRegistry = Pl3xMap.api().getWorldRegistry();
World world = worldRegistry.get(worldName);
if (world == null || !world.isEnabled()) {
String listOfValidWorlds = worldRegistry.values().stream()
.filter(World::isEnabled)
.map(World::getName).collect(Collectors.joining(", "));
handleError(exchange, "Could not find world named '%s'. Available worlds: %s"
.formatted(worldName, listOfValidWorlds));
exchange.endExchange();
return;
}

if (exchange.isInIoThread()) {
exchange.dispatch(world.getServerSentEventHandler().get());
} else {
exchange.getResponseHeaders().put(X_ACCEL_BUFFERING, "no");
world.getServerSentEventHandler().handle(exchange);
}
})
)
)
.build();
this.server.start();
LogFilter.HIDE_UNDERTOW_LOGS = false;
Expand All @@ -105,6 +151,12 @@ public void startServer() {
}
}

private void handleError(HttpServerExchange exchange, String errorMessage) {
exchange.setStatusCode(StatusCodes.NOT_FOUND);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json");
exchange.getResponseSender().send("{\"error\": \"" + errorMessage + "\"}");
}

public void stopServer() {
if (!Config.HTTPD_ENABLED) {
return;
Expand All @@ -116,6 +168,10 @@ public void stopServer() {
}

LogFilter.HIDE_UNDERTOW_LOGS = true;
this.liveDataHandler.closeConnections();
Pl3xMap.api().getWorldRegistry().forEach(world -> {
world.getServerSentEventHandler().closeConnections();
});
this.server.stop();
LogFilter.HIDE_UNDERTOW_LOGS = false;

Expand Down
152 changes: 152 additions & 0 deletions core/src/main/java/net/pl3x/map/core/httpd/LiveDataHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* MIT License
*
* Copyright (c) 2020-2023 William Blake Galbreath
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.pl3x.map.core.httpd;

import io.undertow.Handlers;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.sse.ServerSentEventConnection;
import io.undertow.server.handlers.sse.ServerSentEventHandler;
import java.io.IOException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class LiveDataHandler {
private ServerSentEventHandler serverSentEventHandler;

public LiveDataHandler() {
this.serverSentEventHandler = Handlers.serverSentEvents();
}

/**
*
* @param event The message event
* @param data The message data
* @param success The callback that is called when a message is sucessfully sent.
* @param failure The callback that is called when a message send fails.
*/
public void send(String event, String data, SuccessCallback success, FailureCallback failure) {
if (serverSentEventHandler == null) {
return;
}

Callback callback = new Callback(success, failure);
for (ServerSentEventConnection connection : this.serverSentEventHandler.getConnections()) {
connection.send(data, event, null, callback);
}
}

/**
*
* @param event The message event
* @param data The message data
* @param success The callback that is called when a message is sucessfully sent.
*/
public void send(String event, String data, SuccessCallback success) {
this.send(event, data, success, null);
}

/**
*
* @param event The message event
* @param data The message data
*/
public void send(String event, String data) {
this.send(event, data, null, null);
}

/**
*
* @param data The message data
*/
public void send(String data) {
this.send(null, data);
}

public void closeConnections() {
for (ServerSentEventConnection connection : serverSentEventHandler.getConnections()) {
connection.shutdown();
}
}

public void handle(HttpServerExchange exchange) throws Exception {
this.serverSentEventHandler.handleRequest(exchange);
}

public ServerSentEventHandler get() {
return this.serverSentEventHandler;
}

/**
* Notification that is called when a message is sucessfully sent
*/
@FunctionalInterface
public interface SuccessCallback {
/**
* @param connection The connection
* @param data The message data
* @param event The message event
* @param id The message id
*/
void apply(@NotNull ServerSentEventConnection connection, @Nullable String data, @Nullable String event, @Nullable String id);
}

/**
* Notification that is called when a message send fails.
*/
@FunctionalInterface
public interface FailureCallback {
/**
* @param connection The connection
* @param data The message data
* @param event The message event
* @param id The message id
* @param exception The exception
*/
void apply(@NotNull ServerSentEventConnection connection, @Nullable String data, @Nullable String event, @Nullable String id, @NotNull IOException exception);
}

private class Callback implements ServerSentEventConnection.EventCallback {
private SuccessCallback success;
private FailureCallback failure;

public Callback(SuccessCallback success, FailureCallback failure) {
this.success = success;
this.failure = failure;
}

@Override
public void done(ServerSentEventConnection connection, String data, String event, String id) {
if (success != null) {
success.apply(connection, data, event, id);
}
}

@Override
public void failed(ServerSentEventConnection connection, String data, String event, String id, IOException e) {
if (failure != null) {
failure.apply(connection, data, event, id, e);
}
}
}
}
Loading
Loading