Multi-server Velocity support, HikariCP connection pooling & message formatting overhaul#104
Multi-server Velocity support, HikariCP connection pooling & message formatting overhaul#104gitEpildev wants to merge 16 commits into
Conversation
- Full cross-server payment support over Velocity networks - Real-time payment notifications across servers - Offline notification queue via pending_notifications DB table - Network-wide player tab completion - HikariCP connection pooling (replaces single synchronized connection) - MySQL transactions for atomic transfers - 57% code reduction in MySQLStorageProvider (1448 → 625 lines) - Message colours: gold for sending, green for receiving, red for errors - Redesigned /baltop with styled header and dividers - Uppercase K/M/B/T currency suffixes - Dedicated admin eco give/take messages - Pre-built jars included in jars/ folder - Unit tests updated for uppercase suffixes
There was a problem hiding this comment.
Please remove and add to .gitignore if it's a build file.
There was a problem hiding this comment.
Please remove and add to .gitignore if it's a build file.
| plugin.setCrossServerMessenger(messenger); | ||
| Bukkit.getPluginManager().registerEvents(this, plugin); | ||
|
|
||
| cleanupTaskId = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> { |
There was a problem hiding this comment.
Seems to me this should be optional and not started when not being used.
| // Always persist correct UUID-to-name mapping from Velocity | ||
| try { | ||
| StorageProvider storage = plugin.getStorageOrWarn(); | ||
| if (storage != null) { | ||
| org.bukkit.entity.Player p = event.getPlayer(); | ||
| storage.persistPlayerInfo(p.getUniqueId(), p.getName(), p.getDisplayName()); | ||
| } | ||
| } catch (Exception e) { | ||
| plugin.getLogger().warning("Failed to persist player info on join: " + e.getMessage()); | ||
| } | ||
|
|
||
| // Optionally ensure player is stored in the configured storage backend | ||
| if (!plugin.getConfig().getBoolean("store-on-join.enabled", false)) { |
There was a problem hiding this comment.
Should only run when store-on-join is enabled, otherwise it should get it directly without the storage caching.
| String senderName = in.readUTF(); | ||
| String amount = in.readUTF(); | ||
| String currency = in.readUTF(); | ||
| plugin.getLogger().info("Received cross-server NOTIFY: recipient=" + recipientName |
| Bukkit.getPluginManager().callEvent(payEvent); | ||
| return null; | ||
| }).get(); | ||
| }).get(5, java.util.concurrent.TimeUnit.SECONDS); |
There was a problem hiding this comment.
Replace hard coded 5 seconds timeout with configurable amount or leave to default
There was a problem hiding this comment.
Change to markdown documentation updates in the docs/ folder instead
|
Thank you for the detailed feedback on my pull request. I will address all
of the requested changes and refactor the code accordingly.
My apologies for the oversight on some of these points; I was working late
and definitely feeling the fatigue. I will ensure the next update is much
cleaner and aligned with your preferences regarding the MySQL handling and
logging.
Thanks again for the review.
Best regards,
Blake McBride
…On Tue, 7 Apr 2026 at 12:35, EzPlugins ***@***.***> wrote:
***@***.**** requested changes on this pull request.
Hi,
I've reviewed the code
------------------------------
On jars/ezeconomy-bukkit-2.5.1.jar
<#104 (comment)>:
Please remove and add to .gitignore if it's a build file.
------------------------------
On jars/ezeconomy-velocity-2.5.1.jar
<#104 (comment)>:
Please remove and add to .gitignore if it's a build file.
------------------------------
In
src/main/java/com/skyblockexp/ezeconomy/bootstrap/component/MessagingComponent.java
<#104 (comment)>:
> + private final EzEconomyPlugin plugin;
+ private CrossServerMessenger messenger;
+ private int cleanupTaskId = -1;
+
+ public MessagingComponent(EzEconomyPlugin plugin) {
+ this.plugin = plugin;
+ }
+
+ @OverRide
+ public void start() {
+ messenger = new CrossServerMessenger(plugin);
+ messenger.register();
+ plugin.setCrossServerMessenger(messenger);
+ Bukkit.getPluginManager().registerEvents(this, plugin);
+
+ cleanupTaskId = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> {
Seems to me this should be optional and not started when not being used.
------------------------------
In
src/main/java/com/skyblockexp/ezeconomy/listener/PlayerJoinListener.java
<#104 (comment)>:
> + // Always persist correct UUID-to-name mapping from Velocity
+ try {
+ StorageProvider storage = plugin.getStorageOrWarn();
+ if (storage != null) {
+ org.bukkit.entity.Player p = event.getPlayer();
+ storage.persistPlayerInfo(p.getUniqueId(), p.getName(), p.getDisplayName());
+ }
+ } catch (Exception e) {
+ plugin.getLogger().warning("Failed to persist player info on join: " + e.getMessage());
+ }
+
// Optionally ensure player is stored in the configured storage backend
if (!plugin.getConfig().getBoolean("store-on-join.enabled", false)) {
Should only run when store-on-join is enabled, otherwise it should get it
directly without the storage caching.
------------------------------
In
src/main/java/com/skyblockexp/ezeconomy/messaging/CrossServerMessenger.java
<#104 (comment)>:
> +
+ @OverRide
+ public void onPluginMessageReceived(String channel, Player player, byte[] data) {
+ if (!CHANNEL.equals(channel)) return;
+
+ try {
+ DataInputStream in = new DataInputStream(new ByteArrayInputStream(data));
+ String type = in.readUTF();
+
+ if ("NOTIFY".equals(type)) {
+ String recipientUuidStr = in.readUTF();
+ String recipientName = in.readUTF();
+ String senderName = in.readUTF();
+ String amount = in.readUTF();
+ String currency = in.readUTF();
+ plugin.getLogger().info("Received cross-server NOTIFY: recipient=" + recipientName
Make logging optional
------------------------------
In
src/main/java/com/skyblockexp/ezeconomy/messaging/CrossServerMessenger.java
<#104 (comment)>:
> + String recipientUuidStr = in.readUTF();
+ String recipientName = in.readUTF();
+ String senderName = in.readUTF();
+ String amount = in.readUTF();
+ String currency = in.readUTF();
+ plugin.getLogger().info("Received cross-server NOTIFY: recipient=" + recipientName
+ + " uuid=" + recipientUuidStr + " from=" + senderName + " amount=" + amount);
+
+ Player recipient = Bukkit.getPlayer(UUID.fromString(recipientUuidStr));
+ if (recipient != null && recipient.isOnline()) {
+ String msg = plugin.getMessageProvider().get("received",
+ Map.of("player", senderName, "amount", amount));
+ plugin.getLogger().info("Delivering cross-server message to " + recipientName + ": " + msg);
+ recipient.sendMessage(msg);
+ } else {
+ plugin.getLogger().warning("Cross-server NOTIFY: recipient " + recipientName + " not found locally (uuid=" + recipientUuidStr + ")");
Optional logging
------------------------------
In
src/main/java/com/skyblockexp/ezeconomy/messaging/CrossServerMessenger.java
<#104 (comment)>:
> + String type = in.readUTF();
+
+ if ("NOTIFY".equals(type)) {
+ String recipientUuidStr = in.readUTF();
+ String recipientName = in.readUTF();
+ String senderName = in.readUTF();
+ String amount = in.readUTF();
+ String currency = in.readUTF();
+ plugin.getLogger().info("Received cross-server NOTIFY: recipient=" + recipientName
+ + " uuid=" + recipientUuidStr + " from=" + senderName + " amount=" + amount);
+
+ Player recipient = Bukkit.getPlayer(UUID.fromString(recipientUuidStr));
+ if (recipient != null && recipient.isOnline()) {
+ String msg = plugin.getMessageProvider().get("received",
+ Map.of("player", senderName, "amount", amount));
+ plugin.getLogger().info("Delivering cross-server message to " + recipientName + ": " + msg);
Optional logging
------------------------------
In
src/main/java/com/skyblockexp/ezeconomy/messaging/CrossServerMessenger.java
<#104 (comment)>:
> + public void deliverPendingNotifications(Player player) {
+ StorageProvider storage = plugin.getStorageOrWarn();
+ if (!(storage instanceof MySQLStorageProvider)) return;
+ MySQLStorageProvider mysql = (MySQLStorageProvider) storage;
+ List<String> messages = mysql.pollPendingNotifications(player.getUniqueId());
+ for (String msg : messages) {
+ player.sendMessage(msg);
+ }
+ }
+
+ private void storePendingNotification(UUID recipientUuid, String senderName, String amount, String currency) {
+ StorageProvider storage = plugin.getStorageOrWarn();
+ if (!(storage instanceof MySQLStorageProvider)) return;
+ MySQLStorageProvider mysql = (MySQLStorageProvider) storage;
+ String msg = plugin.getMessageProvider().get("received",
+ Map.of("player", senderName, "amount", amount));
+ mysql.insertPendingNotification(recipientUuid, msg);
+ }
Only available for MySQL as storage provider?
------------------------------
In src/main/java/com/skyblockexp/ezeconomy/service/PaymentExecutor.java
<#104 (comment)>:
> + OfflinePlayer toOffline = null;
+ if (online != null) {
+ toOffline = online;
+ } else {
+ UUID dbUuid = storage.resolvePlayerByName(toName);
+ if (dbUuid != null) {
+ toOffline = Bukkit.getOfflinePlayer(dbUuid);
+ knownOffline = true;
+ } else {
+ var maybe = com.skyblockexp.ezeconomy.util.PlayerLookup.findByName(toName);
+ if (maybe.isPresent()) {
+ toOffline = maybe.get();
+ } else {
+ toOffline = Bukkit.getOfflinePlayer(toName);
+ }
+ }
+ }
Already refactored with local configurable helper/optional caching layer
in #97 <#97>.
------------------------------
In src/main/java/com/skyblockexp/ezeconomy/service/PaymentExecutor.java
<#104 (comment)>:
> @@ -63,7 +79,7 @@ public static boolean execute(EzEconomyPlugin plugin, Player from, String toName
Bukkit.getScheduler().callSyncMethod(plugin, () -> {
Bukkit.getPluginManager().callEvent(payEvent);
return null;
- }).get();
+ }).get(5, java.util.concurrent.TimeUnit.SECONDS);
Replace hard coded 5 seconds timeout with configurable amount or leave to
default
------------------------------
On CHANGES.md
<#104 (comment)>:
Change to markdown documentation updates in the docs/ folder instead
------------------------------
On pom.xml
<#104 (comment)>:
Please pull main to origin and push to get the latest pom.xml
------------------------------
In README.md
<#104 (comment)>:
> +> **Original plugin by ***@***.***(https://github.com/ez-plugins) — [ez-plugins/EzEconomy](https://github.com/ez-plugins/EzEconomy)**
+> This fork contains community contributions by ***@***.***(https://github.com/gitEpildev) — see [CHANGES.md](CHANGES.md) for full details.
+
+---
+
+## 🔀 What's New in This Fork
+
+The following changes were made by **GitEpildev** on top of the original EzEconomy codebase. All original features, APIs, and plugin compatibility are preserved.
+
+- **Full multi-server Velocity network support** — cross-server payments, real-time notifications, offline message queue, network-wide tab completion
+- **HikariCP connection pooling** for MySQL — replaces the single synchronized connection with a pool of 10 concurrent connections, eliminating main-thread freezes
+- **Atomic MySQL transactions** — transfers use `commit`/`rollback` for data integrity
+- **57% code reduction** in `MySQLStorageProvider` (1,448 → 625 lines)
+- **Message formatting redesign** — gold for sending, green for receiving, red for errors, styled `/baltop` with dividers
+- **Uppercase currency suffixes** — K, M, B, T instead of k, m, b, t
+- **Dedicated admin messages** — `/eco give` and `/eco take` have their own distinct messages
+- **Pre-built jars** included in `jars/` for both Bukkit and Velocity
+
+> **Note:** These changes currently apply to the MySQL storage provider only. SQLite, MongoDB, and YML providers are unchanged.
+
Doesn't belong in the README.md
------------------------------
In pom.xml
<#104 (comment)>:
> + <relocation>
+ <pattern>com.zaxxer.hikari</pattern>
+ <shadedPattern>com.skyblockexp.ezeconomy.shaded.hikari</shadedPattern>
+ </relocation>
You are aware that removing 700 +/- lines of code in the MySQL storage
provider is nothing compared to the 1MB+ library you are adding here for
MySQL only?
If Hikari is used my preference would be to require a new ezeconomy-mysql
with a split MySQL handling to prevent ending up with a 5+ MB EzEconomy JAR.
Please make sure this actually helps with the overall performance as well.
—
Reply to this email directly, view it on GitHub
<#104 (review)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AYYRWCYDI2DNS77YIJWPHGT4UTRZRAVCNFSM6AAAAACXPIEZ76VHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHM2DANRXGY3DQMBTHA>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
--
Kind regards,
Blake McBride
|
|
For my test/release planning, do you think it would be possible to resolve the comments before end of tomorrow? This way I can include this in upcoming release, otherwise it would have to wait for next release since it would delay the release. |
|
In #105 the |
|
Hi,
I should be able to resolve those comments by tomorrow. Please allow until
5:00 PM GMT for the updates, as I am based in the UK.
Kind regards,
Blake McBride
…On Wed, 8 Apr 2026 at 08:51, EzPlugins ***@***.***> wrote:
*ez-plugins* left a comment (ez-plugins/EzEconomy#104)
<#104 (comment)>
For my test/release planning, do you think it would be possible to resolve
the comments before end of tomorrow?
This way I can include this in upcoming release, otherwise it would have
to wait for next release since it would delay the release.
—
Reply to this email directly, view it on GitHub
<#104 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AYYRWC6O2674LW6VGIXKBY34UYAH7AVCNFSM6AAAAACXPIEZ76VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DEMBUGY3DENJSGQ>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
That would work, thank you for your fast reply. |
|
Hi,
If you use Discord, I would be happy to discuss the details there at your
convenience. My username is Epildev.
Kind regards,
Blake McBride
…On Wed, 8 Apr 2026 at 08:54, EzPlugins ***@***.***> wrote:
*ez-plugins* left a comment (ez-plugins/EzEconomy#104)
<#104 (comment)>
That would work, thank you for your fast reply.
—
Reply to this email directly, view it on GitHub
<#104 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AYYRWC4FX3PDWCRI6PCYGHD4UYATFAVCNFSM6AAAAACXPIEZ76VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DEMBUGY3TQNJRGM>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
Another thing I just noticed from your description, I did not check the actual code for this. The lock is a security thing, not a performance thing. The lock makes sure that async functionality can not be abused by spamming through packets. Making the pool 10 concurrent might create possibility to withdraw an amount 5 or 10 times while only having balance enough for 1 in the worst case when abused. Added you on Discord. |
|
Thank you for clarifying the purpose of the lock. My primary focus during
development was performance, and I relied on external anti-cheat measures
to prevent the type of exploit you described.
I am happy to re-implement the lock to ensure the security of the plugin
while still addressing the overall performance goals. Please let me know if
you would like me to proceed with adding it back in, or if you have a
preferred approach that balances both security and concurrency.
Kind regards,
Blake McBride
…On Wed, 8 Apr 2026 at 09:06, EzPlugins ***@***.***> wrote:
*ez-plugins* left a comment (ez-plugins/EzEconomy#104)
<#104 (comment)>
Another thing I just noticed from your description, I did not check the
actual code for this.
The lock is a security thing, not a performance thing.
The lock makes sure that async functionality can not be abused by spamming
through packets. Making the pool 10 concurrent might create possibility to
withdraw an amount 5 or 10 times while only having balance enough for 1 in
the worst case when abused.
—
Reply to this email directly, view it on GitHub
<#104 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AYYRWC55GGMNP2YWQRCTO7D4UYB7FAVCNFSM6AAAAACXPIEZ76VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DEMBUG42DEOBXGY>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
I have messaged you on Discord. |
Make cross-server messaging optional with configurable verbosity, gate join-time persistence by config, harden debit paths with lock-aware withdrawal flow, and move/refresh Markdown docs for clearer navigation and release notes while aligning build metadata with upstream.
Keep cross-server pay completion, fork attribution docs, and styled pay messages while retaining upstream pay-all/config additions.
Resolve lock timing values from the active strategy config (redis.yml or bungeecord.yml), keep backward compatibility for legacy redis.* keys, and document the patch in changelog.
Normalize coverage_summary module derivation to always emit stable module names and update MySQLStorageProviderH2IntegrationTest to inject Hikari dataSource instead of legacy connection reflection.
Use strategy-agnostic lock manager protection for Vault withdraw paths, add neutral locking config keys with legacy fallback, and document the new defaults while removing duplicate GUI YAML keys.
Fix leftover lock call mismatches after cherry-pick, remove duplicate cross-server config keys, and align compact-format test expectation with uppercase suffix behavior.
Delete bundled jar binaries from git and make the unit-test coverage comment step skip fork PRs with continue-on-error so permission limits do not fail CI.
Revert the coverage summary script to the previous behavior and remove continue-on-error from the PR comment step so same-repo comment failures are visible while fork PRs still skip the step.
Delete docs/changelog.md from the repository per request.
Add eco_give, eco_take, tax_usage, baltop_footer, usage_bank_deposit, usage_bank_withdraw to nl/es/fr/zh. Also add missing set key to fr/zh and unknown_player to zh. Adds project .gitignore for build outputs, IDE files, and secrets.
/pay * now includes players from all backend servers when cross-server messaging is enabled. Each remote recipient gets a NOTIFY forwarded through the Velocity proxy. Added Velocity setup section, payall permissions, and proxy architecture notes to README. Cleaned up stale assistant comment in docs/commands.md.
- BalanceCommand: add resolveOfflinePlayer helper that checks MySQL storage, Velocity network player list, PlayerLookup, and getOfflinePlayer fallback for cross-server /bal lookups. Handle null getName() for remotely resolved players. - CrossServerMessenger: store UUID-to-name map from Velocity PLAYER_LIST messages with backward-compatible parsing (UUID+name and name-only formats). Broaden catch to Exception to prevent single parse failures from breaking the channel. Add getNetworkPlayerUuid() accessor. - EzEconomyVelocity: send player UUID alongside username in broadcastPlayerList so Bukkit servers can resolve remote players. - PaymentExecutor: check CrossServerMessenger network UUID map before falling back to storage.resolvePlayerByName. - PayCommand: use messenger.getNetworkPlayerUuid for /pay * recipients and single /pay <player> resolution. Add debug logging for pay-all. - plugin.yml: add ezpay/ezpayall aliases to avoid command conflicts. - All language files (en, nl, es, fr, zh): add missing keys no_pending_payment, payment_confirm_required, payment_cancelled, unknown_conversion, usage_bank_create.
Version bump for the cross-server economy release including /bal, /pay *, /payall cross-server support and Velocity UUID broadcasting.
- README: add prerequisites table, MySQL setup example, store-on-join requirement, troubleshooting tips, and command alias note - docs/configuration.md: expand store-on-join and cross-server sections with Velocity context, remove duplicate cross-server block - config.yml: improve inline comments for store-on-join and cross-server to clarify Velocity network requirements
Summary
Community contribution by @gitEpildev adding multi-server Velocity network support, replacing the single MySQL connection with HikariCP pooling, and redesigning message formatting. All original features, APIs, and plugin compatibility are preserved.
Key changes:
synchronized(lock)connection with a pool of 10 concurrent connections, eliminating main-thread freezestransfer()usescommit/rollbackfor data integrityMySQLStorageProvider(1,448 → 625 lines)/baltopwith dividers/eco giveand/eco takehave their own message keysSee CHANGES.md for full details including file-by-file breakdown and setup instructions.
Files Changed
pom.xml(parent)MySQLStorageProvider.javaNumberUtil.javaBaltopCommand.javaGiveSubcommand.javaeco_givemessage keyTakeSubcommand.javaeco_takemessage keyPaymentExecutor.javaCrossServerMessenger.javaEzEconomyVelocity.javaen.ymlTest Plan
/payworks instantly with real-time notifications/eco give,/eco take,/eco setall work without lag/baltopdisplays with new styled formatting