Example #1
0
 private ThreadFactory getThreadFactory(CuratorFrameworkFactory.Builder builder) {
   ThreadFactory threadFactory = builder.getThreadFactory();
   if (threadFactory == null) {
     threadFactory = ThreadUtils.newThreadFactory("CuratorFramework");
   }
   return threadFactory;
 }
/**
 * Ensemble provider that polls a cluster of Exhibitor (https://github.com/Netflix/exhibitor)
 * instances for the connection string. If the set of instances should change, new ZooKeeper
 * connections will use the new connection string.
 */
public class ExhibitorEnsembleProvider implements EnsembleProvider {
  private final Logger log = LoggerFactory.getLogger(getClass());
  private final AtomicReference<Exhibitors> exhibitors = new AtomicReference<Exhibitors>();
  private final AtomicReference<Exhibitors> masterExhibitors = new AtomicReference<Exhibitors>();
  private final ExhibitorRestClient restClient;
  private final String restUriPath;
  private final int pollingMs;
  private final RetryPolicy retryPolicy;
  private final ExecutorService service =
      ThreadUtils.newSingleThreadExecutor("ExhibitorEnsembleProvider");
  private final Random random = new Random();
  private final AtomicReference<String> connectionString = new AtomicReference<String>("");
  private final AtomicReference<State> state = new AtomicReference<State>(State.LATENT);

  private static final String MIME_TYPE = "application/x-www-form-urlencoded";

  private static final String VALUE_PORT = "port";
  private static final String VALUE_COUNT = "count";
  private static final String VALUE_SERVER_PREFIX = "server";

  private enum State {
    LATENT,
    STARTED,
    CLOSED
  }

  /**
   * @param exhibitors the current set of exhibitor instances (can be changed later via {@link
   *     #setExhibitors(Exhibitors)})
   * @param restClient the rest client to use (use {@link DefaultExhibitorRestClient} for most
   *     cases)
   * @param restUriPath the path of the REST call used to get the server set. Usually: <code>
   *     /exhibitor/v1/cluster/list</code>
   * @param pollingMs how ofter to poll the exhibitors for the list
   * @param retryPolicy retry policy to use when connecting to the exhibitors
   */
  public ExhibitorEnsembleProvider(
      Exhibitors exhibitors,
      ExhibitorRestClient restClient,
      String restUriPath,
      int pollingMs,
      RetryPolicy retryPolicy) {
    this.exhibitors.set(exhibitors);
    this.masterExhibitors.set(exhibitors);
    this.restClient = restClient;
    this.restUriPath = restUriPath;
    this.pollingMs = pollingMs;
    this.retryPolicy = retryPolicy;
  }

  /**
   * Change the set of exhibitors to poll
   *
   * @param newExhibitors new set
   */
  public void setExhibitors(Exhibitors newExhibitors) {
    exhibitors.set(newExhibitors);
    masterExhibitors.set(newExhibitors);
  }

  /**
   * Can be called prior to starting the Curator instance to set the current connection string
   *
   * @throws Exception errors
   */
  public void pollForInitialEnsemble() throws Exception {
    Preconditions.checkState(state.get() == State.LATENT, "Cannot be called after start()");
    poll();
  }

