package com.ankurm.ssews; import java.time.Instant; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.apache.tomcat.util.threads.ThreadPoolExecutor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.tomcat.TomcatWebServer; import org.springframework.boot.web.server.context.WebServerApplicationContext; import static org.assertj.core.api.Assertions.assertThat; /** * Forty open SSE streams on a ten-thread Tomcat. * *

This is the test that answers the objection people raise first — "doesn't SSE pin a * thread per client?" It does not. {@code SseEmitter} puts the request into asynchronous mode, * the request thread returns to the pool, and the response stays open with no thread attached to * it. Forty concurrent streams on a pool of ten is the cheapest way to show that; the same test * with a blocking handler deadlocks at the eleventh client. * *

What SSE does cost is a socket per stream, plus one serialise-and-write per stream * per broadcast, both of which the transcript makes visible. */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {"dashboard.broadcast.enabled=false", "server.tomcat.threads.max=10"}) class SseConcurrencyTest { private static final int CLIENTS = 40; @LocalServerPort int port; @Autowired EmitterRegistry registry; @Autowired WebServerApplicationContext context; @Test void fortyStreamsOnTenThreads() throws Exception { List clients = new ArrayList<>(); try { for (int i = 0; i < CLIENTS; i++) { clients.add(new SseClient(port, "/sse/metrics")); } for (int i = 0; i < 200 && registry.open() < CLIENTS; i++) { Thread.sleep(25); } // Read the pool size off THIS context's own connector. Counting threads by name // does not work here: other @SpringBootTest contexts in the same JVM have their own // http-nio pools, and with a random port Tomcat names its threads http-nio-auto-N, // not http-nio-. That mismatch made this assertion pass alone and fail // in the full suite. TomcatWebServer server = (TomcatWebServer) context.getWebServer(); ThreadPoolExecutor pool = (ThreadPoolExecutor) server.getTomcat().getConnector() .getProtocolHandler().getExecutor(); System.out.println("=== server.tomcat.threads.max=10, clients=" + CLIENTS + " ==="); System.out.println("emitters open : " + registry.open()); System.out.println("connector pool size : " + pool.getPoolSize() + " (max " + pool.getMaximumPoolSize() + ")"); System.out.println("connector active : " + pool.getActiveCount()); assertThat(registry.open()).isEqualTo(CLIENTS); assertThat(pool.getMaximumPoolSize()).isEqualTo(10); assertThat(pool.getPoolSize()).isLessThanOrEqualTo(10); int delivered = registry.broadcast("metric", new MetricSnapshot(1, "node-a", 0.5, 300, Instant.now())); System.out.println("one broadcast reached: " + delivered + " streams"); assertThat(delivered).isEqualTo(CLIENTS); int received = 0; for (SseClient c : clients) { if (String.join("\n", c.read(12)).contains("event:metric")) { received++; } } System.out.println("clients that read it : " + received); assertThat(received).isEqualTo(CLIENTS); } finally { for (SseClient c : clients) { try { c.close(); } catch (Exception ignored) { } } } } }