feat: persistence util, PM DRY, Discord rate limiter, missing permissions in plugin.yml

This commit is contained in:
loki5512344 2026-07-05 15:17:15 +02:00
parent dd0fb0f10d
commit 42ebac890c
60 changed files with 418 additions and 374 deletions

View file

@ -2,6 +2,7 @@ package com.loki.lochat.integrations;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.loki.lochat.integrations.discord.DiscordRateLimiter;
import org.bukkit.plugin.java.JavaPlugin;
@ -29,6 +30,7 @@ public class DiscordWebhook {
private final int timeout;
private final int retryAttempts;
private final long retryDelay;
private final DiscordRateLimiter rateLimiter;
// Выделенный пул — не засоряем ForkJoinPool.commonPool()
private final ExecutorService executor = Executors.newSingleThreadExecutor(
@ -39,8 +41,10 @@ public class DiscordWebhook {
}
);
@SuppressWarnings("checkstyle:ParameterNumber")
public DiscordWebhook(JavaPlugin plugin, String webhookUrl, String username, String avatarUrl,
int timeout, int retryAttempts, long retryDelay) {
int timeout, int retryAttempts, long retryDelay,
double maxRequestsPerSecond, int rateLimitBurst) {
this.plugin = plugin;
this.webhookUrl = webhookUrl != null ? webhookUrl.trim() : null;
this.username = username;
@ -48,6 +52,7 @@ public class DiscordWebhook {
this.timeout = timeout * 1000;
this.retryAttempts = retryAttempts;
this.retryDelay = retryDelay;
this.rateLimiter = new DiscordRateLimiter(maxRequestsPerSecond, rateLimitBurst);
}
// ── Публичные методы отправки ─────────────────────────────────────────────
@ -114,6 +119,7 @@ public class DiscordWebhook {
private CompletableFuture<Boolean> sendWebhook(JsonObject json) {
return CompletableFuture.supplyAsync(() -> {
for (int attempt = 1; attempt <= retryAttempts; attempt++) {
rateLimiter.waitIfNeeded();
try {
if (sendWebhookSync(json.toString())) {
return true;

View file

@ -70,6 +70,14 @@ public class DiscordConfig {
public boolean isAsync() {
return config.getBoolean("performance.async", true);
}
public double getMaxRequestsPerSecond() {
return config.getDouble("performance.max-requests-per-second", 5.0);
}
public int getRateLimitBurst() {
return config.getInt("performance.rate-limit-burst", 10);
}
public boolean isChatEnabled() {
return config.getBoolean("chat.enabled", true);

View file

@ -29,7 +29,9 @@ public class DiscordMessageService {
getAvatarUrl(),
config.getTimeout(),
config.getRetryAttempts(),
config.getRetryDelay()
config.getRetryDelay(),
config.getMaxRequestsPerSecond(),
config.getRateLimitBurst()
);
}

View file

@ -0,0 +1,53 @@
package com.loki.lochat.integrations.discord;
public class DiscordRateLimiter {
private final double refillRate;
private final double capacity;
private double tokens;
private long lastRefillTime;
public DiscordRateLimiter(double maxRequestsPerSecond, double burst) {
this.refillRate = maxRequestsPerSecond;
this.capacity = burst;
this.tokens = burst;
this.lastRefillTime = System.nanoTime();
}
public synchronized boolean tryAcquire() {
refill();
if (tokens >= 1.0) {
tokens -= 1.0;
return true;
}
return false;
}
public void waitIfNeeded() {
long sleepMs;
while (true) {
synchronized (this) {
refill();
if (tokens >= 1.0) {
tokens -= 1.0;
return;
}
double waitTimeMs = ((1.0 - tokens) / refillRate) * 1000.0;
sleepMs = Math.min((long) Math.ceil(waitTimeMs), 100);
}
try {
Thread.sleep(Math.max(sleepMs, 1));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
private void refill() {
long now = System.nanoTime();
double elapsed = (now - lastRefillTime) / 1_000_000_000.0;
tokens = Math.min(capacity, tokens + elapsed * refillRate);
lastRefillTime = now;
}
}