Files
core-vitals/docs/10_Database.md
T
2026-05-26 00:47:01 -04:00

4.3 KiB

Database

Shared MySQL pool, exposed to all plugins via DatabaseService from NetworkCore. Use this for durable state that needs to survive restarts and be queried later: player profiles, punishment history, currency balances, leaderboards, anything you'd run a SQL query against.

For ephemeral observability data (counters, latency), use the metrics layer instead. See 04_Metrics.md.

API

public interface DatabaseService {
    DataSource getDataSource();
    boolean isHealthy();
    int getActiveConnections();
    int getIdleConnections();
    int getTotalConnections();
}

That's it. We expose the DataSource and stay out of your way. Use JDBC directly, jOOQ, MyBatis, JdbcTemplate, whatever you like.

Getting a connection

Always use try-with-resources so connections return to the pool:

DatabaseService db = NetworkCore.getInstance().getDatabase();
if (db == null) {
    // MySQL disabled in NetworkCore config; degrade gracefully
    return;
}

try (Connection c = db.getDataSource().getConnection();
     PreparedStatement ps = c.prepareStatement("SELECT name, level FROM players WHERE uuid = ?")) {
    ps.setString(1, uuid.toString());
    try (ResultSet rs = ps.executeQuery()) {
        if (rs.next()) {
            return new PlayerRecord(rs.getString("name"), rs.getInt("level"));
        }
    }
}

Never hold a connection across an RPC call, a thread hop, or a wait. Pool exhaustion is the most common way Hikari-backed plugins fall over.

Threading

The pool is fully thread-safe. Get a connection from whichever thread needs it. JDBC calls themselves block, so:

  • Don't run heavy queries on the Hytale world thread - it stops world ticking.
  • Don't run heavy queries on the MessageBus worker pool - it backs up the bus.
  • Use a dedicated executor or CompletableFuture.supplyAsync for I/O-heavy work.

Schema management

NetworkCore owns the pool, not your schema. Each plugin is responsible for creating and migrating its own tables.

Conventions:

  • Table prefix per plugin. The Staff plugin prefixes everything with staff_. Pick something unique to your plugin so two plugins don't fight over players.
  • Bootstrap on startup. In your plugin's start(), run a CREATE TABLE IF NOT EXISTS ... for every table you need. This makes a fresh install just-work without manual setup.
  • Migrations as numbered scripts. When you change a schema, add a migrations/002_add_email_column.sql and a tiny runner that tracks applied migrations in a <plugin>_schema_version table. Don't reach for Flyway/Liquibase until you've added a third migration - those frameworks earn their weight on real projects, not on the second migration.

Configuration

See the mysql section in 03_Configuration.md. Default is enabled: false so a server that doesn't have MySQL configured doesn't crash on boot. Flip to true once you've put credentials in place via MYSQL_PASSWORD env var.

What you see in /networkcore status

When MySQL is healthy:

Database: ok (active=2 idle=8 total=10)

When disabled:

Database: disabled

When configured but failing:

Database: unhealthy

If you ever see "unhealthy", check the boot log for the connection error and verify host/port/credentials.

What is not in the box

  • No ORM. Plain DataSource only. Pick your own.
  • No migration framework. Bootstrap with CREATE TABLE IF NOT EXISTS per plugin.
  • No automatic encryption at rest. That's MySQL's job, not the plugin's. Use MySQL Enterprise's TDE or AWS RDS encryption.
  • No read replicas. Single pool to a single MySQL endpoint. Read replicas are an MySQL Router or ProxySQL concern, not a Hytale plugin concern.

Sizing the pool

max_pool_size = 10 is fine for a single server with a handful of plugins. Rules of thumb:

  • Each plugin doing heavy queries needs maybe 2-4 connections worth of headroom.
  • The pool is per Hytale-server-instance, not network-wide. If you have 5 servers each with pool=10, MySQL needs to handle 50 concurrent connections.
  • Make sure MySQL's max_connections is comfortably above the sum of all pools that point at it.

Don't crank max_pool_size to "fix" pool exhaustion. Pool exhaustion is almost always a leaked connection or a query that's holding the connection too long. Find the leak, then re-evaluate sizing.