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

com.couchbase.client.vbucket.provider.BucketConfigurationProvider 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.vbucket.provider;

import com.couchbase.client.CouchbaseConnection;
import com.couchbase.client.CouchbaseConnectionFactory;
import com.couchbase.client.CouchbaseProperties;
import com.couchbase.client.vbucket.ConfigurationException;
import com.couchbase.client.vbucket.ConfigurationProviderHTTP;
import com.couchbase.client.vbucket.CouchbaseNodeOrder;
import com.couchbase.client.vbucket.Reconfigurable;
import com.couchbase.client.vbucket.config.Bucket;
import com.couchbase.client.vbucket.config.Config;
import com.couchbase.client.vbucket.config.ConfigurationParser;
import com.couchbase.client.vbucket.config.ConfigurationParserJSON;
import net.spy.memcached.ArrayModNodeLocator;
import net.spy.memcached.BroadcastOpFactory;
import net.spy.memcached.ConnectionObserver;
import net.spy.memcached.MemcachedNode;
import net.spy.memcached.NodeLocator;
import net.spy.memcached.auth.AuthThreadMonitor;
import net.spy.memcached.compat.SpyObject;
import net.spy.memcached.ops.Operation;
import net.spy.memcached.ops.OperationCallback;
import net.spy.memcached.ops.OperationStatus;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/**
 * This {@link ConfigurationProvider} provides the current bucket configuration
 * in a best-effort way, mixing both http and binary fetching techniques
 * (depending on the supported mechanisms on the cluster side).
 */
