96 lines
4.0 KiB
Java
96 lines
4.0 KiB
Java
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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>What SSE does cost is a <em>socket</em> 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<SseClient> 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-<actual port>. 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) { }
|
|
}
|
|
}
|
|
}
|
|
}
|