org.apache.sling.discovery.etcd.EtcdDiscoveryService.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sling.discovery.etcd.EtcdDiscoveryService.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.sling.discovery.etcd;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Map;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.apache.commons.io.Charsets;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.sling.discovery.etcd.backoff.BackOff;
import org.apache.sling.discovery.etcd.backoff.ConstantBackOff;
import org.apache.sling.discovery.etcd.backoff.SquareBackOff;
import org.apache.sling.discovery.etcd.cluster.AutomaticClustering;
import org.apache.sling.discovery.etcd.cluster.Clustering;
import org.apache.sling.discovery.etcd.cluster.ConfigClustering;
import org.apache.sling.discovery.etcd.fsm.Context;
import org.apache.sling.discovery.etcd.fsm.Event;
import org.apache.sling.discovery.etcd.fsm.RunnerFactory;
import org.apache.sling.discovery.etcd.fsm.RunnerFactoryImpl;
import org.apache.sling.discovery.etcd.gzip.GzipRequestInterceptor;
import org.apache.sling.discovery.etcd.gzip.GzipResponseInterceptor;
import org.apache.sling.discovery.etcd.run.Announcer;
import org.apache.sling.discovery.etcd.run.LocalUpdater;
import org.apache.sling.discovery.etcd.fsm.States;
import org.apache.sling.discovery.etcd.run.RemoteUpdater;
import org.apache.sling.etcd.client.EtcdClient;
import org.apache.sling.etcd.client.EtcdClientFactory;
import org.apache.commons.io.IOUtils;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.PropertyOption;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.References;
import org.apache.felix.scr.annotations.Service;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.apache.sling.commons.threads.ThreadPool;
import org.apache.sling.commons.threads.ThreadPoolManager;
import org.apache.sling.discovery.DiscoveryService;
import org.apache.sling.discovery.PropertyProvider;
import org.apache.sling.discovery.TopologyEventListener;
import org.apache.sling.discovery.TopologyView;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
@Component(immediate = true, metatype = true, label = "CoreOS etcd discovery", description = "Note regarding wait function parameters: The mathematical function can either be I. a constant or II. a square function. "
        + "The function is determined by the parameters provided. In order to use a constant function, provides a constant in "
        + "millisecond (e.g. '20000' for a constant interval of 20 seconds). In order to use a power function, provides the minimum "
        + "and maximum interval in milliseconds as well as the number of consecutive steps to go from the minimum to the maximum "
        + "interval (e.g. '10000:60000:3' for an interval of minimum 10 seconds, maximum 60 seconds and a progression in 3 steps).")
@References({
        @Reference(name = "topologyEventListener", referenceInterface = TopologyEventListener.class, bind = "bindTopologyEventListener", unbind = "unbindTopologyEventListener", cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC),
        @Reference(name = "propertyProvider", referenceInterface = PropertyProvider.class, bind = "bindPropertyProvider", unbind = "unbindPropertyProvider", cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC) })
public class EtcdDiscoveryService implements DiscoveryService {

    private static final Logger LOG = LoggerFactory.getLogger(EtcdDiscoveryService.class);

    /**
     * The UTF8 charset.
     */
    private static final String UTF8 = "UTF-8";

    /**
     * Leeway factor to be applied to the announce TTL
     */
    public static final double ANNOUNCE_TTL_LEEWAY = 1.25D;

    /**
     * Define the path of the node holding the clusterId for automatic clustering mode.
     * The path must point to a resource that is shared among all instances.
     */
    private static final String DISCOVERY_PATH = "/etc/discovery/etcd";

    private static final String DEFAULT_ROOT_KEY = "/discovery";

    /**
     * The root key for implementing the discovery protocol.
     */
    @Property(label = "Root key", description = "The etcd root key used for implementing the discovery protocol, e.g. '/discovery'", value = DEFAULT_ROOT_KEY)
    protected static final String ROOT_KEY = "rootKey";