public class BucketConfigurationProvider extends SpyObject
  implements ConfigurationProvider, Reconfigurable {


  private static final int DEFAULT_BINARY_PORT = 11210;
  private static final String ANONYMOUS_BUCKET = "default";

  private final AtomicReference config;
  private final List seedNodes;
  private final List observers;
  private final String bucket;
  private final String password;
  private final CouchbaseConnectionFactory connectionFactory;
  private final ConfigurationParser configurationParser;
  private final AtomicReference httpProvider;
  private final AtomicBoolean refreshingHttp;
  private final AtomicBoolean pollingBinary;
  private final AtomicReference binaryConnection;
  private final boolean disableCarrierBootstrap;
  private final boolean disableHttpBootstrap;
  private volatile boolean isBinary;

  public BucketConfigurationProvider(final List seedNodes,
    final String bucket, final String password,
    final CouchbaseConnectionFactory connectionFactory) {
    config = new AtomicReference();
    configurationParser = new ConfigurationParserJSON();
    httpProvider = new AtomicReference(
      new ConfigurationProviderHTTP(seedNodes, bucket, password)
    );
    refreshingHttp = new AtomicBoolean(false);
    pollingBinary = new AtomicBoolean(false);
    observers = Collections.synchronizedList(new ArrayList());
    binaryConnection = new AtomicReference();

    this.seedNodes = Collections.synchronizedList(new ArrayList(seedNodes));
    this.bucket = bucket;
    this.password = password;
    this.connectionFactory = connectionFactory;
    potentiallyRandomizeNodeList(seedNodes);

    disableCarrierBootstrap = Boolean.parseBoolean(
      CouchbaseProperties.getProperty("disableCarrierBootstrap", "false"));
    disableHttpBootstrap = Boolean.parseBoolean(
      CouchbaseProperties.getProperty("disableHttpBootstrap", "false"));
  }

  @Override
  public Bucket bootstrap() {
    isBinary = false;
    if (!bootstrapBinary() && !bootstrapHttp()) {
      throw new ConfigurationException("Could not fetch a valid Bucket "
        + "configuration.");
    }

    if (isBinary) {
      getLogger().info("Could bootstrap through carrier publication.");
    } else {
      getLogger().info("Carrier config not available, bootstrapped through "
        + "HTTP.");
    }

    monitorBucket();
    return config.get();
  }

  /**
   * Helper method to initiate the binary bootstrap process.
   *
   * If no config is found (either because of an error or it is not supported
   * on the cluster side), false is returned.
   *
   * @return true if the binary bootstrap process was successful.
   */
  boolean bootstrapBinary() {
    if (disableCarrierBootstrap) {
      getLogger().info("Carrier bootstrap manually disabled, skipping.");
      return false;
    }

    isBinary = true;
    List nodes =
      new ArrayList(seedNodes.size());
    for (URI seedNode : seedNodes) {
      nodes.add(new InetSocketAddress(seedNode.getHost(), DEFAULT_BINARY_PORT));
    }

    try {
      for (InetSocketAddress node : nodes) {
        if(tryBinaryBootstrapForNode(node)) {
          return true;
        }
      }

      getLogger().debug("Not a single node returned a carrier publication "
        + "config.");
      isBinary = false;
      return false;
    } catch(Exception ex) {
      getLogger().info("Could not fetch config from carrier publication seed "
        + "nodes.", ex);
      isBinary = false;
      return false;
    }
  }

  /**
   * Try bootstrapping to a binary connection for the given node.
   *
   * @param node the node to connect.
   * @return true if successful, false otherwise.
   * @throws Exception if an error while bootstrapping occurs.
   */
  private boolean tryBinaryBootstrapForNode(InetSocketAddress node)
    throws Exception {
    ConfigurationConnectionFactory fact =
      new ConfigurationConnectionFactory(seedNodes, bucket, password);
    CouchbaseConnectionFactory cf = connectionFactory;
    CouchbaseConnection connection;

    List initialObservers = new ArrayList();
    final CountDownLatch latch = new CountDownLatch(1);
    initialObservers.add(new ConnectionObserver() {
      @Override
      public void connectionEstablished(SocketAddress socketAddress, int i) {
        latch.countDown();
      }

      @Override
      public void connectionLost(SocketAddress socketAddress) {
        // not needed
      }
    });

    try {
       connection = new CouchbaseConfigConnection(
        cf.getReadBufSize(), fact, Collections.singletonList(node),
        initialObservers, cf.getFailureMode(),
        cf.getOperationFactory()
      );

      boolean result = latch.await(5, TimeUnit.SECONDS);
      if (!result) {
        throw new IOException("Connection could not be established to carrier"
          + " port in the given time interval.");
      }
    } catch (Exception ex) {
      getLogger().debug("(Carrier Publication) Could not load config from "
        + node.getHostName() + ", trying next node.", ex);
      return false;
    }

    if (!bucket.equals(ANONYMOUS_BUCKET)) {
      AuthThreadMonitor monitor = new AuthThreadMonitor();
      List connectedNodes = new ArrayList(
        connection.getLocator().getAll());
      for (MemcachedNode connectedNode : connectedNodes) {
        if (connectedNode.getSocketAddress().equals(node)) {
          monitor.authConnection(connection, cf.getOperationFactory(),
            cf.getAuthDescriptor(), connectedNode);
        }
      }
    }

    List configs = getConfigsFromBinaryConnection(connection);

    if (configs.isEmpty()) {
      getLogger().debug("(Carrier Publication) Could not load config from "
        + node.getHostName() + ", trying next node.");
      connection.shutdown();
      return false;
    }

    String appliedConfig = connection.replaceConfigWildcards(
      configs.get(0));
    Bucket config = configurationParser.parseBucket(appliedConfig);
    setConfig(config);
    connection.addObserver(new ConnectionObserver() {
      @Override
      public void connectionEstablished(SocketAddress sa, int reconnectCount) {
        getLogger().debug("Carrier Config Connection established to " + sa);
      }

      @Override
      public void connectionLost(SocketAddress sa) {
        getLogger().debug("Carrier Config Connection lost from " + sa);
        CouchbaseConnection conn = binaryConnection.getAndSet(null);
        try {
          conn.shutdown();
        } catch (IOException e) {
          getLogger().debug("Could not shut down Carrier Config Connection", e);
        }
        signalOutdated();
      }
    });
    binaryConnection.set(connection);
    return true;
  }

  /**
   * Load the configs from a binary connection through a broadcast op.
   *
   * Note that this operation is blocking, so run in a thread pool if needed.
   *
   * @param connection the connection to execute against.
   * @return a list of configs (potentially empty, but not null)
   */
  private List getConfigsFromBinaryConnection(
    final CouchbaseConnection connection) throws Exception {
    final List configs = Collections.synchronizedList(
      new ArrayList());

    CountDownLatch blatch = connection.broadcastOperation(
      new BroadcastOpFactory() {
        @Override
        public Operation newOp(MemcachedNode n, final CountDownLatch latch) {
          return new GetConfigOperationImpl(new OperationCallback() {
            @Override
            public void receivedStatus(OperationStatus status) {
              if (status.isSuccess()) {
                configs.add(status.getMessage());
              }
            }

            @Override
            public void complete() {
              latch.countDown();
            }
          });
        }
      }
    );

    blatch.await(connectionFactory.getOperationTimeout(),
      TimeUnit.MILLISECONDS);
    return configs;
  }

  /**
   * Helper method to initiate the http bootstrap process.
   *
   * If no config is found (because of an error), false is returned. For now,
   * this is delegated to the old HTTP provider, but no monitor is attached
   * for a subsequent streaming connection.
   *
   * @return true if the http bootstrap process was successful.
   */
  boolean bootstrapHttp() {
    if (disableHttpBootstrap) {
      getLogger().info("Http bootstrap manually disabled, skipping.");
      return false;
    }

    try {
      Bucket config = httpProvider.get().getBucketConfiguration(bucket);
      setConfig(config);
      isBinary = false;
      return true;
    } catch(Exception ex) {
      getLogger().info("Could not fetch config from http seed nodes.", ex);
      return false;
    }
  }

  /**
   * Start to monitor the bucket configuration, depending on the provider
   * used.
   */
  private void monitorBucket() {
    if (!isBinary) {
      httpProvider.get().subscribe(bucket, this);
    }
  }

  @Override
  public void reconfigure(final Bucket bucket) {
    setConfig(bucket);
  }

  @Override
  public Bucket getConfig() {
    if (config.get() == null) {
      bootstrap();
    }
    return config.get();
  }

  @Override
  public void setConfig(final Bucket config) {
    if (config.isNotUpdating()) {
      signalOutdated();
      return;
    }
    getLogger().debug("Applying new bucket config for bucket \"" + bucket
      + "\" (carrier publication: " + isBinary + "): " + config);

    this.config.set(config);
    httpProvider.get().updateBucket(config.getName(), config);
    updateSeedNodes();
    notifyObservers();

    manageTaintedConfig(config.getConfig());
  }

  /**
   * Orchestrating method to start/stop background config fetcher for binary
   * configs if tainted/not tainted anymore.
   *
   * @param config the config to check.
   */
  private void manageTaintedConfig(final Config config) {
    if (!isBinary) {
      return;
    }

    if (config.isTainted() && pollingBinary.compareAndSet(false, true)) {
      getLogger().debug("Found tainted configuration, starting carrier "
        + "poller.");
      Thread thread = new Thread(new BinaryConfigPoller());
      thread.setName("couchbase - carrier config poller");
      thread.start();
    }
  }

  /**
   * Updates the current list of seed nodes with a current one from the stored
   * configuration.
   */
  private void updateSeedNodes() {
    Config config = this.config.get().getConfig();

    List clusterNodes = config.getRestEndpoints();
    if (!clusterNodes.isEmpty()) {
      List newNodes = new ArrayList();
      for (String clusterNode : clusterNodes) {
        try {
          newNodes.add(new URI(clusterNode));
        } catch(URISyntaxException ex) {
          getLogger().warn("Could not add node to updated bucket list because "
            + "of a parsing exception.");
          getLogger().debug("Could not parse list because: " + ex);
        }
      }

      if (seedNodesAreDifferent(seedNodes, newNodes)) {
        potentiallyRandomizeNodeList(newNodes);
        synchronized (seedNodes) {
          seedNodes.clear();
          seedNodes.addAll(newNodes);
        }
        httpProvider.get().updateBaseListFromConfig(seedNodes);
      }
    }
  }

  /**
   * Randomize the given node list if set in the factory.
   *
   * @param list the list to shuffle.
   */
  private void potentiallyRandomizeNodeList(List list) {
    if (connectionFactory.getStreamingNodeOrder()
      == CouchbaseNodeOrder.ORDERED) {
      return;
    }
    Collections.shuffle(list);
  }

  /**
   * Checks if two given lists are different.
   *
   * @param left one list to check.
   * @param right the other list to check against.
   * @return true if they are different, false if they are the same.
   */
  private static boolean seedNodesAreDifferent(List left,
    List right) {
    if (left.size() != right.size()) {
      return true;
    }

    for (URI uri : left) {
      if (!right.contains(uri)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public void signalOutdated() {
    if (isBinary) {
      if (binaryConnection.get() == null) {
        bootstrap();
      } else {
        try {
          List configs = getConfigsFromBinaryConnection(binaryConnection.get());
          if (configs.isEmpty()) {
            bootstrap();
            return;
          }
          String appliedConfig = binaryConnection.get().replaceConfigWildcards(
            configs.get(0));
          Bucket config = configurationParser.parseBucket(appliedConfig);
          setConfig(config);
        } catch(Exception ex) {
          getLogger().info("Could not load config from existing "
            + "connection, rerunning bootstrap.", ex);
          bootstrap();
        }
      }
    } else {
      if (refreshingHttp.compareAndSet(false, true)) {
        Thread refresherThread = new Thread(new HttpProviderRefresher());
        refresherThread.setName("HttpConfigurationProvider Reloader");
        refresherThread.start();
      } else {
        getLogger().debug("Suppressing duplicate refreshing attempt.");
      }
    }
  }

  @Override
  public void reloadConfig() {
    if (isBinary) {
      signalOutdated();
    }
  }

  @Override
  public void shutdown() {
    if (httpProvider.get() != null) {
      httpProvider.get().shutdown();
    }
    if (binaryConnection.get() != null) {
      try {
        binaryConnection.get().shutdown();
      } catch (IOException e) {
        getLogger().warn("Could not shutdown carrier publication config "
          + "connection.", e);
      }
    }
  }

  @Override
  public String getAnonymousAuthBucket() {
    return ANONYMOUS_BUCKET;
  }

  @Override
  public void setConfig(final String config) {
    try {
      setConfig(configurationParser.parseBucket(config));
    } catch (Exception ex) {
      getLogger().warn("Got new config to update, but could not decode it. "
        + "Staying with old one.", ex);
    }
  }

  @Override
  public void subscribe(Reconfigurable rec) {
    observers.add(rec);
  }

  @Override
  public void unsubscribe(Reconfigurable rec) {
    observers.remove(rec);
  }

  /**
   * Notify all observers of a new configuration.
   */
  private void notifyObservers() {
    synchronized (observers) {
      for (Reconfigurable rec : observers) {
        getLogger().debug("Notifying Observer of new configuration: "
          + rec.getClass().getSimpleName());
        rec.reconfigure(getConfig());
      }
    }
  }

  class HttpProviderRefresher implements Runnable {

    @Override
    public void run() {
      try {

        long reconnectAttempt = 0;
        long backoffTime = 1000;
        long maxWaitTime = 10000;
        while(true) {
          try {
            long waitTime = reconnectAttempt++ * backoffTime;
            if(reconnectAttempt >= 10) {
              waitTime = maxWaitTime;
            }
            getLogger().info("Reconnect attempt " + reconnectAttempt
              + ", waiting " + waitTime + "ms");
            Thread.sleep(waitTime);

            ConfigurationProviderHTTP oldProvider = httpProvider.get();
            ConfigurationProviderHTTP newProvider =
              new ConfigurationProviderHTTP(seedNodes, bucket, password);
            httpProvider.set(newProvider);
            monitorBucket();
            oldProvider.shutdown();
            return;
          } catch(Exception ex) {
            getLogger().debug("Got exception while trying to reconnect the " +
              "configuration provider.", ex);
            continue;
          }
        }
      } finally {
        refreshingHttp.set(false);
      }
    }
  }

  /**
   * A config poller for carrier configurations.
   */
  class BinaryConfigPoller implements Runnable {

    /**
     * The time to wait between config poll intervals in ms.
     */
    private static final int waitPeriod = 1000;

    /**
     * Counter to log polling attempts for this poller.
     */
    private int attempt;

    /**
     * Calling {@link #signalOutdated()} against a running binary configuration
     * will trigger a config refresh.
     */
    @Override
    public void run() {
      try {
        while (isBinary && getConfig().getConfig().isTainted()) {
          getLogger().debug("Polling for new carrier configuration and " +
            "waiting " + waitPeriod + "ms (Attempt " + ++attempt + ").");
          signalOutdated();
          try {
            Thread.sleep(waitPeriod);
          } catch (InterruptedException e) {
            getLogger().warn("Got interrupted while trying to poll for new " +
              "carrier config.", e);
            break;
          }
        }
      } finally {
        getLogger().debug("Finished polling for new carrier configuration.");
        pollingBinary.set(false);
      }
    }
  }

  static class ConfigurationConnectionFactory
    extends CouchbaseConnectionFactory {
    ConfigurationConnectionFactory(List baseList, String bucketName,
      String password) throws IOException {
      super(baseList, bucketName, password);
    }

    @Override
    public NodeLocator createLocator(List nodes) {
      return new ArrayModNodeLocator(nodes, getHashAlg());
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy