Server adapters release buffers on error/cancel

Review and update Servlet and Undertow adapters to release any data
buffers they be holding on to at the time of error or cancellation.

Also remove onDiscard hooks from Reactor and Undertow request body.
For Reactor we expect it to be handled. For Undertow there isn't
any Reactor Core upstream for the callback to be useful.

Issue: SPR-17410
This commit is contained in:
Rossen Stoyanchev
2018-10-19 21:45:14 -04:00
parent 149d416e8e
commit 862dd23975
10 changed files with 389 additions and 42 deletions

View File

@@ -16,58 +16,95 @@
package org.springframework.http.server.reactive;
import java.io.IOException;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.core.io.buffer.DataBuffer;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.isA;
import static org.mockito.Mockito.mock;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for {@link AbstractListenerReadPublisher}
* Unit tests for {@link AbstractListenerReadPublisher}.
*
* @author Violeta Georgieva
* @since 5.0
* @author Rossen Stoyanchev
*/
public class ListenerReadPublisherTests {
@Test
@SuppressWarnings("unchecked")
public void testReceiveTwoRequestCallsWhenOnSubscribe() {
Subscriber<DataBuffer> subscriber = mock(Subscriber.class);
doAnswer(new SubscriptionAnswer()).when(subscriber).onSubscribe(isA(Subscription.class));
private final TestListenerReadPublisher publisher = new TestListenerReadPublisher();
TestListenerReadPublisher publisher = new TestListenerReadPublisher();
publisher.subscribe(subscriber);
publisher.onDataAvailable();
private final TestSubscriber subscriber = new TestSubscriber();
assertTrue(publisher.getReadCalls() == 2);
@Before
public void setup() {
this.publisher.subscribe(this.subscriber);
}
private static final class TestListenerReadPublisher extends AbstractListenerReadPublisher {
@Test
public void twoReads() {
this.subscriber.getSubscription().request(2);
this.publisher.onDataAvailable();
assertEquals(2, this.publisher.getReadCalls());
}
@Test // SPR-17410
public void discardDataOnError() {
this.subscriber.getSubscription().request(2);
this.publisher.onDataAvailable();
this.publisher.onError(new IllegalStateException());
assertEquals(2, this.publisher.getReadCalls());
assertEquals(1, this.publisher.getDiscardCalls());
}
@Test // SPR-17410
public void discardDataOnCancel() {
this.subscriber.getSubscription().request(2);
this.subscriber.setCancelOnNext(true);
this.publisher.onDataAvailable();
assertEquals(1, this.publisher.getReadCalls());
assertEquals(1, this.publisher.getDiscardCalls());
}
private static final class TestListenerReadPublisher extends AbstractListenerReadPublisher<DataBuffer> {
private int readCalls = 0;
private int discardCalls = 0;
public TestListenerReadPublisher() {
super("");
}
public int getReadCalls() {
return this.readCalls;
}
public int getDiscardCalls() {
return this.discardCalls;
}
@Override
protected void checkOnDataAvailable() {
// no-op
}
@Override
protected DataBuffer read() throws IOException {
readCalls++;
protected DataBuffer read() {
this.readCalls++;
return mock(DataBuffer.class);
}
@@ -76,22 +113,48 @@ public class ListenerReadPublisherTests {
// No-op
}
public int getReadCalls() {
return this.readCalls;
@Override
protected void discardData() {
this.discardCalls++;
}
}
private static final class SubscriptionAnswer implements Answer<Subscription> {
@Override
public Subscription answer(InvocationOnMock invocation) throws Throwable {
Subscription arg = (Subscription) invocation.getArguments()[0];
arg.request(1);
arg.request(1);
return arg;
private static final class TestSubscriber implements Subscriber<DataBuffer> {
private Subscription subscription;
private boolean cancelOnNext;
public Subscription getSubscription() {
return this.subscription;
}
public void setCancelOnNext(boolean cancelOnNext) {
this.cancelOnNext = cancelOnNext;
}
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
}
@Override
public void onNext(DataBuffer dataBuffer) {
if (this.cancelOnNext) {
this.subscription.cancel();
}
}
@Override
public void onError(Throwable t) {
}
@Override
public void onComplete() {
}
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright 2002-2018 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.server.reactive;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.core.io.buffer.DataBuffer;
import static junit.framework.TestCase.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for {@link AbstractListenerWriteProcessor}.
*
* @author Rossen Stoyanchev
*/
public class ListenerWriteProcessorTests {
private final TestListenerWriteProcessor processor = new TestListenerWriteProcessor();
private final TestResultSubscriber resultSubscriber = new TestResultSubscriber();
private final TestSubscription subscription = new TestSubscription();
@Before
public void setup() {
this.processor.subscribe(this.resultSubscriber);
this.processor.onSubscribe(this.subscription);
assertEquals(1, subscription.getDemand());
}
@Test // SPR-17410
public void writePublisherError() {
// Turn off writing so next item will be cached
this.processor.setWritePossible(false);
DataBuffer buffer = mock(DataBuffer.class);
this.processor.onNext(buffer);
// Send error while item cached
this.processor.onError(new IllegalStateException());
assertNotNull("Error should flow to result publisher", this.resultSubscriber.getError());
assertEquals(1, this.processor.getDiscardedBuffers().size());
assertSame(buffer, this.processor.getDiscardedBuffers().get(0));
}
@Test // SPR-17410
public void ioExceptionDuringWrite() {
// Fail on next write
this.processor.setWritePossible(true);
this.processor.setFailOnWrite(true);
// Write
DataBuffer buffer = mock(DataBuffer.class);
this.processor.onNext(buffer);
assertNotNull("Error should flow to result publisher", this.resultSubscriber.getError());
assertEquals(1, this.processor.getDiscardedBuffers().size());
assertSame(buffer, this.processor.getDiscardedBuffers().get(0));
}
@Test // SPR-17410
public void onNextWithoutDemand() {
// Disable writing: next item will be cached..
this.processor.setWritePossible(false);
DataBuffer buffer1 = mock(DataBuffer.class);
this.processor.onNext(buffer1);
// Send more data illegally
DataBuffer buffer2 = mock(DataBuffer.class);
this.processor.onNext(buffer2);
assertNotNull("Error should flow to result publisher", this.resultSubscriber.getError());
assertEquals(2, this.processor.getDiscardedBuffers().size());
assertSame(buffer2, this.processor.getDiscardedBuffers().get(0));
assertSame(buffer1, this.processor.getDiscardedBuffers().get(1));
}
private static final class TestListenerWriteProcessor extends AbstractListenerWriteProcessor<DataBuffer> {
private final List<DataBuffer> discardedBuffers = new ArrayList<>();
private boolean writePossible;
private boolean failOnWrite;
public List<DataBuffer> getDiscardedBuffers() {
return this.discardedBuffers;
}
public void setWritePossible(boolean writePossible) {
this.writePossible = writePossible;
}
public void setFailOnWrite(boolean failOnWrite) {
this.failOnWrite = failOnWrite;
}
@Override
protected boolean isDataEmpty(DataBuffer dataBuffer) {
return false;
}
@Override
protected boolean isWritePossible() {
return this.writePossible;
}
@Override
protected boolean write(DataBuffer dataBuffer) throws IOException {
if (this.failOnWrite) {
throw new IOException("write failed");
}
return true;
}
@Override
protected void writingFailed(Throwable ex) {
cancel();
onError(ex);
}
@Override
protected void discardData(DataBuffer dataBuffer) {
this.discardedBuffers.add(dataBuffer);
}
}
private static final class TestSubscription implements Subscription {
private long demand;
public long getDemand() {
return this.demand;
}
@Override
public void request(long n) {
this.demand = (n == Long.MAX_VALUE ? n : this.demand + n);
}
@Override
public void cancel() {
}
}
private static final class TestResultSubscriber implements Subscriber<Void> {
private Throwable error;
public Throwable getError() {
return this.error;
}
@Override
public void onSubscribe(Subscription subscription) {
}
@Override
public void onNext(Void aVoid) {
}
@Override
public void onError(Throwable ex) {
this.error = ex;
}
@Override
public void onComplete() {
}
}
}