/* * 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 com.addthis.hydra.query.loadbalance; import java.util.concurrent.CancellationException; import java.util.concurrent.TimeoutException; import com.addthis.bundle.channel.DataChannelError; import com.addthis.hydra.query.web.HttpQueryCallHandler; import com.addthis.hydra.query.web.HttpUtils; import com.google.common.base.Throwables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import io.netty.handler.codec.http.HttpResponseStatus; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import io.netty.util.CharsetUtil; import io.netty.util.concurrent.EventExecutor; public class NextQueryTask implements Runnable, ChannelFutureListener { private static final Logger log = LoggerFactory.getLogger(NextQueryTask.class); private final QueryQueue queryQueue; private final EventExecutor executor; public NextQueryTask(QueryQueue queryQueue, EventExecutor executor) { this.queryQueue = queryQueue; this.executor = executor; } @Override public void run() { QueryRequest request; try { request = queryQueue.takeQuery(); } catch (InterruptedException ignored) { log.info("Frame reader thread interrupted -- halting query processing"); return; } try { final ChannelFuture queryFuture = HttpQueryCallHandler.handleQuery( request.querySource, request.kv, request.request, request.ctx, executor); queryFuture.addListener(this); queryFuture.channel().closeFuture().addListener(future -> { if (queryFuture.cancel(false)) { log.warn("cancelling query due to closed output channel"); } }); } catch (Exception e) { log.warn("Exception caught before mesh query master added to pipeline", e); if (request.ctx.channel().isActive()) { HttpUtils.sendError(request.ctx, new HttpResponseStatus(500, e.getMessage())); } } } @Override public void operationComplete(ChannelFuture future) throws Exception { log.trace("complete called"); if (!future.isSuccess()) { safelyHandleQueryFailure(future); } // schedule next query poll executor.execute(this); log.trace("rescheduled"); } private static void safelyHandleQueryFailure(ChannelFuture future) { try { ChannelPipeline pipeline = future.channel().pipeline(); ChannelHandlerContext lastContext = cleanPipelineAndGetLastContext(pipeline); if (lastContext != null) { sendDetailedError(lastContext, future.cause()); } else { logAndFormatErrorDetail(future.cause()); } } catch (Throwable error) { log.warn("unexpected error while trying to report to user; closing channel", error); safelyTryChannelClose(future); } } private static void safelyTryChannelClose(ChannelFuture future) { try { Channel channel = future.channel(); channel.close(); } catch (Throwable error) { log.error("unexpected error while trying to close channel; ignoring to keep frame reader alive", error); } } private static ChannelHandlerContext cleanPipelineAndGetLastContext(ChannelPipeline pipeline) { log.trace("pipeline before pruning {}", pipeline); ChannelHandlerContext lastContext = pipeline.lastContext(); while ((lastContext != null) && !"encoder".equals(lastContext.name())) { pipeline.removeLast(); lastContext = pipeline.lastContext(); } log.trace("pipeline after pruning {}", pipeline); return lastContext; } private static String logAndFormatErrorDetail(Throwable cause) { if ((cause instanceof DataChannelError) && (cause.getCause() != null)) { cause = cause.getCause(); } if (cause == null) { log.warn("query call errored with an empty cause"); return "unknown query error"; } else if (cause instanceof CancellationException) { log.info("query call was cancelled by a user"); return "Query was Cancelled by a User"; } else if (cause instanceof TimeoutException) { log.info("query call was cancelled by the timeout watcher"); return "Query timed out"; } else { log.warn("query call errored due to internal errors or malformed input", cause); return Throwables.getStackTraceAsString(cause); } } private static void sendDetailedError(ChannelHandlerContext ctx, Throwable cause) { if (cause == null) { cause = new RuntimeException("query failed for unknown reasons"); } String reasonPhrase = cause.getMessage(); HttpResponseStatus responseStatus; try { responseStatus = new HttpResponseStatus(500, reasonPhrase); } catch (NullPointerException | IllegalArgumentException ignored) { reasonPhrase = cause.getClass().getSimpleName(); responseStatus = new HttpResponseStatus(500, reasonPhrase); } String detailPhrase = logAndFormatErrorDetail(cause); FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, responseStatus, Unpooled.copiedBuffer(detailPhrase + "\r\n", CharsetUtil.UTF_8)); response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); log.trace("issuing error of {}", responseStatus); // Close the connection as soon as the error message is sent. ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } }