Config (JdbcConfig.java completely restructured into sections):
connection
host, port, use_ssl, user_name, password, db_name, table_prefix, Server_id
general
sync_world, sync_advancements, kick_when_already_online,
kick_message, kick_grace_period_ms, use_legacy_serialization,
item_placeholder_title_override, item_placeholder_description_override
save_triggers
auto_save_interval_minutes (0-1440, default 10)
save_on_dimension_change (default false)
save_on_death (default true)
save_on_respawn (default true)
sync_toggles
sync_inventory, sync_ender_chest, sync_xp, sync_effects,
sync_health_food, sync_curios, sync_accessories, sync_backpacks,
sync_cosmetic_armor, sync_refined_storage (all default true)
performance
heartbeat_interval_seconds (5-600, default 30)
peer_stale_threshold_seconds (10-3600, default 60)
join_poll_max_attempts (10-600, default 120)
join_poll_interval_ms (100-5000, default 500)
pool_stats_interval_minutes (0-1440, default 5)
hikari_pool_max_size (1-200, default 15)
hikari_leak_threshold_ms (2000-600000, default 25000)
safety
refuse_empty_inventory_write (default true) — enforced in writeSnapshotToDB
max_inventory_size_bytes (default 10 MB)
skip_saves_when_tps_below (0-20, default 0 = never)
observability
log_structured_json (future use)
log_rotation_size_mb (default 10)
log_rotation_max_files (default 5)
Wiring
- HeartbeatService reads heartbeat_interval_seconds at start.
- PoolStatsReporter reads pool_stats_interval_minutes (0 disables).
- doPlayerJoin poll uses join_poll_max_attempts + join_poll_interval_ms +
peer_stale_threshold_seconds.
- writeSnapshotToDB: refuse_empty guard + max_inventory_size_bytes guard
before core UPDATE. Both log via SyncLogger.dataLoss / .nbtAnomaly.
- Restore-side toggles: applyCuriosFromData, applyAccessoriesFromData,
applyCosmeticArmorFromData, doBackPackRestore, restoreRefinedStorageDisks
all short-circuit when their toggle is false.
Commands — new /playersync tree (perm level 2 required):
status — server id + heartbeat age + exec/Hikari stats + online
poolstats — log current stats immediately
flush [player] — force save all / one
info <player> — DB row metadata
dump <player> — dump full DB row to server log
resync <player> — clear synced tag + kick to force re-restore
wipe <player> confirm — DELETE all rows (DANGER, double-keyword required)
orphans — list stuck online=1 rows on dead peers
clearorphans [id] — clear orphans (global or by server_id)
peers — list peer servers with ALIVE/STALE/STOPPED tag
peerkill <id> — force-disable a zombie peer
cleanup — orphans + stale peers in one shot
reload — note about runtime reload scope
help — in-chat command reference
Every command logs to SyncLogger as ADMIN_<OP> for audit trail.
Infrastructure
- JDBCsetUp.executePreparedUpdateRet(String, Object...) returns rows-affected
for commands that need meaningful counts.
- VanillaSync.getExecutor() exposes the thread pool for read-only stats access
from admin commands (replaces reflection use in PoolStatsReporter eventually).
97 lines
3.8 KiB
Java
97 lines
3.8 KiB
Java
package vip.fubuki.playersync.util;
|
|
|
|
import com.zaxxer.hikari.HikariPoolMXBean;
|
|
import vip.fubuki.playersync.PlayerSync;
|
|
|
|
import java.lang.reflect.Method;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
/**
|
|
* Periodic reporter that logs executor + HikariCP stats every 5 minutes into
|
|
* the PlayerSync sync.log. Lets admins spot queue saturation or pool
|
|
* exhaustion trends without waiting for a crash. Non-invasive — pure read-only.
|
|
*
|
|
* @author vyrriox
|
|
*/
|
|
public final class PoolStatsReporter {
|
|
|
|
private PoolStatsReporter() {}
|
|
|
|
private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
|
|
private static ScheduledExecutorService scheduler;
|
|
|
|
public static void start() {
|
|
int minutes;
|
|
try {
|
|
minutes = vip.fubuki.playersync.config.JdbcConfig.POOL_STATS_INTERVAL_MINUTES.get();
|
|
} catch (Throwable t) {
|
|
minutes = 5;
|
|
}
|
|
if (minutes <= 0) {
|
|
PlayerSync.LOGGER.info("[pool-stats] disabled (pool_stats_interval_minutes=0)");
|
|
return;
|
|
}
|
|
if (!RUNNING.compareAndSet(false, true)) return;
|
|
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
|
Thread t = new Thread(r, "PlayerSync-pool-stats");
|
|
t.setDaemon(true);
|
|
t.setPriority(Thread.MIN_PRIORITY);
|
|
return t;
|
|
});
|
|
long periodMs = minutes * 60_000L;
|
|
scheduler.scheduleAtFixedRate(PoolStatsReporter::tick, periodMs, periodMs, TimeUnit.MILLISECONDS);
|
|
PlayerSync.LOGGER.info("[pool-stats] reporter started (period={}ms)", periodMs);
|
|
}
|
|
|
|
public static void stop() {
|
|
if (!RUNNING.compareAndSet(true, false)) return;
|
|
if (scheduler != null) {
|
|
scheduler.shutdownNow();
|
|
scheduler = null;
|
|
}
|
|
}
|
|
|
|
private static void tick() {
|
|
try {
|
|
// Pull executor stats via reflection — VanillaSync.executorService is package-private static
|
|
ThreadPoolExecutor exec = getExecutor();
|
|
int active = exec != null ? exec.getActiveCount() : -1;
|
|
int queue = exec != null ? exec.getQueue().size() : -1;
|
|
int idle = exec != null ? exec.getPoolSize() - exec.getActiveCount() : -1;
|
|
|
|
HikariPoolMXBean hikari = JDBCsetUp.getPoolMXBean();
|
|
int hActive = hikari != null ? hikari.getActiveConnections() : -1;
|
|
int hIdle = hikari != null ? hikari.getIdleConnections() : -1;
|
|
|
|
SyncLogger.poolStats(active, queue, idle, hActive, hIdle);
|
|
|
|
// Warn if queue is getting dangerously full
|
|
if (queue > 400) {
|
|
PlayerSync.LOGGER.warn("[pool-stats] executor queue high: {}/512 — risk of CallerRunsPolicy blocking main thread", queue);
|
|
SyncLogger.warnPlayer("SYSTEM", "Executor queue high: " + queue + "/512");
|
|
}
|
|
if (hActive >= 0 && hActive >= 14) {
|
|
PlayerSync.LOGGER.warn("[pool-stats] HikariCP active connections high: {}/15 — risk of connection starvation", hActive);
|
|
SyncLogger.warnPlayer("SYSTEM", "HikariCP active: " + hActive + "/15");
|
|
}
|
|
} catch (Throwable t) {
|
|
PlayerSync.LOGGER.warn("[pool-stats] tick failed: {}", t.getMessage());
|
|
}
|
|
}
|
|
|
|
private static ThreadPoolExecutor getExecutor() {
|
|
try {
|
|
Class<?> c = Class.forName("vip.fubuki.playersync.sync.VanillaSync");
|
|
java.lang.reflect.Field f = c.getDeclaredField("executorService");
|
|
f.setAccessible(true);
|
|
Object o = f.get(null);
|
|
if (o instanceof ThreadPoolExecutor tpe) return tpe;
|
|
} catch (Throwable ignored) {}
|
|
return null;
|
|
}
|
|
}
|