/**
* 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.hadoop.hdfs.qjournal.client;
import static org.junit.Assert.*;
import static org.apache.hadoop.hdfs.qjournal.QJMTestUtil.JID;
import static org.apache.hadoop.hdfs.qjournal.QJMTestUtil.FAKE_NSINFO;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hdfs.qjournal.MiniJournalCluster;
import org.apache.hadoop.hdfs.qjournal.QJMTestUtil;
import org.apache.hadoop.hdfs.qjournal.protocol.JournalConfigKeys;
import org.apache.hadoop.hdfs.qjournal.server.JournalNode;
import org.apache.hadoop.hdfs.server.common.HdfsConstants;
import org.apache.hadoop.hdfs.server.common.HdfsConstants.Transition;
import org.apache.hadoop.hdfs.server.namenode.EditLogInputStream;
import org.apache.hadoop.hdfs.server.namenode.EditLogOutputStream;
import org.apache.hadoop.hdfs.server.namenode.FSEditLogOp;
import org.apache.hadoop.hdfs.server.namenode.RedundantEditLogInputStream;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.google.common.collect.Lists;
public class TestQuorumJournalManagerInputStream {
private static final Log LOG = LogFactory.getLog(
TestQuorumJournalManagerInputStream.class);
private MiniJournalCluster cluster;
private Configuration conf;
private QuorumJournalManager qjm;
@Before
public void setup() throws Exception {
conf = new Configuration();
// Don't retry connections - it just slows down the tests.
conf.setInt("ipc.client.connect.max.retries", 0);
conf.setLong(JournalConfigKeys.DFS_QJOURNAL_CONNECT_TIMEOUT_KEY, 100);
cluster = new MiniJournalCluster.Builder(conf)
.build();
qjm = TestQuorumJournalManager.createSpyingQJM(conf, cluster, JID, FAKE_NSINFO);
qjm.transitionJournal(QJMTestUtil.FAKE_NSINFO, Transition.FORMAT, null);
qjm.recoverUnfinalizedSegments();
assertEquals(1, qjm.getLoggerSetForTests().getEpoch());
}
@After
public void shutdown() throws IOException {
if (cluster != null) {
cluster.shutdown();
}
}
/**
* Get underlying streams.
*/
URLLogInputStream[] getStreams(EditLogInputStream str) throws Exception {
// there is a single redundant stream
RedundantEditLogInputStream relis = (RedundantEditLogInputStream) str;
Field streamsF = RedundantEditLogInputStream.class.getDeclaredField("streams");
streamsF.setAccessible(true);
EditLogInputStream[] underlyingStreams = (EditLogInputStream[])streamsF.get(relis);
URLLogInputStream[] urlStreams = new URLLogInputStream[underlyingStreams.length];
int i = 0;
for(EditLogInputStream elis : underlyingStreams) {
urlStreams[i++] = (URLLogInputStream) elis;
}
return urlStreams;
}
/**
* Assert that refresh works properly, and if one node dies, the corresponding
* stream is in the right state. The position thereof should be preserved, and
* it should be disabled.
*/
@Test
public void testRefresh() throws Exception {
// start new segment
EditLogOutputStream stm = qjm.startLogSegment(0);
// write a bunch of transactions
QJMTestUtil.writeTxns(stm, 0, 10);
// get input stream
List<EditLogInputStream> streams = Lists.newArrayList();
qjm.selectInputStreams(streams, 0, true, false);
URLLogInputStream[] underlyingStreams = getStreams(streams.get(0));
// 1) initial setup
// all streams start at 0
for(EditLogInputStream elis : underlyingStreams) {
assertEquals(0, elis.getFirstTxId());
// read version info
assertEquals(4, elis.getPosition());
}
// 2) refresh to position 10 - all should be refreshed
// refresh redundant stream - this should referesh all underlying streams
streams.get(0).refresh(10, 0);
// all streams are refreshed to 10 (position)
for(EditLogInputStream elis : underlyingStreams) {
assertEquals(0, elis.getFirstTxId());
assertEquals(10, elis.getPosition());
}
// 3) refresh to position 15 after killing one node
// shutdown one journal node
cluster.getJournalNode(0).stopAndJoin(0);
// refresh redundant stream - this should referesh all underlying streams
streams.get(0).refresh(15, 0);
boolean foundCrashedOne = false;
int numSuccessful = 0;
for(EditLogInputStream stream : underlyingStreams) {
URLLogInputStream elis = (URLLogInputStream) stream;
if (!elis.isDisabled()) {
numSuccessful++;
} else if (!foundCrashedOne) {
foundCrashedOne = true;
} else {
fail("This should not happen");
}
}
assertEquals(2, numSuccessful);
assertTrue(foundCrashedOne);
}
/**
* Ensure that refresh functionality does not work for finalized streams (at
* startup)
*/
@Test
public void testRefreshOnlyForInprogress() throws Exception {
// start new segment
EditLogOutputStream stm = qjm.startLogSegment(0);
// write a bunch of transactions
QJMTestUtil.writeTxns(stm, 0, 10);
qjm.finalizeLogSegment(0, 9);
// get input stream
List<EditLogInputStream> streams = Lists.newArrayList();
// get only finalized streams
qjm.selectInputStreams(streams, 0, false, false);
try {
// try refreshing the stream (this is startup mode
// inprogress segments not allowed -> refresh should fail
streams.get(0).refresh(10, 0);
fail("The shream should not allow refreshing");
} catch (IOException e) {
LOG.info("Expected exception: ", e);
}
}
/**
* Test fall back behaviour, after the tailing node fails, we should switch to
* the next one and keep reading transactions.
*/
@Test
public void testReadFailsAfterFailedRefresh() throws Exception {
// start new segment
EditLogOutputStream stm = qjm.startLogSegment(0);
// write a bunch of transactions
QJMTestUtil.writeTxns(stm, 0, 10);
QJMTestUtil.writeTxns(stm, 10, 10);
// get input stream
List<EditLogInputStream> streams = Lists.newArrayList();
// get inprogress streams
qjm.selectInputStreams(streams, 0, true, false);
long lastReadTxId = -1;
EditLogInputStream is = streams.get(0);
for (int i = 0; i < 3; i++) {
FSEditLogOp op = is.readOp();
assertNotNull(op);
lastReadTxId = op.getTransactionId();
LOG.info("Read transaction: " + op + " with txid: "
+ op.getTransactionId());
}
// get the stream we are tailing from
URLLogInputStream[] tailing = new URLLogInputStream[1];
JournalNode jn = getTailingJN(is, tailing);
long position = is.getPosition();
// stop the node
jn.stopAndJoin(0);
// refresh the input stream
is.refresh(position, 0);
LOG.info("Checking failed stream");
// this guy should be disabled
// its position should be fixed
URLLogInputStream urlis = tailing[0];
assertTrue(urlis.isDisabled());
assertEquals(position, urlis.getPosition());
assertEquals(HdfsConstants.INVALID_TXID, urlis.getLastTxId());
try {
urlis.readOp();
fail("This read should fail");
} catch (IOException e) {
LOG.info("Expected exception: ", e);
} // expected
// reads should fall back to another stream
LOG.info("We should be able to read from the stream");
for (int i = 0; i < 3; i++) {
FSEditLogOp op = is.readOp();
assertNotNull(op);
assertEquals(++lastReadTxId, op.getTransactionId());
LOG.info("Read transaction: " + op + " with txid: "
+ op.getTransactionId());
position = is.getPosition();
}
LOG.info("Current state of the input stream: " + is.getName());
// refresh again
is.refresh(position, 0);
assertEquals(position, urlis.getPosition());
assertTrue(urlis.isDisabled());
assertEquals(HdfsConstants.INVALID_TXID, urlis.getLastTxId());
}
/**
* Get the journal node we are tailing from, and indicate which stream this is.
*/
private JournalNode getTailingJN(EditLogInputStream str,
URLLogInputStream[] tailingStream) throws Exception {
RedundantEditLogInputStream is = (RedundantEditLogInputStream) str;
Field curIdxF = RedundantEditLogInputStream.class
.getDeclaredField("curIdx");
curIdxF.setAccessible(true);
int curIdx = curIdxF.getInt(is);
URLLogInputStream[] streams = getStreams(is);
JournalNode jn = null;
for (JournalNode j : cluster.getJournalNodes()) {
if (streams[curIdx].getName().contains(
Integer.toString(j.getBoundHttpAddress().getPort()))) {
jn = j;
break;
}
}
tailingStream[0] = streams[curIdx];
return jn;
}
}