All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.couchbase.client.ViewConnection Maven / Gradle / Ivy

There is a newer version: 1.4.13
Show newest version
/**
 * Copyright (C) 2009-2013 Couchbase, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING
 * IN THE SOFTWARE.
 */

package com.couchbase.client;

import com.couchbase.client.http.HttpResponseCallback;
import com.couchbase.client.http.HttpUtil;
import com.couchbase.client.http.ViewPool;
import com.couchbase.client.protocol.views.HttpOperation;
import com.couchbase.client.vbucket.Reconfigurable;
import com.couchbase.client.vbucket.config.Bucket;
import com.couchbase.client.vbucket.config.CouchbaseConfig;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import net.spy.memcached.compat.SpyObject;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.impl.nio.DefaultHttpClientIODispatch;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.nio.protocol.BasicAsyncRequestProducer;
import org.apache.http.nio.protocol.BasicAsyncResponseConsumer;
import org.apache.http.nio.protocol.HttpAsyncRequestExecutor;
import org.apache.http.nio.protocol.HttpAsyncRequester;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.http.nio.reactor.IOEventDispatch;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpCoreContext;
import org.apache.http.protocol.HttpProcessor;
import org.apache.http.protocol.HttpProcessorBuilder;
import org.apache.http.protocol.RequestConnControl;
import org.apache.http.protocol.RequestContent;
import org.apache.http.protocol.RequestExpectContinue;
import org.apache.http.protocol.RequestTargetHost;
import org.apache.http.protocol.RequestUserAgent;

/**
 * The {@link ViewConnection} is responsible for managing and multiplexing
 * HTTP connections to Couchbase View endpoints.
 *
 * It implements {@link Reconfigurable}, which means that will be fed with
 * reconfiguration updates coming from the server side. This stream changes
 * the collection of {@link HttpHost}s, which represent the view endpoints.
 */
public class ViewConnection extends SpyObject implements Reconfigurable {

  /**
   * The view connection scheme to use for {@link HttpHost}s.
   */
  private static final String SCHEME = "http";

  /**
   * The list of view endpoints to communicate with.
   */
  private final List viewNodes;

  /**
   * The HTTP user to use for authentication.
   */
  private final String user;

  /**
   * The HTTP password to use for authentication.
   */
  private final String password;

  /**
   * The asynchronous IO reactor which executes the requests.
   */
  private final ConnectingIOReactor ioReactor;

  /**
   * The connection pool manager for multiplexing connections.
   */
  private final ViewPool pool;

  /**
   * A requester that helps with asynchronous request/response flow.
   */
  private final HttpAsyncRequester requester;

  /**
   * The connection factory for other references.
   */
  private final CouchbaseConnectionFactory connectionFactory;

  /**
   * The thread where the {@link #ioReactor} executes in.
   */
  private volatile Thread reactorThread;

  /**
   * The next node to pick up from {@link #viewNodes}, selected in a round-robin
   * fashion by {@link #getNextNode()}.
   */
  private volatile int nextNode;

  /**
   * If the connection is running or shut down.
   */
  private volatile boolean running;

