package com.ankurm.protocols; import java.util.Iterator; import java.util.concurrent.atomic.AtomicLong; import com.ankurm.protocols.grpc.QuoteRequest; import com.ankurm.protocols.grpc.QuoteServiceGrpc; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import org.junit.jupiter.api.Test; import org.reactivestreams.Subscription; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.messaging.rsocket.RSocketRequester; import org.springframework.messaging.rsocket.RSocketStrategies; import reactor.core.publisher.BaseSubscriber; import static org.assertj.core.api.Assertions.assertThat; /** * The measurement that decides the article. * *
Each protocol serves an unbounded stream. Each client takes a hundred messages and then does * nothing for a while. The question is: how many did the server produce? * *
The sweep over quiet periods is what makes the answer conclusive. A number that stays flat
* as the quiet period grows means something bounded the producer. A number that grows in
* proportion to the wait means nothing did — the producer is running flat out and
* the only limit is the clock.
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {"spring.rsocket.server.port=7003", "spring.grpc.server.port=9093"})
class BackPressureTest {
private static final int WANTED = 100;
private static final long[] QUIET_MS = {500, 1_000, 2_000};
@LocalServerPort
int httpPort;
@Autowired
RSocketStrategies strategies;
@Test
void howManyDidTheServerProduce() throws Exception {
System.out.println("=== unbounded stream; client takes " + WANTED + ", then goes quiet ===");
System.out.printf("%-14s %10s %10s %10s%n", "protocol", "quiet 0.5s", "quiet 1s", "quiet 2s");
long[] rs = new long[QUIET_MS.length];
long[] gr = new long[QUIET_MS.length];
long[] ws = new long[QUIET_MS.length];
for (int i = 0; i < QUIET_MS.length; i++) {
rs[i] = rsocket(QUIET_MS[i]);
gr[i] = grpc(QUIET_MS[i]);
ws[i] = websocket(QUIET_MS[i]);
}
row("RSocket", rs);
row("gRPC", gr);
row("WebSocket", ws);
System.out.println();
System.out.println("RSocket is flat: the server produced exactly what request(n) asked for.");
System.out.printf("gRPC grows %.1fx between 0.5 s and 2 s; WebSocket grows %.1fx.%n",
gr[2] / (double) gr[0], ws[2] / (double) ws[0]);
System.out.println("Growth that tracks the wait, rather than settling at a buffer size,");
System.out.println("means nothing bounded the producer. (Growth is faster than linear");
System.out.println("because the loop is still being JIT-compiled during the first run.)");
// RSocket delivered exactly the demand, at every quiet period.
assertThat(rs).containsOnly(WANTED);
// The other two grew with the wait rather than settling at a buffer size.
assertThat(gr[2]).isGreaterThan(gr[0] * 2);
assertThat(ws[2]).isGreaterThan(ws[0] * 2);
}
private void row(String label, long[] v) {
System.out.printf("%-14s %,10d %,10d %,10d%n", label, v[0], v[1], v[2]);
}
/**
* RSocket: {@code request(n)} is a frame on the wire. The server is told the number, produces
* that many, and stops. Nothing blocks and nothing buffers — the producer is simply not
* called again.
*/
private long rsocket(long quietMs) throws Exception {
RSocketRequester requester = RSocketRequester.builder()
.rsocketStrategies(strategies).tcp("localhost", 7003);
try {
AtomicLong received = new AtomicLong();
BaseSubscriber subscriber = new BaseSubscriber<>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
subscription.request(WANTED); // and never again
}
@Override
protected void hookOnNext(Quote value) {
received.incrementAndGet();
}
};
requester.route("quotes.unbounded").data("AAPL")
.retrieveFlux(Quote.class).subscribe(subscriber);
Thread.sleep(quietMs);
long produced = requester.route("produced").retrieveMono(Long.class).block();
subscriber.dispose();
assertThat(received.get()).isEqualTo(WANTED);
return produced;
}
finally {
requester.dispose();
}
}
/**
* gRPC: there is no application-level demand signal. The blocking stub's {@code Iterator}
* requests one message per {@code next()}, and the handler writes with
* {@code StreamObserver.onNext}, which never blocks.
*/
private long grpc(long quietMs) throws Exception {
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 9093)
.usePlaintext().build();
try {
var stub = QuoteServiceGrpc.newBlockingStub(channel);
QuoteRequest req = QuoteRequest.newBuilder().setSymbol("AAPL").build();
Iterator