This commit is contained in:
2026-05-26 00:11:45 -04:00
commit 296c233157
41 changed files with 2829 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
# RPC
Request/response between servers, built on top of MessageBus. Use this when you need an answer; use MessageBus when fire-and-forget is enough.
## API
```java
public interface RpcClient {
<Req, Resp> CompletableFuture<Resp> request(
String service,
String method,
Req payload,
Class<Resp> responseType
);
RpcService registerService(String service);
}
public interface RpcService {
<Req, Resp> RpcService method(
String method,
Class<Req> requestType,
Class<Resp> responseType,
Function<Req, Resp> handler
);
void close();
}
```
## Calling
```java
record PlayerLookupReq(UUID uuid) {}
record PlayerLookupResp(String username, int kills) {}
CompletableFuture<PlayerLookupResp> future = core.getRpc().request(
"player-stats", // service name (matches what the server registered)
"lookup", // method name
new PlayerLookupReq(uuid),
PlayerLookupResp.class
);
future.whenComplete((resp, err) -> {
if (err != null) {
// err can be TimeoutException, RpcException (handler threw), or IllegalStateException (closed)
return;
}
use(resp);
});
```
The future completes:
- Normally with the decoded response if the handler returned successfully
- Exceptionally with `TimeoutException` if no response arrives within `rpc.timeout_ms` (default 5000)
- Exceptionally with `RpcClientImpl.RpcException` if the handler on the other side threw
- Exceptionally with `IllegalStateException("RpcClient closed")` if NetworkCore is shutting down
Don't block on `.get()` from a Hytale thread - hop to your own executor or use `.thenAccept` / `.whenComplete`.
## Serving
```java
RpcService stats = core.getRpc().registerService("player-stats");
stats.method("lookup", PlayerLookupReq.class, PlayerLookupResp.class, req -> {
Player p = lookupInDb(req.uuid());
return new PlayerLookupResp(p.username(), p.kills());
});
stats.method("topKills", TopKillsReq.class, TopKillsResp.class, this::handleTopKills);
```
The handler is called on the MessageBus worker pool (not a Hytale world thread). Same caveat as a subscriber - hop to world thread if you touch entities.
Handlers should return reasonably fast (under the RPC timeout, with margin). If a handler is slow or might block, run the work inside the handler and don't block the worker pool indefinitely - the pool is shared with regular MessageBus subscribers.
Throwing from a handler is captured and turned into an error response on the client side. Don't rely on this for control flow.
## How any-server-can-respond works
There's no service registry for RPC. The mechanism is:
1. Client publishes `RpcMessages.Request` on channel `rpc:req:<service>` with a unique correlation id and the client's reply channel.
2. Every server that has called `registerService("<service>")` is subscribed to that channel. They all receive the request.
If multiple servers register the same service name, all of them will process the request and reply. The client takes the first response that arrives and discards the rest (the future has already completed). For a real load-balanced setup, use distinct service names per instance, or have only one server register each service.
## Service naming
Use lowercase kebab-case. Pick names that describe the *capability*, not the server instance. Examples:
- `player-stats` - read player stats from MySQL
- `party-state` - the canonical party data store
- `match-allocator` - pick a server to host a match on
Don't:
- `player-stats-server-1` (couples the name to a specific instance)
- `lookup` (too generic, will collide)
## When RPC is the wrong tool
Use MessageBus instead when:
- You don't need a reply.
- You're broadcasting to all servers.
Use a database (MySQL) instead when:
- Multiple readers need consistent data.
- You need history or transactions.
Use the Hytale event bus (in-process) when:
- The consumer is on the same server.
RPC is for: "I need server X to do something for me and tell me how it went."
## Wire format
For reference. You don't construct these.
Request, published on `rpc:req:<service>` wrapped in the standard `Envelope`:
```json
{
"correlationId": "<uuid>",
"replyTo": "rpc:resp:<callerServerId>",
"method": "lookup",
"body": { ... }
}
```
Response, published on the `replyTo` channel:
```json
{
"correlationId": "<same uuid>",
"ok": true,
"error": null,
"body": { ... }
}
```