Skip to content

Commit

Permalink
feat: Refactor WebSocket message handling, introduce rate limiters fo…
Browse files Browse the repository at this point in the history
…r API requests and add new WS messages
  • Loading branch information
Lotnest committed Mar 4, 2025
1 parent af926de commit c5ff29c
Show file tree
Hide file tree
Showing 56 changed files with 739 additions and 218 deletions.
9 changes: 1 addition & 8 deletions common/src/main/java/dev/lotnest/sequoia/SequoiaMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@
import dev.lotnest.sequoia.core.components.Service;
import dev.lotnest.sequoia.core.components.Services;
import dev.lotnest.sequoia.core.events.SequoiaCrashEvent;
import dev.lotnest.sequoia.core.http.HttpClient;
import dev.lotnest.sequoia.core.persisted.SequoiaConfig;
import dev.lotnest.sequoia.features.WebSocketFeature;
import dev.lotnest.sequoia.features.ws.WebSocketFeature;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -52,7 +51,6 @@ public final class SequoiaMod {
private static boolean isDevelopmentBuild = false;
private static boolean isDevelopmentEnvironment = false;
private static boolean isInitCompleted = false;
private static HttpClient httpClient = null;

public static void error(String message) {
LOGGER.error(message);
Expand Down Expand Up @@ -108,7 +106,6 @@ public static void init(ModLoader modLoader, boolean isDevelopmentEnvironment, S
SequoiaMod.isDevelopmentEnvironment = isDevelopmentEnvironment;
version = "v" + modVersion;
versionInt = Integer.parseInt(modVersion.replaceAll("\\D", ""));
httpClient = HttpClient.newHttpClient();

LOGGER.info(
"Sequoia: Starting version {} (using {} on Minecraft {})",
Expand Down Expand Up @@ -199,10 +196,6 @@ public static WebSocketFeature getWebSocketFeature() {
return Managers.Feature.getFeatureInstance(WebSocketFeature.class);
}

public static HttpClient getHttpClient() {
return httpClient;
}

public enum ModLoader {
FORGE,
FABRIC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import com.wynntils.core.components.Managers;
import dev.lotnest.sequoia.SequoiaMod;
import dev.lotnest.sequoia.core.consumers.command.Command;
import dev.lotnest.sequoia.core.websocket.messages.session.GAuthWSMessage;
import dev.lotnest.sequoia.core.ws.message.ws.session.GAuthWSMessage;
import dev.lotnest.sequoia.utils.wynn.WynnUtils;
import java.util.regex.Pattern;
import net.minecraft.commands.CommandSourceStack;
Expand Down Expand Up @@ -71,7 +71,7 @@ private int authenticate(CommandContext<CommandSourceStack> context) {
}

GAuthWSMessage gAuthWSMessage = new GAuthWSMessage(code);
SequoiaMod.getWebSocketFeature().sendAsJson(gAuthWSMessage);
SequoiaMod.getWebSocketFeature().sendMessage(gAuthWSMessage);
sentGAuthWSMessage = true;
context.getSource()
.sendSuccess(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,4 @@
*/
package dev.lotnest.sequoia.core.components;

public abstract class CoreComponent implements Translatable {
public abstract String getTypeName();
}
public abstract class CoreComponent implements Translatable {}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import dev.lotnest.sequoia.features.OuterVoidTrackerFeature;
import dev.lotnest.sequoia.features.PlayerIgnoreFeature;
import dev.lotnest.sequoia.features.SequoiaOSTFeature;
import dev.lotnest.sequoia.features.WebSocketFeature;
import dev.lotnest.sequoia.features.discordchatbridge.DiscordChatBridgeFeature;
import dev.lotnest.sequoia.features.guildraidtracker.GuildRaidTrackerFeature;
import dev.lotnest.sequoia.features.messagefilter.MessageFilterFeature;
Expand All @@ -31,6 +30,8 @@
import dev.lotnest.sequoia.features.raids.RaidsFeature;
import dev.lotnest.sequoia.features.raids.TNARaidFeature;
import dev.lotnest.sequoia.features.territory.TerritoryFeature;
import dev.lotnest.sequoia.features.ws.WebSocketFeature;
import dev.lotnest.sequoia.features.ws.relayer.GuildMapDataRelayerFeature;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -74,6 +75,7 @@ public void init() {
registerFeature(new PartyRaidCompletionsDisplayFeature());
registerFeature(new TerritoryFeature());
registerFeature(new GuildRewardStorageTrackerFeature());
registerFeature(new GuildMapDataRelayerFeature());

synchronized (McUtils.options()) {
McUtils.options().load();
Expand Down
15 changes: 3 additions & 12 deletions common/src/main/java/dev/lotnest/sequoia/core/http/HttpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,21 @@
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

public class HttpClient {
private static final int[] OK_STATUS_CODES = {200, 201, 202, 203, 204, 205, 206, 207, 208, 226};
private static final int POOL_SIZE = 10;
private static final Gson gson = new GsonBuilder().create();
private static final ConcurrentMap<java.net.http.HttpClient, Boolean> httpClientPool = Maps.newConcurrentMap();

private HttpClient() {
this(1, TimeUnit.MINUTES);
}

private HttpClient(long cacheDuration, TimeUnit cacheDurationUnit) {
protected HttpClient() {
createClientPool();
}

public static HttpClient newHttpClient() {
return new HttpClient();
}

public static HttpClient newHttpClient(long cacheDuration, TimeUnit cacheDurationUnit) {
return new HttpClient(cacheDuration, cacheDurationUnit);
}

private void createClientPool() {
for (int i = 0; i < POOL_SIZE; i++) {
java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient();
Expand Down Expand Up @@ -136,7 +127,7 @@ public <T> T getJson(String url, Class<T> responseType, Gson gson) {

SequoiaMod.debug(response.statusCode() + " " + url + " " + response.body());

if (response != null && response.body() != null && isOkStatusCode(response.statusCode())) {
if (response.body() != null && isOkStatusCode(response.statusCode())) {
return gson.fromJson(response.body(), responseType);
}
return null;
Expand All @@ -150,7 +141,7 @@ public <T> CompletableFuture<T> getJsonAsync(String url, Class<T> responseType,
return getAsync(url).thenApply(response -> {
SequoiaMod.debug("ASYNC " + response.statusCode() + " " + url + " " + response.body());

if (response != null && response.body() != null && isOkStatusCode(response.statusCode())) {
if (response.body() != null && isOkStatusCode(response.statusCode())) {
return gson.fromJson(response.body(), responseType);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright © sequoia-mod 2025.
* This file is released under LGPLv3. See LICENSE for full license details.
*/
package dev.lotnest.sequoia.core.http;

import dev.lotnest.sequoia.core.http.clients.MojangApiHttpClient;
import dev.lotnest.sequoia.core.http.clients.WynncraftApiHttpClient;

public final class HttpClients {
public static final WynncraftApiHttpClient WYNNCRAFT_API = WynncraftApiHttpClient.newHttpClient();
public static final MojangApiHttpClient MOJANG_API = MojangApiHttpClient.newHttpClient();

private HttpClients() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright © sequoia-mod 2025.
* This file is released under LGPLv3. See LICENSE for full license details.
*/
package dev.lotnest.sequoia.core.http.clients;

import dev.lotnest.sequoia.core.http.HttpClient;
import dev.lotnest.sequoia.core.http.ratelimiter.RateLimiters;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

public class MojangApiHttpClient extends HttpClient {
private MojangApiHttpClient() {
super();
}

public static MojangApiHttpClient newHttpClient() {
return new MojangApiHttpClient();
}

@Override
public HttpResponse<String> get(String url) {
RateLimiters.MOJANG_API.acquire();
return super.get(url);
}

@Override
public CompletableFuture<HttpResponse<String>> getAsync(String url) {
return CompletableFuture.runAsync(RateLimiters.MOJANG_API::acquire).thenCompose(v -> super.getAsync(url));
}

@Override
public HttpResponse<String> post(String url, String body) {
RateLimiters.MOJANG_API.acquire();
return super.post(url, body);
}

@Override
public CompletableFuture<HttpResponse<String>> postAsync(String url, String body) {
return CompletableFuture.runAsync(RateLimiters.MOJANG_API::acquire)
.thenCompose(v -> super.postAsync(url, body));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright © sequoia-mod 2025.
* This file is released under LGPLv3. See LICENSE for full license details.
*/
package dev.lotnest.sequoia.core.http.clients;

import dev.lotnest.sequoia.core.http.HttpClient;
import dev.lotnest.sequoia.core.http.ratelimiter.RateLimiters;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

public class WynncraftApiHttpClient extends HttpClient {
private WynncraftApiHttpClient() {
super();
}

public static WynncraftApiHttpClient newHttpClient() {
return new WynncraftApiHttpClient();
}

@Override
public HttpResponse<String> get(String url) {
RateLimiters.WYNNCRAFT_API.acquire();
return super.get(url);
}

@Override
public CompletableFuture<HttpResponse<String>> getAsync(String url) {
return CompletableFuture.runAsync(RateLimiters.WYNNCRAFT_API::acquire).thenCompose(v -> super.getAsync(url));
}

@Override
public HttpResponse<String> post(String url, String body) {
RateLimiters.WYNNCRAFT_API.acquire();
return super.post(url, body);
}

@Override
public CompletableFuture<HttpResponse<String>> postAsync(String url, String body) {
return CompletableFuture.runAsync(RateLimiters.WYNNCRAFT_API::acquire)
.thenCompose(v -> super.postAsync(url, body));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright © sequoia-mod 2025.
* This file is released under LGPLv3. See LICENSE for full license details.
*/
package dev.lotnest.sequoia.core.http.ratelimiter;

import dev.lotnest.sequoia.SequoiaMod;
import java.util.concurrent.Semaphore;

public class RateLimiter {
private final Semaphore concurrencySemaphore;
private final double capacity;
private double tokens;
private final double refillRate;
private long lastRefillTime;

public RateLimiter(int maxConcurrentRequests, double requestsPerMinute) {
this.concurrencySemaphore = new Semaphore(maxConcurrentRequests);
this.capacity = requestsPerMinute;
this.tokens = requestsPerMinute;
this.refillRate = requestsPerMinute / 60000.0;
this.lastRefillTime = System.currentTimeMillis();
}

private void waitForToken() throws InterruptedException {
while (true) {
long sleepTime;
synchronized (this) {
refillTokens();
if (tokens >= 1) {
tokens -= 1;
return;
}
double missingTokens = 1 - tokens;
sleepTime = (long) Math.ceil(missingTokens / refillRate);
}
Thread.sleep(sleepTime);
}
}

private synchronized void refillTokens() {
long now = System.currentTimeMillis();
long elapsed = now - lastRefillTime;
if (elapsed > 0) {
tokens = Math.min(capacity, tokens + elapsed * refillRate);
lastRefillTime = now;
}
}

/**
* Acquires permission for a request. This call blocks until both a token is available
* (ensuring we respect the global rate) and a concurrency permit is available (ensuring
* only a limited number of concurrent requests).
*/
public void acquire() {
try {
waitForToken();
concurrencySemaphore.acquire();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
SequoiaMod.warn("Interrupted while waiting for rate limiter", exception);
}
}

/**
* Releases a previously acquired concurrency permit.
*/
public void release() {
concurrencySemaphore.release();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright © sequoia-mod 2025.
* This file is released under LGPLv3. See LICENSE for full license details.
*/
package dev.lotnest.sequoia.core.http.ratelimiter;

public final class RateLimiters {
public static final RateLimiter WYNNCRAFT_API = new RateLimiter(5, 180);
public static final RateLimiter MOJANG_API = new RateLimiter(5, 60);

private RateLimiters() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ public static class SequoiaOSTFeature {

public static class WebSocketFeature {
public boolean enabled = true;
public boolean autoReconnect = true;
public boolean relayGuildMapData = true;
public boolean relayGuildWarResultsData = true;
public boolean relayLocationServiceData = true;
public boolean relayLootPoolData = true;
}

public static class GuildRaidTrackerFeature {
Expand Down

This file was deleted.

This file was deleted.

Loading

0 comments on commit c5ff29c

Please sign in to comment.