package org.apache.hadoop.hdfs.server.namenode;
import java.io.IOException;
import java.io.PushbackInputStream;
import java.util.Random;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.apache.hadoop.hdfs.server.common.HdfsConstants.StartupOption;
import org.apache.hadoop.hdfs.util.InjectionEvent;
import org.apache.hadoop.util.InjectionEventI;
import org.apache.hadoop.util.InjectionHandler;
import org.junit.After;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
public class TestPersistTxId {
private MiniDFSCluster cluster;
private Configuration conf;
private FileSystem fs;
private Random random;
private static Log LOG = LogFactory.getLog(TestPersistTxId.class);
public void setUp(boolean simulateEditLogCrash) throws IOException {
conf = new Configuration();
MiniDFSCluster.clearBaseDirectory(conf);
conf.set("dfs.secondary.http.address", "0.0.0.0:0");
TestPersistTxIdInjectionHandler h = new TestPersistTxIdInjectionHandler();
h.simulateEditLogCrash = simulateEditLogCrash;
InjectionHandler.set(h);
cluster = new MiniDFSCluster(conf, 3, true, null);
fs = cluster.getFileSystem();
random = new Random();
}
@After
public void tearDown() throws Exception {
if (fs != null)
fs.close();
if (cluster != null)
cluster.shutdown();
InjectionHandler.clear();
}
private long getLastWrittenTxId() {
return cluster.getNameNode().getFSImage().getEditLog().getLastWrittenTxId();
}
public void createEdits(int nEdits) throws IOException {
for (int i = 0; i < nEdits / 2; i++) {
// Create file ends up logging two edits to the edit log, one for create
// file and one for bumping up the generation stamp
fs.create(new Path("/" + random.nextInt()));
}
}
@Test
public void testTxIdMismatchHard() throws IOException {
setUp(false);
// Create some edits and verify.
createEdits(20);
assertEquals(20, getLastWrittenTxId());
cluster.getNameNode().getFSImage().getEditLog().setLastWrittenTxId(50);
// Closing each file would generate 10 edits.
fs.close();
assertEquals(60, getLastWrittenTxId());
// Restart namenode and verify that it fails due to gap in txids.
try {
cluster.restartNameNode(0);
} catch (IOException e) {
System.out.println("Expected exception : " + e);
return;
}
fail("Did not throw IOException");
}
@Test
public void testTxIdMismatchSoft() throws IOException {
setUp(false);
// Create some edits and verify.
createEdits(20);
assertEquals(20, getLastWrittenTxId());
cluster.getNameNode().getFSImage().getEditLog().setLastWrittenTxId(50);
// Closing each file would generate 10 edits.
fs.close();
assertEquals(60, getLastWrittenTxId());
// we will answer "Continue" at txid mismatch
PushbackInputStream stream = new PushbackInputStream(System.in,
100);
System.setIn(stream);
// PushbackInputStream processes in reverse order.
byte input[] = "c".getBytes();
stream.unread(input);
// Restart namenode and verify that it does not fail.
cluster.restartNameNode(0,
new String[] {StartupOption.IGNORETXIDMISMATCH.getName()});
fs = cluster.getFileSystem();
// restarting generates OP_START_LOG_SEGMENT, OP_END_LOG_SEGMENT
assertEquals(60 + 2, getLastWrittenTxId());
createEdits(20);
assertEquals(80 + 2, getLastWrittenTxId());
}
@Test
public void testPersistTxId() throws IOException {
setUp(false);
// Create some edits and verify.
createEdits(20);
assertEquals(20, getLastWrittenTxId());
// Closing each file would generate 10 edits.
fs.close();
assertEquals(30, getLastWrittenTxId());
// Restart namenode and verify.
cluster.restartNameNode(0);
fs = cluster.getFileSystem();
// restarting generates OP_START_LOG_SEGMENT, OP_END_LOG_SEGMENT
assertEquals(30 + 2, getLastWrittenTxId());
// Add some more edits and verify.
createEdits(20);
assertEquals(50 + 2, getLastWrittenTxId());
// Now save namespace and verify edits.
// savenamespace generates OP_START_LOG_SEGMENT, OP_END_LOG_SEGMENT
cluster.getNameNode().saveNamespace(true, false);
assertEquals(50 + 4, getLastWrittenTxId());
createEdits(20);
assertEquals(70 + 4, getLastWrittenTxId());
}
@Test
public void testRestartWithCheckpoint() throws IOException {
setUp(false);
createEdits(20);
assertEquals(20, getLastWrittenTxId());
cluster.getNameNode().saveNamespace(true, false);
// SN generates OP_START_LOG_SEGMENT, OP_END_LOG_SEGMENT
createEdits(20);
assertEquals(40 + 2, getLastWrittenTxId());
cluster.restartNameNode(0);
createEdits(20);
// restarting generates OP_START_LOG_SEGMENT, OP_END_LOG_SEGMENT
assertEquals(60 + 4, getLastWrittenTxId());
}
@Test
public void testCheckpointBySecondary() throws IOException {
setUp(false);
SecondaryNameNode sn = new SecondaryNameNode(conf);
try {
createEdits(20);
assertEquals(20, getLastWrittenTxId());
sn.doCheckpoint();
// checkpoint generates OP_START_LOG_SEGMENT, OP_END_LOG_SEGMENT
assertEquals(20 + 2, getLastWrittenTxId());
createEdits(20);
assertEquals(40 + 2, getLastWrittenTxId());
} finally {
sn.shutdown();
}
}
@Test
public void testCheckpointBySecondaryAcrossRestart() throws IOException {
setUp(false);
SecondaryNameNode sn = new SecondaryNameNode(conf);
try {
createEdits(20);
assertEquals(20, getLastWrittenTxId());
sn.doCheckpoint();
// checkpoint generates OP_START_LOG_SEGMENT, OP_END_LOG_SEGMENT
assertEquals(20 + 2, getLastWrittenTxId());
createEdits(20);
assertEquals(40 + 2, getLastWrittenTxId());
// restart generates OP_START_LOG_SEGMENT, OP_END_LOG_SEGMENT
cluster.restartNameNode(0);
assertEquals(40 + 4, getLastWrittenTxId());
createEdits(20);
assertEquals(60 + 4, getLastWrittenTxId());
} finally {
sn.shutdown();
}
}
@Test
public void testMultipleRestarts() throws IOException {
setUp(false);
int restarts = random.nextInt(10);
restarts = 2;
int totalEdits = 0;
for (int i = 0; i < restarts; i++) {
int edits = getRandomEvenInt(50);
totalEdits += edits;
createEdits(edits);
System.out.println("Restarting namenode");
cluster.restartNameNode(0);
System.out.println("Restart done");
// restart generates OP_START_LOG_SEGMENT, OP_END_LOG_SEGMENT
assertEquals(totalEdits + 2*(i+1), getLastWrittenTxId());
}
System.out.println("Number of restarts : " + restarts);
assertEquals(totalEdits + 2*restarts, getLastWrittenTxId());
}
@Test
public void testMultipleRestartsWithCheckPoint() throws IOException {
setUp(false);
int sn = 0;
int restarts = random.nextInt(10);
int totalEdits = 0;
for (int i = 0; i < restarts; i++) {
int edits = getRandomEvenInt(50);
totalEdits += edits;
createEdits(edits);
if (random.nextBoolean()) {
sn++;
cluster.getNameNode().saveNamespace(true, false);
}
cluster.restartNameNode(0);
// restart generates OP_START_LOG_SEGMENT, OP_END_LOG_SEGMENT
assertEquals(totalEdits + 2*(i+1) + (2*sn), getLastWrittenTxId());
}
System.out.println("Number of restarts : " + restarts);
assertEquals(totalEdits + 2*restarts + (2*sn), getLastWrittenTxId());
}
@Test
public void testMultipleNameNodeCrashWithCheckpoint() throws Exception {
setUp(true);
int sn = 0;
int restarts = random.nextInt(10);
int totalEdits = 0;
for (int i = 0; i < restarts; i++) {
int edits = getRandomEvenInt(50);
totalEdits += edits;
createEdits(edits);
if (random.nextBoolean()) {
sn++;
cluster.getNameNode().saveNamespace(true, false);
}
cluster.getNameNode().getFSImage().getEditLog().logSync();
cluster.getNameNode().getFSImage().storage.unlockAll();
cluster.restartNameNode(0);
// restart/sn generates OP_END_LOG_SEGMENT, as the shutdown crashes
assertEquals(totalEdits + (i+1) + (sn*2), getLastWrittenTxId());
}
System.out.println("Number of restarts : " + restarts);
assertEquals(totalEdits + restarts + (sn*2), getLastWrittenTxId());
}
// Gets a random even int.
private int getRandomEvenInt(int limit) {
int n = random.nextInt(limit);
if (n % 2 == 0) {
return n;
} else {
return n + 1;
}
}
class TestPersistTxIdInjectionHandler extends InjectionHandler {
boolean simulateEditLogCrash = false;
@Override
public boolean _trueCondition(InjectionEventI event, Object... args) {
if (event == InjectionEvent.FSNAMESYSTEM_CLOSE_DIRECTORY
&& simulateEditLogCrash) {
LOG.warn("Simulating edit log crash, not closing edit log cleanly as"
+ "part of shutdown");
return false;
}
return true;
}
}
}