/* * Copyright 2015-2016 the original author or authors. * * Licensed 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.springframework.integration.zookeeper.metadata; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.recipes.cache.ChildData; import org.apache.curator.framework.recipes.cache.PathChildrenCache; import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; import org.apache.curator.utils.CloseableUtils; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.data.Stat; import org.springframework.context.SmartLifecycle; import org.springframework.integration.metadata.ListenableMetadataStore; import org.springframework.integration.metadata.MetadataStoreListener; import org.springframework.integration.support.utils.IntegrationUtils; import org.springframework.util.Assert; /** * Zookeeper-based {@link ListenableMetadataStore} based on a Zookeeper node. Values are stored in the children node, * the names of which are stored as keys. * * @author Marius Bogoevici * @author Gary Russell * @author Artem Bilan * @since 4.2 */ public class ZookeeperMetadataStore implements ListenableMetadataStore, SmartLifecycle { private final Object lifecycleMonitor = new Object(); private final CuratorFramework client; private final List<MetadataStoreListener> listeners = new CopyOnWriteArrayList<MetadataStoreListener>(); /** * An internal map storing local updates, ensuring that they have precedence if the cache contains stale data. * As changes are propagated back from Zookeeper to the cache, entries are removed. */ private final ConcurrentMap<String, LocalChildData> updateMap = new ConcurrentHashMap<String, LocalChildData>(); private volatile String root = "/SpringIntegration-MetadataStore"; private volatile String encoding = "UTF-8"; private volatile PathChildrenCache cache; private volatile boolean running = false; private volatile boolean autoStartup = true; private volatile int phase = Integer.MAX_VALUE; public ZookeeperMetadataStore(CuratorFramework client) throws Exception { Assert.notNull(client, "Client cannot be null"); this.client = client; } /** * Encoding to use when storing data in ZooKeeper * * @param encoding encoding as text */ public void setEncoding(String encoding) { Assert.hasText(encoding, "'encoding' cannot be null or empty."); this.encoding = encoding; } /** * Root node - store entries are children of this node. * * @param root encoding as text */ public void setRoot(String root) { Assert.notNull(root, "'root' must not be null."); Assert.isTrue(root.startsWith("/"), "'root' must start with '/'"); // remove trailing slash, if not root this.root = "/".equals(root) || !root.endsWith("/") ? root : root.substring(0, root.length() - 1); } public String getRoot() { return this.root; } public void setAutoStartup(boolean autoStartup) { this.autoStartup = autoStartup; } public void setPhase(int phase) { this.phase = phase; } @Override public String putIfAbsent(String key, String value) { Assert.notNull(key, "'key' must not be null."); Assert.notNull(value, "'value' must not be null."); synchronized (this.updateMap) { try { createNode(key, value); return null; } catch (KeeperException.NodeExistsException e) { // so the data actually exists, we can read it try { byte[] bytes = this.client.getData().forPath(getPath(key)); return IntegrationUtils.bytesToString(bytes, this.encoding); } catch (Exception exceptionDuringGet) { throw new ZookeeperMetadataStoreException("Exception while reading node with key '" + key + "':", e); } } catch (Exception e) { throw new ZookeeperMetadataStoreException("Error while trying to set '" + key + "':", e); } } } @Override public boolean replace(String key, String oldValue, String newValue) { Assert.notNull(key, "'key' must not be null."); Assert.notNull(oldValue, "'oldValue' must not be null."); Assert.notNull(newValue, "'newValue' must not be null."); synchronized (this.updateMap) { Stat currentStat = new Stat(); try { byte[] bytes = this.client.getData().storingStatIn(currentStat).forPath(getPath(key)); if (oldValue.equals(IntegrationUtils.bytesToString(bytes, this.encoding))) { updateNode(key, newValue, currentStat.getVersion()); } return true; } catch (KeeperException.NoNodeException e) { // ignore, the node doesn't exist there's nothing to replace return false; } catch (KeeperException.BadVersionException e) { // ignore return false; } catch (Exception e) { throw new ZookeeperMetadataStoreException("Cannot replace value"); } } } @Override public void addListener(MetadataStoreListener callback) { this.listeners.add(callback); } @Override public void removeListener(MetadataStoreListener callback) { this.listeners.remove(callback); } @Override public void put(String key, String value) { Assert.notNull(key, "'key' must not be null."); Assert.notNull(value, "'value' must not be null."); synchronized (this.updateMap) { try { Stat currentNode = this.client.checkExists().forPath(getPath(key)); if (currentNode == null) { try { createNode(key, value); } catch (KeeperException.NodeExistsException e) { updateNode(key, value, -1); } } else { updateNode(key, value, -1); } } catch (Exception e) { throw new ZookeeperMetadataStoreException("Error while setting value for key '" + key + "':", e); } } } @Override public String get(String key) { Assert.notNull(key, "'key' must not be null."); synchronized (this.updateMap) { ChildData currentData = this.cache.getCurrentData(getPath(key)); if (currentData == null) { if (this.updateMap.containsKey(key)) { // we have saved the value, but the cache hasn't updated yet // if the value had changed via replication, we would have been notified by the listener return this.updateMap.get(key).getValue(); } else { // the value just doesn't exist return null; } } else { if (this.updateMap.containsKey(key)) { // our version is more recent than the cache if (this.updateMap.get(key).getVersion() >= currentData.getStat().getVersion()) { return this.updateMap.get(key).getValue(); } } return IntegrationUtils.bytesToString(currentData.getData(), this.encoding); } } } @Override public String remove(String key) { Assert.notNull(key, "'key' must not be null."); synchronized (this.updateMap) { try { byte[] bytes = this.client.getData().forPath(getPath(key)); this.client.delete().forPath(getPath(key)); // we guarantee that the deletion will supersede the existing data this.updateMap.put(key, new LocalChildData(null, Integer.MAX_VALUE)); return IntegrationUtils.bytesToString(bytes, this.encoding); } catch (KeeperException.NoNodeException e) { // ignore - the node doesn't exist return null; } catch (Exception e) { throw new ZookeeperMetadataStoreException("Exception while deleting key '" + key + "'", e); } } } private void updateNode(String key, String value, int version) throws Exception { Stat stat = this.client.setData().withVersion(version).forPath(getPath(key), IntegrationUtils.stringToBytes(value, this.encoding)); this.updateMap.put(key, new LocalChildData(value, stat.getVersion())); } private void createNode(String key, String value) throws Exception { this.client.create().forPath(getPath(key), IntegrationUtils.stringToBytes(value, this.encoding)); this.updateMap.put(key, new LocalChildData(value, 0)); } public String getPath(String key) { return "".equals(key) ? this.root : this.root + "/" + key; } @Override public boolean isAutoStartup() { return this.autoStartup; } @Override public void start() { if (!this.running) { synchronized (this.lifecycleMonitor) { if (!this.running) { try { this.client.checkExists() .creatingParentContainersIfNeeded() .forPath(this.root); this.cache = new PathChildrenCache(this.client, this.root, true); this.cache.getListenable() .addListener(new MetadataStoreListenerInvokingPathChildrenCacheListener()); this.cache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE); this.running = true; } catch (Exception e) { throw new ZookeeperMetadataStoreException("Exception while starting bean", e); } } } } } @Override public void stop() { if (this.running) { synchronized (this.lifecycleMonitor) { if (this.running) { if (this.cache != null) { CloseableUtils.closeQuietly(this.cache); } this.cache = null; this.running = false; } } } } @Override public void stop(Runnable callback) { stop(); callback.run(); } @Override public boolean isRunning() { return this.running; } @Override public int getPhase() { return this.phase; } private String getKey(String path) { return path.replace(this.root + "/", ""); } private static final class LocalChildData { private final String value; private final int version; LocalChildData(String value, int version) { this.value = value; this.version = version; } private String getValue() { return this.value; } private int getVersion() { return this.version; } } private class MetadataStoreListenerInvokingPathChildrenCacheListener implements PathChildrenCacheListener { MetadataStoreListenerInvokingPathChildrenCacheListener() { super(); } @Override public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception { synchronized (ZookeeperMetadataStore.this.updateMap) { String eventPath = event.getData().getPath(); String eventKey = getKey(eventPath); byte[] eventData = event.getData().getData(); switch (event.getType()) { case CHILD_ADDED: if (ZookeeperMetadataStore.this.updateMap.containsKey(eventKey)) { if (event.getData().getStat().getVersion() >= ZookeeperMetadataStore.this.updateMap.get(eventKey).getVersion()) { ZookeeperMetadataStore.this.updateMap.remove(eventPath); } } for (MetadataStoreListener listener : ZookeeperMetadataStore.this.listeners) { listener.onAdd(eventKey, IntegrationUtils.bytesToString(eventData, ZookeeperMetadataStore.this.encoding)); } break; case CHILD_UPDATED: if (ZookeeperMetadataStore.this.updateMap.containsKey(eventKey)) { if (event.getData().getStat().getVersion() >= ZookeeperMetadataStore.this.updateMap.get(eventKey).getVersion()) { ZookeeperMetadataStore.this.updateMap.remove(eventPath); } } for (MetadataStoreListener listener : ZookeeperMetadataStore.this.listeners) { listener.onUpdate(eventKey, IntegrationUtils.bytesToString(eventData, ZookeeperMetadataStore.this.encoding)); } break; case CHILD_REMOVED: ZookeeperMetadataStore.this.updateMap.remove(eventKey); for (MetadataStoreListener listener : ZookeeperMetadataStore.this.listeners) { listener.onRemove(eventKey, IntegrationUtils.bytesToString(eventData, ZookeeperMetadataStore.this.encoding)); } break; default: // ignore all other events break; } } } } }