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

io.fabric8.groups.internal.ZooKeeperGroup Maven / Gradle / Ivy

/**
 *  Copyright 2005-2014 Red Hat, Inc.
 *
 *  Red Hat 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 io.fabric8.groups.internal;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.listen.ListenerContainer;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.utils.EnsurePath;
import org.apache.curator.utils.ThreadUtils;
import org.apache.curator.utils.ZKPaths;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.Stat;
import io.fabric8.groups.Group;
import io.fabric8.groups.GroupListener;
import io.fabric8.groups.NodeState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 

A utility that attempts to keep all data from all children of a ZK path locally cached. This class * will watch the ZK path, respond to update/create/delete events, pull down the data, etc. You can * register a listener that will get notified when changes occur.

*

*

IMPORTANT - it's not possible to stay transactionally in sync. Users of this class must * be prepared for false-positives and false-negatives. Additionally, always use the version number * when updating data to avoid overwriting another process' change.

*/ public class ZooKeeperGroup implements Group { static public final ObjectMapper MAPPER = new ObjectMapper(); static private final Logger LOG = LoggerFactory.getLogger(ZooKeeperGroup.class); private final Class clazz; private final CuratorFramework client; private final String path; private final ExecutorService executorService; private final EnsurePath ensurePath; private final BlockingQueue operations = new LinkedBlockingQueue(); private final ListenerContainer> listeners = new ListenerContainer>(); protected final ConcurrentMap> currentData = Maps.newConcurrentMap(); private final AtomicBoolean started = new AtomicBoolean(); private final AtomicBoolean connected = new AtomicBoolean(); protected final SequenceComparator sequenceComparator = new SequenceComparator(); private volatile String id; private volatile T state; private final Watcher childrenWatcher = new Watcher() { @Override public void process(WatchedEvent event) { offerOperation(new RefreshOperation(ZooKeeperGroup.this, RefreshMode.STANDARD)); } }; private final Watcher dataWatcher = new Watcher() { @Override public void process(WatchedEvent event) { try { if (event.getType() == Event.EventType.NodeDeleted) { remove(event.getPath()); } else if (event.getType() == Event.EventType.NodeDataChanged) { offerOperation(new GetDataOperation(ZooKeeperGroup.this, event.getPath())); } } catch (Exception e) { handleException(e); } } }; private final ConnectionStateListener connectionStateListener = new ConnectionStateListener() { @Override public void stateChanged(CuratorFramework client, ConnectionState newState) { handleStateChange(newState); } }; /** * @param client the client * @param path path to watch */ public ZooKeeperGroup(CuratorFramework client, String path, Class clazz) { this(client, path, clazz, Executors.newSingleThreadExecutor(ThreadUtils.newThreadFactory("ZooKeeperGroup"))); } /** * @param client the client * @param path path to watch * @param threadFactory factory to use when creating internal threads */ public ZooKeeperGroup(CuratorFramework client, String path, Class clazz, ThreadFactory threadFactory) { this(client, path, clazz, Executors.newSingleThreadExecutor(threadFactory)); } /** * @param client the client * @param path path to watch * @param executorService ExecutorService to use for the ZooKeeperGroup's background thread */ public ZooKeeperGroup(CuratorFramework client, String path, Class clazz, final ExecutorService executorService) { this.client = client; this.path = path; this.clazz = clazz; this.executorService = executorService; ensurePath = client.newNamespaceAwareEnsurePath(path); } /** * Start the cache. The cache is not started automatically. You must call this method. */ public void start() { if (started.compareAndSet(false, true)) { connected.set(client.getZookeeperClient().isConnected()); client.getConnectionStateListenable().addListener(connectionStateListener); executorService.execute(new Runnable() { @Override public void run() { mainLoop(); } }); if (isConnected()) { handleStateChange(ConnectionState.CONNECTED); } } } /** * Close/end the cache * * @throws IOException errors */ @Override public void close() throws IOException { if (started.compareAndSet(true, false)) { client.getConnectionStateListenable().removeListener(connectionStateListener); executorService.shutdownNow(); try { executorService.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { throw (IOException) new InterruptedIOException().initCause(e); } try { if (isConnected()) { doUpdate(null); callListeners(GroupListener.GroupEvent.DISCONNECTED); } } catch (Exception e) { handleException(e); } listeners.clear(); } } @Override public boolean isConnected() { return connected.get(); } @Override public void add(GroupListener listener) { listeners.addListener(listener); } @Override public void remove(GroupListener listener) { listeners.removeListener(listener); } @Override public void update(T state) { T oldState = this.state; this.state = state; if (started.get()) { boolean update = state == null && oldState != null || state != null && oldState == null || !Arrays.equals(encode(state), encode(oldState)); if (update) { offerOperation(new RefreshOperation(this, RefreshMode.FORCE_GET_DATA_AND_STAT)); offerOperation(new UpdateOperation(this, state)); } } } protected void doUpdate(T state) throws Exception { if (isConnected()) { if (state == null) { if (id != null) { try { client.delete().guaranteed().forPath(id); } catch (KeeperException.NoNodeException e) { // Ignore } finally { id = null; } } } else { if (id == null) { //We explicitly refresh to prevent members() from returning stale data. refresh(RefreshMode.FORCE_GET_DATA_AND_STAT); // We could have created the sequence, but then have crashed and our entry is already registered, // find out by looking up entry by the matching uuid. Map members = members(); for (Map.Entry entry : members.entrySet()) { T v = entry.getValue(); if( state.getContainer().equals(v.getContainer()) ) { id = entry.getKey(); return; } } } if (id == null) { id = client.create().creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL_SEQUENTIAL) .forPath(path + "/0", encode(state)); } else { try { client.setData().forPath(id, encode(state)); } catch (KeeperException.NoNodeException e) { id = client.create().creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL_SEQUENTIAL) .forPath(path + "/0", encode(state)); } } } } } @Override public Map members() { List> children = new ArrayList>(currentData.values()); Collections.sort(children, sequenceComparator); Map members = new LinkedHashMap(); for (ChildData child : children) { members.put(child.getPath(), child.getNode()); } return members; } @Override public boolean isMaster() { List> children = new ArrayList>(currentData.values()); Collections.sort(children, sequenceComparator); return (!children.isEmpty() && children.get(0).getPath().equals(id)); } @Override public T master() { List> children = new ArrayList>(currentData.values()); Collections.sort(children, sequenceComparator); if (children.isEmpty()) { return null; } return children.get(0).getNode(); } @Override public List slaves() { List> children = new ArrayList>(currentData.values()); Collections.sort(children, sequenceComparator); List slaves = new ArrayList(); for (int i = 1; i < children.size(); i++) { slaves.add(children.get(i).getNode()); } return slaves; } @Override public T getLastState() { return this.state; } /** * Return the cache listenable * * @return listenable */ public ListenerContainer> getListenable() { return listeners; } /** * Return the current data. There are no guarantees of accuracy. This is * merely the most recent view of the data. The data is returned in sorted order. * * @return list of children and data */ public List getCurrentData() { return ImmutableList.copyOf(Sets.newTreeSet(currentData.values())); } /** * Return the current data for the given path. There are no guarantees of accuracy. This is * merely the most recent view of the data. If there is no child with that path, null * is returned. * * @param fullPath full path to the node to check * @return data or null */ public ChildData getCurrentData(String fullPath) { return currentData.get(fullPath); } /** * Clear out current data and begin a new query on the path * * @throws Exception errors */ public void clearAndRefresh() throws Exception { clearAndRefresh(false, false); } /** * Clear out current data and begin a new query on the path * * @param force - whether to force clear and refresh to trigger updates * @param sync - whether to run this synchronously (block current thread) or asynchronously * @throws Exception errors */ public void clearAndRefresh(boolean force, boolean sync) throws Exception { RefreshMode mode = force ? RefreshMode.FORCE_GET_DATA_AND_STAT : RefreshMode.STANDARD; currentData.clear(); if (sync) { this.refresh(mode); } else { offerOperation(new RefreshOperation(this, mode)); } } /** * Clears the current data without beginning a new query and without generating any events * for listeners. */ public void clear() { currentData.clear(); } enum RefreshMode { STANDARD, FORCE_GET_DATA_AND_STAT } void refresh(final RefreshMode mode) throws Exception { ensurePath.ensure(client.getZookeeperClient()); List children = client.getChildren().usingWatcher(childrenWatcher).forPath(path); Collections.sort(children, new Comparator() { @Override public int compare(String left, String right) { return left.compareTo(right); } }); processChildren(children, mode); } void callListeners(final GroupListener.GroupEvent event) { listeners.forEach ( new Function, Void>() { @Override public Void apply(GroupListener listener) { try { listener.groupEvent(ZooKeeperGroup.this, event); } catch (Exception e) { handleException(e); } return null; } } ); } void getDataAndStat(final String fullPath) throws Exception { Stat stat = new Stat(); byte[] data = client.getData().storingStatIn(stat).usingWatcher(dataWatcher).forPath(fullPath); applyNewData(fullPath, KeeperException.Code.OK.intValue(), stat, data); } /** * Default behavior is just to log the exception * * @param e the exception */ protected void handleException(Throwable e) { LOG.error("", e); } @VisibleForTesting protected void remove(String fullPath) { ChildData data = currentData.remove(fullPath); if (data != null) { offerOperation(new EventOperation(this, GroupListener.GroupEvent.CHANGED)); } } private void internalRebuildNode(String fullPath) throws Exception { try { Stat stat = new Stat(); byte[] bytes = client.getData().storingStatIn(stat).forPath(fullPath); currentData.put(fullPath, new ChildData(fullPath, stat, bytes, decode(bytes))); } catch (KeeperException.NoNodeException ignore) { // node no longer exists - remove it currentData.remove(fullPath); } } private void handleStateChange(ConnectionState newState) { switch (newState) { case SUSPENDED: case LOST: { connected.set(false); clear(); offerOperation(new EventOperation(this, GroupListener.GroupEvent.DISCONNECTED)); break; } case CONNECTED: case RECONNECTED: { connected.set(true); offerOperation(new RefreshOperation(this, RefreshMode.FORCE_GET_DATA_AND_STAT)); offerOperation(new UpdateOperation(this, state)); offerOperation(new EventOperation(this, GroupListener.GroupEvent.CONNECTED)); break; } } } private void processChildren(List children, RefreshMode mode) throws Exception { List fullPaths = Lists.newArrayList(Lists.transform ( children, new Function() { @Override public String apply(String child) { return ZKPaths.makePath(path, child); } } )); Set removedNodes = Sets.newHashSet(currentData.keySet()); removedNodes.removeAll(fullPaths); for (String fullPath : removedNodes) { remove(fullPath); } for (String name : children) { String fullPath = ZKPaths.makePath(path, name); if ((mode == RefreshMode.FORCE_GET_DATA_AND_STAT) || !currentData.containsKey(fullPath)) { try { getDataAndStat(fullPath); } catch (KeeperException.NoNodeException ignore) {} } } } private void applyNewData(String fullPath, int resultCode, Stat stat, byte[] bytes) { if (resultCode == KeeperException.Code.OK.intValue()) { // otherwise - node must have dropped or something - we should be getting another event ChildData data = new ChildData(fullPath, stat, bytes, decode(bytes)); ChildData previousData = currentData.put(fullPath, data); if (previousData == null || previousData.getStat().getVersion() != stat.getVersion()) { offerOperation(new EventOperation(this, GroupListener.GroupEvent.CHANGED)); } } } private void mainLoop() { while (started.get() && !Thread.currentThread().isInterrupted()) { try { operations.take().invoke(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } catch (Exception e) { handleException(e); } } } private byte[] encode(T state) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); MAPPER.writeValue(baos, state); return baos.toByteArray(); } catch (IOException e) { throw new IllegalStateException("Unable to decode data", e); } } private T decode(byte[] data) { try { return MAPPER.readValue(data, clazz); } catch (IOException e) { throw new IllegalStateException("Unable to decode data", e); } } private void offerOperation(Operation operation) { operations.remove(operation); // avoids herding for refresh operations operations.offer(operation); } public static Map members(CuratorFramework curator, String path, Class clazz) throws Exception { Map map = new TreeMap(); List nodes = curator.getChildren().forPath(path); ObjectMapper mapper = new ObjectMapper(); for (String node : nodes) { byte[] data = curator.getData().forPath(path + "/" + node); T val = mapper.readValue(data, clazz); map.put(node, val); } return map; } public String getId() { return id; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy