OrderedMessageSender throughput improvement

Before this change messages were sent serially across sessions but
ordering is important only within a session. This leads to head of
line blocking when a session is slow to send, and also enforcement of
send buffer size and time limits is precluded because it happens at
a lower level in the transport.

This change ensures messages are held up only if there is another
from the same session is being sent. This allows messages from each
session to flow independent of other.

See gh-25581
This commit is contained in:
Rossen Stoyanchev
2020-08-27 10:50:05 +01:00
parent 568b44eb9d
commit f5c287a6e6
2 changed files with 204 additions and 61 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,10 @@
package org.springframework.messaging.simp.broker;
import java.time.Duration;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@@ -26,7 +30,12 @@ import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.ExecutorSubscribableChannel;
@@ -43,6 +52,8 @@ public class OrderedMessageSenderTests {
private static final Log logger = LogFactory.getLog(OrderedMessageSenderTests.class);
private static final Random random = new Random();
private OrderedMessageSender sender;
@@ -74,46 +85,97 @@ public class OrderedMessageSenderTests {
@Test
public void test() throws InterruptedException {
int start = 1;
int end = 1000;
int sessionCount = 25;
int messagesPerSessionCount = 500;
AtomicInteger index = new AtomicInteger(start);
AtomicReference<Object> result = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
TestMessageHandler handler = new TestMessageHandler(sessionCount * messagesPerSessionCount);
this.channel.subscribe(handler);
this.channel.subscribe(message -> {
int expected = index.getAndIncrement();
Integer actual = (Integer) message.getHeaders().getOrDefault("seq", -1);
if (actual != expected) {
result.set("Expected: " + expected + ", but was: " + actual);
Publisher<Flux<Message<String>>> messageFluxes =
Flux.range(1, sessionCount).map(sessionId ->
Flux.range(1, messagesPerSessionCount)
.map(sequence -> createMessage(sessionId, sequence))
.delayElements(Duration.ofMillis(Math.abs(random.nextLong()) % 5)));
Flux.merge(messageFluxes)
.doOnNext(message -> this.sender.send(message))
.blockLast();
handler.await(20, TimeUnit.SECONDS);
assertThat(handler.getDescription()).isEqualTo("Total processed: " + sessionCount * messagesPerSessionCount);
assertThat(handler.getSequenceBySession()).hasSize(sessionCount);
handler.getSequenceBySession().forEach((key, value) ->
assertThat(value.get()).as(key).isEqualTo(messagesPerSessionCount));
}
private static Message<String> createMessage(Integer sessionId, Integer sequence) {
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
accessor.setSessionId("session" + sessionId);
accessor.setHeader("seq", sequence);
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage("payload", accessor.getMessageHeaders());
}
private static class TestMessageHandler implements MessageHandler {
private final int totalExpected;
private final Map<String, AtomicInteger> sequenceBySession = new ConcurrentHashMap<>();
private final AtomicReference<String> description = new AtomicReference<>();
private final AtomicInteger totalReceived = new AtomicInteger();
private final CountDownLatch latch = new CountDownLatch(1);
TestMessageHandler(int totalExpected) {
this.totalExpected = totalExpected;
}
public void await(long timeout, TimeUnit timeUnit) throws InterruptedException {
latch.await(timeout, timeUnit);
}
public Map<String, AtomicInteger> getSequenceBySession() {
return sequenceBySession;
}
public String getDescription() {
return description.get();
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
String id = SimpMessageHeaderAccessor.getSessionId(message.getHeaders());
Integer seq = (Integer) message.getHeaders().getOrDefault("seq", -1);
AtomicInteger prev = sequenceBySession.computeIfAbsent(id, i -> new AtomicInteger(0));
if (!prev.compareAndSet(seq - 1, seq)) {
description.set("Out of order, session=" + id + ", prev=" + prev + ", next=" + seq);
latch.countDown();
return;
}
if (actual == 100 || actual == 200) {
if (seq == 100) {
try {
Thread.sleep(200);
// Processing delay to cause other session messages to queue up
Thread.sleep(50);
}
catch (InterruptedException ex) {
result.set(ex.toString());
description.set(ex.toString());
latch.countDown();
return;
}
}
if (actual == end) {
result.set("Done");
int total = totalReceived.incrementAndGet();
description.set("Total processed: " + total);
if (total == totalExpected) {
latch.countDown();
}
});
for (int i = start; i <= end; i++) {
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
accessor.setHeader("seq", i);
accessor.setLeaveMutable(true);
this.sender.send(MessageBuilder.createMessage("payload", accessor.getMessageHeaders()));
}
latch.await(10, TimeUnit.SECONDS);
assertThat(result.get()).isEqualTo("Done");
}
}