/*
Copyright (c) 2012 LinkedIn Corp.
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 com.linkedin.d2.discovery.stores.zk;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import org.apache.zookeeper.AsyncCallback;
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.ACL;
import org.apache.zookeeper.data.Stat;
import org.easymock.EasyMock;
import org.easymock.IAnswer;
import org.easymock.IMockBuilder;
import org.testng.Assert;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
public class RetryZooKeeperTest {
private static final int _connectionLossRC = KeeperException.Code.CONNECTIONLOSS.intValue();
private static final int _okRC = KeeperException.Code.OK.intValue();
private static final byte[] _dummyData = {'d', 'u', 'm', 'm', 'y'};
private static final String _dummyPath = "/dummy/path";
private static final String _dummyParentPath = "/dummy";
private static final int _dummyVersion = 1;
private static final List<String> _dummyList = new ArrayList<String>();
private static final List<ACL> _dummyACL = new ArrayList<ACL>();
private static final Object _dummyCtx = new Object();
private static final Stat _dummyStat = new Stat();
private static Constructor<RetryZooKeeper> _rzkCstr1;
private static Constructor<RetryZooKeeper> _rzkCstr2;
private static Watcher _noopWatcher = new Watcher() {
public void process(WatchedEvent event)
{
return;
}
};
private static final AsyncCallback.DataCallback _dummyDataCallback = new AsyncCallback.DataCallback() {
@Override
public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat)
{
return;
}
};
private static final AsyncCallback.ChildrenCallback _dummyChildrenCallback = new AsyncCallback.ChildrenCallback() {
@Override
public void processResult(int rc, String path, Object ctx, List<String> children)
{
return;
}
};
private static final AsyncCallback.StatCallback _dummyStatCallback = new AsyncCallback.StatCallback() {
@Override
public void processResult(int rc, String path, Object ctx, Stat stat) {
return;
}
};
private static final AsyncCallback.VoidCallback _dummyVoidCallback = new AsyncCallback.VoidCallback() {
@Override
public void processResult(int rc, String path, Object ctx) {
return;
}
};
private static final AsyncCallback.StringCallback _dummyStringCallback = new AsyncCallback.StringCallback() {
@Override
public void processResult(int rc, String path, Object ctx, String name) {
return;
}
};
private void expectGetChildCallbackWithCode(final int rc, final List<String> children)
{
EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
@Override
public Object answer() throws Throwable
{
AsyncCallback.ChildrenCallback callback = (AsyncCallback.ChildrenCallback) EasyMock.getCurrentArguments() [2];
callback.processResult(rc, _dummyPath, _dummyCtx, children);
return null;
}
});
}
private void expectGetDataCallbackWithCode(final int rc, final byte[] data)
{
EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
@Override
public Object answer() throws Throwable
{
AsyncCallback.DataCallback callback = (AsyncCallback.DataCallback) EasyMock.getCurrentArguments() [2];
callback.processResult(rc, _dummyPath, _dummyCtx, data, _dummyStat);
return null;
}
});
}
private void expectExistCallbackWithCode(final int rc)
{
EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
@Override
public Object answer() throws Throwable
{
AsyncCallback.StatCallback callback = (AsyncCallback.StatCallback) EasyMock.getCurrentArguments() [2];
callback.processResult(rc, _dummyPath, _dummyCtx, _dummyStat);
return null;
}
});
}
private void expectSetDataCallbackWithCode(final int rc)
{
EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
@Override
public Object answer() throws Throwable
{
AsyncCallback.StatCallback callback = (AsyncCallback.StatCallback) EasyMock.getCurrentArguments() [3];
callback.processResult(rc, _dummyPath, _dummyCtx, _dummyStat);
return null;
}
});
}
private void expectDeleteCallbackWithCode(final int rc)
{
EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
@Override
public Object answer() throws Throwable
{
AsyncCallback.VoidCallback callback = (AsyncCallback.VoidCallback) EasyMock.getCurrentArguments() [2];
callback.processResult(rc, _dummyPath, _dummyCtx);
return null;
}
});
}
private void expectCreateCallbackWithCode(final int rc)
{
EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
@Override
public Object answer() throws Throwable
{
AsyncCallback.StringCallback callback = (AsyncCallback.StringCallback) EasyMock.getCurrentArguments() [4];
callback.processResult(rc, _dummyPath, _dummyCtx, _dummyPath);
return null;
}
});
}
@BeforeTest
public void setUp() throws NoSuchMethodException
{
_rzkCstr1 = RetryZooKeeper.class.getDeclaredConstructor(String.class,
int.class,
Watcher.class,
int.class);
_rzkCstr2 = RetryZooKeeper.class.getDeclaredConstructor(String.class,
int.class,
Watcher.class,
int.class,
boolean.class,
ScheduledExecutorService.class,
long.class);
}
@Test
public void testRetryGetChildren() throws NoSuchMethodException
{
final RetryZooKeeper rzkPartialMock = createMockObject(
RetryZooKeeper.class.getMethod("zkGetChildren",
String.class,
boolean.class,
AsyncCallback.ChildrenCallback.class,
Object.class));
// Here we only test the getChildren without supplying watcher
// the alternative getChildren with a watcher should behave the same
// as the watcher does not affect this operation
// mock up zkGetChildren, which wrapper's ZooKeeper's getChildren
rzkPartialMock.zkGetChildren((String) EasyMock.anyObject(),
EasyMock.anyBoolean(),
(AsyncCallback.ChildrenCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// first try, we got "connection loss"
expectGetChildCallbackWithCode(_connectionLossRC, _dummyList);
// second try, we got "ok"
expectGetChildCallbackWithCode(_okRC, _dummyList);
EasyMock.replay(rzkPartialMock);
rzkPartialMock.getChildren(_dummyPath, false, _dummyChildrenCallback, _dummyCtx);
EasyMock.verify(rzkPartialMock);
}
@Test
public void testRetryGetData() throws NoSuchMethodException
{
final RetryZooKeeper rzkPartialMock = createMockObject(
RetryZooKeeper.class.getMethod("zkGetData",
String.class,
boolean.class,
AsyncCallback.DataCallback.class,
Object.class));
// Similarly, only getData without watcher is tested.
// mock up zkGetData, which wrapper's ZooKeeper's getData
rzkPartialMock.zkGetData((String) EasyMock.anyObject(),
EasyMock.anyBoolean(),
(AsyncCallback.DataCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// first try, "connection loss" happens
expectGetDataCallbackWithCode(_connectionLossRC, _dummyData);
// second try, get the data
expectGetDataCallbackWithCode(_okRC, _dummyData);
EasyMock.replay(rzkPartialMock);
rzkPartialMock.getData(_dummyPath, false, _dummyDataCallback, _dummyCtx);
EasyMock.verify(rzkPartialMock);
}
@Test
public void testRetryExists() throws NoSuchMethodException
{
final RetryZooKeeper rzkPartialMock = createMockObject(
RetryZooKeeper.class.getMethod("zkExists",
String.class,
boolean.class,
AsyncCallback.StatCallback.class,
Object.class));
// as before, only exists without watcher is tested.
// mock up zkExists, which wrapper's ZooKeeper's exists
rzkPartialMock.zkExists(
(String) EasyMock.anyObject(),
EasyMock.anyBoolean(),
(AsyncCallback.StatCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// first try, "connection loss"
expectExistCallbackWithCode(_connectionLossRC);
expectExistCallbackWithCode(_connectionLossRC);
expectExistCallbackWithCode(_connectionLossRC);
expectExistCallbackWithCode(_okRC);
EasyMock.replay(rzkPartialMock);
rzkPartialMock.exists(_dummyPath, false, _dummyStatCallback, _dummyCtx);
EasyMock.verify(rzkPartialMock);
}
@Test
public void testRetrySetData() throws NoSuchMethodException
{
final RetryZooKeeper rzkPartialMock = createMockObject(
RetryZooKeeper.class.getMethod("zkSetData",
String.class,
byte[].class,
int.class,
AsyncCallback.StatCallback.class,
Object.class));
// mock up zkSetData, which wrapper's ZooKeeper's setData
rzkPartialMock.zkSetData((String) EasyMock.anyObject(),
EasyMock.aryEq(_dummyData),
EasyMock.anyInt(),
(AsyncCallback.StatCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// first try, "connection loss"
expectSetDataCallbackWithCode(_connectionLossRC);
// second try, still "connection loss"
expectSetDataCallbackWithCode(_connectionLossRC);
// finally, success
expectSetDataCallbackWithCode(_okRC);
EasyMock.replay(rzkPartialMock);
rzkPartialMock.setData(_dummyPath, _dummyData, _dummyVersion, _dummyStatCallback, _dummyCtx);
EasyMock.verify(rzkPartialMock);
}
@Test
public void testDelete() throws NoSuchMethodException
{
final RetryZooKeeper rzkPartialMock = createMockObject(
RetryZooKeeper.class.getMethod("zkDelete",
String.class,
int.class,
AsyncCallback.VoidCallback.class,
Object.class));
// mock up zkDelete, which wrapper's ZooKeeper's delete
rzkPartialMock.zkDelete(
(String) EasyMock.anyObject(),
EasyMock.anyInt(),
(AsyncCallback.VoidCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// first try, "connection loss"
expectDeleteCallbackWithCode(_connectionLossRC);
// second try, "no node"
expectDeleteCallbackWithCode(KeeperException.Code.NONODE.intValue());
EasyMock.replay(rzkPartialMock);
rzkPartialMock.delete(_dummyPath, _dummyVersion, _dummyVoidCallback, _dummyCtx);
EasyMock.verify(rzkPartialMock);
}
@Test
public void testCreate() throws NoSuchMethodException
{
testCreateHelper(_dummyData);
}
@Test
public void testCreateNullData() throws NoSuchMethodException
{
testCreateHelper(null);
}
@SuppressWarnings("unchecked")
public void testCreateHelper(byte[] data) throws NoSuchMethodException
{
final RetryZooKeeper rzkPartialMock = createMockObject(
RetryZooKeeper.class.getMethod("zkCreate",
String.class,
byte[].class,
List.class,
CreateMode.class,
AsyncCallback.StringCallback.class,
Object.class));
/**
* Testing nonsequential create, PERSISTENT type, but the ephemeral one should be the same
*/
// mock up zkCreate, which wrapper's ZooKeeper's create
rzkPartialMock.zkCreate(
(String) EasyMock.anyObject(),
(byte []) EasyMock.anyObject(),
(List<ACL>) EasyMock.anyObject(),
(CreateMode) EasyMock.anyObject(),
(AsyncCallback.StringCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// first try, "connection loss"
expectCreateCallbackWithCode(_connectionLossRC);
// second try, "connection loss"
expectCreateCallbackWithCode(_connectionLossRC);
// third try, "ok"
expectCreateCallbackWithCode(_okRC);
EasyMock.replay(rzkPartialMock);
rzkPartialMock.create(_dummyPath, data, _dummyACL, CreateMode.PERSISTENT, _dummyStringCallback, _dummyCtx);
EasyMock.verify(rzkPartialMock);
}
@SuppressWarnings("unchecked")
@Test
public void testCreateSequential() throws NoSuchMethodException
{
final RetryZooKeeper rzkPartialMock = createMockObject(
RetryZooKeeper.class.getMethod("zkCreate",
String.class,
byte[].class,
List.class,
CreateMode.class,
AsyncCallback.StringCallback.class,
Object.class),
RetryZooKeeper.class.getMethod("zkGetData",
String.class,
boolean.class,
AsyncCallback.DataCallback.class,
Object.class),
RetryZooKeeper.class.getMethod("zkGetChildren",
String.class,
boolean.class,
AsyncCallback.ChildrenCallback.class,
Object.class));
// mock up zkCreate, which wrapper's ZooKeeper's create
rzkPartialMock.zkCreate(
(String) EasyMock.anyObject(),
(byte[]) EasyMock.anyObject(),
(List<ACL>) EasyMock.anyObject(),
(CreateMode) EasyMock.anyObject(),
(AsyncCallback.StringCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// connection loss in create
expectCreateCallbackWithCode(_connectionLossRC);
List<String> children = new ArrayList<String>();
children.add("ephemeral-3.14159");
children.add("ephemeral-6.26");
rzkPartialMock.zkGetChildren(
(String) EasyMock.anyObject(),
EasyMock.anyBoolean(),
(AsyncCallback.ChildrenCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// getChildren succeeded
expectGetChildCallbackWithCode(_okRC, children);
// no children belong to us, so zkCreate would be triggered
rzkPartialMock.zkCreate(
(String) EasyMock.anyObject(),
(byte[]) EasyMock.anyObject(),
(List<ACL>) EasyMock.anyObject(),
(CreateMode) EasyMock.anyObject(),
(AsyncCallback.StringCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// connection loss in create, again
expectCreateCallbackWithCode(_connectionLossRC);
List<String> childrenWithOurChild = new ArrayList<String>();
childrenWithOurChild.add("ephemeral-3.14159");
childrenWithOurChild.add("ephemeral-6.26");
childrenWithOurChild.add("ephemeral" + rzkPartialMock.getUuid() + "1");
childrenWithOurChild.add("ephemeral" + rzkPartialMock.getUuid() + "2");
rzkPartialMock.zkGetChildren(
(String) EasyMock.anyObject(),
EasyMock.anyBoolean(),
(AsyncCallback.ChildrenCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// getChildren succeeded
expectGetChildCallbackWithCode(_okRC, childrenWithOurChild);
rzkPartialMock.zkGetData(
(String) EasyMock.anyObject(),
EasyMock.anyBoolean(),
(AsyncCallback.DataCallback) EasyMock.anyObject(),
EasyMock.anyObject());
byte[] randomData = {'r', 'a', 'n', 'd', 'o', 'm'};
// connection loss in getData
expectGetDataCallbackWithCode(_connectionLossRC, _dummyData);
// we get some random data, not the one we wanted to create
expectGetDataCallbackWithCode(_okRC, randomData);
expectGetDataCallbackWithCode(_okRC, randomData);
rzkPartialMock.zkCreate(
(String) EasyMock.anyObject(),
(byte[]) EasyMock.anyObject(),
(List<ACL>) EasyMock.anyObject(),
(CreateMode) EasyMock.anyObject(),
(AsyncCallback.StringCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// connection loss in create, again
expectCreateCallbackWithCode(_connectionLossRC);
List<String> childrenWithThatKid = new ArrayList<String>();
childrenWithThatKid.add("ephemeral-3.14159");
childrenWithThatKid.add("ephemeral-6.26");
childrenWithThatKid.add("ephemeral" + rzkPartialMock.getUuid() + "1");
childrenWithThatKid.add("ephemeral" + rzkPartialMock.getUuid() + "2");
childrenWithThatKid.add("ephemeral" + rzkPartialMock.getUuid() + "3");
rzkPartialMock.zkGetChildren(
(String) EasyMock.anyObject(),
EasyMock.anyBoolean(),
(AsyncCallback.ChildrenCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// getChildren succeeded
expectGetChildCallbackWithCode(_okRC, childrenWithThatKid);
rzkPartialMock.zkGetData(
(String) EasyMock.anyObject(),
EasyMock.anyBoolean(),
(AsyncCallback.DataCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// we found the data we wanted to create
expectGetDataCallbackWithCode(_okRC, _dummyData);
expectGetDataCallbackWithCode(_okRC, randomData);
expectGetDataCallbackWithCode(_okRC, randomData);
// so we should have confirmed the success of previous create and returned
// without unnecessary and potentially harmful retry
EasyMock.replay(rzkPartialMock);
rzkPartialMock.createUniqueSequential(_dummyPath, _dummyData, _dummyACL,
CreateMode.EPHEMERAL_SEQUENTIAL, _dummyStringCallback, _dummyCtx);
EasyMock.verify(rzkPartialMock);
}
@Test
public void testRetryLimit() throws NoSuchMethodException
{
// retry limit is set to 2
final RetryZooKeeper rzkPartialMock = EasyMock.createMockBuilder(RetryZooKeeper.class)
.withConstructor(_rzkCstr1)
.withArgs("127.0.0.1:11711",
5000000,
_noopWatcher,
1)
.addMockedMethod(RetryZooKeeper.class.getMethod("zkExists",
String.class,
boolean.class,
AsyncCallback.StatCallback.class,
Object.class))
.createMock();
rzkPartialMock.zkExists(
(String) EasyMock.anyObject(),
EasyMock.anyBoolean(),
(AsyncCallback.StatCallback) EasyMock.anyObject(),
EasyMock.anyObject());
expectExistCallbackWithCode(_connectionLossRC);
expectExistCallbackWithCode(_connectionLossRC);
EasyMock.replay(rzkPartialMock);
rzkPartialMock.exists(_dummyPath, false, _dummyStatCallback, _dummyCtx);
EasyMock.verify(rzkPartialMock);
}
@Test
public void testRetryBackoff() throws NoSuchMethodException, InterruptedException
{
final RetryZooKeeper rzkPartialMock = EasyMock.createMockBuilder(RetryZooKeeper.class)
.withConstructor(_rzkCstr2)
.withArgs("127.0.0.1:11711",
5000000,
_noopWatcher,
10,
true,
Executors.newScheduledThreadPool(1),
20L)
.addMockedMethod(RetryZooKeeper.class.getMethod("zkExists",
String.class,
boolean.class,
AsyncCallback.StatCallback.class,
Object.class))
.createMock();
rzkPartialMock.zkExists(
(String) EasyMock.anyObject(),
EasyMock.anyBoolean(),
(AsyncCallback.StatCallback) EasyMock.anyObject(),
EasyMock.anyObject());
// have to set thread-safe in order for mock to be
// run by two threads
EasyMock.makeThreadSafe(rzkPartialMock, true);
expectExistCallbackWithCode(_connectionLossRC);
expectExistCallbackWithCode(_connectionLossRC);
expectExistCallbackWithCode(_connectionLossRC);
expectExistCallbackWithCode(_connectionLossRC);
expectExistCallbackWithCode(_okRC);
EasyMock.replay(rzkPartialMock);
rzkPartialMock.exists(_dummyPath, false, _dummyStatCallback, _dummyCtx);
Thread.sleep(100);
Assert.assertEquals(rzkPartialMock.getInterval(), 80);
Thread.sleep(100);
Assert.assertEquals(rzkPartialMock.getInterval(), 160);
Thread.sleep(200);
Assert.assertEquals(rzkPartialMock.getInterval(), 20);
EasyMock.verify(rzkPartialMock);
}
private static RetryZooKeeper createMockObject(Method... methods)
{
final IMockBuilder<RetryZooKeeper> mockBuilder = EasyMock.createMockBuilder(RetryZooKeeper.class)
.withConstructor(_rzkCstr1)
.withArgs("127.0.0.1:11711",
5000000,
_noopWatcher,
10);
for (Method m: methods)
{
mockBuilder.addMockedMethod(m);
}
return mockBuilder.createMock();
}
}