Skip to content
imykSeoPublic

About

A Paper plugin that handles MySQL and Redis for other plugins, providing async queries, connection pooling, write batching, cache invalidation, schema migrations, and retries, all off the main thread.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

AsyncData

Shared MySQL and Redis for Paper plugins. Other plugins depend on this one instead of each managing their own connection pools, retry loops, and cache layers. Everything runs off the main thread.

What it gives you

  • A HikariCP connection pool pointed at MySQL, verified on startup with configurable retries.
  • Async query, queryList, and update calls that return CompletableFutures and never touch the server thread.
  • A write batcher that queues cheap writes and flushes them as JDBC batches on a timer or when the queue hits a size cap.
  • A Redis cache backed by a Jedis pool, with get, set, single-key and pattern-based invalidate.
  • writeThrough — does the SQL update, waits for it to land, then busts the cache keys. Callers don't have to remember both halves.
  • Transactions on a single connection with commit/rollback.
  • A migration system that tracks schema versions in the database and uses MySQL advisory locks so two servers starting at once don't collide.
  • Retries with linear backoff on all queries and updates. Batched writes that fail are requeued up to the retry limit, then logged as dropped.
  • If the final batch flush on shutdown fails, the lost writes are dumped to plugins/AsyncData/unsaved-writes.log so you can see what didn't make it.

Getting started

  1. Drop the jar into plugins/.
  2. Start the server once so it writes plugins/AsyncData/config.yml.
  3. Fill in your MySQL and Redis credentials and restart.

Other plugins access AsyncData through the Bukkit service manager or by grabbing the plugin instance and calling getDatabase() / getCache().

Using it from another plugin

var asyncData = (AsyncData) Bukkit.getPluginManager().getPlugin("AsyncData");
var db = asyncData.getDatabase();
var cache = asyncData.getCache();

// run a query
db.query("SELECT coins FROM players WHERE uuid = ?",
        List.of(player.getUniqueId().toString()),
        rs -> rs.getInt("coins"))
    .thenAccept(coins -> { /* back on an IO thread, not the main thread */ });

// write-through: update the row, then bust the cache key
db.writeThrough("UPDATE players SET coins = ? WHERE uuid = ?",
        List.of(500, uuid.toString()),
        "player:" + uuid);

// fire-and-forget batch write (flushed every second or at 200 queued)
db.batchWrite("INSERT INTO logs (uuid, action, ts) VALUES (?, ?, ?)",
        List.of(uuid.toString(), "login", System.currentTimeMillis()));

// transaction
db.transaction(conn -> {
    try (var ps = conn.prepareStatement("UPDATE players SET coins = coins - ? WHERE uuid = ?")) {
        ps.setInt(1, 100);
        ps.setString(2, buyer.toString());
        ps.executeUpdate();
    }
    try (var ps = conn.prepareStatement("UPDATE players SET coins = coins + ? WHERE uuid = ?")) {
        ps.setInt(1, 100);
        ps.setString(2, seller.toString());
        ps.executeUpdate();
    }
    return null;
});

// schema migration on your plugin's enable
db.migrate(List.of(
    new Migration(1, "create_players", conn -> {
        try (var st = conn.createStatement()) {
            st.executeUpdate("CREATE TABLE IF NOT EXISTS players ("
                + "uuid VARCHAR(36) PRIMARY KEY, coins INT DEFAULT 0)");
        }
    }),
    new Migration(2, "add_last_seen", conn -> {
        try (var st = conn.createStatement()) {
            st.executeUpdate("ALTER TABLE players ADD COLUMN last_seen BIGINT DEFAULT 0");
        }
    })
));

Batch writes vs regular updates

update() runs immediately on an IO thread and hands you a future with the affected row count. Use it when you need to know the write landed.

batchWrite() queues the statement and returns nothing. The batcher groups queued writes by SQL text and flushes them as JDBC batches every second, or sooner if the queue reaches 200 entries. Use it for high-volume writes where individual confirmation doesn't matter — stat increments, logs, analytics.

If a batch fails, the writes are requeued and retried on the next flush. After exhausting the retry limit they are logged at SEVERE and dropped.

Cache invalidation

writeThrough is the intended path for keeping MySQL and Redis consistent. It waits for the SQL update to succeed before invalidating the cache keys, so a reader between the write and the invalidation always sees the new data in MySQL rather than a stale cache entry.

There is no batchWriteThrough. Batched writes are deferred by design, so there is no point at which the cache can be reliably invalidated — the write hasn't happened yet when the call returns. If you batch-write data that is also cached, either accept TTL-based staleness or invalidate manually after calling flushBatch().

invalidatePattern("player:") walks the keyspace with SCAN (not KEYS) and deletes everything under asyncdata:player:*. Safe to run on large databases without blocking Redis.

Recovering from trouble

  • Queries and updates retry up to retry.attempts times with linear backoff before giving up. The default is 3 attempts, 2 seconds apart.
  • Batched writes that fail mid-flush are requeued individually and retried on the next cycle. Writes that exceed the retry limit are logged and dropped.
  • If the server shuts down and the final batch flush fails entirely, every lost write is appended to unsaved-writes.log with a timestamp, the SQL, and the parameters. This is evidence, not a replay mechanism — you can inspect and rerun them manually.
  • Startup connection failures are retried with backoff. If the database is still unreachable after all attempts, the plugin disables itself rather than running without storage.
  • Config values are clamped to sane minimums. Setting io-threads: 0 gives you 1, not a crash.

Files

plugins/AsyncData/
  config.yml            MySQL and Redis credentials, pool sizing, retry
                        and batch tuning
  unsaved-writes.log    only created if a shutdown flush fails — contains
                        the SQL and params of every write that was lost

About

A Paper plugin that handles MySQL and Redis for other plugins, providing async queries, connection pooling, write batching, cache invalidation, schema migrations, and retries, all off the main thread.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages