Exemple #1
0
  public FileChunk get() throws IOException {
    if (useLocalFile) {
      startTime = System.currentTimeMillis();
      finishTime = System.currentTimeMillis();
      state = TajoProtos.FetcherState.FETCH_FINISHED;
      return fileChunk;
    }

    this.startTime = System.currentTimeMillis();
    this.state = TajoProtos.FetcherState.FETCH_FETCHING;
    ChannelFuture future = null;
    try {
      future =
          bootstrap
              .clone()
              .connect(new InetSocketAddress(host, port))
              .addListener(ChannelFutureListener.CLOSE_ON_FAILURE);

      // Wait until the connection attempt succeeds or fails.
      Channel channel = future.awaitUninterruptibly().channel();
      if (!future.isSuccess()) {
        state = TajoProtos.FetcherState.FETCH_FAILED;
        throw new IOException(future.cause());
      }

      String query = uri.getPath() + (uri.getRawQuery() != null ? "?" + uri.getRawQuery() : "");
      // Prepare the HTTP request.
      HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, query);
      request.headers().set(HttpHeaders.Names.HOST, host);
      request.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE);
      request.headers().set(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP);

      LOG.info("Status: " + getState() + ", URI:" + uri);
      // Send the HTTP request.
      channel.writeAndFlush(request);

      // Wait for the server to close the connection. throw exception if failed
      channel.closeFuture().syncUninterruptibly();

      fileChunk.setLength(fileChunk.getFile().length());
      return fileChunk;
    } finally {
      if (future != null && future.channel().isOpen()) {
        // Close the channel to exit.
        future.channel().close().awaitUninterruptibly();
      }

      this.finishTime = System.currentTimeMillis();
      LOG.info(
          "Fetcher finished:" + (finishTime - startTime) + " ms, " + getState() + ", URI:" + uri);
    }
  }
Exemple #2
0
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
      if (getState() != TajoProtos.FetcherState.FETCH_FINISHED) {
        // channel is closed, but cannot complete fetcher
        finishTime = System.currentTimeMillis();
        state = TajoProtos.FetcherState.FETCH_FAILED;
      }
      IOUtils.cleanup(LOG, fc, raf);

      super.channelUnregistered(ctx);
    }
Exemple #3
0
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
      if (cause instanceof ReadTimeoutException) {
        LOG.warn(cause.getMessage(), cause);
      } else {
        LOG.error("Fetch failed :", cause);
      }

      // this fetching will be retry
      IOUtils.cleanup(LOG, fc, raf);
      finishTime = System.currentTimeMillis();
      state = TajoProtos.FetcherState.FETCH_FAILED;
      ctx.close();
    }
  public abstract static class DefaultContextImpl implements Context {
    private static final String durationProp =
        System.getProperty(DefaultContextImpl.class.getName() + ".durationMillis");
    private static final long DURATION =
        durationProp != null ? Long.parseLong(durationProp) : 60_000L;
    private final ReentrantLock lock = new ReentrantLock();
    private final long created;
    private final Map<String, Object> attachments = new HashMap<>();

    private boolean valid = true;

    public DefaultContextImpl() {
      created = new Date().getTime();
    }

    @Override
    public void invalidate() {
      attachments.clear();
      valid = false;
    }

    @Override
    public final boolean isValid() {
      final boolean ret = valid && (new Date().getTime() - created) <= DURATION;
      if (!ret) invalidate();
      return ret;
    }

    @Override
    public final Map<String, Object> getAttachments() {
      return attachments;
    }

    @Override
    public final ReentrantLock getLock() {
      return lock;
    }

    @Override
    public boolean watch() {
      return true;
    }
  }
Exemple #5
0
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

      messageReceiveCount++;
      if (msg instanceof HttpResponse) {
        try {
          HttpResponse response = (HttpResponse) msg;

          StringBuilder sb = new StringBuilder();
          if (LOG.isDebugEnabled()) {
            sb.append("STATUS: ")
                .append(response.getStatus())
                .append(", VERSION: ")
                .append(response.getProtocolVersion())
                .append(", HEADER: ");
          }
          if (!response.headers().names().isEmpty()) {
            for (String name : response.headers().names()) {
              for (String value : response.headers().getAll(name)) {
                if (LOG.isDebugEnabled()) {
                  sb.append(name).append(" = ").append(value);
                }
                if (this.length == -1 && name.equals("Content-Length")) {
                  this.length = Long.parseLong(value);
                }
              }
            }
          }
          if (LOG.isDebugEnabled()) {
            LOG.debug(sb.toString());
          }

          if (response.getStatus().code() == HttpResponseStatus.NO_CONTENT.code()) {
            LOG.warn("There are no data corresponding to the request");
            length = 0;
            return;
          } else if (response.getStatus().code() != HttpResponseStatus.OK.code()) {
            LOG.error(response.getStatus().reasonPhrase());
            state = TajoProtos.FetcherState.FETCH_FAILED;
            return;
          }
        } catch (Exception e) {
          LOG.error(e.getMessage(), e);
        } finally {
          ReferenceCountUtil.release(msg);
        }
      }

      if (msg instanceof HttpContent) {
        try {
          HttpContent httpContent = (HttpContent) msg;
          ByteBuf content = httpContent.content();
          if (content.isReadable()) {
            content.readBytes(fc, content.readableBytes());
          }

          if (msg instanceof LastHttpContent) {
            if (raf != null) {
              fileLen = file.length();
            }

            finishTime = System.currentTimeMillis();
            if (state != TajoProtos.FetcherState.FETCH_FAILED) {
              state = TajoProtos.FetcherState.FETCH_FINISHED;
            }

            IOUtils.cleanup(LOG, fc, raf);
          }
        } catch (Exception e) {
          LOG.error(e.getMessage(), e);
        } finally {
          ReferenceCountUtil.release(msg);
        }
      }
    }
  public void channelRead0(final ChannelHandlerContext ctx, final FullHttpRequest nettyRequest)
      throws Exception {
    if (!nettyRequest.getDecoderResult().isSuccess()) {
      sendError(ctx, HttpResponseStatus.BAD_REQUEST);
      return;
    }

    final long startTime = System.nanoTime();

    final Request request =
        new DefaultRequest(
            new NettyHeadersBackedHeaders(nettyRequest.headers()),
            nettyRequest.getMethod().name(),
            nettyRequest.getUri(),
            nettyRequest.content());

    final Channel channel = ctx.channel();

    final DefaultMutableStatus responseStatus = new DefaultMutableStatus();
    final HttpHeaders httpHeaders = new DefaultHttpHeaders(false);
    final MutableHeaders responseHeaders = new NettyHeadersBackedMutableHeaders(httpHeaders);
    FileHttpTransmitter fileHttpTransmitter =
        new DefaultFileHttpTransmitter(
            nettyRequest,
            httpHeaders,
            channel,
            compressResponses,
            addResponseTimeHeader ? startTime : -1);

    final DefaultEventController<RequestOutcome> requestOutcomeEventController =
        new DefaultEventController<>();

    // We own the lifecycle
    nettyRequest.content().retain();

    final Response response =
        new DefaultResponse(
            responseStatus,
            responseHeaders,
            fileHttpTransmitter,
            ctx.alloc(),
            new Action<ByteBuf>() {
              @Override
              public void execute(final ByteBuf byteBuf) throws Exception {
                final HttpResponse nettyResponse =
                    new CustomHttpResponse(responseStatus.getResponseStatus(), httpHeaders);

                nettyRequest.content().release();
                responseHeaders.set(HttpHeaderConstants.CONTENT_LENGTH, byteBuf.writerIndex());
                boolean shouldClose = true;
                if (channel.isOpen()) {
                  if (isKeepAlive(nettyRequest)) {
                    responseHeaders.set(
                        HttpHeaderConstants.CONNECTION, HttpHeaderConstants.KEEP_ALIVE);
                    shouldClose = false;
                  }

                  long stopTime = System.nanoTime();
                  if (addResponseTimeHeader) {
                    responseHeaders.set(
                        "X-Response-Time", NumberUtil.toMillisDiffString(startTime, stopTime));
                  }

                  execController.getExecution().complete();

                  channel.writeAndFlush(nettyResponse);
                  channel.write(new DefaultHttpContent(byteBuf));
                  ChannelFuture future = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);

                  if (requestOutcomeEventController.isHasListeners()) {
                    Headers headers = new DelegatingHeaders(responseHeaders);
                    Status status =
                        new DefaultStatus(responseStatus.getCode(), responseStatus.getMessage());
                    SentResponse sentResponse = new DefaultSentResponse(headers, status);
                    RequestOutcome requestOutcome =
                        new DefaultRequestOutcome(
                            request, sentResponse, System.currentTimeMillis());
                    requestOutcomeEventController.fire(requestOutcome);
                  }
                  if (shouldClose) {
                    future.addListener(ChannelFutureListener.CLOSE);
                  }
                }
              }
            });

    InetSocketAddress socketAddress = (InetSocketAddress) channel.localAddress();
    final BindAddress bindAddress = new InetSocketAddressBackedBindAddress(socketAddress);

    Action<Action<Object>> subscribeHandler =
        new Action<Action<Object>>() {
          @Override
          public void execute(Action<Object> thing) throws Exception {
            channelSubscriptions.put(channel, thing);
            channel
                .closeFuture()
                .addListener(
                    new ChannelFutureListener() {
                      @Override
                      public void operationComplete(ChannelFuture future) throws Exception {
                        channelSubscriptions.remove(channel);
                      }
                    });
          }
        };

    final DirectChannelAccess directChannelAccess =
        new DefaultDirectChannelAccess(channel, subscribeHandler);

    execController.start(
        new Action<Execution>() {
          @Override
          public void execute(Execution execution) throws Exception {
            DefaultContext.RequestConstants requestConstants =
                new DefaultContext.RequestConstants(
                    applicationConstants,
                    bindAddress,
                    request,
                    response,
                    directChannelAccess,
                    requestOutcomeEventController.getRegistry(),
                    execution);

            new DefaultContext(requestConstants, registry, handlers, 0, return404).next();
          }
        });
  }
  public void channelRead0(final ChannelHandlerContext ctx, final FullHttpRequest nettyRequest)
      throws Exception {
    if (!nettyRequest.getDecoderResult().isSuccess()) {
      sendError(ctx, HttpResponseStatus.BAD_REQUEST);
      return;
    }

    final long startTime = addResponseTimeHeader ? System.nanoTime() : 0;
    final Request request =
        new DefaultRequest(
            new NettyHeadersBackedHeaders(nettyRequest.headers()),
            nettyRequest.getMethod().name(),
            nettyRequest.getUri(),
            nettyRequest.content());
    final Channel channel = ctx.channel();
    final DefaultMutableStatus responseStatus = new DefaultMutableStatus();
    final HttpHeaders nettyHeaders = new DefaultHttpHeaders(false);
    final MutableHeaders responseHeaders = new NettyHeadersBackedMutableHeaders(nettyHeaders);
    final MimeTypes mimeTypes = registry.get(MimeTypes.class);
    final DefaultEventController<RequestOutcome> requestOutcomeEventController =
        new DefaultEventController<>();
    final AtomicBoolean transmitted = new AtomicBoolean(false);

    final ResponseTransmitter responseTransmitter =
        new DefaultResponseTransmitter(
            transmitted,
            channel,
            nettyRequest,
            request,
            nettyHeaders,
            responseStatus,
            requestOutcomeEventController,
            startTime);
    final Action<Action<? super ResponseTransmitter>> responseTransmitterWrapper =
        Actions.wrap(responseTransmitter);

    final FileHttpTransmitter fileHttpTransmitter =
        new DefaultFileHttpTransmitter(
            nettyHeaders,
            mimeTypes,
            compressResponses,
            compressionMinSize,
            compressionMimeTypeWhiteList,
            compressionMimeTypeBlackList,
            responseTransmitterWrapper);
    StreamTransmitter streamTransmitter =
        new DefaultStreamTransmitter(nettyRequest, nettyHeaders, channel);

    final Response response =
        new DefaultResponse(
            responseStatus,
            responseHeaders,
            fileHttpTransmitter,
            streamTransmitter,
            ctx.alloc(),
            new Action<ByteBuf>() {
              @Override
              public void execute(final ByteBuf byteBuf) throws Exception {
                responseTransmitterWrapper.execute(
                    new Action<ResponseTransmitter>() {
                      @Override
                      public void execute(ResponseTransmitter responseTransmitter)
                          throws Exception {
                        nettyHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, byteBuf.writerIndex());
                        responseTransmitter.transmit(new DefaultHttpContent(byteBuf));
                      }
                    });
              }
            });

    InetSocketAddress socketAddress = (InetSocketAddress) channel.localAddress();
    final BindAddress bindAddress = new InetSocketAddressBackedBindAddress(socketAddress);

    Action<Action<Object>> subscribeHandler =
        new Action<Action<Object>>() {
          @Override
          public void execute(Action<Object> thing) throws Exception {
            transmitted.set(true);
            channelSubscriptions.put(channel, thing);
            channel
                .closeFuture()
                .addListener(
                    new ChannelFutureListener() {
                      @Override
                      public void operationComplete(ChannelFuture future) throws Exception {
                        channelSubscriptions.remove(channel);
                      }
                    });
          }
        };

    final DirectChannelAccess directChannelAccess =
        new DefaultDirectChannelAccess(channel, subscribeHandler);

    final DefaultContext.RequestConstants requestConstants =
        new DefaultContext.RequestConstants(
            applicationConstants,
            bindAddress,
            request,
            response,
            directChannelAccess,
            requestOutcomeEventController.getRegistry());

    DefaultContext.start(
        execController.getControl(),
        requestConstants,
        registry,
        handlers,
        return404,
        new Action<Execution>() {
          @Override
          public void execute(Execution execution) throws Exception {
            if (!transmitted.get()) {
              Handler lastHandler = requestConstants.handler;
              StringBuilder description = new StringBuilder();
              description
                  .append("No response sent for ")
                  .append(request.getMethod().getName())
                  .append(" request to ")
                  .append(request.getUri())
                  .append(" (last handler: ");

              if (lastHandler instanceof DescribingHandler) {
                ((DescribingHandler) lastHandler).describeTo(description);
              } else {
                DescribingHandlers.describeTo(lastHandler, description);
              }

              description.append(")");
              String message = description.toString();
              LOGGER.warn(message);

              response.status(500);

              if (launchConfig.isDevelopment()) {
                response.send(message);
              } else {
                response.send();
              }
            }
          }
        });
  }
/** @author circlespainter */
public class WebActorHandler extends SimpleChannelInboundHandler<Object> {
  // @FunctionalInterface
  public interface WebActorContextProvider {
    Context get(ChannelHandlerContext ctx, FullHttpRequest req);
  }

  public interface Context {
    boolean isValid();

    void invalidate();

    ActorRef<? extends WebMessage> getRef();

    ReentrantLock getLock();

    Map<String, Object> getAttachments();

    boolean handlesWithHttp(String uri);

    boolean handlesWithWebSocket(String uri);

    boolean watch();
  }

  public abstract static class DefaultContextImpl implements Context {
    private static final String durationProp =
        System.getProperty(DefaultContextImpl.class.getName() + ".durationMillis");
    private static final long DURATION =
        durationProp != null ? Long.parseLong(durationProp) : 60_000L;
    private final ReentrantLock lock = new ReentrantLock();
    private final long created;
    private final Map<String, Object> attachments = new HashMap<>();

    private boolean valid = true;

    public DefaultContextImpl() {
      created = new Date().getTime();
    }

    @Override
    public void invalidate() {
      attachments.clear();
      valid = false;
    }

    @Override
    public final boolean isValid() {
      final boolean ret = valid && (new Date().getTime() - created) <= DURATION;
      if (!ret) invalidate();
      return ret;
    }

    @Override
    public final Map<String, Object> getAttachments() {
      return attachments;
    }

    @Override
    public final ReentrantLock getLock() {
      return lock;
    }

    @Override
    public boolean watch() {
      return true;
    }
  }

  public WebActorHandler(WebActorContextProvider selector) {
    this(selector, null);
  }

  public WebActorHandler(WebActorContextProvider selector, String httpResponseEncoderName) {
    this.selector = selector;
    this.httpResponseEncoderName = httpResponseEncoderName;
  }

  @Override
  public final void channelReadComplete(ChannelHandlerContext ctx) {
    ctx.flush();
  }

  @Override
  public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    if (ctx.channel().isOpen()) ctx.close();
    log.error("Exception caught", cause);
  }

  @Override
  protected final void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg instanceof FullHttpRequest) {
      handleHttpRequest(ctx, (FullHttpRequest) msg);
    } else if (msg instanceof WebSocketFrame) {
      handleWebSocketFrame(ctx, (WebSocketFrame) msg);
    } else {
      throw new AssertionError("Unexpected message " + msg);
    }
  }

  protected static boolean sessionsEnabled() {
    return "always".equals(trackSession) || "sse".equals(trackSession);
  }

  protected static final String SESSION_COOKIE_KEY = "JSESSIONID";
  protected static final Map<String, Context> sessions =
      Collections.synchronizedMap(new WeakHashMap<String, Context>());
  protected static final String TRACK_SESSION_PROP =
      HttpChannelAdapter.class.getName() + ".trackSession";
  protected static final String trackSession = System.getProperty(TRACK_SESSION_PROP, "sse");

  protected static final String OMIT_DATE_HEADER_PROP =
      HttpChannelAdapter.class.getName() + ".omitDateHeader";
  protected static final Boolean omitDateHeader =
      SystemProperties.isEmptyOrTrue(OMIT_DATE_HEADER_PROP);

  private static final String ACTOR_KEY = "co.paralleluniverse.comsat.webactors.sessionActor";

  private static final WeakHashMap<Class<?>, List<Pair<String, String>>> classToUrlPatterns =
      new WeakHashMap<>();
  private static final InternalLogger log =
      InternalLoggerFactory.getInstance(AutoWebActorHandler.class);

  private final WebActorContextProvider selector;
  private final String httpResponseEncoderName;

  private WebSocketServerHandshaker handshaker;
  private WebSocketActorAdapter webSocketActor;

  private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
    // Check for closing frame
    if (frame instanceof CloseWebSocketFrame) {
      handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
      return;
    }

    if (frame instanceof PingWebSocketFrame) {
      ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
      return;
    }

    if (frame instanceof ContinuationWebSocketFrame) return;

    if (frame instanceof TextWebSocketFrame)
      webSocketActor.onMessage(((TextWebSocketFrame) frame).text());
    else webSocketActor.onMessage(frame.content().nioBuffer());
  }

  private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req)
      throws SuspendExecution {
    // Handle a bad request.
    if (!req.getDecoderResult().isSuccess()) {
      sendHttpResponse(
          ctx, req, new DefaultFullHttpResponse(req.getProtocolVersion(), BAD_REQUEST), false);
      return;
    }

    final String uri = req.getUri();

    final Context actorCtx = selector.get(ctx, req);
    assert actorCtx != null;

    final ReentrantLock lock = actorCtx.getLock();
    assert lock != null;

    lock.lock();

    try {
      final ActorRef<? extends WebMessage> userActorRef = actorCtx.getRef();
      ActorImpl internalActor = (ActorImpl) actorCtx.getAttachments().get(ACTOR_KEY);

      if (userActorRef != null) {
        if (actorCtx.handlesWithWebSocket(uri)) {
          if (internalActor == null || !(internalActor instanceof WebSocketActorAdapter)) {
            //noinspection unchecked
            webSocketActor =
                new WebSocketActorAdapter(ctx, (ActorRef<? super WebMessage>) userActorRef);
            addActorToContextAndUnlock(actorCtx, webSocketActor, lock);
          }
          // Handshake
          final WebSocketServerHandshakerFactory wsFactory =
              new WebSocketServerHandshakerFactory(uri, null, true);
          handshaker = wsFactory.newHandshaker(req);
          if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
          } else {
            @SuppressWarnings("unchecked")
            final ActorRef<WebMessage> userActorRef0 =
                (ActorRef<WebMessage>) webSocketActor.userActor;
            handshaker
                .handshake(ctx.channel(), req)
                .addListener(
                    new GenericFutureListener<ChannelFuture>() {
                      @Override
                      public void operationComplete(ChannelFuture future) throws Exception {
                        FiberUtil.runInFiber(
                            new SuspendableRunnable() {
                              @Override
                              public void run() throws SuspendExecution, InterruptedException {
                                userActorRef0.send(
                                    new WebSocketOpened(WebActorHandler.this.webSocketActor.ref()));
                              }
                            });
                      }
                    });
          }
          return;
        } else if (actorCtx.handlesWithHttp(uri)) {
          if (internalActor == null || !(internalActor instanceof HttpActorAdapter)) {
            //noinspection unchecked
            internalActor =
                new HttpActorAdapter(
                    (ActorRef<HttpRequest>) userActorRef, actorCtx, httpResponseEncoderName);
            addActorToContextAndUnlock(actorCtx, internalActor, lock);
          }
          //noinspection unchecked
          ((HttpActorAdapter) internalActor).service(ctx, req);
          return;
        }
      }
    } finally {
      if (lock.isHeldByCurrentStrand() && lock.isLocked()) lock.unlock();
    }

    sendHttpResponse(
        ctx, req, new DefaultFullHttpResponse(req.getProtocolVersion(), NOT_FOUND), false);
  }

  private void addActorToContextAndUnlock(
      Context actorContext, ActorImpl actor, ReentrantLock lock) {
    actorContext.getAttachments().put(ACTOR_KEY, actor);
    lock.unlock();
  }

  private static final class WebSocketActorAdapter extends FakeActor<WebDataMessage> {
    ActorRef<? super WebMessage> userActor;

    private ChannelHandlerContext ctx;

    public WebSocketActorAdapter(
        ChannelHandlerContext ctx, ActorRef<? super WebMessage> userActor) {
      super(userActor.getName(), new WebSocketChannelAdapter(ctx));
      ((WebSocketChannelAdapter) (SendPort) getMailbox()).actor = this;
      this.ctx = ctx;
      this.userActor = userActor;
      watch(userActor);
    }

    @Override
    public final void interrupt() {
      die(new InterruptedException());
    }

    @Override
    public final String toString() {
      return "WebSocketActorAdapter{" + "userActor=" + userActor + '}';
    }

    private void onMessage(final ByteBuffer message) {
      try {
        userActor.send(new WebDataMessage(ref(), message));
      } catch (SuspendExecution ex) {
        throw new AssertionError(ex);
      }
    }

    private void onMessage(final String message) {
      try {
        userActor.send(new WebDataMessage(ref(), message));
      } catch (SuspendExecution ex) {
        throw new AssertionError(ex);
      }
    }

    @Override
    protected final WebDataMessage handleLifecycleMessage(LifecycleMessage m) {
      if (m instanceof ExitMessage) {
        ExitMessage em = (ExitMessage) m;
        if (em.getActor() != null && em.getActor().equals(userActor)) die(em.getCause());
      }
      return null;
    }

    @Override
    protected final void throwIn(RuntimeException e) {
      die(e);
    }

    @Override
    protected final void die(Throwable cause) {
      super.die(cause);
      if (ctx.channel().isOpen()) ctx.close();

      // Ensure to release server references
      userActor = null;
      ctx = null;
    }
  }

  private static final class WebSocketChannelAdapter implements SendPort<WebDataMessage> {
    private final ChannelHandlerContext ctx;

    WebSocketActorAdapter actor;

    public WebSocketChannelAdapter(ChannelHandlerContext ctx) {
      this.ctx = ctx;
    }

    @Override
    public final void send(WebDataMessage message) throws SuspendExecution, InterruptedException {
      trySend(message);
    }

    @Override
    public final boolean send(WebDataMessage message, long timeout, TimeUnit unit)
        throws SuspendExecution, InterruptedException {
      return trySend(message);
    }

    @Override
    public final boolean send(WebDataMessage message, Timeout timeout)
        throws SuspendExecution, InterruptedException {
      return send(message, timeout.nanosLeft(), TimeUnit.NANOSECONDS);
    }

    @Override
    public final boolean trySend(WebDataMessage message) {
      if (!message.isBinary()) ctx.writeAndFlush(new TextWebSocketFrame(message.getStringBody()));
      else
        ctx.writeAndFlush(
            new BinaryWebSocketFrame(Unpooled.wrappedBuffer(message.getByteBufferBody())));
      return true;
    }

    @Override
    public final void close() {
      if (ctx.channel().isOpen()) ctx.close();
      if (actor != null) actor.die(null);
    }

    @Override
    public final void close(Throwable t) {
      if (actor != null) actor.die(t);
      close();
    }
  }

  private static final class HttpActorAdapter extends FakeActor<HttpResponse> {
    private ActorRef<? super HttpRequest> userActor;
    private Context context;

    private volatile boolean dead;

    private volatile ChannelHandlerContext ctx;
    private volatile FullHttpRequest req;
    private volatile Object watchToken;

    HttpActorAdapter(
        ActorRef<? super HttpRequest> userActor,
        Context actorContext,
        String httpResponseEncoderName) {
      super("HttpActorAdapter", new HttpChannelAdapter(actorContext, httpResponseEncoderName));

      if (actorContext.watch()) ((HttpChannelAdapter) (SendPort) getMailbox()).actor = this;

      this.userActor = userActor;
      this.context = actorContext;
    }

    final void service(ChannelHandlerContext ctx, FullHttpRequest req) throws SuspendExecution {
      if (context.watch()) watchToken = watch(userActor);

      this.ctx = ctx;
      this.req = req;

      if (isDone()) {
        handleDeath(getDeathCause());
        return;
      }

      userActor.send(new HttpRequestWrapper(ref(), ctx, req));
    }

    final void unwatch() {
      if (watchToken != null && userActor != null) {
        unwatch(userActor, watchToken);
        watchToken = null;
      }
    }

    private void handleDeath(Throwable cause) {
      if (cause != null)
        sendHttpResponse(
            ctx,
            req,
            new DefaultFullHttpResponse(
                req.getProtocolVersion(),
                INTERNAL_SERVER_ERROR,
                Unpooled.wrappedBuffer(
                    ("Actor is dead because of " + cause.getMessage()).getBytes())),
            false);
      else
        sendHttpResponse(
            ctx,
            req,
            new DefaultFullHttpResponse(
                req.getProtocolVersion(),
                INTERNAL_SERVER_ERROR,
                Unpooled.wrappedBuffer(("Actor has terminated.").getBytes())),
            false);
    }

    @Override
    protected final HttpResponse handleLifecycleMessage(LifecycleMessage m) {
      if (m instanceof ExitMessage) {
        final ExitMessage em = (ExitMessage) m;
        if (em.getActor() != null && em.getActor().equals(userActor)) {
          handleDeath(em.getCause());
          die(em.getCause());
        }
      }
      return null;
    }

    @Override
    protected final void die(Throwable cause) {
      if (dead) return;
      dead = true;
      super.die(cause);
      try {
        context.invalidate();
      } catch (final Exception ignored) {
      }

      // Ensure to release references to server objects
      unwatch();
      userActor = null;
      watchToken = null;
      context = null;
      ctx = null;
      req = null;
    }

    @Override
    protected final void throwIn(RuntimeException e) {
      die(e);
    }

    @Override
    protected final void interrupt() {
      die(new InterruptedException());
    }

    @Override
    public final String toString() {
      return "HttpActorAdapter{" + userActor + "}";
    }
  }

  private static final class HttpChannelAdapter implements SendPort<HttpResponse> {
    HttpActorAdapter actor;

    private final String httpResponseEncoderName;

    private Context actorContext;

    public HttpChannelAdapter(Context actorContext, String httpResponseEncoderName) {
      this.actorContext = actorContext;
      this.httpResponseEncoderName = httpResponseEncoderName;
    }

    @Override
    public final void send(HttpResponse message) throws SuspendExecution, InterruptedException {
      trySend(message);
    }

    @Override
    public final boolean send(HttpResponse message, long timeout, TimeUnit unit)
        throws SuspendExecution, InterruptedException {
      send(message);
      return true;
    }

    @Override
    public final boolean send(HttpResponse message, Timeout timeout)
        throws SuspendExecution, InterruptedException {
      return send(message, timeout.nanosLeft(), TimeUnit.NANOSECONDS);
    }

    @Override
    public final boolean trySend(HttpResponse message) {
      try {
        final HttpRequestWrapper nettyRequest = (HttpRequestWrapper) message.getRequest();
        final FullHttpRequest req = nettyRequest.req;
        final ChannelHandlerContext ctx = nettyRequest.ctx;

        final HttpResponseStatus status = HttpResponseStatus.valueOf(message.getStatus());

        if (message.getStatus() >= 400 && message.getStatus() < 600) {
          sendHttpResponse(
              ctx, req, new DefaultFullHttpResponse(req.getProtocolVersion(), status), false);
          close();
          return true;
        }

        if (message.getRedirectPath() != null) {
          sendHttpRedirect(ctx, req, message.getRedirectPath());
          close();
          return true;
        }

        FullHttpResponse res;
        if (message.getStringBody() != null)
          res =
              new DefaultFullHttpResponse(
                  req.getProtocolVersion(),
                  status,
                  Unpooled.wrappedBuffer(message.getStringBody().getBytes()));
        else if (message.getByteBufferBody() != null)
          res =
              new DefaultFullHttpResponse(
                  req.getProtocolVersion(),
                  status,
                  Unpooled.wrappedBuffer(message.getByteBufferBody()));
        else res = new DefaultFullHttpResponse(req.getProtocolVersion(), status);

        if (message.getCookies() != null) {
          final ServerCookieEncoder enc = ServerCookieEncoder.STRICT;
          for (final Cookie c : message.getCookies())
            HttpHeaders.setHeader(res, COOKIE, enc.encode(getNettyCookie(c)));
        }
        if (message.getHeaders() != null) {
          for (final Map.Entry<String, String> h : message.getHeaders().entries())
            HttpHeaders.setHeader(res, h.getKey(), h.getValue());
        }

        if (message.getContentType() != null) {
          String ct = message.getContentType();
          if (message.getCharacterEncoding() != null)
            ct = ct + "; charset=" + message.getCharacterEncoding().name();
          HttpHeaders.setHeader(res, CONTENT_TYPE, ct);
        }

        final boolean sseStarted = message.shouldStartActor();
        if (trackSession(sseStarted)) {
          final String sessionId = UUID.randomUUID().toString();
          res.headers()
              .add(SET_COOKIE, ServerCookieEncoder.STRICT.encode(SESSION_COOKIE_KEY, sessionId));
          startSession(sessionId, actorContext);
        }
        if (!sseStarted) {
          final String stringBody = message.getStringBody();
          long contentLength = 0L;
          if (stringBody != null) contentLength = stringBody.getBytes().length;
          else {
            final ByteBuffer byteBufferBody = message.getByteBufferBody();
            if (byteBufferBody != null) contentLength = byteBufferBody.remaining();
          }
          res.headers().add(CONTENT_LENGTH, contentLength);
        }

        final HttpStreamActorAdapter httpStreamActorAdapter;
        if (sseStarted)
          // This will copy the request content, which must still be referenceable, doing before the
          // request handler
          // unallocates it (unfortunately it is explicitly reference-counted in Netty)
          httpStreamActorAdapter = new HttpStreamActorAdapter(ctx, req);
        else httpStreamActorAdapter = null;

        sendHttpResponse(ctx, req, res, false);

        if (sseStarted) {
          if (httpResponseEncoderName != null) {
            ctx.pipeline().remove(httpResponseEncoderName);
          } else {
            final ChannelPipeline pl = ctx.pipeline();
            final List<String> handlerKeysToBeRemoved = new ArrayList<>();
            for (final Map.Entry<String, ChannelHandler> e : pl) {
              if (e.getValue() instanceof HttpResponseEncoder)
                handlerKeysToBeRemoved.add(e.getKey());
            }
            for (final String k : handlerKeysToBeRemoved) pl.remove(k);
          }

          try {
            message.getFrom().send(new HttpStreamOpened(httpStreamActorAdapter.ref(), message));
          } catch (SuspendExecution e) {
            throw new AssertionError(e);
          }
        }

        return true;
      } finally {
        if (actor != null) actor.unwatch();
      }
    }

    private io.netty.handler.codec.http.cookie.Cookie getNettyCookie(Cookie c) {
      io.netty.handler.codec.http.cookie.Cookie ret =
          new io.netty.handler.codec.http.cookie.DefaultCookie(c.getName(), c.getValue());
      ret.setDomain(c.getDomain());
      ret.setHttpOnly(c.isHttpOnly());
      ret.setMaxAge(c.getMaxAge());
      ret.setPath(c.getPath());
      ret.setSecure(c.isSecure());
      return ret;
    }

    @Override
    public final void close() {
      if (actor != null) actor.die(null);
      actorContext = null;
    }

    @Override
    public final void close(Throwable t) {
      log.error("Exception while closing HTTP adapter", t);
      if (actor != null) actor.die(t);
    }
  }

  protected static boolean trackSession(boolean sseStarted) {
    return trackSession != null
        && ("always".equals(trackSession) || sseStarted && "sse".equals(trackSession));
  }

  protected static boolean handlesWithHttp(String uri, Class<?> actorClass) {
    return match(uri, actorClass).equals("websocket");
  }

  protected static boolean handlesWithWebSocket(String uri, Class<?> actorClass) {
    return match(uri, actorClass).equals("ws");
  }

  private static final class HttpStreamActorAdapter extends FakeActor<WebDataMessage> {
    private volatile boolean dead;

    public HttpStreamActorAdapter(final ChannelHandlerContext ctx, final FullHttpRequest req) {
      super(req.toString(), new HttpStreamChannelAdapter(ctx, req));
      ((HttpStreamChannelAdapter) (SendPort) getMailbox()).actor = this;
    }

    @Override
    protected WebDataMessage handleLifecycleMessage(LifecycleMessage m) {
      if (m instanceof ShutdownMessage) {
        die(null);
      }
      return null;
    }

    @Override
    protected void throwIn(RuntimeException e) {
      die(e);
    }

    @Override
    public void interrupt() {
      die(new InterruptedException());
    }

    @Override
    protected void die(Throwable cause) {
      if (dead) return;
      this.dead = true;
      mailbox().close();
      super.die(cause);
    }

    @Override
    public String toString() {
      return "HttpStreamActorAdapter{request + " + getName() + "}";
    }
  }

  private static final class HttpStreamChannelAdapter implements SendPort<WebDataMessage> {
    private final Charset encoding;
    private final ChannelHandlerContext ctx;

    HttpStreamActorAdapter actor;

    public HttpStreamChannelAdapter(ChannelHandlerContext ctx, FullHttpRequest req) {
      this.ctx = ctx;
      this.encoding = HttpRequestWrapper.extractCharacterEncodingOrDefault(req.headers());
    }

    @Override
    public final void send(WebDataMessage message) throws SuspendExecution, InterruptedException {
      trySend(message);
    }

    @Override
    public final boolean send(WebDataMessage message, long timeout, TimeUnit unit)
        throws SuspendExecution, InterruptedException {
      send(message);
      return true;
    }

    @Override
    public final boolean send(WebDataMessage message, Timeout timeout)
        throws SuspendExecution, InterruptedException {
      return send(message, timeout.nanosLeft(), TimeUnit.NANOSECONDS);
    }

    @Override
    public final boolean trySend(WebDataMessage res) {
      final ByteBuf buf;
      final String stringBody = res.getStringBody();
      if (stringBody != null) {
        byte[] bs = stringBody.getBytes(encoding);
        buf = Unpooled.wrappedBuffer(bs);
      } else {
        buf = Unpooled.wrappedBuffer(res.getByteBufferBody());
      }
      ctx.writeAndFlush(buf);
      return true;
    }

    @Override
    public final void close() {
      if (ctx.channel().isOpen()) ctx.close();
      if (actor != null) actor.die(null);
    }

    @Override
    public final void close(Throwable t) {
      if (actor != null) actor.die(t);
      close();
    }
  }

  private static void sendHttpResponse(
      ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res, Boolean close) {
    writeHttpResponse(ctx, req, res, close);
  }

  private static void sendHttpRedirect(
      ChannelHandlerContext ctx, FullHttpRequest req, String newUri) {
    final FullHttpResponse res = new DefaultFullHttpResponse(req.getProtocolVersion(), FOUND);
    HttpHeaders.setHeader(res, LOCATION, newUri);
    writeHttpResponse(ctx, req, res, true);
  }

  private static void writeHttpResponse(
      ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res, Boolean close) {
    if (!omitDateHeader && !res.headers().contains(DefaultHttpHeaders.Names.DATE))
      DefaultHttpHeaders.addDateHeader(res, DefaultHttpHeaders.Names.DATE, new Date());

    // Send the response and close the connection if necessary.
    if (!HttpHeaders.isKeepAlive(req) || res.getStatus().code() != 200 || close == null || close) {
      res.headers().set(CONNECTION, HttpHeaders.Values.CLOSE);
      ctx.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE);
    } else {
      res.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
      write(ctx, res);
    }
  }

  private static ChannelFuture write(ChannelHandlerContext ctx, Object res) {
    return ctx.writeAndFlush(res);
    // : ctx.write(res);
  }

  private static String match(String uri, Class<?> actorClass) {
    if (uri != null && actorClass != null) {
      for (final Pair<String, String> e : lookupOrInsert(actorClass)) {
        if (servletMatch(e.getFirst(), uri)) return e.getSecond();
      }
    }
    return "";
  }

  private static List<Pair<String, String>> lookupOrInsert(Class<?> actorClass) {
    if (actorClass != null) {
      final List<Pair<String, String>> lookup = classToUrlPatterns.get(actorClass);
      if (lookup != null) return lookup;
      return insert(actorClass);
    }
    return null;
  }

  private static List<Pair<String, String>> insert(Class<?> actorClass) {
    if (actorClass != null) {
      final WebActor wa = actorClass.getAnnotation(WebActor.class);
      final List<Pair<String, String>> ret = new ArrayList<>(4);
      for (String httpP : wa.httpUrlPatterns()) addPattern(ret, httpP, "websocket");
      for (String wsP : wa.webSocketUrlPatterns()) addPattern(ret, wsP, "ws");
      classToUrlPatterns.put(actorClass, ret);
      return ret;
    }
    return null;
  }

  private static void addPattern(List<Pair<String, String>> ret, String p, String type) {
    if (p != null) {
      @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
      final Pair<String, String> entry = new Pair<>(p, type);
      if (p.endsWith("*") || p.startsWith("*.") || p.equals("/")) // Wildcard -> end
      ret.add(entry);
      else // Exact -> beginning
      ret.add(0, entry);
    }
  }

  private static boolean servletMatch(String pattern, String uri) {
    // As per servlet spec
    if (pattern != null && uri != null) {
      if (pattern.startsWith("/") && pattern.endsWith("*"))
        return uri.startsWith(pattern.substring(0, pattern.length() - 1));
      if (pattern.startsWith("*.")) return uri.endsWith(pattern.substring(2));
      if (pattern.isEmpty()) return uri.equals("/");
      return pattern.equals("/") || pattern.equals(uri);
    }
    return false;
  }

  private static void startSession(String sessionId, Context actorContext) {
    sessions.put(sessionId, actorContext);
  }
}