  @Override
  public void start() throws Exception {
    Preconditions.checkState(state.compareAndSet(State.LATENT, State.STARTED), "Already started");

    service.submit(
        new Runnable() {
          @Override
          public void run() {
            try {
              while (!Thread.currentThread().isInterrupted()) {
                poll();
                Thread.sleep(pollingMs);
              }
            } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
            }
          }
        });
  }

  @Override
  public void close() throws IOException {
    Preconditions.checkState(state.compareAndSet(State.STARTED, State.CLOSED), "Already closed");

    service.shutdownNow();
  }

  @Override
  public String getConnectionString() {
    return connectionString.get();
  }

  @VisibleForTesting
  protected void poll() {
    Exhibitors localExhibitors = exhibitors.get();
    Map<String, String> values = queryExhibitors(localExhibitors);

    int count = getCountFromValues(values);
    if (count == 0) {
      log.warn("0 count returned from Exhibitors. Using backup connection values.");
      values = useBackup(localExhibitors);
      count = getCountFromValues(values);
    }

    if (count > 0) {
      int port = Integer.parseInt(values.get(VALUE_PORT));
      StringBuilder newConnectionString = new StringBuilder();
      List<String> newHostnames = Lists.newArrayList();

      for (int i = 0; i < count; ++i) {
        if (newConnectionString.length() > 0) {
          newConnectionString.append(",");
        }
        String server = values.get(VALUE_SERVER_PREFIX + i);
        newConnectionString.append(server).append(":").append(port);
        newHostnames.add(server);
      }

      String newConnectionStringValue = newConnectionString.toString();
      if (!newConnectionStringValue.equals(connectionString.get())) {
        log.info(
            "Connection string has changed. Old value (%s), new value (%s)",
            connectionString.get(), newConnectionStringValue);
      }
      Exhibitors newExhibitors =
          new Exhibitors(
              newHostnames,
              localExhibitors.getRestPort(),
              new Exhibitors.BackupConnectionStringProvider() {
                @Override
                public String getBackupConnectionString() throws Exception {
                  return masterExhibitors
                      .get()
                      .getBackupConnectionString(); // this may be overloaded by clients. Make sure
                                                    // there is always a method call to get the
                                                    // value.
                }
              });
      connectionString.set(newConnectionStringValue);
      exhibitors.set(newExhibitors);
    }
  }

  private int getCountFromValues(Map<String, String> values) {
    try {
      return Integer.parseInt(values.get(VALUE_COUNT));
    } catch (NumberFormatException e) {
      // ignore
    }
    return 0;
  }

  private Map<String, String> useBackup(Exhibitors localExhibitors) {
    Map<String, String> values = newValues();

    try {
      String backupConnectionString = localExhibitors.getBackupConnectionString();

      int thePort = -1;
      int count = 0;
      for (String spec : backupConnectionString.split(",")) {
        spec = spec.trim();
        String[] parts = spec.split(":");
        if (parts.length == 2) {
          String hostname = parts[0];
          int port = Integer.parseInt(parts[1]);
          if (thePort < 0) {
            thePort = port;
          } else if (port != thePort) {
            log.warn("Inconsistent port in connection component: " + spec);
          }
          values.put(VALUE_SERVER_PREFIX + count, hostname);
          ++count;
        } else {
          log.warn("Bad backup connection component: " + spec);
        }
      }
      values.put(VALUE_COUNT, Integer.toString(count));
      values.put(VALUE_PORT, Integer.toString(thePort));
    } catch (Exception e) {
      log.error("Couldn't get backup connection string", e);
    }
    return values;
  }

  private Map<String, String> newValues() {
    Map<String, String> values = Maps.newHashMap();
    values.put(VALUE_COUNT, "0");
    return values;
  }

  private static Map<String, String> decodeExhibitorList(String str)
      throws UnsupportedEncodingException {
    Map<String, String> values = Maps.newHashMap();
    for (String spec : str.split("&")) {
      String[] parts = spec.split("=");
      if (parts.length == 2) {
        values.put(parts[0], URLDecoder.decode(parts[1], "UTF-8"));
      }
    }

    return values;
  }

  private Map<String, String> queryExhibitors(Exhibitors localExhibitors) {
    Map<String, String> values = newValues();

    long start = System.currentTimeMillis();
    int retries = 0;
    boolean done = false;
    while (!done) {
      List<String> hostnames = Lists.newArrayList(localExhibitors.getHostnames());
      if (hostnames.size() == 0) {
        done = true;
      } else {
        String hostname = hostnames.get(random.nextInt(hostnames.size()));
        try {
          String encoded =
              restClient.getRaw(hostname, localExhibitors.getRestPort(), restUriPath, MIME_TYPE);
          values.putAll(decodeExhibitorList(encoded));
          done = true;
        } catch (Throwable e) {
          if (retryPolicy.allowRetry(
              retries++, System.currentTimeMillis() - start, RetryLoop.getDefaultRetrySleeper())) {
            log.warn("Couldn't get servers from Exhibitor. Retrying.", e);
          } else {
            log.error("Couldn't get servers from Exhibitor. Giving up.", e);
            done = true;
          }
        }
      }
    }

    return values;
  }
}