/** * 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.server.namenode; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import java.io.File; import java.io.IOException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.impl.Log4JLogger; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FileUtil; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.permission.FsPermission; import org.apache.hadoop.fs.permission.PermissionStatus; import org.apache.hadoop.hdfs.DFSConfigKeys; import org.apache.hadoop.hdfs.DFSTestUtil; import org.apache.hadoop.hdfs.HdfsConfiguration; import org.apache.hadoop.hdfs.MiniDFSCluster; import org.apache.hadoop.hdfs.protocol.AlreadyBeingCreatedException; import org.apache.hadoop.hdfs.protocol.DatanodeID; import org.apache.hadoop.hdfs.server.blockmanagement.BlockInfo; import org.apache.hadoop.hdfs.server.blockmanagement.BlockInfoUnderConstruction; import org.apache.hadoop.hdfs.server.blockmanagement.DatanodeDescriptor; import org.apache.hadoop.hdfs.server.common.HdfsServerConstants; import org.apache.hadoop.hdfs.server.common.HdfsServerConstants.NamenodeRole; import org.apache.hadoop.test.GenericTestUtils; import org.apache.log4j.Level; import org.junit.After; import org.junit.Before; import org.junit.Test; public class TestNNLeaseRecovery { private static final Log LOG = LogFactory.getLog(TestNNLeaseRecovery.class); private static final String NAME_DIR = MiniDFSCluster.getBaseDirectory() + "name"; FSNamesystem fsn; Configuration conf; static { ((Log4JLogger)FSNamesystem.LOG).getLogger().setLevel(Level.ALL); ((Log4JLogger)LOG).getLogger().setLevel(Level.ALL); } /** * Initiates and sets a spied on FSNamesystem so tests can hook its methods * @throws IOException if an error occurred */ @Before public void startUp() throws IOException { conf = new HdfsConfiguration(); conf.set(DFSConfigKeys.DFS_NAMENODE_NAME_DIR_KEY, NAME_DIR); conf.set(DFSConfigKeys.DFS_NAMENODE_EDITS_DIR_KEY, NAME_DIR); // avoid stubbing access control conf.setBoolean(DFSConfigKeys.DFS_PERMISSIONS_ENABLED_KEY, false); NameNode.initMetrics(conf, NamenodeRole.NAMENODE); FileSystem.setDefaultUri(conf, "hdfs://localhost:0"); conf.set(DFSConfigKeys.DFS_NAMENODE_HTTP_ADDRESS_KEY, "0.0.0.0:0"); DFSTestUtil.formatNameNode(conf); fsn = spy(new FSNamesystem(conf)); } /** * Cleans the resources and closes the instance of FSNamesystem * @throws IOException if an error occurred */ @After public void tearDown() throws IOException { if (fsn != null) { try { fsn.close(); } catch(Exception e) { LOG.error("Cannot close: ", e); } finally { File dir = new File(NAME_DIR); if (dir != null) assertTrue("Cannot delete name-node dirs", FileUtil.fullyDelete(dir)); } } } // Release the lease for the given file private boolean releaseLease(FSNamesystem ns, LeaseManager.Lease lm, Path file) throws IOException { fsn.writeLock(); try { return fsn.internalReleaseLease(lm, file.toString(), null); } finally { fsn.writeUnlock(); } } /** * Mocks FSNamesystem instance, adds an empty file and invokes lease recovery * method. * @throws IOException in case of an error */ @Test public void testInternalReleaseLease_allCOMPLETE () throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Running " + GenericTestUtils.getMethodName()); } LeaseManager.Lease lm = mock(LeaseManager.Lease.class); Path file = spy(new Path("/test.dat")); DatanodeDescriptor dnd = mock(DatanodeDescriptor.class); PermissionStatus ps = new PermissionStatus("test", "test", new FsPermission((short)0777)); fsn.dir.addFile(file.toString(), ps, (short)3, 1l, "test", "test-machine", dnd, 1001l); assertTrue("True has to be returned in this case", releaseLease(fsn, lm, file)); } /** * Mocks FSNamesystem instance, adds an empty file, sets status of last two * blocks to non-defined and UNDER_CONSTRUCTION and invokes lease recovery * method. IOException is expected for releasing a create lock on a * closed file. * @throws IOException as the result */ @Test(expected=IOException.class) public void testInternalReleaseLease_UNKNOWN_COMM () throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Running " + GenericTestUtils.getMethodName()); } LeaseManager.Lease lm = mock(LeaseManager.Lease.class); Path file = spy(new Path("/" + GenericTestUtils.getMethodName() + "_test.dat")); DatanodeDescriptor dnd = mock(DatanodeDescriptor.class); PermissionStatus ps = new PermissionStatus("test", "test", new FsPermission((short)0777)); mockFileBlocks(2, null, HdfsServerConstants.BlockUCState.UNDER_CONSTRUCTION, file, dnd, ps, false); releaseLease(fsn, lm, file); fail("FSNamesystem.internalReleaseLease suppose to throw " + "IOException here"); } /** * Mocks FSNamesystem instance, adds an empty file, sets status of last two * blocks to COMMITTED and COMMITTED and invokes lease recovery * method. AlreadyBeingCreatedException is expected. * @throws AlreadyBeingCreatedException as the result */ @Test(expected=AlreadyBeingCreatedException.class) public void testInternalReleaseLease_COMM_COMM () throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Running " + GenericTestUtils.getMethodName()); } LeaseManager.Lease lm = mock(LeaseManager.Lease.class); Path file = spy(new Path("/" + GenericTestUtils.getMethodName() + "_test.dat")); DatanodeDescriptor dnd = mock(DatanodeDescriptor.class); PermissionStatus ps = new PermissionStatus("test", "test", new FsPermission((short)0777)); mockFileBlocks(2, HdfsServerConstants.BlockUCState.COMMITTED, HdfsServerConstants.BlockUCState.COMMITTED, file, dnd, ps, false); releaseLease(fsn, lm, file); fail("FSNamesystem.internalReleaseLease suppose to throw " + "IOException here"); } /** * Mocks FSNamesystem instance, adds an empty file with 0 blocks * and invokes lease recovery method. */ @Test public void testInternalReleaseLease_0blocks () throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Running " + GenericTestUtils.getMethodName()); } LeaseManager.Lease lm = mock(LeaseManager.Lease.class); Path file = spy(new Path("/" + GenericTestUtils.getMethodName() + "_test.dat")); DatanodeDescriptor dnd = mock(DatanodeDescriptor.class); PermissionStatus ps = new PermissionStatus("test", "test", new FsPermission((short)0777)); mockFileBlocks(0, null, null, file, dnd, ps, false); assertTrue("True has to be returned in this case", releaseLease(fsn, lm, file)); } /** * Mocks FSNamesystem instance, adds an empty file with 1 block * and invokes lease recovery method. * AlreadyBeingCreatedException is expected. * @throws AlreadyBeingCreatedException as the result */ @Test(expected=AlreadyBeingCreatedException.class) public void testInternalReleaseLease_1blocks () throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Running " + GenericTestUtils.getMethodName()); } LeaseManager.Lease lm = mock(LeaseManager.Lease.class); Path file = spy(new Path("/" + GenericTestUtils.getMethodName() + "_test.dat")); DatanodeDescriptor dnd = mock(DatanodeDescriptor.class); PermissionStatus ps = new PermissionStatus("test", "test", new FsPermission((short)0777)); mockFileBlocks(1, null, HdfsServerConstants.BlockUCState.COMMITTED, file, dnd, ps, false); releaseLease(fsn, lm, file); fail("FSNamesystem.internalReleaseLease suppose to throw " + "IOException here"); } /** * Mocks FSNamesystem instance, adds an empty file, sets status of last two * blocks to COMMITTED and UNDER_CONSTRUCTION and invokes lease recovery * method. <code>false</code> is expected as the result * @throws IOException in case of an error */ @Test public void testInternalReleaseLease_COMM_CONSTRUCTION () throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Running " + GenericTestUtils.getMethodName()); } LeaseManager.Lease lm = mock(LeaseManager.Lease.class); Path file = spy(new Path("/" + GenericTestUtils.getMethodName() + "_test.dat")); DatanodeDescriptor dnd = mock(DatanodeDescriptor.class); PermissionStatus ps = new PermissionStatus("test", "test", new FsPermission((short)0777)); mockFileBlocks(2, HdfsServerConstants.BlockUCState.COMMITTED, HdfsServerConstants.BlockUCState.UNDER_CONSTRUCTION, file, dnd, ps, false); assertFalse("False is expected in return in this case", releaseLease(fsn, lm, file)); } @Test public void testCommitBlockSynchronization_BlockNotFound () throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Running " + GenericTestUtils.getMethodName()); } long recoveryId = 2002; long newSize = 273487234; Path file = spy(new Path("/" + GenericTestUtils.getMethodName() + "_test.dat")); DatanodeDescriptor dnd = mock(DatanodeDescriptor.class); PermissionStatus ps = new PermissionStatus("test", "test", new FsPermission((short)0777)); mockFileBlocks(2, HdfsServerConstants.BlockUCState.COMMITTED, HdfsServerConstants.BlockUCState.UNDER_CONSTRUCTION, file, dnd, ps, false); BlockInfo lastBlock = fsn.dir.getFileINode(anyString()).getLastBlock(); try { fsn.commitBlockSynchronization(fsn.getExtendedBlock(lastBlock), recoveryId, newSize, true, false, new DatanodeID[1]); } catch (IOException ioe) { assertTrue(ioe.getMessage().startsWith("Block (=")); } } @Test public void testCommitBlockSynchronization_notUR () throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Running " + GenericTestUtils.getMethodName()); } long recoveryId = 2002; long newSize = 273487234; Path file = spy(new Path("/" + GenericTestUtils.getMethodName() + "_test.dat")); DatanodeDescriptor dnd = mock(DatanodeDescriptor.class); PermissionStatus ps = new PermissionStatus("test", "test", new FsPermission((short)0777)); mockFileBlocks(2, HdfsServerConstants.BlockUCState.COMMITTED, HdfsServerConstants.BlockUCState.COMPLETE, file, dnd, ps, true); BlockInfo lastBlock = fsn.dir.getFileINode(anyString()).getLastBlock(); when(lastBlock.isComplete()).thenReturn(true); try { fsn.commitBlockSynchronization(fsn.getExtendedBlock(lastBlock), recoveryId, newSize, true, false, new DatanodeID[1]); } catch (IOException ioe) { assertTrue(ioe.getMessage().startsWith("Unexpected block (=")); } } @Test public void testCommitBlockSynchronization_WrongGreaterRecoveryID() throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Running " + GenericTestUtils.getMethodName()); } long recoveryId = 2002; long newSize = 273487234; Path file = spy(new Path("/" + GenericTestUtils.getMethodName() + "_test.dat")); DatanodeDescriptor dnd = mock(DatanodeDescriptor.class); PermissionStatus ps = new PermissionStatus("test", "test", new FsPermission((short)0777)); mockFileBlocks(2, HdfsServerConstants.BlockUCState.COMMITTED, HdfsServerConstants.BlockUCState.UNDER_CONSTRUCTION, file, dnd, ps, true); BlockInfo lastBlock = fsn.dir.getFileINode(anyString()).getLastBlock(); when(((BlockInfoUnderConstruction)lastBlock).getBlockRecoveryId()).thenReturn(recoveryId-100); try { fsn.commitBlockSynchronization(fsn.getExtendedBlock(lastBlock), recoveryId, newSize, true, false, new DatanodeID[1]); } catch (IOException ioe) { assertTrue(ioe.getMessage().startsWith("The recovery id " + recoveryId + " does not match current recovery id " + (recoveryId-100))); } } @Test public void testCommitBlockSynchronization_WrongLesserRecoveryID() throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Running " + GenericTestUtils.getMethodName()); } long recoveryId = 2002; long newSize = 273487234; Path file = spy(new Path("/" + GenericTestUtils.getMethodName() + "_test.dat")); DatanodeDescriptor dnd = mock(DatanodeDescriptor.class); PermissionStatus ps = new PermissionStatus("test", "test", new FsPermission((short)0777)); mockFileBlocks(2, HdfsServerConstants.BlockUCState.COMMITTED, HdfsServerConstants.BlockUCState.UNDER_CONSTRUCTION, file, dnd, ps, true); BlockInfo lastBlock = fsn.dir.getFileINode(anyString()).getLastBlock(); when(((BlockInfoUnderConstruction)lastBlock).getBlockRecoveryId()).thenReturn(recoveryId+100); try { fsn.commitBlockSynchronization(fsn.getExtendedBlock(lastBlock), recoveryId, newSize, true, false, new DatanodeID[1]); } catch (IOException ioe) { assertTrue(ioe.getMessage().startsWith("The recovery id " + recoveryId + " does not match current recovery id " + (recoveryId+100))); } } @Test public void testCommitBlockSynchronization_EqualRecoveryID() throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Running " + GenericTestUtils.getMethodName()); } long recoveryId = 2002; long newSize = 273487234; Path file = spy(new Path("/" + GenericTestUtils.getMethodName() + "_test.dat")); DatanodeDescriptor dnd = mock(DatanodeDescriptor.class); PermissionStatus ps = new PermissionStatus("test", "test", new FsPermission((short)0777)); mockFileBlocks(2, HdfsServerConstants.BlockUCState.COMMITTED, HdfsServerConstants.BlockUCState.UNDER_CONSTRUCTION, file, dnd, ps, true); BlockInfo lastBlock = fsn.dir.getFileINode(anyString()).getLastBlock(); when(((BlockInfoUnderConstruction)lastBlock).getBlockRecoveryId()).thenReturn(recoveryId); boolean recoveryChecked = false; try { fsn.commitBlockSynchronization(fsn.getExtendedBlock(lastBlock), recoveryId, newSize, true, false, new DatanodeID[1]); } catch (NullPointerException ioe) { // It is fine to get NPE here because the datanodes array is empty LOG.info("Exception ", ioe); recoveryChecked = true; } assertTrue("commitBlockSynchronization had to throw NPE here", recoveryChecked); } private void mockFileBlocks(int fileBlocksNumber, HdfsServerConstants.BlockUCState penUltState, HdfsServerConstants.BlockUCState lastState, Path file, DatanodeDescriptor dnd, PermissionStatus ps, boolean setStoredBlock) throws IOException { BlockInfo b = mock(BlockInfo.class); BlockInfoUnderConstruction b1 = mock(BlockInfoUnderConstruction.class); when(b.getBlockUCState()).thenReturn(penUltState); when(b1.getBlockUCState()).thenReturn(lastState); BlockInfo[] blocks; FSDirectory fsDir = mock(FSDirectory.class); INodeFileUnderConstruction iNFmock = mock(INodeFileUnderConstruction.class); fsn.dir.close(); fsn.dir = fsDir; FSImage fsImage = mock(FSImage.class); FSEditLog editLog = mock(FSEditLog.class); when(fsn.getFSImage()).thenReturn(fsImage); when(fsn.getFSImage().getEditLog()).thenReturn(editLog); fsn.getFSImage().setFSNamesystem(fsn); switch (fileBlocksNumber) { case 0: blocks = new BlockInfo[0]; break; case 1: blocks = new BlockInfo[]{b1}; when(iNFmock.getLastBlock()).thenReturn(b1); break; default: when(iNFmock.getPenultimateBlock()).thenReturn(b); when(iNFmock.getLastBlock()).thenReturn(b1); blocks = new BlockInfo[]{b, b1}; } when(iNFmock.getBlocks()).thenReturn(blocks); when(iNFmock.numBlocks()).thenReturn(blocks.length); when(iNFmock.isUnderConstruction()).thenReturn(true); when(iNFmock.convertToInodeFile()).thenReturn(iNFmock); fsDir.addFile(file.toString(), ps, (short)3, 1l, "test", "test-machine", dnd, 1001l); fsn.leaseManager = mock(LeaseManager.class); fsn.leaseManager.addLease("mock-lease", file.toString()); if (setStoredBlock) { when(b1.getINode()).thenReturn(iNFmock); fsn.getBlockManager().addINode(b1, iNFmock); } when(fsDir.getFileINode(anyString())).thenReturn(iNFmock); } }