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:
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user