Change OutputStreamPublisher default chunk size

This commit set the default chunk size to 1024 (from 8192).
This commit is contained in:
Arjen Poutsma 2023-07-06 11:55:17 +02:00
parent 3d2befc84a
commit 68b5eedde1
2 changed files with 119 additions and 48 deletions

View File

@ -41,17 +41,23 @@ import org.springframework.util.Assert;
*/
final class OutputStreamPublisher<T> implements Flow.Publisher<T> {
private static final int DEFAULT_CHUNK_SIZE = 1024;
private final OutputStreamHandler outputStreamHandler;
private final ByteMapper<T> byteMapper;
private final Executor executor;
private final int chunkSize;
private OutputStreamPublisher(OutputStreamHandler outputStreamHandler, ByteMapper<T> byteMapper, Executor executor) {
private OutputStreamPublisher(OutputStreamHandler outputStreamHandler, ByteMapper<T> byteMapper, Executor executor, int chunkSize) {
this.outputStreamHandler = outputStreamHandler;
this.byteMapper = byteMapper;
this.executor = executor;
this.chunkSize = chunkSize;
}
@ -64,16 +70,18 @@ final class OutputStreamPublisher<T> implements Flow.Publisher<T> {
* subscription of the returned {@code Publisher}, when the first
* item is
* {@linkplain Flow.Subscription#request(long) requested}.</li>
* <li>Each {@link OutputStream#write(byte[], int, int) OutputStream.write()}
* invocation that {@code outputStreamHandler} makes will result in a
* <li>{@link OutputStream#write(byte[], int, int) OutputStream.write()}
* invocations made by {@code outputStreamHandler} are buffered until they
* exceed the default chunk size of 1024, and then result in a
* {@linkplain Flow.Subscriber#onNext(Object) published} item
* if there is {@linkplain Flow.Subscription#request(long) demand}.</li>
* <li>If there is <em>no demand</em>, {@code OutputStream.write()} will block
* until there is.</li>
* <li>If the subscription is {@linkplain Flow.Subscription#cancel() cancelled},
* {@code OutputStream.write()} will throw a {@code IOException}.</li>
* <li>{@linkplain OutputStream#close() Closing} the {@code OutputStream}
* will result in a {@linkplain Flow.Subscriber#onComplete() complete} signal.</li>
* <li>The subscription is
* {@linkplain Flow.Subscriber#onComplete() completed} when
* {@code outputStreamHandler} completes.</li>
* <li>Any {@code IOException}s thrown from {@code outputStreamHandler} will
* be dispatched to the {@linkplain Flow.Subscriber#onError(Throwable) Subscriber}.
* </ul>
@ -91,15 +99,58 @@ final class OutputStreamPublisher<T> implements Flow.Publisher<T> {
Assert.notNull(byteMapper, "ByteMapper must not be null");
Assert.notNull(executor, "Executor must not be null");
return new OutputStreamPublisher<>(outputStreamHandler, byteMapper, executor);
return new OutputStreamPublisher<>(outputStreamHandler, byteMapper, executor, DEFAULT_CHUNK_SIZE);
}
/**
* Creates a new {@code Publisher<T>} based on bytes written to a
* {@code OutputStream}. The parameter {@code byteMapper} is used to map
* from written bytes to the published type.
* <ul>
* <li>The parameter {@code outputStreamHandler} is invoked once per
* subscription of the returned {@code Publisher}, when the first
* item is
* {@linkplain Flow.Subscription#request(long) requested}.</li>
* <li>{@link OutputStream#write(byte[], int, int) OutputStream.write()}
* invocations made by {@code outputStreamHandler} are buffered until they
* exceed {@code chunkSize}, and then result in a
* {@linkplain Flow.Subscriber#onNext(Object) published} item
* if there is {@linkplain Flow.Subscription#request(long) demand}.</li>
* <li>If there is <em>no demand</em>, {@code OutputStream.write()} will block
* until there is.</li>
* <li>If the subscription is {@linkplain Flow.Subscription#cancel() cancelled},
* {@code OutputStream.write()} will throw a {@code IOException}.</li>
* <li>The subscription is
* {@linkplain Flow.Subscriber#onComplete() completed} when
* {@code outputStreamHandler} completes.</li>
* <li>Any {@code IOException}s thrown from {@code outputStreamHandler} will
* be dispatched to the {@linkplain Flow.Subscriber#onError(Throwable) Subscriber}.
* </ul>
* @param outputStreamHandler invoked when the first buffer is requested
* @param byteMapper maps written bytes to {@code T}
* @param executor used to invoke the {@code outputStreamHandler}
* @param <T> the publisher type
* @return a {@code Publisher<T>} based on bytes written by
* {@code outputStreamHandler} mapped by {@code byteMapper}
*/
public static <T> Flow.Publisher<T> create(OutputStreamHandler outputStreamHandler, ByteMapper<T> byteMapper,
Executor executor, int chunkSize) {
Assert.notNull(outputStreamHandler, "OutputStreamHandler must not be null");
Assert.notNull(byteMapper, "ByteMapper must not be null");
Assert.notNull(executor, "Executor must not be null");
Assert.isTrue(chunkSize > 0, "ChunkSize must be larger than 0");
return new OutputStreamPublisher<>(outputStreamHandler, byteMapper, executor, chunkSize);
}
@Override
public void subscribe(Flow.Subscriber<? super T> subscriber) {
Objects.requireNonNull(subscriber, "Subscriber must not be null");
OutputStreamSubscription<T> subscription = new OutputStreamSubscription<>(subscriber, this.outputStreamHandler,
this.byteMapper);
this.byteMapper, this.chunkSize);
subscriber.onSubscribe(subscription);
this.executor.execute(subscription::invokeHandler);
}
@ -162,16 +213,18 @@ final class OutputStreamPublisher<T> implements Flow.Publisher<T> {
static final Object READY = new Object();
private final Flow.Subscriber<? super T> actual;
private final Flow.Subscriber<? super T> actual;
private final OutputStreamHandler outputStreamHandler;
private final ByteMapper<T> byteMapper;
private final int chunkSize;
private final AtomicLong requested = new AtomicLong();
private final AtomicReference<Object> parkedThreadAtomic = new AtomicReference<>();
private final AtomicReference<Object> parkedThread = new AtomicReference<>();
@Nullable
private volatile Throwable error;
@ -180,10 +233,11 @@ final class OutputStreamPublisher<T> implements Flow.Publisher<T> {
public OutputStreamSubscription(Flow.Subscriber<? super T> actual, OutputStreamHandler outputStreamHandler,
ByteMapper<T> byteMapper) {
ByteMapper<T> byteMapper, int chunkSize) {
this.actual = actual;
this.byteMapper = byteMapper;
this.outputStreamHandler = outputStreamHandler;
this.chunkSize = chunkSize;
}
@Override
@ -248,7 +302,7 @@ final class OutputStreamPublisher<T> implements Flow.Publisher<T> {
// use BufferedOutputStream, so that written bytes are buffered
// before publishing as byte buffer
try (OutputStream outputStream = new BufferedOutputStream(this)) {
try (OutputStream outputStream = new BufferedOutputStream(this, this.chunkSize)) {
this.outputStreamHandler.handle(outputStream);
}
catch (IOException ex) {
@ -323,7 +377,7 @@ final class OutputStreamPublisher<T> implements Flow.Publisher<T> {
Thread toUnpark = Thread.currentThread();
while (true) {
Object current = this.parkedThreadAtomic.get();
Object current = this.parkedThread.get();
if (current == READY) {
break;
}
@ -332,19 +386,19 @@ final class OutputStreamPublisher<T> implements Flow.Publisher<T> {
throw new IllegalStateException("Only one (Virtual)Thread can await!");
}
if (this.parkedThreadAtomic.compareAndSet(null, toUnpark)) {
if (this.parkedThread.compareAndSet(null, toUnpark)) {
LockSupport.park();
// we don't just break here because park() can wake up spuriously
// if we got a proper resume, get() == READY and the loop will quit above
}
}
// clear the resume indicator so that the next await call will park without a resume()
this.parkedThreadAtomic.lazySet(null);
this.parkedThread.lazySet(null);
}
private void resume() {
if (this.parkedThreadAtomic.get() != READY) {
Object old = this.parkedThreadAtomic.getAndSet(READY);
if (this.parkedThread.get() != READY) {
Object old = this.parkedThread.getAndSet(READY);
if (old != READY) {
LockSupport.unpark((Thread)old);
}

View File

@ -17,7 +17,6 @@
package org.springframework.http.client;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@ -38,6 +37,13 @@ import static org.assertj.core.api.Assertions.assertThatIOException;
*/
class OutputStreamPublisherTests {
private static final byte[] FOO = "foo".getBytes(StandardCharsets.UTF_8);
private static final byte[] BAR = "bar".getBytes(StandardCharsets.UTF_8);
private static final byte[] BAZ = "baz".getBytes(StandardCharsets.UTF_8);
private final Executor executor = Executors.newSingleThreadExecutor();
private final OutputStreamPublisher.ByteMapper<byte[]> byteMapper =
@ -59,11 +65,9 @@ class OutputStreamPublisherTests {
@Test
void basic() {
Flow.Publisher<byte[]> flowPublisher = OutputStreamPublisher.create(outputStream -> {
try (Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
writer.write("foo");
writer.write("bar");
writer.write("baz");
}
outputStream.write(FOO);
outputStream.write(BAR);
outputStream.write(BAZ);
}, this.byteMapper, this.executor);
Flux<String> flux = toString(flowPublisher);
@ -75,14 +79,12 @@ class OutputStreamPublisherTests {
@Test
void flush() {
Flow.Publisher<byte[]> flowPublisher = OutputStreamPublisher.create(outputStream -> {
try (Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
writer.write("foo");
writer.flush();
writer.write("bar");
writer.flush();
writer.write("baz");
writer.flush();
}
outputStream.write(FOO);
outputStream.flush();
outputStream.write(BAR);
outputStream.flush();
outputStream.write(BAZ);
outputStream.flush();
}, this.byteMapper, this.executor);
Flux<String> flux = toString(flowPublisher);
@ -93,22 +95,37 @@ class OutputStreamPublisherTests {
.verifyComplete();
}
@Test
void chunkSize() {
Flow.Publisher<byte[]> flowPublisher = OutputStreamPublisher.create(outputStream -> {
outputStream.write(FOO);
outputStream.write(BAR);
outputStream.write(BAZ);
}, this.byteMapper, this.executor, 3);
Flux<String> flux = toString(flowPublisher);
StepVerifier.create(flux)
.assertNext(s -> assertThat(s).isEqualTo("foo"))
.assertNext(s -> assertThat(s).isEqualTo("bar"))
.assertNext(s -> assertThat(s).isEqualTo("baz"))
.verifyComplete();
}
@Test
void cancel() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
Flow.Publisher<byte[]> flowPublisher = OutputStreamPublisher.create(outputStream -> {
try (Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
assertThatIOException()
.isThrownBy(() -> {
writer.write("foo");
writer.flush();
writer.write("bar");
writer.flush();
})
.withMessage("Subscription has been terminated");
latch.countDown();
}
assertThatIOException()
.isThrownBy(() -> {
outputStream.write(FOO);
outputStream.flush();
outputStream.write(BAR);
outputStream.flush();
})
.withMessage("Subscription has been terminated");
latch.countDown();
}, this.byteMapper, this.executor);
Flux<String> flux = toString(flowPublisher);
@ -125,7 +142,7 @@ class OutputStreamPublisherTests {
CountDownLatch latch = new CountDownLatch(1);
Flow.Publisher<byte[]> flowPublisher = OutputStreamPublisher.create(outputStream -> {
Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
writer.write("foo");
writer.close();
assertThatIOException().isThrownBy(() -> writer.write("bar"))
@ -146,12 +163,12 @@ class OutputStreamPublisherTests {
CountDownLatch latch = new CountDownLatch(1);
Flow.Publisher<byte[]> flowPublisher = OutputStreamPublisher.create(outputStream -> {
try(Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
writer.write("foo");
writer.flush();
writer.write("foo");
writer.flush();
}
try (outputStream) {
outputStream.write(FOO);
outputStream.flush();
outputStream.write(BAR);
outputStream.flush();
}
finally {
latch.countDown();
}