curator-master/curator-framework/src/main/java/org/apache/curator/framework/imps/EnsembleTracker.java
/*
 * 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.curator.framework.imps;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.curator.ensemble.EnsembleProvider;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.WatcherRemoveCuratorFramework;
import org.apache.curator.framework.api.BackgroundCallback;
import org.apache.curator.framework.api.CuratorEvent;
import org.apache.curator.framework.api.CuratorEventType;
import org.apache.curator.framework.api.CuratorWatcher;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.utils.Compatibility;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.server.quorum.QuorumPeer;
import org.apache.zookeeper.server.quorum.flexible.QuorumMaj;
import org.apache.zookeeper.server.quorum.flexible.QuorumVerifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@VisibleForTesting
public class EnsembleTracker implements Closeable, CuratorWatcher {
    private final Logger log = LoggerFactory.getLogger(getClass());
    private final WatcherRemoveCuratorFramework client;
    private final EnsembleProvider ensembleProvider;
    private final AtomicReference<State> state = new AtomicReference<>(State.LATENT);
    private final AtomicInteger outstanding = new AtomicInteger(0);
    private final AtomicReference<QuorumMaj> currentConfig =
            new AtomicReference<>(new QuorumMaj(Maps.<Long, QuorumPeer.QuorumServer>newHashMap()));
    private final ConnectionStateListener connectionStateListener = new ConnectionStateListener() {
        @Override
        public void stateChanged(CuratorFramework client, ConnectionState newState) {
            if ((newState == ConnectionState.CONNECTED) || (newState == ConnectionState.RECONNECTED)) {
                try {
                    reset();
                } catch (Exception e) {
                    log.error("Trying to reset after reconnection", e);
                }
            }
        }

        @Override
        public boolean doNotProxy() {
            return true;
        }
    };

    private enum State {
        LATENT,
        STARTED,
        CLOSED
    }

    EnsembleTracker(CuratorFramework client, EnsembleProvider ensembleProvider) {
        this.client = client.newWatcherRemoveCuratorFramework();
        this.ensembleProvider = ensembleProvider;
    }

    public void start() throws Exception {
        Preconditions.checkState(state.compareAndSet(State.LATENT, State.STARTED), "Cannot be started more than once");
        client.getConnectionStateListenable().addListener(connectionStateListener);
        reset();
    }

    @Override
    public void close() {
        if (state.compareAndSet(State.STARTED, State.CLOSED)) {
            client.removeWatchers();
            client.getConnectionStateListenable().removeListener(connectionStateListener);
        }
    }

    @Override
    public void process(WatchedEvent event) throws Exception {
        if (event.getType() == Watcher.Event.EventType.NodeDataChanged) {
            reset();
        }
    }

    /**
     * Return the current quorum config
     *
     * @return config
     */
    public QuorumVerifier getCurrentConfig() {
        return currentConfig.get();
    }

    @VisibleForTesting
    public boolean hasOutstanding() {
        return outstanding.get() > 0;
    }

    private void reset() throws Exception {
        if ((client.getState() == CuratorFrameworkState.STARTED) && (state.get() == State.STARTED)) {
            BackgroundCallback backgroundCallback = new BackgroundCallback() {
                @Override
                public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
                    outstanding.decrementAndGet();
                    if ((event.getType() == CuratorEventType.GET_CONFIG)
                            && (event.getResultCode() == KeeperException.Code.OK.intValue())) {
                        processConfigData(event.getData());
                    }
                }
            };
            outstanding.incrementAndGet();
            try {
                client.getConfig()
                        .usingWatcher(this)
                        .inBackground(backgroundCallback)
                        .forEnsemble();
                outstanding.incrementAndGet(); // finally block will decrement
            } finally {
                outstanding.decrementAndGet();
            }
        }
    }

    @VisibleForTesting
    public static String configToConnectionString(QuorumVerifier data) throws Exception {
        StringBuilder sb = new StringBuilder();
        for (QuorumPeer.QuorumServer server : data.getAllMembers().values()) {
            if (server.clientAddr == null) {
                // Invalid client address configuration in zoo.cfg
                continue;
            }
            if (sb.length() != 0) {
                sb.append(",");
            }
            sb.append(getHostString(server)).append(":").append(server.clientAddr.getPort());
        }

        return sb.toString();
    }

    private static String getHostString(QuorumPeer.QuorumServer server) {
        InetSocketAddress clientAddr = server.clientAddr;
        InetAddress clientIpAddr = clientAddr.getAddress();
        if (clientIpAddr != null && clientIpAddr.isAnyLocalAddress()) {
            return Compatibility.getHostString(server);
        } else {
            return clientAddr.getHostString();
        }
    }

    private void processConfigData(byte[] data) throws Exception {
        Properties properties = new Properties();
        properties.load(new ByteArrayInputStream(data));
        log.info("New config event received: {}", properties);

        if (!properties.isEmpty()) {
            QuorumMaj newConfig = new QuorumMaj(properties);
            String connectionString = configToConnectionString(newConfig).trim();
            if (!connectionString.isEmpty()) {
                currentConfig.set(newConfig);
                String oldConnectionString = ensembleProvider.getConnectionString();
                int i = oldConnectionString.indexOf('/');
                if (i >= 0) {
                    String chroot = oldConnectionString.substring(i);
                    connectionString += chroot;
                }
                ensembleProvider.setConnectionString(connectionString);
            } else {
                log.debug("Invalid config event received: {}", properties);
            }
        } else {
            log.debug("Ignoring new config as it is empty");
        }
    }
}
