Package net.kuujo.vertigo.io.connection.impl

Source Code of net.kuujo.vertigo.io.connection.impl.DefaultOutputConnection

/*
* Copyright 2014 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 net.kuujo.vertigo.io.connection.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;

import net.kuujo.vertigo.hook.OutputHook;
import net.kuujo.vertigo.io.connection.OutputConnection;
import net.kuujo.vertigo.io.connection.OutputConnectionContext;
import net.kuujo.vertigo.io.group.OutputGroup;
import net.kuujo.vertigo.io.impl.OutputSerializer;

import org.vertx.java.core.AsyncResult;
import org.vertx.java.core.Handler;
import org.vertx.java.core.Vertx;
import org.vertx.java.core.buffer.Buffer;
import org.vertx.java.core.eventbus.EventBus;
import org.vertx.java.core.eventbus.Message;
import org.vertx.java.core.eventbus.ReplyException;
import org.vertx.java.core.eventbus.ReplyFailure;
import org.vertx.java.core.impl.DefaultFutureResult;
import org.vertx.java.core.json.JsonArray;
import org.vertx.java.core.json.JsonObject;
import org.vertx.java.core.logging.Logger;
import org.vertx.java.core.logging.impl.LoggerFactory;

/**
* Default output connection implementation.
*
* @author <a href="http://github.com/kuujo">Jordan Halterman</a>
*/
public class DefaultOutputConnection implements OutputConnection {
  private static final int DEFAULT_MAX_QUEUE_SIZE = 1000;
  private final Logger log;
  private final Vertx vertx;
  private final EventBus eventBus;
  private final OutputConnectionContext context;
  private final String outAddress;
  private final String inAddress;
  private final OutputSerializer serializer = new OutputSerializer();
  private List<OutputHook> hooks = new ArrayList<>();
  private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
  private Handler<Void> drainHandler;
  private long currentMessage = 1;
  private final TreeMap<Long, JsonObject> messages = new TreeMap<>();
  private final Map<String, DefaultConnectionOutputGroup> groups = new HashMap<>();
  private DefaultConnectionOutputBatch currentBatch;
  private boolean open;
  private boolean full;
  private boolean paused;

  private final Handler<Message<JsonObject>> internalMessageHandler = new Handler<Message<JsonObject>>() {
    @Override
    public void handle(Message<JsonObject> message) {
      String action = message.body().getString("action");
      if (action != null) {
        switch (action) {
          case "group":
            doStartGroup(message.body().getString("group"));
            break;
          case "batch":
            doStartBatch(message.body().getString("batch"));
            break;
          case "ack":
            doAck(message.body().getLong("id"));
            break;
          case "fail":
            doFail(message.body().getLong("id"));
            break;
          case "pause":
            doPause(message.body().getLong("id"));
            break;
          case "resume":
            doResume(message.body().getLong("id"));
            break;
        }
      }
    }
  };

  public DefaultOutputConnection(Vertx vertx, String address) {
    this(vertx, DefaultOutputConnectionContext.Builder.newBuilder().setAddress(address).build());
  }

  public DefaultOutputConnection(Vertx vertx, OutputConnectionContext context) {
    this.vertx = vertx;
    this.eventBus = vertx.eventBus();
    this.context = context;
    this.hooks = context.hooks();
    this.outAddress = String.format("%s.out", context.address());
    this.inAddress = String.format("%s.in", context.address());
    this.log = LoggerFactory.getLogger(String.format("%s-%s", DefaultOutputConnection.class.getName(), context.target()));
  }

  @Override
  public String address() {
    return context.address();
  }

  @Override
  public OutputConnectionContext context() {
    return context;
  }

  @Override
  public Vertx vertx() {
    return vertx;
  }

  @Override
  public OutputConnection open() {
    return open(null);
  }

  @Override
  public OutputConnection open(final Handler<AsyncResult<Void>> doneHandler) {
    eventBus.registerHandler(outAddress, internalMessageHandler, new Handler<AsyncResult<Void>>() {
      @Override
      public void handle(AsyncResult<Void> result) {
        if (result.failed()) {
          new DefaultFutureResult<Void>(result.cause()).setHandler(doneHandler);
        } else {
          connect(doneHandler);
        }
      }
    });
    return this;
  }