  /**
   * Boostrap the {@link ViewConnection}s.
   *
   * This will also start the reactor in a separate thread, waiting for requests
   * to be written.
   *
   * @param cf the configuration factory to access other parts of the system.
   * @param seedAddrs the list of nodes to initially connect to.
   * @param user the HTTP username for authentication purposes.
   * @param password the HTTP password for authentication purposes.
   * @throws IOException if the IOReactor could not be created.
   */
  public ViewConnection(final CouchbaseConnectionFactory cf,
    final List seedAddrs, final String user,
    final String password) throws IOException {
    nextNode = 0;
    this.user = user;
    this.password = password;
    connectionFactory = cf;

    viewNodes = Collections.synchronizedList(new ArrayList());
    for (InetSocketAddress addr : seedAddrs) {
      viewNodes.add(createHttpHost(addr.getHostName(), addr.getPort()));
    }

    HttpProcessor httpProc = HttpProcessorBuilder.create()
      .add(new RequestContent())
      .add(new RequestTargetHost())
      .add(new RequestConnControl())
      .add(new RequestUserAgent("JCBC/1.2"))
      .add(new RequestExpectContinue(true)).build();

    requester = new HttpAsyncRequester(httpProc);

    ioReactor = new DefaultConnectingIOReactor(IOReactorConfig.custom()
      .setConnectTimeout(5000)
      .setSoTimeout(5000)
      .setTcpNoDelay(true)
      .setIoThreadCount(cf.getViewWorkerSize())
      .build());

    pool = new ViewPool(ioReactor, ConnectionConfig.DEFAULT);
    pool.setDefaultMaxPerRoute(cf.getViewConnsPerNode());
    updateMaxTotalRequests();

    initializeReactorThread();
  }

  /**
   * Write an operation to the next {@link HttpHost}.
   *
   * To make sure that the operations are distributed throughout the cluster,
   * the {@link HttpHost} is changed every time a new operation is added. Since
   * the {@link #getNextNode()} method increments the {@link HttpHost} index and
   * calculates the modulo, the nodes are selected in a round-robin fashion.
   *
   * Also, the target host will be added directly, so that a DNS lookup is
   * avoided, potentially causing delays and timeouts.
   *
   * @param op the operation to schedule.
   */
  public void addOp(final HttpOperation op) {
    if (!running) {
      throw new IllegalStateException("Shutting down");
    }

    HttpCoreContext coreContext = HttpCoreContext.create();

    if (viewNodes.isEmpty()) {
      getLogger().error("No server connections. Cancelling op.");
      op.cancel();
    } else {
      if (!"default".equals(user)) {
        try {
          op.addAuthHeader(HttpUtil.buildAuthHeader(user, password));
        } catch (UnsupportedEncodingException ex) {
          getLogger().error("Could not create auth header for request, "
            + "could not encode credentials into base64. Canceling op."
            + op, ex);
          op.cancel();
          return;
        }
      }

      HttpHost httpHost = getNextNode();
      HttpRequest request = op.getRequest();

      request.addHeader(HTTP.TARGET_HOST, httpHost.toHostString());
      requester.execute(
        new BasicAsyncRequestProducer(httpHost, request),
        new BasicAsyncResponseConsumer(),
        pool,
        coreContext,
        new HttpResponseCallback(op, this, httpHost)
      );
    }
  }

  /**
   * Helper method to signal an outdated config and potentially force a
   * refresh of the connection provider.
   */
  public void signalOutdatedConfig() {
    connectionFactory.checkConfigUpdate();
  }

  /**
   * Shuts down the active {@link ViewConnection}.
   *
   * @return false if a shutdown attempt is already in progress, true otherwise.
   * @throws IOException if the reactor cannot be shut down properly.
   */
  public boolean shutdown() throws IOException {
    if (!running) {
      getLogger().info("Suppressing duplicate attempt to shut down");
      return false;
    }
    running = false;

    ioReactor.shutdown();
    try {
      reactorThread.join(0);
    } catch (InterruptedException ex) {
      getLogger().error("Interrupt " + ex + " received while waiting for "
        + "view thread to shut down.");
    }
    return true;
  }

