Files
core-vitals/docs/06_ServerRegistry.md
T
2026-05-26 00:11:45 -04:00

3.6 KiB

ServerRegistry

Every server in the network heartbeats its presence to Redis. Every server keeps a local snapshot of who's online and reacts to changes via Hytale events.

API

public interface ServerRegistry {
    ServerInfo getSelf();
    Collection<ServerInfo> getServers();
    Collection<ServerInfo> getServersByRole(String role);
    @Nullable ServerInfo getServer(String id);
}

All four are local cache reads. No Redis round-trip per call.

public final class ServerInfo {
    String  id();         // e.g. "lobby-3f2a91bd"
    String  role();       // e.g. "lobby", "bridge_duel"
    String  address();    // currently unused, reserved for future
    int     players();    // last reported player count
    long    startedAt();  // epoch millis when the server booted
    long    lastSeen();   // epoch millis of last heartbeat we observed
}

How it works

On start():

  1. Subscribe to network:events (presence channel).
  2. Write own heartbeat to network:servers:<id> (TTL 15s).
  3. Scan network:servers:* to populate the local cache and fire join events for everyone discovered.
  4. Publish a hello on network:events.
  5. Schedule a heartbeat task every 5s. Each tick rewrites the heartbeat (resetting TTL) and re-scans.

On stop() (clean shutdown only):

  1. Publish a goodbye on network:events.
  2. Delete the registry key.

If a server hard-crashes (kill -9, OOM, network partition), no goodbye is sent. Other servers learn it's gone when the 15s TTL expires the key and the next scan doesn't see it.

Detection latency

Event When other servers learn
New server boots Within a few hundred ms (immediate hello over pub/sub)
Server clean-shutdown Within a few hundred ms (immediate goodbye over pub/sub)
Server hard-crash Up to 20s (TTL 15s + up to 5s for the next scan)

Events

When the registry's view of the network changes, it dispatches these on the Hytale event bus:

import net.kewwbec.networkcore.api.events.ServerJoinedNetworkEvent;
import net.kewwbec.networkcore.api.events.ServerLeftNetworkEvent;

getEventRegistry().registerGlobal(ServerJoinedNetworkEvent.class, event -> {
    ServerInfo server = event.getServer();
    LOGGER.at(Level.INFO).log("Server joined: %s (role=%s)", server.id(), server.role());
});

getEventRegistry().registerGlobal(ServerLeftNetworkEvent.class, event -> {
    ServerInfo server = event.getServer();
    LOGGER.at(Level.INFO).log("Server left: %s", server.id());
});

Event handlers run on the Hytale event-bus dispatch thread. Same world-thread caveat as MessageBus subscribers: if you touch entities, hop to world.execute(...).

Server id and role

The id is either the value in server.id in config.json, or auto-generated as <role>-<8 hex chars> on every boot.

The role is a free-form string from server.role. Use snake_case. Examples:

  • lobby - hub/main world
  • bridge_duel - one Bridge Duel match host
  • arena - PvP arena
  • staff - admin-only world

Roles are how plugins route. Example: a queue plugin running on a lobby finds an available bridge_duel server with:

Collection<ServerInfo> available = core.getServerRegistry()
    .getServersByRole("bridge_duel")
    .stream()
    .filter(s -> s.players() < 20)
    .toList();

Redis layout

For reference, what NetworkCore writes:

network:servers:<id>          HASH    server heartbeat record, TTL 15s
                              fields: id, role, players, started_at, last_seen
network:events                CHANNEL pub/sub for presence (hello/goodbye)

network: is the default redis.key_prefix. Change in config if needed.