  /**
   * Connects to the other side of the connection.
   */
  private void connect(final Handler<AsyncResult<Void>> doneHandler) {
    // Recursively send "connect" messages to the other side of the connection
    // until we get a response. This gives the other side of the connection time
    // to open and ensures that the connection doesn't claim it's open until
    // the other side has registered a handler and responded at least once.
    eventBus.sendWithTimeout(inAddress, new JsonObject().putString("action", "connect"), 1000, new Handler<AsyncResult<Message<Boolean>>>() {
      @Override
      public void handle(AsyncResult<Message<Boolean>> result) {
        if (result.failed()) {
          ReplyException failure = (ReplyException) result.cause();
          if (failure.failureType().equals(ReplyFailure.RECIPIENT_FAILURE)) {
            log.warn(String.format("%s - Failed to connect to %s", DefaultOutputConnection.this, context.target()), result.cause());
            new DefaultFutureResult<Void>(failure).setHandler(doneHandler);
          } else if (failure.failureType().equals(ReplyFailure.TIMEOUT)) {
            log.warn(String.format("%s - Connection to %s failed, retrying", DefaultOutputConnection.this, context.target()));
            connect(doneHandler);
          } else {
            log.debug(String.format("%s - Connection to %s failed, retrying", DefaultOutputConnection.this, context.target()));
            vertx.setTimer(500, new Handler<Long>() {
              @Override
              public void handle(Long timerID) {
                connect(doneHandler);
              }
            });
          }
        } else if (result.result().body()) {
          log.info(String.format("%s - Connected to %s", DefaultOutputConnection.this, context.target()));
          open = true;
          new DefaultFutureResult<Void>((Void) null).setHandler(doneHandler);
        } else {
          log.debug(String.format("%s - Connection to %s failed, retrying", DefaultOutputConnection.this, context.target()));
          vertx.setTimer(500, new Handler<Long>() {
            @Override
            public void handle(Long timerID) {
              connect(doneHandler);
            }
          });
        }
      }
    });
  }

  @Override
  public OutputConnection setSendQueueMaxSize(int maxSize) {
    this.maxQueueSize = maxSize;
    return this;
  }

  @Override
  public int getSendQueueMaxSize() {
    return maxQueueSize;
  }

  @Override
  public int size() {
    return messages.size();
  }

  @Override
  public boolean sendQueueFull() {
    return paused || messages.size() >= maxQueueSize;
  }

  @Override
  public OutputConnection drainHandler(Handler<Void> handler) {
    this.drainHandler = handler;
    return this;
  }

  @Override
  public OutputConnection batch(final String id, final Object args, final Handler<ConnectionOutputBatch> handler) {
    // If there's already a batch open then don't open the new batch until
    // the previous batch has been ended. This ensures that only one batch
    // can be open at any given time on the connection.
    if (currentBatch != null) {
      currentBatch.endHandler(new Handler<Void>() {
        @Override
        public void handle(Void _) {
          currentBatch = new DefaultConnectionOutputBatch(id, args, DefaultOutputConnection.this);
          currentBatch.start(handler);
        }
      });
    } else {
      currentBatch = new DefaultConnectionOutputBatch(id, args, this);
      currentBatch.start(handler);
    }
    return this;
  }

  @Override
  public OutputConnection group(Handler<OutputGroup> handler) {
    return group(UUID.randomUUID().toString(), null, handler);
  }

  @Override
  public OutputConnection group(String name, Handler<OutputGroup> handler) {
    return group(name, null, handler);
  }

  @Override
  public OutputConnection group(String name, Object args, Handler<OutputGroup> handler) {
    DefaultConnectionOutputGroup group = new DefaultConnectionOutputGroup(UUID.randomUUID().toString(), name, args, this);
    groups.put(group.id(), group);
    group.start(handler);
    return this;
  }

  DefaultConnectionOutputGroup group(String name, Object args, String parent, Handler<OutputGroup> handler) {
    DefaultConnectionOutputGroup group = new DefaultConnectionOutputGroup(UUID.randomUUID().toString(), name, args, parent, this);
    groups.put(group.id(), group);
    group.start(handler);
    return group;
  }

  @Override
  public void close() {
    close(null);
  }

  @Override
  public void close(final Handler<AsyncResult<Void>> doneHandler) {
    eventBus.unregisterHandler(outAddress, internalMessageHandler, new Handler<AsyncResult<Void>>() {
      @Override
      public void handle(AsyncResult<Void> result) {
        if (result.failed()) {
          new DefaultFutureResult<Void>(result.cause()).setHandler(doneHandler);
        } else {
          disconnect(doneHandler);
        }
      }
    });
  }

