Batch SSE events writes when possible
Prior to this commit, the `SseEventBuilder` would be used to create SSE events and write them to the connection using the `ResponseBodyEmitter`. This would send each data item one by one, effectively writing and flushing to the network for each. Since multiple data lines are prepared by the `SseEventBuilder`, a typical write of an SSE event performs multiple flushes operations. This commit adds a method on `ResponseBodyEmitter` to perform batch writes (given a `Set<DataWithMediaType>`) and only flush once all elements of the set have been written. This also applies in case of early writes, where now all buffered elements are written then flushed altogether. Fixes gh-30912
This commit is contained in:
@@ -365,6 +365,11 @@ public class ReactiveTypeHandlerTests {
|
||||
this.values.add(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Set<ResponseBodyEmitter.DataWithMediaType> items) throws IOException {
|
||||
items.forEach(item -> this.values.add(item.getData()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void complete() {
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIOException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anySet;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
@@ -52,34 +52,33 @@ public class ResponseBodyEmitterTests {
|
||||
|
||||
|
||||
@Test
|
||||
public void sendBeforeHandlerInitialized() throws Exception {
|
||||
void sendBeforeHandlerInitialized() throws Exception {
|
||||
this.emitter.send("foo", MediaType.TEXT_PLAIN);
|
||||
this.emitter.send("bar", MediaType.TEXT_PLAIN);
|
||||
this.emitter.complete();
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
|
||||
this.emitter.initialize(this.handler);
|
||||
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).send("bar", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).send(anySet());
|
||||
verify(this.handler).complete();
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendDuplicateBeforeHandlerInitialized() throws Exception {
|
||||
void sendDuplicateBeforeHandlerInitialized() throws Exception {
|
||||
this.emitter.send("foo", MediaType.TEXT_PLAIN);
|
||||
this.emitter.send("foo", MediaType.TEXT_PLAIN);
|
||||
this.emitter.complete();
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
|
||||
this.emitter.initialize(this.handler);
|
||||
verify(this.handler, times(2)).send("foo", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).send(anySet());
|
||||
verify(this.handler).complete();
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendBeforeHandlerInitializedWithError() throws Exception {
|
||||
void sendBeforeHandlerInitializedWithError() throws Exception {
|
||||
IllegalStateException ex = new IllegalStateException();
|
||||
this.emitter.send("foo", MediaType.TEXT_PLAIN);
|
||||
this.emitter.send("bar", MediaType.TEXT_PLAIN);
|
||||
@@ -87,21 +86,20 @@ public class ResponseBodyEmitterTests {
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
|
||||
this.emitter.initialize(this.handler);
|
||||
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).send("bar", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).send(anySet());
|
||||
verify(this.handler).completeWithError(ex);
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendFailsAfterComplete() throws Exception {
|
||||
void sendFailsAfterComplete() throws Exception {
|
||||
this.emitter.complete();
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
this.emitter.send("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendAfterHandlerInitialized() throws Exception {
|
||||
void sendAfterHandlerInitialized() throws Exception {
|
||||
this.emitter.initialize(this.handler);
|
||||
verify(this.handler).onTimeout(any());
|
||||
verify(this.handler).onError(any());
|
||||
@@ -119,7 +117,7 @@ public class ResponseBodyEmitterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendAfterHandlerInitializedWithError() throws Exception {
|
||||
void sendAfterHandlerInitializedWithError() throws Exception {
|
||||
this.emitter.initialize(this.handler);
|
||||
verify(this.handler).onTimeout(any());
|
||||
verify(this.handler).onError(any());
|
||||
@@ -138,7 +136,7 @@ public class ResponseBodyEmitterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendWithError() throws Exception {
|
||||
void sendWithError() throws Exception {
|
||||
this.emitter.initialize(this.handler);
|
||||
verify(this.handler).onTimeout(any());
|
||||
verify(this.handler).onError(any());
|
||||
@@ -154,7 +152,7 @@ public class ResponseBodyEmitterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onTimeoutBeforeHandlerInitialized() throws Exception {
|
||||
void onTimeoutBeforeHandlerInitialized() throws Exception {
|
||||
Runnable runnable = mock();
|
||||
this.emitter.onTimeout(runnable);
|
||||
this.emitter.initialize(this.handler);
|
||||
@@ -169,7 +167,7 @@ public class ResponseBodyEmitterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onTimeoutAfterHandlerInitialized() throws Exception {
|
||||
void onTimeoutAfterHandlerInitialized() throws Exception {
|
||||
this.emitter.initialize(this.handler);
|
||||
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
@@ -185,7 +183,7 @@ public class ResponseBodyEmitterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCompletionBeforeHandlerInitialized() throws Exception {
|
||||
void onCompletionBeforeHandlerInitialized() throws Exception {
|
||||
Runnable runnable = mock();
|
||||
this.emitter.onCompletion(runnable);
|
||||
this.emitter.initialize(this.handler);
|
||||
@@ -200,7 +198,7 @@ public class ResponseBodyEmitterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCompletionAfterHandlerInitialized() throws Exception {
|
||||
void onCompletionAfterHandlerInitialized() throws Exception {
|
||||
this.emitter.initialize(this.handler);
|
||||
|
||||
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
|
||||
|
||||
@@ -20,12 +20,14 @@ import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event;
|
||||
@@ -60,6 +62,7 @@ public class SseEmitterTests {
|
||||
this.handler.assertObject(0, "data:", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertObject(1, "foo");
|
||||
this.handler.assertObject(2, "\n\n", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertWriteCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -69,12 +72,14 @@ public class SseEmitterTests {
|
||||
this.handler.assertObject(0, "data:", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertObject(1, "foo", MediaType.TEXT_PLAIN);
|
||||
this.handler.assertObject(2, "\n\n", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertWriteCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendEventEmpty() throws Exception {
|
||||
this.emitter.send(event());
|
||||
this.handler.assertSentObjectCount(0);
|
||||
this.handler.assertWriteCount(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -84,6 +89,7 @@ public class SseEmitterTests {
|
||||
this.handler.assertObject(0, "data:", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertObject(1, "foo");
|
||||
this.handler.assertObject(2, "\n\n", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertWriteCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -95,6 +101,7 @@ public class SseEmitterTests {
|
||||
this.handler.assertObject(2, "\ndata:", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertObject(3, "bar");
|
||||
this.handler.assertObject(4, "\n\n", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertWriteCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -104,6 +111,7 @@ public class SseEmitterTests {
|
||||
this.handler.assertObject(0, ":blah\nevent:test\nretry:5000\nid:1\ndata:", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertObject(1, "foo");
|
||||
this.handler.assertObject(2, "\n\n", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertWriteCount(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -115,14 +123,17 @@ public class SseEmitterTests {
|
||||
this.handler.assertObject(2, "\ndata:", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertObject(3, "bar");
|
||||
this.handler.assertObject(4, "\nevent:test\nretry:5000\nid:1\n\n", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertWriteCount(1);
|
||||
}
|
||||
|
||||
|
||||
private static class TestHandler implements ResponseBodyEmitter.Handler {
|
||||
|
||||
private List<Object> objects = new ArrayList<>();
|
||||
private final List<Object> objects = new ArrayList<>();
|
||||
|
||||
private List<MediaType> mediaTypes = new ArrayList<>();
|
||||
private final List<MediaType> mediaTypes = new ArrayList<>();
|
||||
|
||||
private int writeCount;
|
||||
|
||||
|
||||
public void assertSentObjectCount(int size) {
|
||||
@@ -139,10 +150,24 @@ public class SseEmitterTests {
|
||||
assertThat(this.mediaTypes.get(index)).isEqualTo(mediaType);
|
||||
}
|
||||
|
||||
public void assertWriteCount(int writeCount) {
|
||||
assertThat(this.writeCount).isEqualTo(writeCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Object data, MediaType mediaType) throws IOException {
|
||||
public void send(Object data, @Nullable MediaType mediaType) throws IOException {
|
||||
this.objects.add(data);
|
||||
this.mediaTypes.add(mediaType);
|
||||
this.writeCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Set<ResponseBodyEmitter.DataWithMediaType> items) throws IOException {
|
||||
for (ResponseBodyEmitter.DataWithMediaType item : items) {
|
||||
this.objects.add(item.getData());
|
||||
this.mediaTypes.add(item.getMediaType());
|
||||
}
|
||||
this.writeCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user