use of org.jboss.netty.channel.ChannelStateEvent in project databus by linkedin.
the class TestNettyHttpDatabusBootstrapConnection method testServerBootstrapDisconnect.
@Test
public /**
* This is a unit test for DDSDBUS-3537. There is a lag between a network channel disconnect and the
* state change in AbstractNettyHttpConnection. This can cause a race condition in various requestXXX objects which
* check the state of the connection using the network channel. As a result, they may attempt to reconnect while
* AbstractNettyHttpConnection is still in CONNECTED state which causes an error for an incorrect transition
* CONNECTED -> CONNECTING.
*
* The test simulates the above condition by injecting a handler in the client pipeline which artificially holds up
* the channelClosed message. As a result we can inject a request while the netty channel is disconnected but the
* AbstractNettyHttpConnection object has not detected this yet.
*/
void testServerBootstrapDisconnect() throws Exception {
final Logger log = Logger.getLogger("TestNettyHttpDatabusBootstrapConnection.testServerBootstrapDisconnect");
log.info("starting");
log.info("setup the client");
TestingConnectionCallback callback = TestingConnectionCallback.createAndStart("testServerSourcesDisconnect");
DummyRemoteExceptionHandler remoteExceptionHandler = new DummyRemoteExceptionHandler();
final NettyHttpDatabusBootstrapConnection conn = (NettyHttpDatabusBootstrapConnection) CONN_FACTORY.createConnection(BOOTSTRAP_SERVER_INFO, callback, remoteExceptionHandler);
try {
log.info("initial setup");
final List<String> sourceNamesList = Arrays.asList(SOURCE1_NAME);
final Checkpoint cp = Checkpoint.createOnlineConsumptionCheckpoint(0);
BootstrapCheckpointHandler cpHandler = new BootstrapCheckpointHandler(sourceNamesList);
cpHandler.createInitialBootstrapCheckpoint(cp, 0L);
final DummyDatabusBootstrapConnectionStateMessage bstCallback = new DummyDatabusBootstrapConnectionStateMessage(log);
log.info("process a normal startSCN which should establish the connection");
sendStartScnHappyPath(conn, cp, bstCallback, SOURCE1_NAME, 100L, log);
Assert.assertTrue(conn.isConnected());
// wait for the response
TestUtil.assertWithBackoff(new ConditionCheck() {
@Override
public boolean check() {
return null != bstCallback.getCheckpoint();
}
}, "wait for /startSCN response", 100, log);
log.info("verify /startSCN response");
final Checkpoint startScnCp = bstCallback.getCheckpoint();
Assert.assertNotNull(startScnCp);
Assert.assertEquals(100L, startScnCp.getBootstrapStartScn().longValue());
log.info("instrument the client pipeline so that we can intercept and delay the channelClosed message");
final Semaphore passMessage = new Semaphore(1);
final CountDownLatch closeSent = new CountDownLatch(1);
passMessage.acquire();
conn._channel.getPipeline().addBefore("handler", "closeChannelDelay", new SimpleChannelHandler() {
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
closeSent.countDown();
passMessage.acquire();
try {
super.channelClosed(ctx, e);
} finally {
passMessage.release();
}
}
});
final Channel serverChannel = getServerChannelForClientConn(conn);
Thread asyncChannelClose = new Thread(new Runnable() {
@Override
public void run() {
log.info("closing server channel");
serverChannel.close();
log.info("server channel: closed");
closeSent.countDown();
}
}, "asyncChannelCloseThread");
asyncChannelClose.setDaemon(true);
Thread asyncBootstrapReq = new Thread(new Runnable() {
@Override
public void run() {
conn.requestStream("1", null, 10000, startScnCp, bstCallback);
}
}, "asyncBootstrapReqThread");
asyncBootstrapReq.setDaemon(true);
log.info("simultaneously closing connection and sending /bootstrap request");
bstCallback.reset();
asyncChannelClose.start();
Assert.assertTrue(closeSent.await(1000, TimeUnit.MILLISECONDS));
TestUtil.assertWithBackoff(new ConditionCheck() {
@Override
public boolean check() {
return !conn._channel.isConnected();
}
}, "waiting for disconnect on the client side", 1000, log);
Assert.assertEquals(AbstractNettyHttpConnection.State.CONNECTED, conn.getNetworkState());
log.info("asynchronously sending /bootstrap");
asyncBootstrapReq.start();
log.info("letting channelClose get through");
TestUtil.assertWithBackoff(new ConditionCheck() {
@Override
public boolean check() {
return bstCallback.isStreamRequestError();
}
}, "wait for streamRequestError callback", 1000, log);
passMessage.release();
log.info("finished");
} finally {
conn.close();
callback.shutdown();
log.info("cleaned");
}
}
use of org.jboss.netty.channel.ChannelStateEvent in project druid by druid-io.
the class NettyHttpClient method go.
@Override
public <Intermediate, Final> ListenableFuture<Final> go(final Request request, final HttpResponseHandler<Intermediate, Final> handler, final Duration requestReadTimeout) {
final HttpMethod method = request.getMethod();
final URL url = request.getUrl();
final Multimap<String, String> headers = request.getHeaders();
final String requestDesc = method + " " + url;
if (log.isDebugEnabled()) {
log.debug("[%s] starting", requestDesc);
}
// Block while acquiring a channel from the pool, then complete the request asynchronously.
final Channel channel;
final String hostKey = getPoolKey(url);
final ResourceContainer<ChannelFuture> channelResourceContainer = pool.take(hostKey);
final ChannelFuture channelFuture = channelResourceContainer.get().awaitUninterruptibly();
if (!channelFuture.isSuccess()) {
// Some other poor sap will have to deal with it...
channelResourceContainer.returnResource();
return Futures.immediateFailedFuture(new ChannelException("Faulty channel in resource pool", channelFuture.getCause()));
} else {
channel = channelFuture.getChannel();
// In case we get a channel that never had its readability turned back on.
channel.setReadable(true);
}
final String urlFile = StringUtils.nullToEmptyNonDruidDataString(url.getFile());
final HttpRequest httpRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, method, urlFile.isEmpty() ? "/" : urlFile);
if (!headers.containsKey(HttpHeaders.Names.HOST)) {
httpRequest.headers().add(HttpHeaders.Names.HOST, getHost(url));
}
// If Accept-Encoding is set in the Request, use that. Otherwise use the default from "compressionCodec".
if (!headers.containsKey(HttpHeaders.Names.ACCEPT_ENCODING)) {
httpRequest.headers().set(HttpHeaders.Names.ACCEPT_ENCODING, compressionCodec.getEncodingString());
}
for (Map.Entry<String, Collection<String>> entry : headers.asMap().entrySet()) {
String key = entry.getKey();
for (String obj : entry.getValue()) {
httpRequest.headers().add(key, obj);
}
}
if (request.hasContent()) {
httpRequest.setContent(request.getContent());
}
final long readTimeout = getReadTimeout(requestReadTimeout);
final SettableFuture<Final> retVal = SettableFuture.create();
if (readTimeout > 0) {
channel.getPipeline().addLast(READ_TIMEOUT_HANDLER_NAME, new ReadTimeoutHandler(timer, readTimeout, TimeUnit.MILLISECONDS));
}
channel.getPipeline().addLast(LAST_HANDLER_NAME, new SimpleChannelUpstreamHandler() {
private volatile ClientResponse<Intermediate> response = null;
// Chunk number most recently assigned.
private long currentChunkNum = 0;
// Suspend and resume watermarks (respectively: last chunk number that triggered a suspend, and that was
// provided to the TrafficCop's resume method). Synchronized access since they are not always accessed
// from an I/O thread. (TrafficCops can be called from any thread.)
private final Object watermarkLock = new Object();
private long suspendWatermark = -1;
private long resumeWatermark = -1;
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
if (log.isDebugEnabled()) {
log.debug("[%s] messageReceived: %s", requestDesc, e.getMessage());
}
try {
Object msg = e.getMessage();
if (msg instanceof HttpResponse) {
HttpResponse httpResponse = (HttpResponse) msg;
if (log.isDebugEnabled()) {
log.debug("[%s] Got response: %s", requestDesc, httpResponse.getStatus());
}
HttpResponseHandler.TrafficCop trafficCop = resumeChunkNum -> {
synchronized (watermarkLock) {
resumeWatermark = Math.max(resumeWatermark, resumeChunkNum);
if (suspendWatermark >= 0 && resumeWatermark >= suspendWatermark) {
suspendWatermark = -1;
channel.setReadable(true);
long backPressureDuration = System.nanoTime() - backPressureStartTimeNs;
log.debug("[%s] Resumed reads from channel (chunkNum = %,d).", requestDesc, resumeChunkNum);
return backPressureDuration;
}
}
// If we didn't resume, don't know if backpressure was happening
return 0;
};
response = handler.handleResponse(httpResponse, trafficCop);
if (response.isFinished()) {
retVal.set((Final) response.getObj());
}
assert currentChunkNum == 0;
possiblySuspendReads(response);
if (!httpResponse.isChunked()) {
finishRequest();
}
} else if (msg instanceof HttpChunk) {
HttpChunk httpChunk = (HttpChunk) msg;
if (log.isDebugEnabled()) {
log.debug("[%s] Got chunk: %sB, last=%s", requestDesc, httpChunk.getContent().readableBytes(), httpChunk.isLast());
}
if (httpChunk.isLast()) {
finishRequest();
} else {
response = handler.handleChunk(response, httpChunk, ++currentChunkNum);
if (response.isFinished() && !retVal.isDone()) {
retVal.set((Final) response.getObj());
}
possiblySuspendReads(response);
}
} else {
throw new ISE("Unknown message type[%s]", msg.getClass());
}
} catch (Exception ex) {
log.warn(ex, "[%s] Exception thrown while processing message, closing channel.", requestDesc);
if (!retVal.isDone()) {
retVal.set(null);
}
channel.close();
channelResourceContainer.returnResource();
throw ex;
}
}
private void possiblySuspendReads(ClientResponse<?> response) {
if (!response.isContinueReading()) {
synchronized (watermarkLock) {
suspendWatermark = Math.max(suspendWatermark, currentChunkNum);
if (suspendWatermark > resumeWatermark) {
channel.setReadable(false);
backPressureStartTimeNs = System.nanoTime();
log.debug("[%s] Suspended reads from channel (chunkNum = %,d).", requestDesc, currentChunkNum);
}
}
}
}
private void finishRequest() {
ClientResponse<Final> finalResponse = handler.done(response);
if (!finalResponse.isFinished() || !finalResponse.isContinueReading()) {
throw new ISE("[%s] Didn't get a completed ClientResponse Object from [%s] (finished = %s, continueReading = %s)", requestDesc, handler.getClass(), finalResponse.isFinished(), finalResponse.isContinueReading());
}
if (!retVal.isDone()) {
retVal.set(finalResponse.getObj());
}
removeHandlers();
channel.setReadable(true);
channelResourceContainer.returnResource();
}
@Override
public void exceptionCaught(ChannelHandlerContext context, ExceptionEvent event) {
if (log.isDebugEnabled()) {
final Throwable cause = event.getCause();
if (cause == null) {
log.debug("[%s] Caught exception", requestDesc);
} else {
log.debug(cause, "[%s] Caught exception", requestDesc);
}
}
retVal.setException(event.getCause());
// response is non-null if we received initial chunk and then exception occurs
if (response != null) {
handler.exceptionCaught(response, event.getCause());
}
try {
if (channel.isOpen()) {
channel.close();
}
} catch (Exception e) {
log.warn(e, "Error while closing channel");
} finally {
channelResourceContainer.returnResource();
}
}
@Override
public void channelDisconnected(ChannelHandlerContext context, ChannelStateEvent event) {
if (log.isDebugEnabled()) {
log.debug("[%s] Channel disconnected", requestDesc);
}
// response is non-null if we received initial chunk and then exception occurs
if (response != null) {
handler.exceptionCaught(response, new ChannelException("Channel disconnected"));
}
channel.close();
channelResourceContainer.returnResource();
if (!retVal.isDone()) {
log.warn("[%s] Channel disconnected before response complete", requestDesc);
retVal.setException(new ChannelException("Channel disconnected"));
}
}
private void removeHandlers() {
if (readTimeout > 0) {
channel.getPipeline().remove(READ_TIMEOUT_HANDLER_NAME);
}
channel.getPipeline().remove(LAST_HANDLER_NAME);
}
});
channel.write(httpRequest).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (!future.isSuccess()) {
channel.close();
channelResourceContainer.returnResource();
if (!retVal.isDone()) {
retVal.setException(new ChannelException(StringUtils.format("[%s] Failed to write request to channel", requestDesc), future.getCause()));
}
}
}
});
return retVal;
}
Aggregations