/**
* 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.
*/
package org.apache.zookeeper.server.quorum;
import static org.apache.zookeeper.test.ClientBase.CONNECTION_TIMEOUT;
import java.io.ByteArrayOutputStream;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.log4j.Layout;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.WriterAppender;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.PortAssignment;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.OpCode;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper.States;
import org.apache.zookeeper.server.quorum.Leader.Proposal;
import org.apache.zookeeper.server.quorum.QuorumPeerTestBase.MainThread;
import org.apache.zookeeper.test.ClientBase;
import org.junit.Assert;
import org.junit.Test;
/**
* Test stand-alone server.
*
*/
public class QuorumPeerMainTest extends QuorumPeerTestBase {
/**
* Verify the ability to start a cluster.
*/
@Test
public void testQuorum() throws Exception {
ClientBase.setupTestEnv();
final int CLIENT_PORT_QP1 = PortAssignment.unique();
final int CLIENT_PORT_QP2 = PortAssignment.unique();
String quorumCfgSection =
"server.1=127.0.0.1:" + PortAssignment.unique()
+ ":" + PortAssignment.unique()
+ "\nserver.2=127.0.0.1:" + PortAssignment.unique()
+ ":" + PortAssignment.unique();
MainThread q1 = new MainThread(1, CLIENT_PORT_QP1, quorumCfgSection);
MainThread q2 = new MainThread(2, CLIENT_PORT_QP2, quorumCfgSection);
q1.start();
q2.start();
Assert.assertTrue("waiting for server 1 being up",
ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP1,
CONNECTION_TIMEOUT));
Assert.assertTrue("waiting for server 2 being up",
ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP2,
CONNECTION_TIMEOUT));
ZooKeeper zk = new ZooKeeper("127.0.0.1:" + CLIENT_PORT_QP1,
ClientBase.CONNECTION_TIMEOUT, this);
zk.create("/foo_q1", "foobar1".getBytes(), Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
Assert.assertEquals(new String(zk.getData("/foo_q1", null, null)), "foobar1");
zk.close();
zk = new ZooKeeper("127.0.0.1:" + CLIENT_PORT_QP2,
ClientBase.CONNECTION_TIMEOUT, this);
zk.create("/foo_q2", "foobar2".getBytes(), Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
Assert.assertEquals(new String(zk.getData("/foo_q2", null, null)), "foobar2");
zk.close();
q1.shutdown();
q2.shutdown();
Assert.assertTrue("waiting for server 1 down",
ClientBase.waitForServerDown("127.0.0.1:" + CLIENT_PORT_QP1,
ClientBase.CONNECTION_TIMEOUT));
Assert.assertTrue("waiting for server 2 down",
ClientBase.waitForServerDown("127.0.0.1:" + CLIENT_PORT_QP2,
ClientBase.CONNECTION_TIMEOUT));
}
/**
* Test early leader abandonment.
*/
@Test
public void testEarlyLeaderAbandonment() throws Exception {
ClientBase.setupTestEnv();
final int SERVER_COUNT = 3;
final int clientPorts[] = new int[SERVER_COUNT];
StringBuilder sb = new StringBuilder();
for(int i = 0; i < SERVER_COUNT; i++) {
clientPorts[i] = PortAssignment.unique();
sb.append("server."+i+"=127.0.0.1:"+PortAssignment.unique()+":"+PortAssignment.unique()+"\n");
}
String quorumCfgSection = sb.toString();
MainThread mt[] = new MainThread[SERVER_COUNT];
ZooKeeper zk[] = new ZooKeeper[SERVER_COUNT];
for(int i = 0; i < SERVER_COUNT; i++) {
mt[i] = new MainThread(i, clientPorts[i], quorumCfgSection);
mt[i].start();
zk[i] = new ZooKeeper("127.0.0.1:" + clientPorts[i], ClientBase.CONNECTION_TIMEOUT, this);
}
waitForAll(zk, States.CONNECTED);
// we need to shutdown and start back up to make sure that the create session isn't the first transaction since
// that is rather innocuous.
for(int i = 0; i < SERVER_COUNT; i++) {
mt[i].shutdown();
}
waitForAll(zk, States.CONNECTING);
for(int i = 0; i < SERVER_COUNT; i++) {
mt[i].start();
}
waitForAll(zk, States.CONNECTED);
// ok lets find the leader and kill everything else, we have a few
// seconds, so it should be plenty of time
int leader = -1;
Map<Long, Proposal> outstanding = null;
for(int i = 0; i < SERVER_COUNT; i++) {
if (mt[i].main.quorumPeer.leader == null) {
mt[i].shutdown();
} else {
leader = i;
outstanding = mt[leader].main.quorumPeer.leader.outstandingProposals;
}
}
try {
zk[leader].create("/zk"+leader, "zk".getBytes(), Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
Assert.fail("create /zk" + leader + " should have failed");
} catch(KeeperException e) {}
// just make sure that we actually did get it in process at the
// leader
Assert.assertTrue(outstanding.size() == 1);
Assert.assertTrue(((Proposal)outstanding.values().iterator().next()).request.hdr.getType() == OpCode.create);
// make sure it has a chance to write it to disk
Thread.sleep(1000);
mt[leader].shutdown();
waitForAll(zk, States.CONNECTING);
for(int i = 0; i < SERVER_COUNT; i++) {
if (i != leader) {
mt[i].start();
}
}
for(int i = 0; i < SERVER_COUNT; i++) {
if (i != leader) {
waitForOne(zk[i], States.CONNECTED);
zk[i].create("/zk" + i, "zk".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
mt[leader].start();
waitForAll(zk, States.CONNECTED);
// make sure everything is consistent
for(int i = 0; i < SERVER_COUNT; i++) {
for(int j = 0; j < SERVER_COUNT; j++) {
if (i == leader) {
Assert.assertTrue((j==leader?("Leader ("+leader+")"):("Follower "+j))+" should not have /zk" + i, zk[j].exists("/zk"+i, false) == null);
} else {
Assert.assertTrue((j==leader?("Leader ("+leader+")"):("Follower "+j))+" does not have /zk" + i, zk[j].exists("/zk"+i, false) != null);
}
}
}
for(int i = 0; i < SERVER_COUNT; i++) {
zk[i].close();
}
for(int i = 0; i < SERVER_COUNT; i++) {
mt[i].shutdown();
}
}
/**
* Test the case of server with highest zxid not present at leader election and joining later.
* This test case is for reproducing the issue and fixing the bug mentioned in ZOOKEEPER-1154
* and ZOOKEEPER-1156.
*/
@Test
public void testHighestZxidJoinLate() throws Exception {
int numServers = 3;
Servers svrs = LaunchServers(numServers);
String path = "/hzxidtest";
int leader=-1;
// find the leader
for (int i=0; i < numServers; i++) {
if (svrs.mt[i].main.quorumPeer.leader != null) {
leader = i;
}
}
// make sure there is a leader
Assert.assertTrue("There should be a leader", leader >=0);
int nonleader = (leader+1)%numServers;
byte[] input = new byte[1];
input[0] = 1;
byte[] output;
// Create a couple of nodes
svrs.zk[leader].create(path+leader, input, Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
svrs.zk[leader].create(path+nonleader, input, Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// make sure the updates indeed committed. If it is not
// the following statement will throw.
output = svrs.zk[leader].getData(path+nonleader, false, null);
// Shutdown every one else but the leader
for (int i=0; i < numServers; i++) {
if (i != leader) {
svrs.mt[i].shutdown();
}
}
input[0] = 2;
// Update the node on the leader
svrs.zk[leader].setData(path+leader, input, -1, null, null);
// wait some time to let this get written to disk
Thread.sleep(500);
// shut the leader down
svrs.mt[leader].shutdown();
System.gc();
waitForAll(svrs.zk, States.CONNECTING);
// Start everyone but the leader
for (int i=0; i < numServers; i++) {
if (i != leader) {
svrs.mt[i].start();
}
}
// wait to connect to one of these
waitForOne(svrs.zk[nonleader], States.CONNECTED);
// validate that the old value is there and not the new one
output = svrs.zk[nonleader].getData(path+leader, false, null);
Assert.assertEquals(
"Expecting old value 1 since 2 isn't committed yet",
output[0], 1);
// Do some other update, so we bump the maxCommttedZxid
// by setting the value to 2
svrs.zk[nonleader].setData(path+nonleader, input, -1);
// start the old leader
svrs.mt[leader].start();
// connect to it
waitForOne(svrs.zk[leader], States.CONNECTED);
// make sure it doesn't have the new value that it alone had logged
output = svrs.zk[leader].getData(path+leader, false, null);
Assert.assertEquals(
"Validating that the deposed leader has rolled back that change it had written",
output[0], 1);
// make sure the leader has the subsequent changes that were made while it was offline
output = svrs.zk[leader].getData(path+nonleader, false, null);
Assert.assertEquals(
"Validating that the deposed leader caught up on changes it missed",
output[0], 2);
}
private void waitForOne(ZooKeeper zk, States state) throws InterruptedException {
while(zk.getState() != state) {
Thread.sleep(500);
}
}
private void waitForAll(ZooKeeper[] zks, States state) throws InterruptedException {
int iterations = 10;
boolean someoneNotConnected = true;
while(someoneNotConnected) {
if (iterations-- == 0) {
throw new RuntimeException("Waiting too long");
}
someoneNotConnected = false;
for(ZooKeeper zk: zks) {
if (zk.getState() != state) {
someoneNotConnected = true;
}
}
Thread.sleep(1000);
}
}
// This class holds the servers and clients for those servers
private class Servers {
MainThread mt[];
ZooKeeper zk[];
}
/**
* This is a helper function for launching a set of servers
*
* @param numServers
* @return
* @throws IOException
* @throws InterruptedException
*/
private Servers LaunchServers(int numServers) throws IOException, InterruptedException {
int SERVER_COUNT = numServers;
Servers svrs = new Servers();
final int clientPorts[] = new int[SERVER_COUNT];
StringBuilder sb = new StringBuilder();
for(int i = 0; i < SERVER_COUNT; i++) {
clientPorts[i] = PortAssignment.unique();
sb.append("server."+i+"=127.0.0.1:"+PortAssignment.unique()+":"+PortAssignment.unique()+"\n");
}
String quorumCfgSection = sb.toString();
MainThread mt[] = new MainThread[SERVER_COUNT];
ZooKeeper zk[] = new ZooKeeper[SERVER_COUNT];
for(int i = 0; i < SERVER_COUNT; i++) {
mt[i] = new MainThread(i, clientPorts[i], quorumCfgSection);
mt[i].start();
zk[i] = new ZooKeeper("127.0.0.1:" + clientPorts[i], ClientBase.CONNECTION_TIMEOUT, this);
}
waitForAll(zk, States.CONNECTED);
svrs.mt = mt;
svrs.zk = zk;
return svrs;
}
/**
* Verify handling of bad quorum address
*/
@Test
public void testBadPeerAddressInQuorum() throws Exception {
ClientBase.setupTestEnv();
// setup the logger to capture all logs
Layout layout =
Logger.getRootLogger().getAppender("CONSOLE").getLayout();
ByteArrayOutputStream os = new ByteArrayOutputStream();
WriterAppender appender = new WriterAppender(layout, os);
appender.setThreshold(Level.WARN);
Logger qlogger = Logger.getLogger("org.apache.zookeeper.server.quorum");
qlogger.addAppender(appender);
try {
final int CLIENT_PORT_QP1 = PortAssignment.unique();
final int CLIENT_PORT_QP2 = PortAssignment.unique();
String quorumCfgSection =
"server.1=127.0.0.1:" + PortAssignment.unique()
+ ":" + PortAssignment.unique()
+ "\nserver.2=fee.fii.foo.fum:" + PortAssignment.unique()
+ ":" + PortAssignment.unique();
MainThread q1 = new MainThread(1, CLIENT_PORT_QP1, quorumCfgSection);
q1.start();
boolean isup =
ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP1,
5000);
Assert.assertFalse("Server never came up", isup);
q1.shutdown();
Assert.assertTrue("waiting for server 1 down",
ClientBase.waitForServerDown("127.0.0.1:" + CLIENT_PORT_QP1,
ClientBase.CONNECTION_TIMEOUT));
} finally {
qlogger.removeAppender(appender);
}
LineNumberReader r = new LineNumberReader(new StringReader(os.toString()));
String line;
boolean found = false;
Pattern p =
Pattern.compile(".*Cannot open channel to .* at election address .*");
while ((line = r.readLine()) != null) {
found = p.matcher(line).matches();
if (found) {
break;
}
}
Assert.assertTrue("complains about host", found);
}
/**
* Verify handling of inconsistent peer type
*/
@Test
public void testInconsistentPeerType() throws Exception {
ClientBase.setupTestEnv();
// setup the logger to capture all logs
Layout layout =
Logger.getRootLogger().getAppender("CONSOLE").getLayout();
ByteArrayOutputStream os = new ByteArrayOutputStream();
WriterAppender appender = new WriterAppender(layout, os);
appender.setThreshold(Level.INFO);
Logger qlogger = Logger.getLogger("org.apache.zookeeper.server.quorum");
qlogger.addAppender(appender);
// test the most likely situation only: server is stated as observer in
// servers list, but there's no "peerType=observer" token in config
try {
final int CLIENT_PORT_QP1 = PortAssignment.unique();
final int CLIENT_PORT_QP2 = PortAssignment.unique();
final int CLIENT_PORT_QP3 = PortAssignment.unique();
String quorumCfgSection =
"server.1=127.0.0.1:" + PortAssignment.unique()
+ ":" + PortAssignment.unique()
+ "\nserver.2=127.0.0.1:" + PortAssignment.unique()
+ ":" + PortAssignment.unique()
+ "\nserver.3=127.0.0.1:" + PortAssignment.unique()
+ ":" + PortAssignment.unique() + ":observer";
MainThread q1 = new MainThread(1, CLIENT_PORT_QP1, quorumCfgSection);
MainThread q2 = new MainThread(2, CLIENT_PORT_QP2, quorumCfgSection);
MainThread q3 = new MainThread(3, CLIENT_PORT_QP3, quorumCfgSection);
q1.start();
q2.start();
q3.start();
Assert.assertTrue("waiting for server 1 being up",
ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP1,
CONNECTION_TIMEOUT));
Assert.assertTrue("waiting for server 2 being up",
ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP2,
CONNECTION_TIMEOUT));
Assert.assertTrue("waiting for server 3 being up",
ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP3,
CONNECTION_TIMEOUT));
q1.shutdown();
q2.shutdown();
q3.shutdown();
Assert.assertTrue("waiting for server 1 down",
ClientBase.waitForServerDown("127.0.0.1:" + CLIENT_PORT_QP1,
ClientBase.CONNECTION_TIMEOUT));
Assert.assertTrue("waiting for server 2 down",
ClientBase.waitForServerDown("127.0.0.1:" + CLIENT_PORT_QP2,
ClientBase.CONNECTION_TIMEOUT));
Assert.assertTrue("waiting for server 3 down",
ClientBase.waitForServerDown("127.0.0.1:" + CLIENT_PORT_QP3,
ClientBase.CONNECTION_TIMEOUT));
} finally {
qlogger.removeAppender(appender);
}
LineNumberReader r = new LineNumberReader(new StringReader(os.toString()));
String line;
boolean warningPresent = false;
boolean defaultedToObserver = false;
Pattern pWarn =
Pattern.compile(".*Peer type from servers list.* doesn't match peerType.*");
Pattern pObserve = Pattern.compile(".*OBSERVING.*");
while ((line = r.readLine()) != null) {
if (pWarn.matcher(line).matches()) {
warningPresent = true;
}
if (pObserve.matcher(line).matches()) {
defaultedToObserver = true;
}
if (warningPresent && defaultedToObserver) {
break;
}
}
Assert.assertTrue("Should warn about inconsistent peer type",
warningPresent && defaultedToObserver);
}
/**
* verify if bad packets are being handled properly
* at the quorum port
* @throws Exception
*/
@Test
public void testBadPackets() throws Exception {
ClientBase.setupTestEnv();
final int CLIENT_PORT_QP1 = PortAssignment.unique();
final int CLIENT_PORT_QP2 = PortAssignment.unique();
int electionPort1 = PortAssignment.unique();
int electionPort2 = PortAssignment.unique();
String quorumCfgSection =
"server.1=127.0.0.1:" + PortAssignment.unique()
+ ":" + electionPort1
+ "\nserver.2=127.0.0.1:" + PortAssignment.unique()
+ ":" + electionPort2;
MainThread q1 = new MainThread(1, CLIENT_PORT_QP1, quorumCfgSection);
MainThread q2 = new MainThread(2, CLIENT_PORT_QP2, quorumCfgSection);
q1.start();
q2.start();
Assert.assertTrue("waiting for server 1 being up",
ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP1,
CONNECTION_TIMEOUT));
Assert.assertTrue("waiting for server 2 being up",
ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP2,
CONNECTION_TIMEOUT));
byte[] b = new byte[4];
int length = 1024*1024*1024;
ByteBuffer buff = ByteBuffer.wrap(b);
buff.putInt(length);
buff.position(0);
SocketChannel s = SocketChannel.open(new InetSocketAddress("127.0.0.1", electionPort1));
s.write(buff);
s.close();
buff.position(0);
s = SocketChannel.open(new InetSocketAddress("127.0.0.1", electionPort2));
s.write(buff);
s.close();
ZooKeeper zk = new ZooKeeper("127.0.0.1:" + CLIENT_PORT_QP1,
ClientBase.CONNECTION_TIMEOUT, this);
zk.create("/foo_q1", "foobar1".getBytes(), Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
Assert.assertEquals(new String(zk.getData("/foo_q1", null, null)), "foobar1");
zk.close();
q1.shutdown();
q2.shutdown();
}
/**
* Verify handling of quorum defaults
* * default electionAlg is fast leader election
*/
@Test
public void testQuorumDefaults() throws Exception {
ClientBase.setupTestEnv();
// setup the logger to capture all logs
Layout layout =
Logger.getRootLogger().getAppender("CONSOLE").getLayout();
ByteArrayOutputStream os = new ByteArrayOutputStream();
WriterAppender appender = new WriterAppender(layout, os);
appender.setImmediateFlush(true);
appender.setThreshold(Level.INFO);
Logger zlogger = Logger.getLogger("org.apache.zookeeper");
zlogger.addAppender(appender);
try {
final int CLIENT_PORT_QP1 = PortAssignment.unique();
final int CLIENT_PORT_QP2 = PortAssignment.unique();
String quorumCfgSection =
"server.1=127.0.0.1:" + PortAssignment.unique()
+ ":" + PortAssignment.unique()
+ "\nserver.2=127.0.0.1:" + PortAssignment.unique()
+ ":" + PortAssignment.unique();
MainThread q1 = new MainThread(1, CLIENT_PORT_QP1, quorumCfgSection);
MainThread q2 = new MainThread(2, CLIENT_PORT_QP2, quorumCfgSection);
q1.start();
q2.start();
Assert.assertTrue("waiting for server 1 being up",
ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP1,
CONNECTION_TIMEOUT));
Assert.assertTrue("waiting for server 2 being up",
ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP2,
CONNECTION_TIMEOUT));
q1.shutdown();
q2.shutdown();
Assert.assertTrue("waiting for server 1 down",
ClientBase.waitForServerDown("127.0.0.1:" + CLIENT_PORT_QP1,
ClientBase.CONNECTION_TIMEOUT));
Assert.assertTrue("waiting for server 2 down",
ClientBase.waitForServerDown("127.0.0.1:" + CLIENT_PORT_QP2,
ClientBase.CONNECTION_TIMEOUT));
} finally {
zlogger.removeAppender(appender);
}
os.close();
LineNumberReader r = new LineNumberReader(new StringReader(os.toString()));
String line;
boolean found = false;
Pattern p =
Pattern.compile(".*FastLeaderElection.*");
while ((line = r.readLine()) != null) {
found = p.matcher(line).matches();
if (found) {
break;
}
}
Assert.assertTrue("fastleaderelection used", found);
}
/**
* Verifies that QuorumPeer exits immediately
*/
@Test
public void testQuorumPeerExitTime() throws Exception {
long maxwait = 3000;
final int CLIENT_PORT_QP1 = PortAssignment.unique();
String quorumCfgSection =
"server.1=127.0.0.1:" + PortAssignment.unique()
+ ":" + PortAssignment.unique()
+ "\nserver.2=127.0.0.1:" + PortAssignment.unique()
+ ":" + PortAssignment.unique();
MainThread q1 = new MainThread(1, CLIENT_PORT_QP1, quorumCfgSection);
q1.start();
// Let the notifications timeout
Thread.sleep(30000);
long start = System.currentTimeMillis();
q1.shutdown();
long end = System.currentTimeMillis();
if ((end - start) > maxwait) {
Assert.fail("QuorumPeer took " + (end -start) +
" to shutdown, expected " + maxwait);
}
}
}