package org.apache.blur.zookeeper; /** * 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. */ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.blur.CachedMap; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.ZooDefs.Ids; import org.apache.zookeeper.ZooKeeper; import org.apache.zookeeper.data.Stat; /** * This is an simple implementation of a set-once map of string-to-string that * is backed by ZooKeeper. Meaning that once the value is set a single time it * cannot be set to a different value. The clear cache method is called when the * internal cache is to be cleared and re-read from ZooKeeper. <br> * <br> * Usage:<br> * <br> * ZkCachedMap map = new ZkCachedMap(zooKeeper, path);<br> * String key = "key";<br> * String newValue = "value";<br> * String value = map.get(key);<br> * if (value == null) {<br> *   if (map.putIfMissing(key, newValue)) {<br> *     System.out.println("Yay! My value was taken.");<br> *     value = newValue;<br> *   } else {<br> *     System.out.println("Boo! Someone beat me to it.");<br> *     value = map.get(key);<br> *   }<br> * }<br> * System.out.println("key [" + key + "] value [" + value + "]");<br> * */ public class ZkCachedMap extends CachedMap { private static final String SEP = "-"; private final Map<String, String> cache = new ConcurrentHashMap<String, String>(); private final ZooKeeper zooKeeper; private final String basePath; public ZkCachedMap(ZooKeeper zooKeeper, String basePath) { this.zooKeeper = zooKeeper; this.basePath = basePath; } @Override public void clearCache() { cache.clear(); } /** * Checks the in memory map first, then fetches from ZooKeeper. * * @param key * the key. * @return the value, null if it does not exist. * @exception IOException * if there is an io error. */ @Override public String get(String key) throws IOException { String value = cache.get(key); if (value != null) { return value; } return getFromZooKeeper(key); } /** * Checks the in memory map first, if it exists then return true. If missing * then check ZooKeeper. * * @param key * the key. * @param value * the value. * @return boolean, true if the put was successful, false if a value already * exists. * @exception IOException * if there is an io error. */ @Override public boolean putIfMissing(String key, String value) throws IOException { String existingValue = cache.get(key); if (existingValue != null) { return false; } return putIfMissingFromZooKeeper(key, value); } private String getFromZooKeeper(String key) throws IOException { try { List<String> keys = new ArrayList<String>(zooKeeper.getChildren(basePath, false)); Collections.sort(keys); for (String k : keys) { String realKey = getRealKey(k); if (realKey.equals(key)) { String path = getPath(k); byte[] data = getValue(path); if (data == null) { return null; } String value = new String(data); cache.put(key, value); return value; } } return null; } catch (KeeperException e) { throw new IOException(e); } catch (InterruptedException e) { throw new IOException(e); } } private byte[] getValue(String path) throws KeeperException, InterruptedException { Stat stat = zooKeeper.exists(path, false); if (stat == null) { return null; } byte[] data = zooKeeper.getData(path, false, stat); if (data == null) { return null; } return data; } private boolean putIfMissingFromZooKeeper(String key, String value) throws IOException { try { String path = getPath(key); String newPath = zooKeeper.create(path + SEP, value.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL); String keyWithSeq = getKeyWithSeq(newPath); List<String> keys = new ArrayList<String>(zooKeeper.getChildren(basePath, false)); Collections.sort(keys); for (String k : keys) { String realKey = getRealKey(k); if (realKey.equals(key)) { if (keyWithSeq.equals(k)) { // got the lock cache.put(key, value); return true; } else { // remove duplicate key zooKeeper.delete(newPath, -1); return false; } } } return false; } catch (KeeperException e) { throw new IOException(e); } catch (InterruptedException e) { throw new IOException(e); } } private String getKeyWithSeq(String newPath) { int lastIndexOf = newPath.lastIndexOf('/'); if (lastIndexOf < 0) { throw new RuntimeException("Path [" + newPath + "] does not contain [/]"); } return newPath.substring(lastIndexOf + 1); } private String getRealKey(String keyWithSeq) { int lastIndexOf = keyWithSeq.lastIndexOf(SEP); if (lastIndexOf < 0) { throw new RuntimeException("Key [" + keyWithSeq + "] does not contain [" + SEP + "]"); } return keyWithSeq.substring(0, lastIndexOf); } private String getPath(String key) { return basePath + "/" + key; } @Override public Map<String, String> fetchAllFromSource() throws IOException { try { Map<String, String> result = new HashMap<String, String>(); List<String> keys = new ArrayList<String>(zooKeeper.getChildren(basePath, false)); Collections.sort(keys); for (String k : keys) { String realKey = getRealKey(k); String path = getPath(k); byte[] value = getValue(path); if (value != null) { result.put(realKey, new String(value)); } } return result; } catch (KeeperException e) { throw new IOException(e); } catch (InterruptedException e) { throw new IOException(e); } } }