  /**
   * Updates the list of active {@link HttpHost}s based on the incoming
   * {@link Bucket} configuration.
   *
   * If the node is in the bucket configuration, but has no active vbuckets
   * assigned to it, it will not be added as an active node (because it will
   * not return useful results without partitions assigned). Also, a node
   * will be removed if during reconfiguration it is determined that all
   * partitions have been movd away from it.
   *
   * @param bucket the updated bucket configuration.
   */
  @Override
  public void reconfigure(final Bucket bucket) {
    CouchbaseConfig config = (CouchbaseConfig) bucket.getConfig();
    int sizeBeforeReconfigure = viewNodes.size();

    List currentViewServers = new ArrayList();
    for (URL server : bucket.getConfig().getCouchServers()) {
      HttpHost host = createHttpHost(server.getHost(), server.getPort());
      currentViewServers.add(host);

      if (!viewNodes.contains(host) && hasActiveVBuckets(config, host)) {
        getLogger().debug("Adding view node: " + host);
        viewNodes.add(host);
      }
    }

    List connectionsToClose = new ArrayList();
    synchronized (viewNodes) {
      Iterator iter = viewNodes.iterator();
      while (iter.hasNext()) {
        HttpHost current = iter.next();
        if (!currentViewServers.contains(current)
          || !hasActiveVBuckets(config, current)) {
          connectionsToClose.add(current);
          iter.remove();
          getLogger().debug("Removing view node: " + current);
        }
      }
    }

    for (HttpHost host : connectionsToClose) {
      getLogger().debug("Closing old connections for node: " + host);
      pool.closeConnectionsForHost(host);
    }

    if (sizeBeforeReconfigure != viewNodes.size()) {
      updateMaxTotalRequests();
    }
  }

  /**
   * Initialize the reactor IO thread.
   */
  private void initializeReactorThread() {
    final IOEventDispatch ioEventDispatch = new DefaultHttpClientIODispatch(
      new HttpAsyncRequestExecutor(),
      ConnectionConfig.DEFAULT
    );

    reactorThread = new Thread(new Runnable() {
      @Override
      public void run() {
        try {
          ioReactor.execute(ioEventDispatch);
        } catch (InterruptedIOException ex) {
          getLogger().error("I/O reactor Interrupted", ex);
        } catch (IOException e) {
          getLogger().error("I/O error: " + e.getMessage(), e);
        }
        getLogger().info("I/O reactor terminated");
      }
    }, "Couchbase View Thread");
    reactorThread.start();

    running = true;
  }

  /**
   * Update the absolute maximum of parallel connection requests.
   *
   * The algorithm sets the number of total parallel connection requests to
   * the number of nodes multiplied by the number of connections per node
   * allowed. Note that this number will only be hit under high-pressure
   * scenarios, the reactor itself is optimized to reuse connections if they
   * are idle.
   */
  private void updateMaxTotalRequests() {
    int size = viewNodes.size();

    if (size > 0) {
      pool.setMaxTotal(viewNodes.size() * pool.getDefaultMaxPerRoute());
    } else {
      getLogger().warn("No View nodes are present, this could be a bug or "
        + "no node has vBuckets attached.");
      pool.setMaxTotal(1);
    }
  }

  /**
   * Calculates the next node to run the request against.
   *
   * @return the next index in the {@link #viewNodes} list.
   */
  HttpHost getNextNode() {
    HttpHost host = null;
    while (host == null) {
      host = viewNodes.get(nextNode++ % viewNodes.size());
    }
    return host;
  }

  /**
   * Helper method to create {@link HttpHost} instances.
   *
   * @param host the hostname to use.
   * @param port the port to use.
   * @return a {@link HttpHost} instance.
   */
  private static HttpHost createHttpHost(final String host, final int port) {
    return new HttpHost(host, port, SCHEME);
  }

  /**
   * Check if the given {@link HttpHost} has active partitions assigned.
   *
   * If there are no active VBuckets assigned to the host, there is no need
   * to run the view operations against it, because it cannot serve the
   * incoming request properly.
   *
   * @param node the node to check.
   * @return true if it has active vbuckets, false if not.
   */
  private static boolean hasActiveVBuckets(final CouchbaseConfig config,
    final HttpHost node) {
    return config.nodeHasActiveVBuckets(
      new InetSocketAddress(node.getHostName(), node.getPort())
    );
  }

  /**
   * Lists the currently serviceable {@link HttpHost}s for verification
   * purposes.
   *
   * @return a list of currently serviceable {@link HttpHost}s.
   */
  List getConnectedHosts() {
    return viewNodes;
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy