forked from Kweebec_Network/core-vitals
107 lines
3.6 KiB
Markdown
107 lines
3.6 KiB
Markdown
# 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
|
|
|
|
```java
|
|
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.
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
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.
|