From 1c4449fb8801d902b2c0ce61d491afc3100ecca7 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 9 Jun 2026 11:27:14 +0000 Subject: [PATCH] perf: run auth HTTP endpoints off the Netty event loop (P1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /api/auth/* handlers ran inline on the Netty worker event loop, so their blocking work — BCrypt (cost 12 ~tens of ms), JDBC, the Turnstile HTTPS round-trip and SMTP — stalled every other client multiplexed on that thread; a burst of logins/registers could freeze game traffic. Dispatch each auth request to a dedicated bounded pool (4..16 daemon threads, bounded queue, 503 on saturation) instead. It is deliberately SEPARATE from the shared game ThreadPooling so auth load can't starve room cycles either. Netty writes are thread-safe, so the endpoints' sendJson calls work unchanged from the worker; the FullHttpRequest is released when the task finishes. Caveat: this allows concurrent handling of pipelined requests on a single keep-alive connection (out-of-order responses) — not a concern for the Nitro client which is strictly request/response, but worth load-testing before prod. --- .../gameserver/auth/AuthHttpHandler.java | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java index a743a30b..bf1e9369 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java @@ -9,8 +9,15 @@ import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.util.ReferenceCountUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.nio.charset.StandardCharsets; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.MAX_BODY_BYTES; import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.errorPayload; @@ -21,6 +28,25 @@ import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.sendJson; public class AuthHttpHandler extends ChannelInboundHandlerAdapter { + private static final Logger LOGGER = LoggerFactory.getLogger(AuthHttpHandler.class); + + // Dedicated, bounded pool for the auth endpoints. Their work blocks on + // BCrypt, JDBC, the Turnstile HTTPS round-trip and SMTP — running that on the + // Netty event loop stalls every client on the same worker. A SEPARATE pool + // (not the shared game ThreadPooling) also keeps it from starving room cycles. + private static final ThreadPoolExecutor AUTH_EXECUTOR = new ThreadPoolExecutor( + 4, 16, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(512), + new java.util.concurrent.ThreadFactory() { + private final AtomicInteger counter = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "auth-http-worker-" + counter.getAndIncrement()); + t.setDaemon(true); + return t; + } + }); + static final String LOGIN_PATH = "/api/auth/login"; static final String REGISTER_PATH = "/api/auth/register"; static final String FORGOT_PATH = "/api/auth/forgot-password"; @@ -52,10 +78,30 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { return; } + // Offload the (potentially blocking) auth work off the event loop. Netty + // writes are thread-safe, so the endpoints' sendJson/writeAndFlush calls + // are fine from the worker; the request is released once the work ends. try { - handle(ctx, req, path); - } finally { - ReferenceCountUtil.release(req); + AUTH_EXECUTOR.execute(() -> { + try { + handle(ctx, req, path); + } catch (Throwable t) { + LOGGER.error("Auth handler failed for {}", path, t); + try { + sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Internal error.")); + } catch (Throwable ignored) { + // response may already be partially written — nothing else to do + } + } finally { + ReferenceCountUtil.release(req); + } + }); + } catch (RejectedExecutionException rejected) { + try { + sendJson(ctx, req, HttpResponseStatus.SERVICE_UNAVAILABLE, errorPayload("Server busy, try again shortly.")); + } finally { + ReferenceCountUtil.release(req); + } } }