  /**
   * Disconnects from the other side of the connection.
   */
  private void disconnect(final Handler<AsyncResult<Void>> doneHandler) {
    eventBus.sendWithTimeout(inAddress, new JsonObject().putString("action", "disconnect"), 5000, new Handler<AsyncResult<Message<Boolean>>>() {
      @Override
      public void handle(AsyncResult<Message<Boolean>> result) {
        if (result.failed()) {
          ReplyException failure = (ReplyException) result.cause();
          if (failure.failureType().equals(ReplyFailure.RECIPIENT_FAILURE)) {
            log.warn(String.format("%s - Failed to disconnect from %s", DefaultOutputConnection.this, context.target()), result.cause());
            new DefaultFutureResult<Void>(failure).setHandler(doneHandler);
          } else if (failure.failureType().equals(ReplyFailure.NO_HANDLERS)) {
            log.info(String.format("%s - Disconnected from %s", DefaultOutputConnection.this, context.target()));
            new DefaultFutureResult<Void>((Void) null).setHandler(doneHandler);
          } else {
            log.debug(String.format("%s - Disconnection from %s failed, retrying", DefaultOutputConnection.this, context.target()));
            disconnect(doneHandler);
          }
        } else if (result.result().body()) {
          log.info(String.format("%s - Disconnected from %s", DefaultOutputConnection.this, context.target()));
          open = false;
          new DefaultFutureResult<Void>((Void) null).setHandler(doneHandler);
        } else {
          log.debug(String.format("%s - Disconnection from %s failed, retrying", DefaultOutputConnection.this, context.target()));
          vertx.setTimer(500, new Handler<Long>() {
            @Override
            public void handle(Long timerID) {
              disconnect(doneHandler);
            }
          });
        }
      }
    });
  }

  /**
   * Checks whether the connection is open.
   */
  private void checkOpen() {
    if (!open) throw new IllegalStateException(String.format("%s - Connection to %s not open.", this, context.target()));
  }

  /**
   * Checks whether the connection is full.
   */
  private void checkFull() {
    if (!full && messages.size() >= maxQueueSize) {
      full = true;
      log.debug(String.format("%s - Connection to %s is full", this, context.target()));
    }
  }

  /**
   * Checks whether the connection has been drained.
   */
  private void checkDrain() {
    if (full && !paused && messages.size() < maxQueueSize / 2) {
      full = false;
      log.debug(String.format("%s - Connection to %s is drained", this, context.target()));
      if (drainHandler != null) {
        drainHandler.handle((Void) null);
      }
    }
  }

  /**
   * Handles a group start.
   */
  private void doStartGroup(String groupID) {
    DefaultConnectionOutputGroup group = groups.get(groupID);
    if (group != null) {
      group.handleStart();
    }
  }

  /**
   * Handles a batch start.
   */
  private void doStartBatch(String batchID) {
    if (currentBatch != null && currentBatch.id().equals(batchID)) {
      currentBatch.handleStart();
    }
  }

  /**
   * Handles a batch ack.
   */
  private void doAck(long id) {
    // The other side of the connection has sent a message indicating which
    // messages it has seen. We can clear any messages before the indicated ID.
    if (log.isDebugEnabled()) {
      log.debug(String.format("%s - Received ack for messages up to %d, removing all previous messages from memory", this, id));
    }
    if (messages.containsKey(id+1)) {
      messages.tailMap(id+1);
    } else {
      messages.clear();
    }
    checkDrain();
  }

  /**
   * Handles a batch fail.
   */
  private void doFail(long id) {
    if (log.isDebugEnabled()) {
      log.debug(String.format("%s - Received resend request for messages starting at %d", this, id));
    }

    // Ack all the entries before the given ID.
    doAck(id);

    // Now that all the entries before the given ID have been removed,
    // just iterate over the messages map and resend all the messages.
    Iterator<Map.Entry<Long, JsonObject>> iter = messages.entrySet().iterator();
    while (iter.hasNext()) {
      eventBus.send(inAddress, iter.next().getValue());
    }
  }

  /**
   * Handles a connection pause.
   */
  private void doPause(long id) {
    log.debug(String.format("%s - Paused connection to %s", this, context.target()));
    paused = true;
  }

  /**
   * Handles a connection resume.
   */
  private void doResume(long id) {
    if (paused) {
      log.debug(String.format("%s - Resumed connection to %s", this, context.target()));
      paused = false;
      checkDrain();
    }
  }