    private static final String DEFAULT_ENDPOINT = "http://127.0.0.1:4001";

    /**
     * The etcd endpoint to access the etcd peers.
     */
    @Property(label = "etcd endpoint", description = "The endpoint uri to access the etcd peers (e.g. 'http://127.0.0.1:4001'). "
            + "If the client is communicating with an etcd cluster, then the endpoint should reference a readwrite etcd proxy node. "
            + "If the client is communicating with a single etcd node, the endpoint could reference it directly.", value = DEFAULT_ENDPOINT)
    protected static final String ENDPOINT = "endpoint";

    private static final int DEFAULT_SOCKET_TIMEOUT = 5000;

    /**
     * The connection socket timeout.
     * @link org.apache.http.client.configRequestConfig#getSocketTimeout()
     */
    @Property(label = "Socket Timeout", description = "Defines the socket timeout (SO_TIMEOUT) in milliseconds, which "
            + "is the timeout for waiting for data. A timeout value of zero is interpreted as an infinite timeout. "
            + "A negative value is interpreted as undefined (system default).", intValue = DEFAULT_SOCKET_TIMEOUT)
    protected static final String SOCKET_TIMEOUT = "socketTimeout";

    private static final int DEFAULT_CONNECTION_TIMEOUT = 2500;

    /**
     * The connection socket connection timeout.
     * @link org.apache.http.client.configRequestConfig#getConnectTimeout()
     */
    @Property(label = "Connection Timeout", description = "Determines the timeout in milliseconds until a connection is "
            + "established. A timeout value of zero is interpreted as an infinite timeout. A timeout value of zero is "
            + "interpreted as an infinite timeout. A negative value is interpreted as undefined (system default).", intValue = DEFAULT_CONNECTION_TIMEOUT)
    protected static final String CONNECTION_TIMEOUT = "connectionTimeout";

    private static final String DEFAULT_ANNOUNCE_RENEWAL_PERIOD = "20000";

    @Property(label = "Announce Renewal Period", description = "The parameters of the mathematical function used to compute the time "
            + "interval between two announce renewals. An announce renewal consists of extending the ttl of the local instance announce key on etcd."
            + "See 'wait function parameters' above for details regarding the possible functions."
            + "The steps are incremented whenever the operation is successful and reset to 0 in case of failure.", value = DEFAULT_ANNOUNCE_RENEWAL_PERIOD)
    protected static final String ANNOUNCE_RENEWAL_PERIOD = "announceRenewalPeriod";

    private static final String DEFAULT_TOPOLOGY_UPDATE_PERIOD = "10000";

    @Property(label = "Remote Topology Update Period", description = "The parameters of the mathematical function used to compute the time "
            + "interval between two remote topology updates. A remote topology update consists of fetching the announces and properties (if needed) from etcd for the remote instances."
            + "See 'wait function parameters' above for details regarding the possible functions."
            + "The steps are incremented whenever the operation is successful and reset to 0 in case of failure.", value = DEFAULT_TOPOLOGY_UPDATE_PERIOD)
    protected static final String TOPOLOGY_UPDATE_PERIOD = "topologyUpdatePeriod";

    private static final String DEFAULT_VIEW_UPDATE_PERIOD = "2000";

    @Property(label = "Topology View Update Period", description = "The period in seconds between two updates of the"
            + "topology view", value = DEFAULT_VIEW_UPDATE_PERIOD, propertyPrivate = true)
    protected static final String VIEW_UPDATE_PERIOD = "viewUpdatePeriod";

    /**
     * 1 second to 20 minutes in 50 steps.
     */
    private static final String DEFAULT_ETCD_BACK_OFF = "1000:1200000:50";

    @Property(label = "etcd error Back-off period", description = "The parameters of the mathematical function used to compute the back-off "
            + "interval to wait upon etcd internal errors."
            + "See 'wait function parameters' above for details regarding the possible functions."
            + "The steps are incremented whenever the etcd produces an internal error and reset to 0 in case of non erroneous behavior.", value = DEFAULT_ETCD_BACK_OFF)
    protected static final String ETCD_BACK_OFF = "etcdBackOff";

    /**
     * 1 seconds to 10 minutes in 50 steps.
     */
    private static final String DEFAULT_IO_ERROR_BACK_OFF = "1000:600000:50";

    @Property(label = "etcd I/O error back-off period", description = "The parameters of the mathematical function used to compute the "
            + "back-off interval upon I/O errors."
            + "See 'wait function parameters' above for details regarding the possible functions."
            + "The steps are incremented whenever the communication with etcd produces an I/O error and reset to 0 in case of  successful communication.", value = DEFAULT_IO_ERROR_BACK_OFF)
    protected static final String IO_ERROR_BACK_OFF = "ioErrorBackOff";

    /**
     * Automatic clustering leverages the repository for defining the
     * cluster id in which the local instance belongs.
     */
    protected static final String AUTOMATIC_CLUSTERING = "autoClustering";

    /**
     * Config clustering uses a configuration to define the cluster id in
     * which the local instance belongs.
     */
    protected static final String CONFIG_CLUSTERING = "configClustering";

    @Property(label = "Clustering mode", description = "With 'Automatic' mode (default) the implementation computes "
            + "the cluster identifier of the local instance automatically, by using the repository. "
            + "The 'Automatic' mode create clusters of instances that share the same repository. "
            + "The 'Automatic' mode is only supported by Oak based repositories. "
            + "If the 'Automatic' mode is selected but the repository is not supported, then the implementation "
            + "falls back to the 'Configuration' mode."
            + "The 'Configuration' mode allows to specify the cluster identifier the local instance belongs to, "
            + "via the 'clusterId' property.", value = AUTOMATIC_CLUSTERING, options = {
                    @PropertyOption(name = AUTOMATIC_CLUSTERING, value = "Automatic"),
                    @PropertyOption(name = CONFIG_CLUSTERING, value = "Configuration") })
    protected static final String CLUSTERING_MODE = "clusteringMode";

    /**
     * Default cluster identifier.
     */
    private static final String DEFAULT_CLUSTER_ID = "default";

    @Property(label = "Cluster identifier", description = "The cluster identifier in which the instance belongs to. "
            + "This identifier is used with 'Configuration' clustering mode and when the 'Automatic' clustering mode "
            + "is not supported by the repository. The cluster identifier is case sensitive and must contain only "
            + "alphanumeric characters as well as the characters '-' and '_'. If the configured cluster identifier does "
            + "not comply, the default cluster id '" + DEFAULT_CLUSTER_ID
            + "' will be used.", value = DEFAULT_CLUSTER_ID)
    protected static final String CLUSTER_ID = "clusterId";

    @Property(label = "Keystore file path", description = "The path to the keystore containing key material "
            + "(private key, certificate) for the local instance and/or trust material (certificates) used by the "
            + "etcd client. The configuration is optional. If the path is blank, the Keystore provided by the standard "
            + "JSSE mechanism is used. This configuration is aiming at deployments which use certificates signed by "
            + "a non standard CA root and which want to use the Keystore for etcd support only. The keystore default "
            + "type is JKS but can be overriden with the 'keystore.type' system property.", value = "")
    protected static final String KEYSTORE_FILE_PATH = "keystoreFilePath";

    @Property(label = "Keystore password file path", description = "The path to the UTF-8 encoded file that contains "
            + "the KeyStore password in clear text. The password is required only when the 'keystoreFilePath' "
            + "is defined.", value = "")
    protected static final String KEYSTORE_PWD_FILE_PATH = "keystorePwdFilePath";

    @Reference
    private EtcdClientFactory etcdClientFactory;

    @Reference
    private SlingSettingsService slingSettingsService;

    @Reference
    private ThreadPoolManager threadPoolManager;

    @Reference
    private SlingRepository slingRepository;

    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    private ThreadPool etcdThreadPool;

