/*
* Copyright (c) 2015 AsyncHttpClient Project. All rights reserved.
*
* This program is licensed to you under the Apache License Version 2.0,
* and you may not use this file except in compliance with the Apache License Version 2.0.
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the Apache License Version 2.0 is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/
package org.asynchttpclient.netty.handler;
import static org.asynchttpclient.Dsl.asyncHttpClient;
import static org.asynchttpclient.test.TestUtils.LARGE_IMAGE_BYTES;
import static org.testng.Assert.assertTrue;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.HttpResponseBodyPart;
import org.asynchttpclient.handler.AsyncHandlerExtensions;
import org.asynchttpclient.netty.handler.StreamedResponsePublisher;
import org.asynchttpclient.netty.request.NettyRequest;
import org.asynchttpclient.reactivestreams.ReactiveStreamsTest;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.Test;
public class NettyReactiveStreamsTest extends ReactiveStreamsTest {
@Test(groups = "standalone")
public void testRetryingOnFailingStream() throws Exception {
try (AsyncHttpClient client = asyncHttpClient()) {
final CountDownLatch streamStarted = new CountDownLatch(1); // allows us to wait until subscriber has received the first body chunk
final CountDownLatch streamOnHold = new CountDownLatch(1); // allows us to hold the subscriber from processing further body chunks
final CountDownLatch replayingRequest = new CountDownLatch(1); // allows us to block until the request is being replayed ( this is what we want to test here!)
// a ref to the publisher is needed to get a hold on the channel (if there is a better way, this should be changed)
final AtomicReference<StreamedResponsePublisher> publisherRef = new AtomicReference<>(null);
// executing the request
client.preparePost(getTargetUrl())
.setBody(LARGE_IMAGE_BYTES)
.execute(new ReplayedSimpleAsyncHandler(replayingRequest,
new BlockedStreamSubscriber(streamStarted, streamOnHold)) {
@Override
public State onStream(Publisher<HttpResponseBodyPart> publisher) {
if(!(publisher instanceof StreamedResponsePublisher)) {
throw new IllegalStateException(String.format("publisher %s is expected to be an instance of %s", publisher, StreamedResponsePublisher.class));
}
else if(!publisherRef.compareAndSet(null, (StreamedResponsePublisher) publisher)) {
// abort on retry
return State.ABORT;
}
return super.onStream(publisher);
}
});
// before proceeding, wait for the subscriber to receive at least one body chunk
streamStarted.await();
// The stream has started, hence `StreamedAsyncHandler.onStream(publisher)` was called, and `publisherRef` was initialized with the `publisher` passed to `onStream`
assertTrue(publisherRef.get() != null, "Expected a not null publisher.");
// close the channel to emulate a connection crash while the response body chunks were being received.
StreamedResponsePublisher publisher = publisherRef.get();
final CountDownLatch channelClosed = new CountDownLatch(1);
getChannel(publisher).close().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
channelClosed.countDown();
}
});
streamOnHold.countDown(); // the subscriber is set free to process new incoming body chunks.
channelClosed.await(); // the channel is confirmed to be closed
// now we expect a new connection to be created and AHC retry logic to kick-in automatically
replayingRequest.await(); // wait until we are notified the request is being replayed
// Change this if there is a better way of stating the test succeeded
assertTrue(true);
}
}
private Channel getChannel(StreamedResponsePublisher publisher) throws Exception {
Field field = publisher.getClass().getDeclaredField("channel");
field.setAccessible(true);
return (Channel) field.get(publisher);
}
private static class BlockedStreamSubscriber extends SimpleSubscriber<HttpResponseBodyPart> {
private static final Logger LOGGER = LoggerFactory.getLogger(BlockedStreamSubscriber.class);
private final CountDownLatch streamStarted;
private final CountDownLatch streamOnHold;
public BlockedStreamSubscriber(CountDownLatch streamStarted, CountDownLatch streamOnHold) {
this.streamStarted = streamStarted;
this.streamOnHold = streamOnHold;
}
@Override
public void onNext(HttpResponseBodyPart t) {
streamStarted.countDown();
try {
streamOnHold.await();
} catch (InterruptedException e) {
LOGGER.error("`streamOnHold` latch was interrupted", e);
}
super.onNext(t);
}
}
private static class ReplayedSimpleAsyncHandler extends SimpleStreamedAsyncHandler implements AsyncHandlerExtensions {
private final CountDownLatch replaying;
public ReplayedSimpleAsyncHandler(CountDownLatch replaying, SimpleSubscriber<HttpResponseBodyPart> subscriber) {
super(subscriber);
this.replaying = replaying;
}
@Override
public void onHostnameResolutionAttempt(String name) {}
@Override
public void onHostnameResolutionSuccess(String name, List<InetSocketAddress> addresses) {}
@Override
public void onHostnameResolutionFailure(String name, Throwable cause) {}
@Override
public void onTcpConnectAttempt(InetSocketAddress address) {}
@Override
public void onTcpConnectSuccess(InetSocketAddress address, Channel connection) {}
@Override
public void onTcpConnectFailure(InetSocketAddress address, Throwable cause) {}
@Override
public void onTlsHandshakeAttempt() {}
@Override
public void onTlsHandshakeSuccess() {}
@Override
public void onTlsHandshakeFailure(Throwable cause) {}
@Override
public void onConnectionPoolAttempt() {}
@Override
public void onConnectionPooled(Channel connection) {}
@Override
public void onConnectionOffer(Channel connection) {}
@Override
public void onRequestSend(NettyRequest request) {}
@Override
public void onRetry() { replaying.countDown(); }
}
}