  /**
   * Sends a message.
   */
  private OutputConnection doSend(final Object value) {
    checkOpen();
    JsonObject message = createMessage(value)
        .putString("action", "message");
    if (open && !paused) {
      if (log.isDebugEnabled()) {
        log.debug(String.format("%s - Send: Message[id=%d, message=%s]", this, message.getLong("id"), value));
      }
      eventBus.send(inAddress, message);
    }
    for (OutputHook hook : hooks) {
      hook.handleSend(value);
    }
    checkFull();
    return this;
  }

  /**
   * Sends a group start message.
   */
  void doGroupStart(String group, String name, Object args, String parent) {
    checkOpen();
    JsonObject message = createMessage(args)
        .putString("group", group)
        .putString("name", name)
        .putString("parent", parent)
        .putString("action", "startGroup");
    if (open && !paused) {
      if (log.isDebugEnabled()) {
        if (parent != null) {
          log.debug(String.format("%s - Group start: Group[name=%s, group=%s, parent=%s, args=%s]", this, name, group, parent, args));
        } else {
          log.debug(String.format("%s - Group start: Group[name=%s, group=%s, args=%s]", this, name, group, args));
        }
      }
      eventBus.send(inAddress, message);
    }
    checkFull();
  }

  /**
   * Sends a group message.
   */
  void doGroupSend(String group, Object value) {
    checkOpen();
    JsonObject message = createMessage(value)
        .putString("action", "group")
        .putString("group", group);
    if (open && !paused) {
      if (log.isDebugEnabled()) {
        log.debug(String.format("%s - Group send: Group[group=%s, id=%d, message=%s", this, group, message.getLong("id"), value));
      }
      eventBus.send(inAddress, message);
    }
    for (OutputHook hook : hooks) {
      hook.handleSend(value);
    }
    checkFull();
  }

  /**
   * Sends a group end message.
   */
  void doGroupEnd(String group, Object args) {
    checkOpen();
    JsonObject message = createMessage(args)
        .putString("action", "endGroup")
        .putString("group", group);
    if (open && !paused) {
      if (log.isDebugEnabled()) {
        log.debug(String.format("%s - Group end: Group[group=%s, args=%s]", this, group, args));
      }
      eventBus.send(inAddress, message);
    }
    groups.remove(group);
  }

  /**
   * Sends a batch start message.
   */
  void doBatchStart(String batch, Object args) {
    checkOpen();
    JsonObject message = createMessage(args)
        .putString("batch", batch)
        .putString("action", "startBatch");
    if (open && !paused) {
      if (log.isDebugEnabled()) {
        log.debug(String.format("%s - Batch start: Batch[batch=%s]", this, batch));
      }
      eventBus.send(inAddress, message);
    }
    checkFull();
  }

  /**
   * Sends a batch message.
   */
  void doBatchSend(String batch, Object value) {
    checkOpen();
    JsonObject message = createMessage(value)
        .putString("action", "batch")
        .putString("batch", batch);
    if (open && !paused) {
      if (log.isDebugEnabled()) {
        log.debug(String.format("%s - Batch send: Batch[batch=%s, id=%d, message=%s]", this, batch, message.getLong("id"), value));
      }
      eventBus.send(inAddress, message);
    }
    for (OutputHook hook : hooks) {
      hook.handleSend(value);
    }
    checkFull();
  }

  /**
   * Sends a batch end message.
   */
  void doBatchEnd(String batch, Object args) {
    checkOpen();
    JsonObject message = createMessage(args)
        .putString("action", "endBatch")
        .putString("batch", batch);
    if (open && !paused) {
      if (log.isDebugEnabled()) {
        log.debug(String.format("%s - Batch end: Batch[batch=%s, args=%s]", this, batch, args));
      }
      eventBus.send(inAddress, message);
    }
    if (currentBatch != null && currentBatch.id().equals(batch)) {
      currentBatch = null;
    }
  }

  /**
   * Creates a value message.
   */
  private JsonObject createMessage(Object value) {
    // Tag the message with a monotonically increasing ID. The ID
    // will be used by the other side of the connection to guarantee
    // ordering.
    JsonObject message = serializer.serialize(value);
    long id = currentMessage++;
    message.putNumber("id", id);
    messages.put(id, message);
    return message;
  }

  @Override
  public OutputConnection send(final Object message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(String message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(Boolean message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(Character message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(Short message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(Integer message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(Long message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(Double message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(Float message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(JsonObject message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(JsonArray message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(Byte message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(byte[] message) {
    return doSend(message);
  }

  @Override
  public OutputConnection send(Buffer message) {
    return doSend(message);
  }

  @Override
  public String toString() {
    return context.toString();
  }

}
TOP

Related Classes of net.kuujo.vertigo.io.connection.impl.DefaultOutputConnection

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.