Java tutorial
/** * 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.server; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.URL; import java.util.concurrent.ExecutionException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileUtil; import org.apache.hadoop.hdfs.protocol.FSConstants; import org.apache.hadoop.hdfs.qjournal.protocol.JournalConfigKeys; import org.apache.hadoop.hdfs.MiniDFSCluster; import org.apache.hadoop.hdfs.qjournal.MiniJournalCluster; import org.apache.hadoop.hdfs.qjournal.QJMTestUtil; import org.apache.hadoop.hdfs.qjournal.client.IPCLoggerChannel; import org.apache.hadoop.hdfs.qjournal.client.QuorumJournalManager; import org.apache.hadoop.hdfs.qjournal.protocol.QJournalProtocolProtos.NewEpochResponseProto; import org.apache.hadoop.hdfs.qjournal.protocol.QJournalProtocolProtos.PrepareRecoveryResponseProto; import org.apache.hadoop.hdfs.qjournal.server.Journal; import org.apache.hadoop.hdfs.qjournal.server.JournalNode; import org.apache.hadoop.hdfs.server.common.HdfsConstants.Transition; import org.apache.hadoop.hdfs.server.protocol.NamespaceInfo; import org.apache.hadoop.test.GenericTestUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; import com.google.common.base.Charsets; import com.google.common.base.Stopwatch; import com.google.common.primitives.Bytes; import com.google.common.primitives.Ints; public class TestJournalNode { static final Log LOG = LogFactory.getLog(TestJournalNode.class); private static final NamespaceInfo FAKE_NSINFO = new NamespaceInfo(12345, 0L, 0); private JournalNode jn; private Journal journal; private Configuration conf = new Configuration(); private IPCLoggerChannel ch; private String journalId; // TODO assert metrics //static { // Avoid an error when we double-initialize JvmMetrics // DefaultMetricsSystem.setMiniClusterMode(true); //} @Before public void setup() throws Exception { File editsDir = new File(MiniDFSCluster.getBaseDirectory(null) + File.separator + "TestJournalNode"); FileUtil.fullyDelete(editsDir); conf.set(JournalConfigKeys.DFS_JOURNALNODE_DIR_KEY, editsDir.getAbsolutePath()); conf.set(JournalConfigKeys.DFS_JOURNALNODE_RPC_ADDRESS_KEY, "0.0.0.0:0"); MiniJournalCluster.getFreeHttpPortAndUpdateConf(conf, true); jn = new JournalNode(); jn.setConf(conf); jn.start(); journalId = "test-journalid-" + QJMTestUtil.uniqueSequenceId(); journal = jn.getOrCreateJournal(QuorumJournalManager.journalIdStringToBytes(journalId)); journal.transitionJournal(FAKE_NSINFO, Transition.FORMAT, null); ch = new IPCLoggerChannel(conf, FAKE_NSINFO, journalId, jn.getBoundIpcAddress()); } @After public void teardown() throws Exception { jn.stop(0); } @Test public void testJournal() throws Exception { //MetricsRecordBuilder metrics = MetricsAsserts.getMetrics( // journal.getMetricsForTests().getName()); //MetricsAsserts.assertCounter("BatchesWritten", 0L, metrics); //MetricsAsserts.assertCounter("BatchesWrittenWhileLagging", 0L, metrics); //MetricsAsserts.assertGauge("CurrentLagTxns", 0L, metrics); IPCLoggerChannel ch = new IPCLoggerChannel(conf, FAKE_NSINFO, journalId, jn.getBoundIpcAddress()); ch.newEpoch(1).get(); ch.setEpoch(1); ch.startLogSegment(1).get(); ch.sendEdits(1L, 1, 1, "hello".getBytes(Charsets.UTF_8)).get(); //metrics = MetricsAsserts.getMetrics( // journal.getMetricsForTests().getName()); //MetricsAsserts.assertCounter("BatchesWritten", 1L, metrics); //MetricsAsserts.assertCounter("BatchesWrittenWhileLagging", 0L, metrics); //MetricsAsserts.assertGauge("CurrentLagTxns", 0L, metrics); ch.setCommittedTxId(100L, false); ch.sendEdits(1L, 2, 1, "goodbye".getBytes(Charsets.UTF_8)).get(); //metrics = MetricsAsserts.getMetrics( // journal.getMetricsForTests().getName()); //MetricsAsserts.assertCounter("BatchesWritten", 2L, metrics); //MetricsAsserts.assertCounter("BatchesWrittenWhileLagging", 1L, metrics); //MetricsAsserts.assertGauge("CurrentLagTxns", 98L, metrics); } @Test public void testReturnsSegmentInfoAtEpochTransition() throws Exception { ch.newEpoch(1).get(); ch.setEpoch(1); ch.startLogSegment(1).get(); ch.sendEdits(1L, 1, 2, QJMTestUtil.createTxnData(1, 2)).get(); // Switch to a new epoch without closing earlier segment NewEpochResponseProto response = ch.newEpoch(2).get(); ch.setEpoch(2); assertEquals(1, response.getLastSegmentTxId()); ch.finalizeLogSegment(1, 2).get(); // Switch to a new epoch after just closing the earlier segment. response = ch.newEpoch(3).get(); ch.setEpoch(3); assertEquals(1, response.getLastSegmentTxId()); // Start a segment but don't write anything, check newEpoch segment info ch.startLogSegment(3).get(); response = ch.newEpoch(4).get(); ch.setEpoch(4); // Because the new segment is empty, it is equivalent to not having // started writing it. Hence, we should return the prior segment txid. assertEquals(1, response.getLastSegmentTxId()); } @Test public void testHttpServer() throws Exception { InetSocketAddress addr = jn.getBoundHttpAddress(); assertTrue(addr.getPort() > 0); String urlRoot = "http://localhost:" + addr.getPort(); // TODO other servlets // Create some edits on server side int numTxns = 10; byte[] EDITS_DATA = QJMTestUtil.createTxnData(1, numTxns); IPCLoggerChannel ch = new IPCLoggerChannel(conf, FAKE_NSINFO, journalId, jn.getBoundIpcAddress()); ch.newEpoch(1).get(); ch.setEpoch(1); ch.startLogSegment(1).get(); ch.sendEdits(1L, 1, numTxns, EDITS_DATA).get(); ch.finalizeLogSegment(1, numTxns).get(); // Attempt to retrieve via HTTP, ensure we get the data back // including the header we expected byte[] retrievedViaHttp = QJMTestUtil .urlGetBytes(new URL(urlRoot + "/getJournal?segmentTxId=1&position=0&jid=" + journalId)); byte[] expected = Bytes.concat(Ints.toByteArray(FSConstants.LAYOUT_VERSION), EDITS_DATA); // retrieve partial edits int pos = 100; byte[] expectedPart = new byte[expected.length - pos]; System.arraycopy(expected, pos, expectedPart, 0, expectedPart.length); retrievedViaHttp = QJMTestUtil .urlGetBytes(new URL(urlRoot + "/getJournal?segmentTxId=1&position=" + pos + "&jid=" + journalId)); assertArrayEquals(expectedPart, retrievedViaHttp); // Attempt to fetch a non-existent file, check that we get an // error status code URL badUrl = new URL(urlRoot + "/getJournal?segmentTxId=12345&position=0&jid=" + journalId); HttpURLConnection connection = (HttpURLConnection) badUrl.openConnection(); try { assertEquals(404, connection.getResponseCode()); } finally { connection.disconnect(); } } /** * Test that the JournalNode performs correctly as a Paxos * <em>Acceptor</em> process. */ @Test public void testAcceptRecoveryBehavior() throws Exception { // We need to run newEpoch() first, or else we have no way to distinguish // different proposals for the same decision. try { ch.prepareRecovery(1L).get(); fail("Did not throw IllegalState when trying to run paxos without an epoch"); } catch (ExecutionException ise) { GenericTestUtils.assertExceptionContains("bad epoch", ise); } ch.newEpoch(1).get(); ch.setEpoch(1); // prepare() with no previously accepted value and no logs present PrepareRecoveryResponseProto prep = ch.prepareRecovery(1L).get(); System.err.println("Prep: " + prep); assertFalse(prep.hasAcceptedInEpoch()); assertFalse(prep.hasSegmentState()); // Make a log segment, and prepare again -- this time should see the // segment existing. ch.startLogSegment(1L).get(); ch.sendEdits(1L, 1L, 1, QJMTestUtil.createTxnData(1, 1)).get(); prep = ch.prepareRecovery(1L).get(); System.err.println("Prep: " + prep); assertFalse(prep.hasAcceptedInEpoch()); assertTrue(prep.hasSegmentState()); // accept() should save the accepted value in persistent storage ch.acceptRecovery(prep.getSegmentState(), "file:///dev/null").get(); // So another prepare() call from a new epoch would return this value ch.newEpoch(2); ch.setEpoch(2); prep = ch.prepareRecovery(1L).get(); assertEquals(1L, prep.getAcceptedInEpoch()); assertEquals(1L, prep.getSegmentState().getEndTxId()); // A prepare() or accept() call from an earlier epoch should now be rejected ch.setEpoch(1); try { ch.prepareRecovery(1L).get(); fail("prepare from earlier epoch not rejected"); } catch (ExecutionException ioe) { GenericTestUtils.assertExceptionContains("epoch 1 is less than the last promised epoch 2", ioe); } try { ch.acceptRecovery(prep.getSegmentState(), "file:///dev/null").get(); fail("accept from earlier epoch not rejected"); } catch (ExecutionException ioe) { GenericTestUtils.assertExceptionContains("epoch 1 is less than the last promised epoch 2", ioe); } } @Test public void testFailToStartWithBadConfig() throws Exception { Configuration conf = new Configuration(); conf.set(JournalConfigKeys.DFS_JOURNALNODE_DIR_KEY, "non-absolute-path"); MiniJournalCluster.getFreeHttpPortAndUpdateConf(conf, true); assertJNFailsToStart(conf, "should be an absolute path"); // Existing file which is not a directory conf.set(JournalConfigKeys.DFS_JOURNALNODE_DIR_KEY, "/dev/null"); assertJNFailsToStart(conf, "is not a directory"); // Directory which cannot be created conf.set(org.apache.hadoop.hdfs.qjournal.protocol.JournalConfigKeys.DFS_JOURNALNODE_DIR_KEY, "/proc/does-not-exist"); assertJNFailsToStart(conf, "Could not create"); } private static void assertJNFailsToStart(Configuration conf, String errString) { try { JournalNode jn = new JournalNode(); jn.setConf(conf); jn.start(); } catch (Exception e) { GenericTestUtils.assertExceptionContains(errString, e); } } /** * Simple test of how fast the code path is to write edits. * This isn't a true unit test, but can be run manually to * check performance. * * At the time of development, this test ran in ~4sec on an * SSD-enabled laptop (1.8ms/batch). */ @Test(timeout = 100000) public void testPerformance() throws Exception { doPerfTest(8192, 1024); // 8MB } private void doPerfTest(int editsSize, int numEdits) throws Exception { byte[] data = new byte[editsSize]; ch.newEpoch(1).get(); ch.setEpoch(1); ch.startLogSegment(1).get(); Stopwatch sw = new Stopwatch().start(); for (int i = 1; i < numEdits; i++) { ch.sendEdits(1L, i, 1, data).get(); } long time = sw.elapsedMillis(); System.err.println("Wrote " + numEdits + " batches of " + editsSize + " bytes in " + time + "ms"); float avgRtt = (float) time / (float) numEdits; long throughput = ((long) numEdits * editsSize * 1000L) / time; System.err.println("Time per batch: " + avgRtt + "ms"); System.err.println("Throughput: " + throughput + " bytes/sec"); } }