    private Announcer announcer;

    private RemoteUpdater remoteUpdater;

    private LocalUpdater localUpdater;

    private CloseableHttpClient httpClient;

    private EtcdService etcdService;

    private int socketTimeout;

    private int connectionTimeout;

    private Context context;

    private PoolingHttpClientConnectionManager connectionManager;

    private final ViewManager viewManager = new ViewManager();

    private final PropertiesService propertiesService = new PropertiesService();

    private volatile EtcdStats etcdStats;

    @Activate
    protected void activate(ComponentContext cc) {

        final ThreadPoolManager threadPoolManager = this.threadPoolManager;
        if (threadPoolManager == null) {
            throw new IllegalStateException("ThreadPoolManager service not found");
        }

        final SlingSettingsService slingSettingsService = this.slingSettingsService;
        if (slingSettingsService == null) {
            throw new IllegalStateException("SlingSettingsService service not found");
        }

        final EtcdClientFactory etcdClientFactory = this.etcdClientFactory;
        if (etcdClientFactory == null) {
            throw new IllegalStateException("EtcdClientFactory service not found");
        }

        Dictionary props = cc.getProperties();
        String rootKey = PropertiesUtil.toString(props.get(ROOT_KEY), DEFAULT_ROOT_KEY);
        if (rootKey.endsWith("/") && rootKey.length() > 1) {
            rootKey = rootKey.substring(0, rootKey.length() - 1);
            LOG.debug("Chopped last slash from rootKey: {}", rootKey);
        }
        socketTimeout = PropertiesUtil.toInteger(props.get(SOCKET_TIMEOUT), DEFAULT_SOCKET_TIMEOUT);
        connectionTimeout = PropertiesUtil.toInteger(props.get(CONNECTION_TIMEOUT), DEFAULT_SOCKET_TIMEOUT);
        String slingId = slingSettingsService.getSlingId();
        String serverInfo = getServerInfo(cc);
        LOG.debug("slingId: {} serverInfo: {}", new String[] { slingId, serverInfo });
        BackOff announceRenewalPeriod = build(
                PropertiesUtil.toString(props.get(ANNOUNCE_RENEWAL_PERIOD), DEFAULT_ANNOUNCE_RENEWAL_PERIOD),
                DEFAULT_ANNOUNCE_RENEWAL_PERIOD);
        LOG.debug("announce renewal period: {}", announceRenewalPeriod);
        BackOff topologyUpdatePeriod = build(
                PropertiesUtil.toString(props.get(TOPOLOGY_UPDATE_PERIOD), DEFAULT_TOPOLOGY_UPDATE_PERIOD),
                DEFAULT_TOPOLOGY_UPDATE_PERIOD);
        LOG.debug("topology update period: {}", topologyUpdatePeriod);
        BackOff viewUpdatePeriod = build(
                PropertiesUtil.toString(props.get(VIEW_UPDATE_PERIOD), DEFAULT_VIEW_UPDATE_PERIOD),
                DEFAULT_VIEW_UPDATE_PERIOD);
        LOG.debug("view update period: {}", viewUpdatePeriod);
        BackOff etcdBackOff = build(PropertiesUtil.toString(props.get(ETCD_BACK_OFF), DEFAULT_ETCD_BACK_OFF),
                DEFAULT_ETCD_BACK_OFF);
        LOG.debug("etcd back-off: {}", etcdBackOff);
        BackOff ioErrorBackOff = build(
                PropertiesUtil.toString(props.get(IO_ERROR_BACK_OFF), DEFAULT_IO_ERROR_BACK_OFF),
                DEFAULT_IO_ERROR_BACK_OFF);
        LOG.debug("I/O error back-off: {}", ioErrorBackOff);
        int maxAnnounceTtl = new BigDecimal(
                announceRenewalPeriod.max() * EtcdDiscoveryService.ANNOUNCE_TTL_LEEWAY / 1000.0D)
                        .setScale(0, BigDecimal.ROUND_UP).intValue();
        LOG.debug("max announce ttl in second: {}", maxAnnounceTtl);
        String clusterId = PropertiesUtil.toString(props.get(CLUSTER_ID), DEFAULT_CLUSTER_ID);
        String clusteringMode = PropertiesUtil.toString(props.get(CLUSTERING_MODE), AUTOMATIC_CLUSTERING);
        final Clustering clustering;
        if (AUTOMATIC_CLUSTERING.equals(clusteringMode)) {

            final SlingRepository slingRepository = this.slingRepository;
            if (slingRepository == null) {
                throw new IllegalStateException("SlingRepository service not found");
            }

            final ResourceResolverFactory resourceResolverFactory = this.resourceResolverFactory;
            if (resourceResolverFactory == null) {
                throw new IllegalStateException("ResourceResolverFactory service not found");
            }

            Clustering automatic = new AutomaticClustering(slingRepository, resourceResolverFactory,
                    DISCOVERY_PATH);
            if (automatic.isSupported()) {
                // repository mode supported.
                // we don't get the cluster id here,
                // as the method call may take time.
                clustering = automatic;
                LOG.info("Clustering mode 'Automatic' enabled.");
            } else {
                LOG.warn(
                        "The repository does not support 'Automatic' clustering mode. Falling back to 'Configuration' clustering mode.");
                clustering = buildConfigClustering(clusterId);
            }
        } else {
            clustering = buildConfigClustering(clusterId);
        }

        buildHttpClient(PropertiesUtil.toString(props.get(KEYSTORE_FILE_PATH), "").trim(),
                PropertiesUtil.toString(props.get(KEYSTORE_PWD_FILE_PATH), "").trim());
        URI endpoint = parseEndpoint(PropertiesUtil.toString(props.get(ENDPOINT), DEFAULT_ENDPOINT));
        EtcdClient etcdClient = etcdClientFactory.create(httpClient, endpoint);
        etcdStats = new EtcdStats(etcdClient);
        PropertiesMap propertiesMap = new PropertiesMap(slingId);
        Announce initAnnounce = buildInitAnnounce(slingId, serverInfo);
        AnnouncesMap announcesMap = new AnnouncesMap(initAnnounce);
        viewManager.updateView(buildInitView(initAnnounce, slingId)); // must happen before starting the LocalUpdater thread.
        etcdService = new EtcdService(etcdClient, rootKey);
        RunnerFactory factory = new RunnerFactoryImpl(etcdService, announcesMap, clustering, etcdBackOff,
                ioErrorBackOff, slingId, serverInfo, maxAnnounceTtl);
        etcdThreadPool = threadPoolManager.get("CoreOS etcd client threads");
        context = new Context(States.GET_CLUSTER, factory, etcdThreadPool);
        context.init(States.GET_CLUSTER);
        announcer = new Announcer(context, etcdService, announcesMap, propertiesMap, slingId, serverInfo,
                announceRenewalPeriod);
        etcdThreadPool.execute(announcer);
        remoteUpdater = new RemoteUpdater(context, etcdService, topologyUpdatePeriod, announcesMap, propertiesMap,
                slingId);
        etcdThreadPool.execute(remoteUpdater);
        localUpdater = new LocalUpdater(context, propertiesService, viewManager, viewUpdatePeriod, announcesMap,
                propertiesMap, slingId, serverInfo);
        etcdThreadPool.execute(localUpdater);
        LOG.info("Activated etcd discovery service for slingId: {}, serverInfo: {}, rootKey: {}",
                new Object[] { slingId, serverInfo, rootKey });
    }

    @Deactivate
    protected void deactivate() {
        if (context != null) {
            context.next(Event.STOPPED);
        }
        IOUtils.closeQuietly(httpClient);
        if (connectionManager != null) {
            IOUtils.closeQuietly(connectionManager);
        }
        if (announcer != null) {
            announcer.stop();
        }
        if (remoteUpdater != null) {
            remoteUpdater.stop();
        }
        if (localUpdater != null) {
            localUpdater.stop();
        }
        if (etcdThreadPool != null) {
            threadPoolManager.release(etcdThreadPool);
            etcdThreadPool = null;
        }
        httpClient = null;
        connectionManager = null;
        etcdService = null;
        LOG.info("Deactivated etcd discovery service");
    }

    /**
     * {@inheritDoc}
     */
    @Nonnull
    public TopologyView getTopology() {
        return viewManager.getView();
    }

    @Nonnull
    protected Context getContext() {
        return context;
    }

    @Nullable
    protected EtcdStats getEtcdStats() {
        return etcdStats;
    }

    private void buildHttpClient(@Nonnull String keystoreFilePath, @Nonnull String keystorePwdFilePath) {

        boolean hasKeyStore = !isEmpty(keystoreFilePath);

        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
                .setConnectTimeout(connectionTimeout).setRedirectsEnabled(true).setStaleConnectionCheckEnabled(true)
                .build();

        HttpClientBuilder builder = HttpClients.custom().setDefaultRequestConfig(requestConfig)
                .addInterceptorFirst(new GzipRequestInterceptor())
                .addInterceptorFirst(new GzipResponseInterceptor());

        if (hasKeyStore) {

            final SSLContextBuilder sslContextBuilder = SSLContexts.custom();

            LOG.info("Loading keystore from file: {}", keystoreFilePath);
            char[] pwd = readPwd(keystorePwdFilePath);
            try {
                KeyStore keystore = loadKeyStore(keystoreFilePath, pwd);
                sslContextBuilder.loadTrustMaterial(keystore);
                sslContextBuilder.loadKeyMaterial(keystore, pwd);
                LOG.info("Setup custom SSL context");
                SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
                        sslContextBuilder.build());
                Registry<ConnectionSocketFactory> connectionSocketFactory = RegistryBuilder
                        .<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.INSTANCE)
                        .register("https", sslConnectionSocketFactory).build();

                builder.setSSLSocketFactory(sslConnectionSocketFactory);

                connectionManager = new PoolingHttpClientConnectionManager(connectionSocketFactory);

            } catch (UnrecoverableKeyException e) {
                throw wrap(e);
            } catch (NoSuchAlgorithmException e) {
                throw wrap(e);
            } catch (KeyStoreException e) {
                throw wrap(e);
            } catch (KeyManagementException e) {
                throw wrap(e);
            } finally {
                reset(pwd);
            }

        } else {
            connectionManager = new PoolingHttpClientConnectionManager();
        }

        builder.setConnectionManager(connectionManager);
        httpClient = builder.build();
    }

    @Nonnull
    private URI parseEndpoint(@Nonnull String ep) {
        try {
            return new URI(ep);
        } catch (URISyntaxException e) {
            LOG.info("Failed to parse endpoint: {}, using default endpoint: {}",
                    new Object[] { ep, DEFAULT_ENDPOINT });
            try {
                return new URI(DEFAULT_ENDPOINT);
            } catch (URISyntaxException shouldNeverHappen) {
                throw new IllegalStateException(
                        String.format("Failed to create uri from the default endpoint: %s", e.getMessage()));
            }
        }
    }

    @Nonnull
    private String getServerInfo(ComponentContext componentContext) {
        String servicePort = componentContext.getBundleContext().getProperty("org.osgi.service.http.port");
        String port = servicePort != null ? servicePort : "";
        try {
            return InetAddress.getLocalHost().getCanonicalHostName() + ":" + port;
        } catch (Exception e) {
            LOG.warn("Failed to filterBySlingId the instance canonical hostname", e);
            return "localhost:" + port;
        }
    }

    @Nonnull
    private Announce buildInitAnnounce(@Nonnull String slingId, @Nonnull String serverInfo) {
        AnnounceData data = new AnnounceData(slingId, serverInfo, "isolated", 0);
        return new Announce(data, "/isolated/0");
    }

    @Nonnull
    private EtcdTopologyView buildInitView(@Nonnull Announce local, @Nonnull String slingId) {
        return new EtcdTopologyView(new Announces(local), Collections.<String, Map<String, String>>emptyMap(),
                slingId, false);
    }

    @Nonnull
    private BackOff build(@Nonnull String config, @Nonnull String defaultConfig) {
        try {
            return build(config);
        } catch (Exception e) {
            LOG.info("Failed to parse back-off: {}, using default: {}", new String[] { config, defaultConfig });
            return build(defaultConfig);
        }
    }

    @Nonnull
    private BackOff build(@Nonnull String config) {
        String[] chunks = config.trim().split(":");
        if (chunks.length == 3) {
            // power back-off
            return new SquareBackOff(Long.valueOf(chunks[0]), Long.valueOf(chunks[1]), Integer.valueOf(chunks[2]));
        } else {
            return new ConstantBackOff(Long.parseLong(config));
        }
    }

    private Clustering buildConfigClustering(String clusterId) {
        Clustering clustering = new ConfigClustering(clusterId, DEFAULT_CLUSTER_ID);
        LOG.info(String.format("Clustering mode 'Configuration' enabled with cluster id: %s",
                clustering.getClusterId()));
        return clustering;
    }

    private boolean isEmpty(@Nullable String value) {
        return (value == null) || ("".equals(value));
    }

    @Nullable
    private char[] readPwd(@Nonnull String filePath) {
        if (!isEmpty(filePath)) {
            InputStream fis = null;
            try {
                fis = new FileInputStream(checkFile(new File(filePath)));
                return IOUtils.toString(fis, Charsets.toCharset(UTF8)).trim().toCharArray();
            } catch (FileNotFoundException e) {
                throw wrap(e);
            } catch (IOException e) {
                throw wrap(e);
            } finally {
                IOUtils.closeQuietly(fis);
            }
        }
        return null;

    }

    private void reset(@Nullable char[] array) {
        if (array != null) {
            Arrays.fill(array, ' ');
        }
    }

    @Nonnull
    private KeyStore loadKeyStore(@Nonnull String filePath, @Nullable char[] pwd) {
        InputStream is = null;
        try {
            is = new FileInputStream(checkFile(new File(filePath)));
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(is, pwd);
            return keyStore;
        } catch (CertificateException e) {
            throw wrap(e);
        } catch (NoSuchAlgorithmException e) {
            throw wrap(e);
        } catch (KeyStoreException e) {
            throw wrap(e);
        } catch (IOException e) {
            throw wrap(e);
        } finally {
            IOUtils.closeQuietly(is);
        }
    }

    private File checkFile(@Nonnull File file) {
        if (!file.exists()) {
            throw new EtcdDiscoveryRuntimeException(String.format("The file: %s does not exist", file));
        }

        if (file.isDirectory()) {
            throw new EtcdDiscoveryRuntimeException(String.format("The file: %s is a directory", file));
        }

        if (!file.canRead()) {
            throw new EtcdDiscoveryRuntimeException(String.format("The file: %s can't be read", file));
        }
        return file;
    }

    private EtcdDiscoveryRuntimeException wrap(Exception e) {
        return new EtcdDiscoveryRuntimeException(e.getMessage(), e);
    }

    protected void bindTopologyEventListener(TopologyEventListener listener) {
        viewManager.bind(listener);
    }

    protected void unbindTopologyEventListener(TopologyEventListener listener) {
        viewManager.unbind(listener);
    }

    protected void bindPropertyProvider(PropertyProvider propertyProvider, Map<String, Object> properties) {
        propertiesService.bind(propertyProvider, properties);
    }

    protected void unbindPropertyProvider(PropertyProvider propertyProvider, Map<String, Object> properties) {
        propertiesService.unbind(propertyProvider, properties);
    }

}