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.fs.azure; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeNotNull; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileStatus; 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.security.UserGroupInformation; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.apache.hadoop.fs.azure.AzureException; import org.apache.hadoop.fs.azure.NativeAzureFileSystem.FolderRenamePending; import com.microsoft.azure.storage.AccessCondition; import com.microsoft.azure.storage.StorageException; import com.microsoft.azure.storage.blob.CloudBlob; /* * Tests the Native Azure file system (WASB) against an actual blob store if * provided in the environment. * Subclasses implement createTestAccount() to hit local&mock storage with the same test code. * * For hand-testing: remove "abstract" keyword and copy in an implementation of createTestAccount * from one of the subclasses */ public abstract class NativeAzureFileSystemBaseTest { protected FileSystem fs; private AzureBlobStorageTestAccount testAccount; private final long modifiedTimeErrorMargin = 5 * 1000; // Give it +/-5 seconds protected abstract AzureBlobStorageTestAccount createTestAccount() throws Exception; public static final Log LOG = LogFactory.getLog(NativeAzureFileSystemBaseTest.class); @Before public void setUp() throws Exception { testAccount = createTestAccount(); if (testAccount != null) { fs = testAccount.getFileSystem(); } assumeNotNull(testAccount); } @After public void tearDown() throws Exception { if (testAccount != null) { testAccount.cleanup(); testAccount = null; fs = null; } } @Test public void testCheckingNonExistentOneLetterFile() throws Exception { assertFalse(fs.exists(new Path("/a"))); } @Test public void testStoreRetrieveFile() throws Exception { Path testFile = new Path("unit-test-file"); writeString(testFile, "Testing"); assertTrue(fs.exists(testFile)); FileStatus status = fs.getFileStatus(testFile); assertNotNull(status); // By default, files should be have masked permissions // that grant RW to user, and R to group/other assertEquals(new FsPermission((short) 0644), status.getPermission()); assertEquals("Testing", readString(testFile)); fs.delete(testFile, true); } @Test public void testStoreDeleteFolder() throws Exception { Path testFolder = new Path("storeDeleteFolder"); assertFalse(fs.exists(testFolder)); assertTrue(fs.mkdirs(testFolder)); assertTrue(fs.exists(testFolder)); FileStatus status = fs.getFileStatus(testFolder); assertNotNull(status); assertTrue(status.isDirectory()); // By default, directories should be have masked permissions // that grant RWX to user, and RX to group/other assertEquals(new FsPermission((short) 0755), status.getPermission()); Path innerFile = new Path(testFolder, "innerFile"); assertTrue(fs.createNewFile(innerFile)); assertTrue(fs.exists(innerFile)); assertTrue(fs.delete(testFolder, true)); assertFalse(fs.exists(innerFile)); assertFalse(fs.exists(testFolder)); } @Test public void testFileOwnership() throws Exception { Path testFile = new Path("ownershipTestFile"); writeString(testFile, "Testing"); testOwnership(testFile); } @Test public void testFolderOwnership() throws Exception { Path testFolder = new Path("ownershipTestFolder"); fs.mkdirs(testFolder); testOwnership(testFolder); } private void testOwnership(Path pathUnderTest) throws IOException { FileStatus ret = fs.getFileStatus(pathUnderTest); UserGroupInformation currentUser = UserGroupInformation.getCurrentUser(); assertTrue(ret.getOwner().equals(currentUser.getShortUserName())); fs.delete(pathUnderTest, true); } private static FsPermission ignoreStickyBit(FsPermission original) { return new FsPermission(original.getUserAction(), original.getGroupAction(), original.getOtherAction()); } // When FsPermission applies a UMask, it loses sticky bit information. // And since we always apply UMask, we should ignore whether the sticky // bit is equal or not. private static void assertEqualsIgnoreStickyBit(FsPermission expected, FsPermission actual) { assertEquals(ignoreStickyBit(expected), ignoreStickyBit(actual)); } @Test public void testFilePermissions() throws Exception { Path testFile = new Path("permissionTestFile"); FsPermission permission = FsPermission.createImmutable((short) 644); createEmptyFile(testFile, permission); FileStatus ret = fs.getFileStatus(testFile); assertEqualsIgnoreStickyBit(permission, ret.getPermission()); fs.delete(testFile, true); } @Test public void testFolderPermissions() throws Exception { Path testFolder = new Path("permissionTestFolder"); FsPermission permission = FsPermission.createImmutable((short) 644); fs.mkdirs(testFolder, permission); FileStatus ret = fs.getFileStatus(testFolder); assertEqualsIgnoreStickyBit(permission, ret.getPermission()); fs.delete(testFolder, true); } void testDeepFileCreationBase(String testFilePath, String firstDirPath, String middleDirPath, short permissionShort, short umaskedPermissionShort) throws Exception { Path testFile = new Path(testFilePath); Path firstDir = new Path(firstDirPath); Path middleDir = new Path(middleDirPath); FsPermission permission = FsPermission.createImmutable(permissionShort); FsPermission umaskedPermission = FsPermission.createImmutable(umaskedPermissionShort); createEmptyFile(testFile, permission); FsPermission rootPerm = fs.getFileStatus(firstDir.getParent()).getPermission(); FsPermission inheritPerm = FsPermission.createImmutable((short) (rootPerm.toShort() | 0300)); assertTrue(fs.exists(testFile)); assertTrue(fs.exists(firstDir)); assertTrue(fs.exists(middleDir)); // verify that the indirectly created directory inherited its permissions from the root directory FileStatus directoryStatus = fs.getFileStatus(middleDir); assertTrue(directoryStatus.isDirectory()); assertEqualsIgnoreStickyBit(inheritPerm, directoryStatus.getPermission()); // verify that the file itself has the permissions as specified FileStatus fileStatus = fs.getFileStatus(testFile); assertFalse(fileStatus.isDirectory()); assertEqualsIgnoreStickyBit(umaskedPermission, fileStatus.getPermission()); assertTrue(fs.delete(firstDir, true)); assertFalse(fs.exists(testFile)); // An alternative test scenario would've been to delete the file first, // and then check for the existence of the upper folders still. But that // doesn't actually work as expected right now. } @Test public void testDeepFileCreation() throws Exception { // normal permissions in user home testDeepFileCreationBase("deep/file/creation/test", "deep", "deep/file/creation", (short) 0644, (short) 0644); // extra permissions in user home. umask will change the actual permissions. testDeepFileCreationBase("deep/file/creation/test", "deep", "deep/file/creation", (short) 0777, (short) 0755); // normal permissions in root testDeepFileCreationBase("/deep/file/creation/test", "/deep", "/deep/file/creation", (short) 0644, (short) 0644); // less permissions in root testDeepFileCreationBase("/deep/file/creation/test", "/deep", "/deep/file/creation", (short) 0700, (short) 0700); // one indirectly created directory in root testDeepFileCreationBase("/deep/file", "/deep", "/deep", (short) 0644, (short) 0644); // one indirectly created directory in user home testDeepFileCreationBase("deep/file", "deep", "deep", (short) 0644, (short) 0644); } private static enum RenameVariation { NormalFileName, SourceInAFolder, SourceWithSpace, SourceWithPlusAndPercent } @Test public void testRename() throws Exception { for (RenameVariation variation : RenameVariation.values()) { System.out.printf("Rename variation: %s\n", variation); Path originalFile; switch (variation) { case NormalFileName: originalFile = new Path("fileToRename"); break; case SourceInAFolder: originalFile = new Path("file/to/rename"); break; case SourceWithSpace: originalFile = new Path("file to rename"); break; case SourceWithPlusAndPercent: originalFile = new Path("file+to%rename"); break; default: throw new Exception("Unknown variation"); } Path destinationFile = new Path("file/resting/destination"); assertTrue(fs.createNewFile(originalFile)); assertTrue(fs.exists(originalFile)); assertFalse(fs.rename(originalFile, destinationFile)); // Parent directory // doesn't exist assertTrue(fs.mkdirs(destinationFile.getParent())); boolean result = fs.rename(originalFile, destinationFile); assertTrue(result); assertTrue(fs.exists(destinationFile)); assertFalse(fs.exists(originalFile)); fs.delete(destinationFile.getParent(), true); } } @Test public void testRenameImplicitFolder() throws Exception { Path testFile = new Path("deep/file/rename/test"); FsPermission permission = FsPermission.createImmutable((short) 644); createEmptyFile(testFile, permission); boolean renameResult = fs.rename(new Path("deep/file"), new Path("deep/renamed")); assertTrue(renameResult); assertFalse(fs.exists(testFile)); FileStatus newStatus = fs.getFileStatus(new Path("deep/renamed/rename/test")); assertNotNull(newStatus); assertEqualsIgnoreStickyBit(permission, newStatus.getPermission()); assertTrue(fs.delete(new Path("deep"), true)); } private static enum RenameFolderVariation { CreateFolderAndInnerFile, CreateJustInnerFile, CreateJustFolder } @Test public void testRenameFolder() throws Exception { for (RenameFolderVariation variation : RenameFolderVariation.values()) { Path originalFolder = new Path("folderToRename"); if (variation != RenameFolderVariation.CreateJustInnerFile) { assertTrue(fs.mkdirs(originalFolder)); } Path innerFile = new Path(originalFolder, "innerFile"); Path innerFile2 = new Path(originalFolder, "innerFile2"); if (variation != RenameFolderVariation.CreateJustFolder) { assertTrue(fs.createNewFile(innerFile)); assertTrue(fs.createNewFile(innerFile2)); } Path destination = new Path("renamedFolder"); assertTrue(fs.rename(originalFolder, destination)); assertTrue(fs.exists(destination)); if (variation != RenameFolderVariation.CreateJustFolder) { assertTrue(fs.exists(new Path(destination, innerFile.getName()))); assertTrue(fs.exists(new Path(destination, innerFile2.getName()))); } assertFalse(fs.exists(originalFolder)); assertFalse(fs.exists(innerFile)); assertFalse(fs.exists(innerFile2)); fs.delete(destination, true); } } @Test public void testCopyFromLocalFileSystem() throws Exception { Path localFilePath = new Path(System.getProperty("test.build.data", "azure_test")); FileSystem localFs = FileSystem.get(new Configuration()); localFs.delete(localFilePath, true); try { writeString(localFs, localFilePath, "Testing"); Path dstPath = new Path("copiedFromLocal"); assertTrue(FileUtil.copy(localFs, localFilePath, fs, dstPath, false, fs.getConf())); assertTrue(fs.exists(dstPath)); assertEquals("Testing", readString(fs, dstPath)); fs.delete(dstPath, true); } finally { localFs.delete(localFilePath, true); } } @Test public void testListDirectory() throws Exception { Path rootFolder = new Path("testingList"); assertTrue(fs.mkdirs(rootFolder)); FileStatus[] listed = fs.listStatus(rootFolder); assertEquals(0, listed.length); Path innerFolder = new Path(rootFolder, "inner"); assertTrue(fs.mkdirs(innerFolder)); listed = fs.listStatus(rootFolder); assertEquals(1, listed.length); assertTrue(listed[0].isDirectory()); Path innerFile = new Path(innerFolder, "innerFile"); writeString(innerFile, "testing"); listed = fs.listStatus(rootFolder); assertEquals(1, listed.length); assertTrue(listed[0].isDirectory()); listed = fs.listStatus(innerFolder); assertEquals(1, listed.length); assertFalse(listed[0].isDirectory()); assertTrue(fs.delete(rootFolder, true)); } @Test public void testStatistics() throws Exception { FileSystem.clearStatistics(); FileSystem.Statistics stats = FileSystem.getStatistics("wasb", NativeAzureFileSystem.class); assertEquals(0, stats.getBytesRead()); assertEquals(0, stats.getBytesWritten()); Path newFile = new Path("testStats"); writeString(newFile, "12345678"); assertEquals(8, stats.getBytesWritten()); assertEquals(0, stats.getBytesRead()); String readBack = readString(newFile); assertEquals("12345678", readBack); assertEquals(8, stats.getBytesRead()); assertEquals(8, stats.getBytesWritten()); assertTrue(fs.delete(newFile, true)); assertEquals(8, stats.getBytesRead()); assertEquals(8, stats.getBytesWritten()); } @Test public void testUriEncoding() throws Exception { fs.create(new Path("p/t%5Fe")).close(); FileStatus[] listing = fs.listStatus(new Path("p")); assertEquals(1, listing.length); assertEquals("t%5Fe", listing[0].getPath().getName()); assertTrue(fs.rename(new Path("p"), new Path("q"))); assertTrue(fs.delete(new Path("q"), true)); } @Test public void testUriEncodingMoreComplexCharacters() throws Exception { // Create a file name with URI reserved characters, plus the percent String fileName = "!#$'()*;=[]%"; String directoryName = "*;=[]%!#$'()"; fs.create(new Path(directoryName, fileName)).close(); FileStatus[] listing = fs.listStatus(new Path(directoryName)); assertEquals(1, listing.length); assertEquals(fileName, listing[0].getPath().getName()); FileStatus status = fs.getFileStatus(new Path(directoryName, fileName)); assertEquals(fileName, status.getPath().getName()); InputStream stream = fs.open(new Path(directoryName, fileName)); assertNotNull(stream); stream.close(); assertTrue(fs.delete(new Path(directoryName, fileName), true)); assertTrue(fs.delete(new Path(directoryName), true)); } @Test public void testChineseCharacters() throws Exception { // Create a file and a folder with Chinese (non-ASCI) characters String chinese = "" + '\u963f' + '\u4db5'; String fileName = "filename" + chinese; String directoryName = chinese; fs.create(new Path(directoryName, fileName)).close(); FileStatus[] listing = fs.listStatus(new Path(directoryName)); assertEquals(1, listing.length); assertEquals(fileName, listing[0].getPath().getName()); FileStatus status = fs.getFileStatus(new Path(directoryName, fileName)); assertEquals(fileName, status.getPath().getName()); InputStream stream = fs.open(new Path(directoryName, fileName)); assertNotNull(stream); stream.close(); assertTrue(fs.delete(new Path(directoryName, fileName), true)); assertTrue(fs.delete(new Path(directoryName), true)); } @Test public void testChineseCharactersFolderRename() throws Exception { // Create a file and a folder with Chinese (non-ASCI) characters String chinese = "" + '\u963f' + '\u4db5'; String fileName = "filename" + chinese; String srcDirectoryName = chinese; String targetDirectoryName = "target" + chinese; fs.create(new Path(srcDirectoryName, fileName)).close(); fs.rename(new Path(srcDirectoryName), new Path(targetDirectoryName)); FileStatus[] listing = fs.listStatus(new Path(targetDirectoryName)); assertEquals(1, listing.length); assertEquals(fileName, listing[0].getPath().getName()); FileStatus status = fs.getFileStatus(new Path(targetDirectoryName, fileName)); assertEquals(fileName, status.getPath().getName()); assertTrue(fs.delete(new Path(targetDirectoryName, fileName), true)); assertTrue(fs.delete(new Path(targetDirectoryName), true)); } @Test public void testReadingDirectoryAsFile() throws Exception { Path dir = new Path("/x"); assertTrue(fs.mkdirs(dir)); try { fs.open(dir).close(); assertTrue("Should've thrown", false); } catch (FileNotFoundException ex) { assertEquals("/x is a directory not a file.", ex.getMessage()); } } @Test public void testCreatingFileOverDirectory() throws Exception { Path dir = new Path("/x"); assertTrue(fs.mkdirs(dir)); try { fs.create(dir).close(); assertTrue("Should've thrown", false); } catch (IOException ex) { assertEquals("Cannot create file /x; already exists as a directory.", ex.getMessage()); } } @Test public void testInputStreamReadWithZeroSizeBuffer() throws Exception { Path newFile = new Path("zeroSizeRead"); OutputStream output = fs.create(newFile); output.write(10); output.close(); InputStream input = fs.open(newFile); int result = input.read(new byte[2], 0, 0); assertEquals(0, result); } @Test public void testInputStreamReadWithBufferReturnsMinusOneOnEof() throws Exception { Path newFile = new Path("eofRead"); OutputStream output = fs.create(newFile); output.write(10); output.close(); // Read first byte back InputStream input = fs.open(newFile); byte[] buff = new byte[1]; int result = input.read(buff, 0, 1); assertEquals(1, result); assertEquals(10, buff[0]); // Issue another read and make sure it returns -1 buff[0] = 2; result = input.read(buff, 0, 1); assertEquals(-1, result); // Buffer is intact assertEquals(2, buff[0]); } @Test public void testInputStreamReadWithBufferReturnsMinusOneOnEofForLargeBuffer() throws Exception { Path newFile = new Path("eofRead2"); OutputStream output = fs.create(newFile); byte[] outputBuff = new byte[97331]; for (int i = 0; i < outputBuff.length; ++i) { outputBuff[i] = (byte) (Math.random() * 255); } output.write(outputBuff); output.close(); // Read the content of the file InputStream input = fs.open(newFile); byte[] buff = new byte[131072]; int result = input.read(buff, 0, buff.length); assertEquals(outputBuff.length, result); for (int i = 0; i < outputBuff.length; ++i) { assertEquals(outputBuff[i], buff[i]); } // Issue another read and make sure it returns -1 buff = new byte[131072]; result = input.read(buff, 0, buff.length); assertEquals(-1, result); } @Test public void testInputStreamReadIntReturnsMinusOneOnEof() throws Exception { Path newFile = new Path("eofRead3"); OutputStream output = fs.create(newFile); output.write(10); output.close(); // Read first byte back InputStream input = fs.open(newFile); int value = input.read(); assertEquals(10, value); // Issue another read and make sure it returns -1 value = input.read(); assertEquals(-1, value); } @Test public void testSetPermissionOnFile() throws Exception { Path newFile = new Path("testPermission"); OutputStream output = fs.create(newFile); output.write(13); output.close(); FsPermission newPermission = new FsPermission((short) 0700); fs.setPermission(newFile, newPermission); FileStatus newStatus = fs.getFileStatus(newFile); assertNotNull(newStatus); assertEquals(newPermission, newStatus.getPermission()); assertEquals("supergroup", newStatus.getGroup()); assertEquals(UserGroupInformation.getCurrentUser().getShortUserName(), newStatus.getOwner()); // Don't check the file length for page blobs. Only block blobs // provide the actual length of bytes written. if (!(this instanceof TestNativeAzureFSPageBlobLive)) { assertEquals(1, newStatus.getLen()); } } @Test public void testSetPermissionOnFolder() throws Exception { Path newFolder = new Path("testPermission"); assertTrue(fs.mkdirs(newFolder)); FsPermission newPermission = new FsPermission((short) 0600); fs.setPermission(newFolder, newPermission); FileStatus newStatus = fs.getFileStatus(newFolder); assertNotNull(newStatus); assertEquals(newPermission, newStatus.getPermission()); assertTrue(newStatus.isDirectory()); } @Test public void testSetOwnerOnFile() throws Exception { Path newFile = new Path("testOwner"); OutputStream output = fs.create(newFile); output.write(13); output.close(); fs.setOwner(newFile, "newUser", null); FileStatus newStatus = fs.getFileStatus(newFile); assertNotNull(newStatus); assertEquals("newUser", newStatus.getOwner()); assertEquals("supergroup", newStatus.getGroup()); // File length is only reported to be the size of bytes written to the file for block blobs. // So only check it for block blobs, not page blobs. if (!(this instanceof TestNativeAzureFSPageBlobLive)) { assertEquals(1, newStatus.getLen()); } fs.setOwner(newFile, null, "newGroup"); newStatus = fs.getFileStatus(newFile); assertNotNull(newStatus); assertEquals("newUser", newStatus.getOwner()); assertEquals("newGroup", newStatus.getGroup()); } @Test public void testSetOwnerOnFolder() throws Exception { Path newFolder = new Path("testOwner"); assertTrue(fs.mkdirs(newFolder)); fs.setOwner(newFolder, "newUser", null); FileStatus newStatus = fs.getFileStatus(newFolder); assertNotNull(newStatus); assertEquals("newUser", newStatus.getOwner()); assertTrue(newStatus.isDirectory()); } @Test public void testModifiedTimeForFile() throws Exception { Path testFile = new Path("testFile"); fs.create(testFile).close(); testModifiedTime(testFile); } @Test public void testModifiedTimeForFolder() throws Exception { Path testFolder = new Path("testFolder"); assertTrue(fs.mkdirs(testFolder)); testModifiedTime(testFolder); } @Test public void testFolderLastModifiedTime() throws Exception { Path parentFolder = new Path("testFolder"); Path innerFile = new Path(parentFolder, "innerfile"); assertTrue(fs.mkdirs(parentFolder)); // Create file long lastModifiedTime = fs.getFileStatus(parentFolder).getModificationTime(); // Wait at least the error margin Thread.sleep(modifiedTimeErrorMargin + 1); assertTrue(fs.createNewFile(innerFile)); // The parent folder last modified time should have changed because we // create an inner file. assertFalse(testModifiedTime(parentFolder, lastModifiedTime)); testModifiedTime(parentFolder); // Rename file lastModifiedTime = fs.getFileStatus(parentFolder).getModificationTime(); Path destFolder = new Path("testDestFolder"); assertTrue(fs.mkdirs(destFolder)); long destLastModifiedTime = fs.getFileStatus(destFolder).getModificationTime(); Thread.sleep(modifiedTimeErrorMargin + 1); Path destFile = new Path(destFolder, "innerfile"); assertTrue(fs.rename(innerFile, destFile)); // Both source and destination folder last modified time should have changed // because of renaming. assertFalse(testModifiedTime(parentFolder, lastModifiedTime)); assertFalse(testModifiedTime(destFolder, destLastModifiedTime)); testModifiedTime(parentFolder); testModifiedTime(destFolder); // Delete file destLastModifiedTime = fs.getFileStatus(destFolder).getModificationTime(); // Wait at least the error margin Thread.sleep(modifiedTimeErrorMargin + 1); fs.delete(destFile, false); // The parent folder last modified time should have changed because we // delete an inner file. assertFalse(testModifiedTime(destFolder, destLastModifiedTime)); testModifiedTime(destFolder); } /** * Verify we can get file status of a directory with various forms of * the directory file name, including the nonstandard but legal form * ending in "/.". Check that we're getting status for a directory. */ @Test public void testListSlash() throws Exception { Path testFolder = new Path("/testFolder"); Path testFile = new Path(testFolder, "testFile"); assertTrue(fs.mkdirs(testFolder)); assertTrue(fs.createNewFile(testFile)); FileStatus status; status = fs.getFileStatus(new Path("/testFolder")); assertTrue(status.isDirectory()); status = fs.getFileStatus(new Path("/testFolder/")); assertTrue(status.isDirectory()); status = fs.getFileStatus(new Path("/testFolder/.")); assertTrue(status.isDirectory()); } @Test public void testCannotCreatePageBlobByDefault() throws Exception { // Verify that the page blob directory list configuration setting // is not set in the default configuration. Configuration conf = new Configuration(); String[] rawPageBlobDirs = conf.getStrings(AzureNativeFileSystemStore.KEY_PAGE_BLOB_DIRECTORIES); assertTrue(rawPageBlobDirs == null); } /* * Set up a situation where a folder rename is partway finished. * Then apply redo to finish the rename. * * The original source folder *would* have had contents * folderToRename (0 byte dummy file for directory) * folderToRename/innerFile * folderToRename/innerFile2 * * The actual source folder (after partial rename and failure) * * folderToRename * folderToRename/innerFile2 * * The actual target folder (after partial rename and failure) * * renamedFolder * renamedFolder/innerFile */ @Test public void testRedoRenameFolder() throws IOException { // create original folder String srcKey = "folderToRename"; Path originalFolder = new Path(srcKey); assertTrue(fs.mkdirs(originalFolder)); Path innerFile = new Path(originalFolder, "innerFile"); assertTrue(fs.createNewFile(innerFile)); Path innerFile2 = new Path(originalFolder, "innerFile2"); assertTrue(fs.createNewFile(innerFile2)); String dstKey = "renamedFolder"; // propose (but don't do) the rename Path home = fs.getHomeDirectory(); String relativeHomeDir = getRelativePath(home.toString()); NativeAzureFileSystem.FolderRenamePending pending = new NativeAzureFileSystem.FolderRenamePending( relativeHomeDir + "/" + srcKey, relativeHomeDir + "/" + dstKey, null, (NativeAzureFileSystem) fs); // get the rename pending file contents String renameDescription = pending.makeRenamePendingFileContents(); // Remove one file from source folder to simulate a partially done // rename operation. assertTrue(fs.delete(innerFile, false)); // Create the destination folder with just one file in it, again // to simulate a partially done rename. Path destination = new Path(dstKey); Path innerDest = new Path(destination, "innerFile"); assertTrue(fs.createNewFile(innerDest)); // Create a rename-pending file and write rename information to it. final String renamePendingStr = "folderToRename-RenamePending.json"; Path renamePendingFile = new Path(renamePendingStr); FSDataOutputStream out = fs.create(renamePendingFile, true); assertTrue(out != null); writeString(out, renameDescription); // Redo the rename operation based on the contents of the -RenamePending.json file. // Trigger the redo by checking for existence of the original folder. It must appear // to not exist. assertFalse(fs.exists(originalFolder)); // Verify that the target is there, and the source is gone. assertTrue(fs.exists(destination)); assertTrue(fs.exists(new Path(destination, innerFile.getName()))); assertTrue(fs.exists(new Path(destination, innerFile2.getName()))); assertFalse(fs.exists(originalFolder)); assertFalse(fs.exists(innerFile)); assertFalse(fs.exists(innerFile2)); // Verify that there's no RenamePending file left. assertFalse(fs.exists(renamePendingFile)); // Verify that we can list the target directory. FileStatus[] listed = fs.listStatus(destination); assertEquals(2, listed.length); // List the home directory and show the contents is a directory. Path root = fs.getHomeDirectory(); listed = fs.listStatus(root); assertEquals(1, listed.length); assertTrue(listed[0].isDirectory()); } /** * If there is a folder to be renamed inside a parent folder, * then when you list the parent folder, you should only see * the final result, after the rename. */ @Test public void testRedoRenameFolderInFolderListing() throws IOException { // create original folder String parent = "parent"; Path parentFolder = new Path(parent); assertTrue(fs.mkdirs(parentFolder)); Path inner = new Path(parentFolder, "innerFolder"); assertTrue(fs.mkdirs(inner)); Path inner2 = new Path(parentFolder, "innerFolder2"); assertTrue(fs.mkdirs(inner2)); Path innerFile = new Path(inner2, "file"); assertTrue(fs.createNewFile(innerFile)); Path inner2renamed = new Path(parentFolder, "innerFolder2Renamed"); // propose (but don't do) the rename of innerFolder2 Path home = fs.getHomeDirectory(); String relativeHomeDir = getRelativePath(home.toString()); NativeAzureFileSystem.FolderRenamePending pending = new NativeAzureFileSystem.FolderRenamePending( relativeHomeDir + "/" + inner2, relativeHomeDir + "/" + inner2renamed, null, (NativeAzureFileSystem) fs); // Create a rename-pending file and write rename information to it. final String renamePendingStr = inner2 + FolderRenamePending.SUFFIX; Path renamePendingFile = new Path(renamePendingStr); FSDataOutputStream out = fs.create(renamePendingFile, true); assertTrue(out != null); writeString(out, pending.makeRenamePendingFileContents()); // Redo the rename operation based on the contents of the // -RenamePending.json file. Trigger the redo by checking for existence of // the original folder. It must appear to not exist. FileStatus[] listed = fs.listStatus(parentFolder); assertEquals(2, listed.length); assertTrue(listed[0].isDirectory()); assertTrue(listed[1].isDirectory()); // The rename pending file is not a directory, so at this point we know the // redo has been done. assertFalse(fs.exists(inner2)); // verify original folder is gone assertTrue(fs.exists(inner2renamed)); // verify the target is there assertTrue(fs.exists(new Path(inner2renamed, "file"))); } /** * Test the situation where a rename pending file exists but the rename * is really done. This could happen if the rename process died just * before deleting the rename pending file. It exercises a non-standard * code path in redo(). */ @Test public void testRenameRedoFolderAlreadyDone() throws IOException { // create only destination folder String orig = "originalFolder"; String dest = "renamedFolder"; Path destPath = new Path(dest); assertTrue(fs.mkdirs(destPath)); // propose (but don't do) the rename of innerFolder2 Path home = fs.getHomeDirectory(); String relativeHomeDir = getRelativePath(home.toString()); NativeAzureFileSystem.FolderRenamePending pending = new NativeAzureFileSystem.FolderRenamePending( relativeHomeDir + "/" + orig, relativeHomeDir + "/" + dest, null, (NativeAzureFileSystem) fs); // Create a rename-pending file and write rename information to it. final String renamePendingStr = orig + FolderRenamePending.SUFFIX; Path renamePendingFile = new Path(renamePendingStr); FSDataOutputStream out = fs.create(renamePendingFile, true); assertTrue(out != null); writeString(out, pending.makeRenamePendingFileContents()); try { pending.redo(); } catch (Exception e) { fail(); } // Make sure rename pending file is gone. FileStatus[] listed = fs.listStatus(new Path("/")); assertEquals(1, listed.length); assertTrue(listed[0].isDirectory()); } @Test public void testRedoFolderRenameAll() throws IllegalArgumentException, IOException { { FileFolder original = new FileFolder("folderToRename"); original.add("innerFile").add("innerFile2"); FileFolder partialSrc = original.copy(); FileFolder partialDst = original.copy(); partialDst.setName("renamedFolder"); partialSrc.setPresent(0, false); partialDst.setPresent(1, false); testRenameRedoFolderSituation(original, partialSrc, partialDst); } { FileFolder original = new FileFolder("folderToRename"); original.add("file1").add("file2").add("file3"); FileFolder partialSrc = original.copy(); FileFolder partialDst = original.copy(); partialDst.setName("renamedFolder"); // Set up this state before the redo: // folderToRename: file1 file3 // renamedFolder: file1 file2 // This gives code coverage for all 3 expected cases for individual file // redo. partialSrc.setPresent(1, false); partialDst.setPresent(2, false); testRenameRedoFolderSituation(original, partialSrc, partialDst); } { // Simulate a situation with folder with a large number of files in it. // For the first half of the files, they will be in the destination // but not the source. For the second half, they will be in the source // but not the destination. There will be one file in the middle that is // in both source and destination. Then trigger redo and verify. // For testing larger folder sizes, manually change this, temporarily, and // edit the SIZE value. final int SIZE = 5; assertTrue(SIZE >= 3); // Try a lot of files in the folder. FileFolder original = new FileFolder("folderToRename"); for (int i = 0; i < SIZE; i++) { original.add("file" + Integer.toString(i)); } FileFolder partialSrc = original.copy(); FileFolder partialDst = original.copy(); partialDst.setName("renamedFolder"); for (int i = 0; i < SIZE; i++) { partialSrc.setPresent(i, i >= SIZE / 2); partialDst.setPresent(i, i <= SIZE / 2); } testRenameRedoFolderSituation(original, partialSrc, partialDst); } { // Do a nested folder, like so: // folderToRename: // nestedFolder: a, b, c // p // q // // Then delete file 'a' from the source and add it to destination. // Then trigger redo. FileFolder original = new FileFolder("folderToRename"); FileFolder nested = new FileFolder("nestedFolder"); nested.add("a").add("b").add("c"); original.add(nested).add("p").add("q"); FileFolder partialSrc = original.copy(); FileFolder partialDst = original.copy(); partialDst.setName("renamedFolder"); // logically remove 'a' from source partialSrc.getMember(0).setPresent(0, false); // logically eliminate b, c from destination partialDst.getMember(0).setPresent(1, false); partialDst.getMember(0).setPresent(2, false); testRenameRedoFolderSituation(original, partialSrc, partialDst); } } private void testRenameRedoFolderSituation(FileFolder fullSrc, FileFolder partialSrc, FileFolder partialDst) throws IllegalArgumentException, IOException { // make file folder tree for source fullSrc.create(); // set up rename pending file fullSrc.makeRenamePending(partialDst); // prune away some files (as marked) from source to simulate partial rename partialSrc.prune(); // Create only the files indicated for the destination to indicate a partial rename. partialDst.create(); // trigger redo assertFalse(fullSrc.exists()); // verify correct results partialDst.verifyExists(); fullSrc.verifyGone(); // delete the new folder to leave no garbage behind fs.delete(new Path(partialDst.getName()), true); } // Mock up of a generalized folder (which can also be a leaf-level file) // for rename redo testing. private class FileFolder { private String name; // For rename testing, indicates whether an expected // file is present in the source or target folder. private boolean present; ArrayList<FileFolder> members; // Null if a leaf file, otherwise not null. // Make a new, empty folder (not a regular leaf file). public FileFolder(String name) { this.name = name; this.present = true; members = new ArrayList<FileFolder>(); } public FileFolder getMember(int i) { return members.get(i); } // Verify a folder and all its contents are gone. This is only to // be called on the root of a FileFolder. public void verifyGone() throws IllegalArgumentException, IOException { assertFalse(fs.exists(new Path(name))); assertTrue(isFolder()); verifyGone(new Path(name), members); } private void verifyGone(Path prefix, ArrayList<FileFolder> members2) throws IOException { for (FileFolder f : members2) { f.verifyGone(prefix); } } private void verifyGone(Path prefix) throws IOException { assertFalse(fs.exists(new Path(prefix, name))); if (isLeaf()) { return; } for (FileFolder f : members) { f.verifyGone(new Path(prefix, name)); } } public void verifyExists() throws IllegalArgumentException, IOException { // verify the root is present assertTrue(fs.exists(new Path(name))); assertTrue(isFolder()); // check the members verifyExists(new Path(name), members); } private void verifyExists(Path prefix, ArrayList<FileFolder> members2) throws IOException { for (FileFolder f : members2) { f.verifyExists(prefix); } } private void verifyExists(Path prefix) throws IOException { // verify this file/folder is present assertTrue(fs.exists(new Path(prefix, name))); // verify members are present if (isLeaf()) { return; } for (FileFolder f : members) { f.verifyExists(new Path(prefix, name)); } } public boolean exists() throws IOException { return fs.exists(new Path(name)); } // Make a rename pending file for the situation where we rename // this object (the source) to the specified destination. public void makeRenamePending(FileFolder dst) throws IOException { // Propose (but don't do) the rename. Path home = fs.getHomeDirectory(); String relativeHomeDir = getRelativePath(home.toString()); NativeAzureFileSystem.FolderRenamePending pending = new NativeAzureFileSystem.FolderRenamePending( relativeHomeDir + "/" + this.getName(), relativeHomeDir + "/" + dst.getName(), null, (NativeAzureFileSystem) fs); // Get the rename pending file contents. String renameDescription = pending.makeRenamePendingFileContents(); // Create a rename-pending file and write rename information to it. final String renamePendingStr = this.getName() + "-RenamePending.json"; Path renamePendingFile = new Path(renamePendingStr); FSDataOutputStream out = fs.create(renamePendingFile, true); assertTrue(out != null); writeString(out, renameDescription); } // set whether a child is present or not public void setPresent(int i, boolean b) { members.get(i).setPresent(b); } // Make an uninitialized folder private FileFolder() { this.present = true; } public void setPresent(boolean value) { present = value; } public FileFolder makeLeaf(String name) { FileFolder f = new FileFolder(); f.setName(name); return f; } void setName(String name) { this.name = name; } public String getName() { return name; } public boolean isLeaf() { return members == null; } public boolean isFolder() { return members != null; } FileFolder add(FileFolder folder) { members.add(folder); return this; } // Add a leaf file (by convention, if you pass a string argument, you get a leaf). FileFolder add(String file) { FileFolder leaf = makeLeaf(file); members.add(leaf); return this; } public FileFolder copy() { if (isLeaf()) { return makeLeaf(name); } else { FileFolder f = new FileFolder(name); for (FileFolder member : members) { f.add(member.copy()); } return f; } } // Create the folder structure. Return true on success, or else false. public void create() throws IllegalArgumentException, IOException { create(null); } private void create(Path prefix) throws IllegalArgumentException, IOException { if (isFolder()) { if (present) { assertTrue(fs.mkdirs(makePath(prefix, name))); } create(makePath(prefix, name), members); } else if (isLeaf()) { if (present) { assertTrue(fs.createNewFile(makePath(prefix, name))); } } else { assertTrue("The object must be a (leaf) file or a folder.", false); } } private void create(Path prefix, ArrayList<FileFolder> members2) throws IllegalArgumentException, IOException { for (FileFolder f : members2) { f.create(prefix); } } private Path makePath(Path prefix, String name) { if (prefix == null) { return new Path(name); } else { return new Path(prefix, name); } } // Remove the files marked as not present. public void prune() throws IOException { prune(null); } private void prune(Path prefix) throws IOException { Path path = null; if (prefix == null) { path = new Path(name); } else { path = new Path(prefix, name); } if (isLeaf() && !present) { assertTrue(fs.delete(path, false)); } else if (isFolder() && !present) { assertTrue(fs.delete(path, true)); } else if (isFolder()) { for (FileFolder f : members) { f.prune(path); } } } } private String getRelativePath(String path) { // example input: wasb://wasbtests-ehans-1404322046279@ehans9.blob.core.windows.net/user/ehans/folderToRename // example result: user/ehans/folderToRename // Find the third / position and return input substring after that. int slashCount = 0; // number of slashes so far int i; for (i = 0; i < path.length(); i++) { if (path.charAt(i) == '/') { slashCount++; if (slashCount == 3) { return path.substring(i + 1, path.length()); } } } throw new RuntimeException("Incorrect path prefix -- expected wasb://.../..."); } @Test public void testCloseFileSystemTwice() throws Exception { //make sure close() can be called multiple times without doing any harm fs.close(); fs.close(); } // Test the available() method for the input stream returned by fs.open(). // This works for both page and block blobs. int FILE_SIZE = 4 * 1024 * 1024 + 1; // Make this 1 bigger than internal // buffer used in BlobInputStream // to exercise that case. int MAX_STRIDE = FILE_SIZE + 1; Path PATH = new Path("/available.dat"); @Test public void testAvailable() throws IOException { // write FILE_SIZE bytes to page blob FSDataOutputStream out = fs.create(PATH); byte[] data = new byte[FILE_SIZE]; Arrays.fill(data, (byte) 5); out.write(data, 0, FILE_SIZE); out.close(); // Test available() for different read sizes verifyAvailable(1); verifyAvailable(100); verifyAvailable(5000); verifyAvailable(FILE_SIZE); verifyAvailable(MAX_STRIDE); fs.delete(PATH, false); } // Verify that available() for the input stream is always >= 1 unless we've // consumed all the input, and then it is 0. This is to match expectations by // HBase which were set based on behavior of DFSInputStream.available(). private void verifyAvailable(int readStride) throws IOException { FSDataInputStream in = fs.open(PATH); try { byte[] inputBuffer = new byte[MAX_STRIDE]; int position = 0; int bytesRead = 0; while (bytesRead != FILE_SIZE) { bytesRead += in.read(inputBuffer, position, readStride); int available = in.available(); if (bytesRead < FILE_SIZE) { if (available < 1) { fail(String.format( "expected available > 0 but got: " + "position = %d, bytesRead = %d, in.available() = %d", position, bytesRead, available)); } } } int available = in.available(); assertTrue(available == 0); } finally { in.close(); } } @Test public void testGetFileSizeFromListing() throws IOException { Path path = new Path("file.dat"); final int PAGE_SIZE = 512; final int FILE_SIZE = PAGE_SIZE + 1; // write FILE_SIZE bytes to page blob FSDataOutputStream out = fs.create(path); byte[] data = new byte[FILE_SIZE]; Arrays.fill(data, (byte) 5); out.write(data, 0, FILE_SIZE); out.close(); // list the file to get its properties FileStatus[] status = fs.listStatus(path); assertEquals(1, status.length); // The file length should report the number of bytes // written for either page or block blobs (subclasses // of this test class will exercise both). assertEquals(FILE_SIZE, status[0].getLen()); } private boolean testModifiedTime(Path testPath, long time) throws Exception { FileStatus fileStatus = fs.getFileStatus(testPath); final long errorMargin = modifiedTimeErrorMargin; long lastModified = fileStatus.getModificationTime(); return (lastModified > (time - errorMargin) && lastModified < (time + errorMargin)); } @SuppressWarnings("deprecation") @Test public void testCreateNonRecursive() throws Exception { Path testFolder = new Path("/testFolder"); Path testFile = new Path(testFolder, "testFile"); try { fs.createNonRecursive(testFile, true, 1024, (short) 1, 1024, null); assertTrue("Should've thrown", false); } catch (FileNotFoundException e) { } fs.mkdirs(testFolder); fs.createNonRecursive(testFile, true, 1024, (short) 1, 1024, null).close(); assertTrue(fs.exists(testFile)); } public void testFileEndingInDot() throws Exception { Path testFolder = new Path("/testFolder."); Path testFile = new Path(testFolder, "testFile."); assertTrue(fs.mkdirs(testFolder)); assertTrue(fs.createNewFile(testFile)); assertTrue(fs.exists(testFile)); FileStatus[] listed = fs.listStatus(testFolder); assertEquals(1, listed.length); assertEquals("testFile.", listed[0].getPath().getName()); } private void testModifiedTime(Path testPath) throws Exception { Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")); long currentUtcTime = utc.getTime().getTime(); FileStatus fileStatus = fs.getFileStatus(testPath); final long errorMargin = 10 * 1000; // Give it +/-10 seconds assertTrue( "Modification time " + new Date(fileStatus.getModificationTime()) + " is not close to now: " + utc.getTime(), fileStatus.getModificationTime() > (currentUtcTime - errorMargin) && fileStatus.getModificationTime() < (currentUtcTime + errorMargin)); } private void createEmptyFile(Path testFile, FsPermission permission) throws IOException { FSDataOutputStream outputStream = fs.create(testFile, permission, true, 4096, (short) 1, 1024, null); outputStream.close(); } private String readString(Path testFile) throws IOException { return readString(fs, testFile); } private String readString(FileSystem fs, Path testFile) throws IOException { FSDataInputStream inputStream = fs.open(testFile); String ret = readString(inputStream); inputStream.close(); return ret; } private String readString(FSDataInputStream inputStream) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); final int BUFFER_SIZE = 1024; char[] buffer = new char[BUFFER_SIZE]; int count = reader.read(buffer, 0, BUFFER_SIZE); if (count > BUFFER_SIZE) { throw new IOException("Exceeded buffer size"); } inputStream.close(); return new String(buffer, 0, count); } private void writeString(Path path, String value) throws IOException { writeString(fs, path, value); } private void writeString(FileSystem fs, Path path, String value) throws IOException { FSDataOutputStream outputStream = fs.create(path, true); writeString(outputStream, value); } private void writeString(FSDataOutputStream outputStream, String value) throws IOException { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream)); writer.write(value); writer.close(); } @Test // Acquire and free a Lease object. Wait for more than the lease // timeout, to make sure the lease renews itself. public void testSelfRenewingLease() throws IllegalArgumentException, IOException, InterruptedException, StorageException { SelfRenewingLease lease; final String FILE_KEY = "file"; fs.create(new Path(FILE_KEY)); NativeAzureFileSystem nfs = (NativeAzureFileSystem) fs; String fullKey = nfs.pathToKey(nfs.makeAbsolute(new Path(FILE_KEY))); AzureNativeFileSystemStore store = nfs.getStore(); lease = store.acquireLease(fullKey); assertTrue(lease.getLeaseID() != null); // The sleep time for the keep-alive thread is 40 seconds, so sleep just // a little beyond that, to make sure the keep-alive thread wakes up // and renews the lease. Thread.sleep(42000); lease.free(); // Check that the lease is really freed. CloudBlob blob = lease.getCloudBlob(); // Try to acquire it again, using direct Azure blob access. // If that succeeds, then the lease was already freed. String differentLeaseID = null; try { differentLeaseID = blob.acquireLease(15, null); } catch (Exception e) { e.printStackTrace(); fail("Caught exception trying to directly re-acquire lease from Azure"); } finally { assertTrue(differentLeaseID != null); AccessCondition accessCondition = AccessCondition.generateEmptyCondition(); accessCondition.setLeaseID(differentLeaseID); blob.releaseLease(accessCondition); } } @Test // Acquire a SelfRenewingLease object. Wait for more than the lease // timeout, to make sure the lease renews itself. Delete the file. // That will automatically free the lease. // (that should work without any failures). public void testSelfRenewingLeaseFileDelete() throws IllegalArgumentException, IOException, InterruptedException, StorageException { SelfRenewingLease lease; final String FILE_KEY = "file"; final Path path = new Path(FILE_KEY); fs.create(path); NativeAzureFileSystem nfs = (NativeAzureFileSystem) fs; String fullKey = nfs.pathToKey(nfs.makeAbsolute(path)); lease = nfs.getStore().acquireLease(fullKey); assertTrue(lease.getLeaseID() != null); // The sleep time for the keep-alive thread is 40 seconds, so sleep just // a little beyond that, to make sure the keep-alive thread wakes up // and renews the lease. Thread.sleep(42000); nfs.getStore().delete(fullKey, lease); // Check that the file is really gone and the lease is freed. assertTrue(!fs.exists(path)); assertTrue(lease.isFreed()); } // Variables to check assertions in next test. private long firstEndTime; private long secondStartTime; // Create two threads. One will get a lease on a file. // The second one will try to get the lease and thus block. // Then the first one will free the lease and the second // one will get it and proceed. @Test public void testLeaseAsDistributedLock() throws IllegalArgumentException, IOException { final String LEASE_LOCK_FILE_KEY = "file"; fs.create(new Path(LEASE_LOCK_FILE_KEY)); NativeAzureFileSystem nfs = (NativeAzureFileSystem) fs; String fullKey = nfs.pathToKey(nfs.makeAbsolute(new Path(LEASE_LOCK_FILE_KEY))); Thread first = new Thread(new LeaseLockAction("first-thread", fullKey)); first.start(); Thread second = new Thread(new LeaseLockAction("second-thread", fullKey)); second.start(); try { // Wait for the two threads to finish. first.join(); second.join(); assertTrue(firstEndTime < secondStartTime); } catch (InterruptedException e) { fail("Unable to wait for threads to finish"); Thread.currentThread().interrupt(); } } private class LeaseLockAction implements Runnable { private String name; private String key; LeaseLockAction(String name, String key) { this.name = name; this.key = key; } @Override public void run() { LOG.info("starting thread " + name); SelfRenewingLease lease = null; NativeAzureFileSystem nfs = (NativeAzureFileSystem) fs; if (name.equals("first-thread")) { try { lease = nfs.getStore().acquireLease(key); LOG.info(name + " acquired lease " + lease.getLeaseID()); } catch (AzureException e) { assertTrue("Unanticipated exception", false); } assertTrue(lease != null); try { // Sleep long enough for the lease to renew once. Thread.sleep(SelfRenewingLease.LEASE_RENEWAL_PERIOD + 2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } try { firstEndTime = System.currentTimeMillis(); lease.free(); LOG.info(name + " freed lease " + lease.getLeaseID()); } catch (StorageException e) { fail("Unanticipated exception"); } } else if (name.equals("second-thread")) { try { // sleep 2 sec to let first thread get ahead of this one Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } try { LOG.info(name + " before getting lease"); lease = nfs.getStore().acquireLease(key); secondStartTime = System.currentTimeMillis(); LOG.info(name + " acquired lease " + lease.getLeaseID()); } catch (AzureException e) { assertTrue("Unanticipated exception", false); } assertTrue(lease != null); try { lease.free(); LOG.info(name + " freed lease " + lease.getLeaseID()); } catch (StorageException e) { assertTrue("Unanticipated exception", false); } } else { assertTrue("Unknown thread name", false); } LOG.info(name + " is exiting."); } } }