Change OutputStreamPublisher default chunk size
This commit set the default chunk size to 1024 (from 8192).
This commit is contained in:
		
							parent
							
								
									3d2befc84a
								
							
						
					
					
						commit
						68b5eedde1
					
				| 
						 | 
				
			
			@ -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);
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue