/*
* Copyright © 2014 Cask Data, Inc.
*
* 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 co.cask.cdap.security.zookeeper;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.guice.ConfigModule;
import co.cask.cdap.common.guice.ZKClientModule;
import co.cask.cdap.common.io.Codec;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.SettableFuture;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.apache.hadoop.hbase.HBaseTestingUtility;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.zookeeper.MiniZooKeeperCluster;
import org.apache.twill.zookeeper.ZKClientService;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.data.ACL;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
/**
* Tests covering the {@link SharedResourceCache} implementation.
*/
public class SharedResourceCacheTest {
private static final String ZK_NAMESPACE = "/SharedResourceCacheTest";
private static final Logger LOG = LoggerFactory.getLogger(SharedResourceCacheTest.class);
private static MiniZooKeeperCluster zkCluster;
private static String zkConnectString;
private static Injector injector1;
private static Injector injector2;
@BeforeClass
public static void startUp() throws Exception {
HBaseTestingUtility testUtil = new HBaseTestingUtility();
zkCluster = testUtil.startMiniZKCluster();
zkConnectString = testUtil.getConfiguration().get(HConstants.ZOOKEEPER_QUORUM) + ":"
+ zkCluster.getClientPort();
LOG.info("Running ZK cluster at " + zkConnectString);
CConfiguration cConf = CConfiguration.create();
cConf.set(Constants.Zookeeper.QUORUM, zkConnectString);
injector1 = Guice.createInjector(new ConfigModule(cConf, testUtil.getConfiguration()),
new ZKClientModule());
injector2 = Guice.createInjector(new ConfigModule(cConf, testUtil.getConfiguration()),
new ZKClientModule());
}
@AfterClass
public static void tearDown() throws Exception {
zkCluster.shutdown();
}
@Test
public void testCache() throws Exception {
String parentZNode = ZK_NAMESPACE + "/testCache";
List<ACL> acls = Lists.newArrayList(ZooDefs.Ids.OPEN_ACL_UNSAFE);
// create 2 cache instances
ZKClientService zkClient1 = injector1.getInstance(ZKClientService.class);
zkClient1.startAndWait();
SharedResourceCache<String> cache1 =
new SharedResourceCache<>(zkClient1, new StringCodec(), parentZNode, acls);
cache1.init();
// add items to one and wait for them to show up in the second
String key1 = "key1";
String value1 = "value1";
cache1.put(key1, value1);
ZKClientService zkClient2 = injector2.getInstance(ZKClientService.class);
zkClient2.startAndWait();
SharedResourceCache<String> cache2 =
new SharedResourceCache<>(zkClient2, new StringCodec(), parentZNode, acls);
cache2.init();
waitForEntry(cache2, key1, value1, 10000);
assertEquals(cache1.get(key1), cache2.get(key1));
final String key2 = "key2";
String value2 = "value2";
cache1.put(key2, value2);
waitForEntry(cache2, key2, value2, 10000);
assertEquals(cache1.get(key2), cache2.get(key2));
final String key3 = "key3";
String value3 = "value3";
cache2.put(key3, value3);
waitForEntry(cache1, key3, value3, 10000);
assertEquals(cache2.get(key3), cache1.get(key3));
// replace an existing key
String value2new = "value2.2";
final SettableFuture<String> value2future = SettableFuture.create();
ResourceListener<String> value2listener = new BaseResourceListener<String>() {
@Override
public void onResourceUpdate(String name, String instance) {
LOG.info("Resource updated: {}={}", name, instance);
if (name.equals(key2)) {
value2future.set(instance);
}
}
};
cache2.addListener(value2listener);
cache1.put(key2, value2new);
//String newValue = value2future.get(10000, TimeUnit.MILLISECONDS);
String newValue = value2future.get();
assertEquals(value2new, newValue);
assertEquals(value2new, cache2.get(key2));
cache2.removeListener(value2listener);
// remove items from the second and wait for them to disappear from the first
// Use a latch to make sure both cache see the changes
final CountDownLatch key3RemoveLatch = new CountDownLatch(2);
cache1.addListener(new BaseResourceListener<String>() {
@Override
public void onResourceDelete(String name) {
LOG.info("Resource deleted on cache 1 {}", name);
if (name.equals(key3)) {
key3RemoveLatch.countDown();
}
}
});
final SettableFuture<String> key3RemoveFuture = SettableFuture.create();
ResourceListener<String> key3Listener = new BaseResourceListener<String>() {
@Override
public void onResourceDelete(String name) {
LOG.info("Resource deleted on cache 2 {}", name);
if (name.equals(key3)) {
key3RemoveFuture.set(name);
key3RemoveLatch.countDown();
}
}
};
cache2.addListener(key3Listener);
cache1.remove(key3);
String removedKey = key3RemoveFuture.get();
assertEquals(key3, removedKey);
assertNull(cache2.get(key3));
key3RemoveLatch.await(5, TimeUnit.SECONDS);
// verify that cache contents are equal
assertEquals(cache1, cache2);
}
private static final class StringCodec implements Codec<String> {
@Override
public byte[] encode(String object) throws IOException {
return Bytes.toBytes(object);
}
@Override
public String decode(byte[] data) throws IOException {
return Bytes.toString(data);
}
}
private void waitForEntry(SharedResourceCache<String> cache, String key, String expectedValue,
long timeToWaitMillis) throws InterruptedException {
String value = cache.get(key);
boolean isPresent = expectedValue.equals(value);
Stopwatch watch = new Stopwatch().start();
while (!isPresent && watch.elapsedTime(TimeUnit.MILLISECONDS) < timeToWaitMillis) {
TimeUnit.MILLISECONDS.sleep(200);
value = cache.get(key);
isPresent = expectedValue.equals(value);
}
if (!isPresent) {
throw new RuntimeException("Timed out waiting for expected value '" + expectedValue + "' in cache");
}
}
}