Repository navigation
Add Control transport diagnostics and safe proxy method switching #1589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6c646e9
836f8e7
1e2343c
9df6143
7e141bf
263d703
29d26fa
94ed7be
6e8e674
cac2657
2cbb387
e39c3ac
1b72508
c5a9a59
99be239
ae8a30f
a3f04df
edddcf7
24ab420
1c9ae8c
e6d0133
5366339
aa3d40c
53882fd
894f318
078eb67
9cb5d8d
da8c0fa
8d6cdc2
a655706
be4e9a9
52bc7df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ | |
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.UUID; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.Executors; | ||
|
|
@@ -171,6 +172,7 @@ public abstract class VotingPluginProxy { | |
| private final Map<UUID, PendingPresenceHandoff> pendingPresenceHandoffs = new HashMap<>(); | ||
| private final Set<String> pendingBackendRecoverySnapshots = ConcurrentHashMap.newKeySet(); | ||
| private final Map<String, Long> controlEnrollmentNextAllowed = new ConcurrentHashMap<>(); | ||
| private final Map<UUID, PendingCommunicationTest> pendingCommunicationTests = new ConcurrentHashMap<>(); | ||
| private volatile ControlConnector controlConnector; | ||
| private volatile HostedControlManager hostedControlManager; | ||
| private final Object controlLifecycleLock = new Object(); | ||
|
|
@@ -1521,11 +1523,10 @@ && isPresenceGenerationValid(snapshot.backendIncarnationId, snapshot.backendStar | |
| } | ||
| }); | ||
|
|
||
| globalMessageProxyHandler.addListener(new GlobalMessageListener("statusokay") { | ||
| globalMessageProxyHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_STATUS_OKAY) { | ||
| @Override | ||
| public void onReceive(JsonEnvelope message) { | ||
| String server = message.getFields().getOrDefault("server", ""); | ||
| log("Status okay for " + server); | ||
| handleStatusOkay(message); | ||
| } | ||
| }); | ||
|
|
||
|
|
@@ -2402,6 +2403,7 @@ public void prepareForRuntimeReplacement() { | |
|
|
||
| /** Best-effort remainder of runtime teardown after the Control overlap gate has succeeded. */ | ||
| public void completeRuntimeReplacementShutdown() { | ||
| cancelCommunicationTests("Proxy runtime stopped before the backend replied"); | ||
| runCleanup("vote cache", () -> getVoteCacheHandler().saveVoteCache()); | ||
| runCleanup("proxy MySQL messenger", () -> { | ||
| if (getProxyMysqlMessenger() != null) getProxyMysqlMessenger().shutdown(); | ||
|
|
@@ -2891,6 +2893,100 @@ public void status() { | |
| } | ||
| } | ||
|
|
||
| /** Runs a correlated, non-vote round trip over the active backend transport. */ | ||
| public CompletableFuture<CommunicationTestResult> testBackendCommunication(String requestedServer, | ||
| long timeoutMillis) { | ||
| String server = requestedServer == null ? "" : requestedServer.trim(); | ||
| BungeeMethod activeMethod = method; | ||
| if (server.isEmpty() || !getAllAvailableServers().contains(server)) { | ||
| return CompletableFuture.completedFuture(CommunicationTestResult.failure(server, activeMethod, | ||
| "UNKNOWN_BACKEND", "The backend is not configured on this proxy")); | ||
| } | ||
| if (activeMethod == null || globalMessageProxyHandler == null) { | ||
| return CompletableFuture.completedFuture(CommunicationTestResult.failure(server, activeMethod, | ||
| "TRANSPORT_UNAVAILABLE", "The proxy communication transport is not running")); | ||
|
Comment on lines
+2905
to
+2907
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When MQTT initialization fails, Useful? React with 👍 / 👎. |
||
| } | ||
| if (activeMethod == BungeeMethod.PLUGINMESSAGING && !isSomeoneOnlineServerForVoteRouting(server)) { | ||
| return CompletableFuture.completedFuture(CommunicationTestResult.failure(server, activeMethod, | ||
| "PLAYER_REQUIRED", "Plugin messaging requires an online player on the selected backend")); | ||
| } | ||
| ScheduledExecutorService scheduler = getScheduler(); | ||
| if (scheduler == null) { | ||
| return CompletableFuture.completedFuture(CommunicationTestResult.failure(server, activeMethod, | ||
| "TRANSPORT_UNAVAILABLE", "The proxy scheduler is not running")); | ||
| } | ||
| long boundedTimeout = Math.max(500L, Math.min(timeoutMillis, 30000L)); | ||
| UUID requestId = UUID.randomUUID(); | ||
| CompletableFuture<CommunicationTestResult> result = new CompletableFuture<>(); | ||
| PendingCommunicationTest pending = new PendingCommunicationTest(server, activeMethod, System.nanoTime(), result); | ||
| pendingCommunicationTests.put(requestId, pending); | ||
| result.whenComplete((ignored, failure) -> pendingCommunicationTests.remove(requestId, pending)); | ||
| try { | ||
| if (!sendCommunicationTestEnvelopeNow(server, VotingPluginWire.status(server, requestId))) { | ||
| result.complete(CommunicationTestResult.failure(server, activeMethod, "TRANSPORT_UNAVAILABLE", | ||
| "The active transport could not accept the communication test")); | ||
| return result; | ||
| } | ||
| scheduler.schedule(() -> result.complete(CommunicationTestResult.failure(server, activeMethod, | ||
| "TIMEOUT", "No correlated reply arrived before the timeout")), boundedTimeout, TimeUnit.MILLISECONDS); | ||
| } catch (RuntimeException failure) { | ||
| result.complete(CommunicationTestResult.failure(server, activeMethod, "SEND_FAILED", | ||
| "The proxy could not send the communication test")); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| /** Sends a diagnostic immediately and reports whether the active transport accepted it. */ | ||
| protected boolean sendCommunicationTestEnvelopeNow(String server, JsonEnvelope envelope) { | ||
| return sendProxyBroadcastEnvelopeNow(server, envelope); | ||
| } | ||
|
|
||
| protected void handleStatusOkay(JsonEnvelope message) { | ||
| String server = message.getFields().getOrDefault(VotingPluginWire.K_SERVER, ""); | ||
| String request = message.getFields().getOrDefault(VotingPluginWire.K_REQUEST_ID, ""); | ||
| if (request.isEmpty()) { | ||
| log("Status okay for " + server); | ||
| return; | ||
| } | ||
| UUID requestId; | ||
| try { | ||
| requestId = UUID.fromString(request); | ||
| } catch (IllegalArgumentException ignored) { | ||
| debug("Ignored status reply with an invalid request ID from " + server); | ||
| return; | ||
| } | ||
| PendingCommunicationTest pending = pendingCommunicationTests.get(requestId); | ||
| if (pending == null || !pending.server().equals(server)) { | ||
| debug("Ignored unexpected status reply from " + server); | ||
| return; | ||
| } | ||
| long roundTripMillis = Math.max(0L, | ||
| TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - pending.startedAtNanos())); | ||
| pending.result().complete(CommunicationTestResult.success(server, pending.method(), roundTripMillis)); | ||
| } | ||
|
|
||
| private void cancelCommunicationTests(String message) { | ||
| pendingCommunicationTests.forEach((requestId, pending) -> pending.result().complete( | ||
| CommunicationTestResult.failure(pending.server(), pending.method(), "TRANSPORT_STOPPED", message))); | ||
| pendingCommunicationTests.clear(); | ||
| } | ||
|
|
||
| public record CommunicationTestResult(boolean success, String code, String message, String server, | ||
| String method, long roundTripMillis) { | ||
| private static CommunicationTestResult success(String server, BungeeMethod method, long roundTripMillis) { | ||
| return new CommunicationTestResult(true, "OK", "Backend replied over the active transport", server, | ||
| method == null ? "" : method.name(), roundTripMillis); | ||
| } | ||
|
|
||
| private static CommunicationTestResult failure(String server, BungeeMethod method, String code, String message) { | ||
| return new CommunicationTestResult(false, code, message, server, | ||
| method == null ? "" : method.name(), -1L); | ||
| } | ||
| } | ||
|
|
||
| private record PendingCommunicationTest(String server, BungeeMethod method, long startedAtNanos, | ||
| CompletableFuture<CommunicationTestResult> result) { } | ||
|
|
||
| private void sendVoteDelayRejected(String player, String uuid, String service, boolean playerOnline, | ||
| String playerServer) { | ||
| if (!playerOnline || playerServer == null || !getAllAvailableServers().contains(playerServer)) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a legacy or minimal
BungeeSettings.ymlomitsMQTT.BrokerURL, the runningBungeeSettingssuppliestcp://localhost:1883, but this raw-YAML preflight substitutes an empty string and rejects switching to MQTT. Use the same broker default as the runtime so an otherwise valid configuration can be previewed and applied.Useful? React with 👍 / 👎.