diff --git a/LICENSE-binary b/LICENSE-binary index 980b9c7f2b62a..499485263906a 100644 --- a/LICENSE-binary +++ b/LICENSE-binary @@ -231,7 +231,7 @@ com.github.stephenc.jcip:jcip-annotations:1.0-1 com.google:guice:4.0 com.google:guice-servlet:4.0 com.google.api.grpc:proto-google-common-protos:1.0.0 -com.google.code.gson:2.2.4 +com.google.code.gson:2.9.0 com.google.errorprone:error_prone_annotations:2.2.0 com.google.j2objc:j2objc-annotations:1.1 com.google.json-simple:json-simple:1.1.1 diff --git a/dev-support/Jenkinsfile b/dev-support/Jenkinsfile index 9d668790fc1f1..51225268b653a 100644 --- a/dev-support/Jenkinsfile +++ b/dev-support/Jenkinsfile @@ -47,7 +47,7 @@ pipeline { options { buildDiscarder(logRotator(numToKeepStr: '5')) - timeout (time: 24, unit: 'HOURS') + timeout (time: 48, unit: 'HOURS') timestamps() checkoutToSubdirectory('src') } diff --git a/hadoop-client-modules/hadoop-client-minicluster/pom.xml b/hadoop-client-modules/hadoop-client-minicluster/pom.xml index 06e36837a2098..4c8900dc2af0d 100644 --- a/hadoop-client-modules/hadoop-client-minicluster/pom.xml +++ b/hadoop-client-modules/hadoop-client-minicluster/pom.xml @@ -757,6 +757,12 @@ META-INF/versions/11/module-info.class + + com.google.code.gson:gson + + META-INF/versions/9/module-info.class + + diff --git a/hadoop-client-modules/hadoop-client-runtime/pom.xml b/hadoop-client-modules/hadoop-client-runtime/pom.xml index 35fbd7665fb26..98756c2439544 100644 --- a/hadoop-client-modules/hadoop-client-runtime/pom.xml +++ b/hadoop-client-modules/hadoop-client-runtime/pom.xml @@ -249,6 +249,13 @@ META-INF/versions/11/module-info.class + + com.google.code.gson:gson + + META-INF/versions/9/module-info.class + + + diff --git a/hadoop-cloud-storage-project/hadoop-cos/pom.xml b/hadoop-cloud-storage-project/hadoop-cos/pom.xml index fa47e354c7998..ca7c4bf516cad 100644 --- a/hadoop-cloud-storage-project/hadoop-cos/pom.xml +++ b/hadoop-cloud-storage-project/hadoop-cos/pom.xml @@ -109,7 +109,7 @@ com.qcloud cos_api-bundle - 5.6.19 + 5.6.69 compile diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java index cf17aca15ceac..f25602c67d4a3 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java @@ -18,7 +18,6 @@ package org.apache.hadoop.security.authentication.util; import java.io.ByteArrayInputStream; -import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.PublicKey; import java.security.cert.CertificateException; diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/KerberosUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/KerberosUtil.java index fc6f957b9622e..5125be078d67b 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/KerberosUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/KerberosUtil.java @@ -236,7 +236,7 @@ public static final String getServicePrincipal(String service, */ static final String[] getPrincipalNames(String keytabFileName) throws IOException { Keytab keytab = Keytab.loadKeytab(new File(keytabFileName)); - Set principals = new HashSet(); + Set principals = new HashSet<>(); List entries = keytab.getPrincipals(); for (PrincipalName entry : entries) { principals.add(entry.getName().replace("\\", "/")); diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/KerberosTestUtils.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/KerberosTestUtils.java index 8fc08e2171f67..293871bcd0620 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/KerberosTestUtils.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/KerberosTestUtils.java @@ -108,9 +108,9 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) { public static T doAs(String principal, final Callable callable) throws Exception { LoginContext loginContext = null; try { - Set principals = new HashSet(); + Set principals = new HashSet<>(); principals.add(new KerberosPrincipal(KerberosTestUtils.getClientPrincipal())); - Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); + Subject subject = new Subject(false, principals, new HashSet<>(), new HashSet<>()); loginContext = new LoginContext("", subject, null, new KerberosConfiguration(principal)); loginContext.login(); subject = loginContext.getSubject(); diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestJWTRedirectAuthenticationHandler.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestJWTRedirectAuthenticationHandler.java index 5a2db9ba6fd97..7587bca2012d0 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestJWTRedirectAuthenticationHandler.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestJWTRedirectAuthenticationHandler.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.ArrayList; import java.util.Properties; -import java.util.Vector; import java.util.Date; import javax.servlet.ServletException; diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestPseudoAuthenticationHandler.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestPseudoAuthenticationHandler.java index b52915d9cc4ac..ac6221d642a6f 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestPseudoAuthenticationHandler.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/server/TestPseudoAuthenticationHandler.java @@ -13,7 +13,6 @@ */ package org.apache.hadoop.security.authentication.server; -import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.apache.hadoop.security.authentication.client.PseudoAuthenticator; import org.junit.Assert; import org.junit.Test; diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java index 5582c923ae0e7..ed6b1aeccc7c2 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java @@ -18,7 +18,6 @@ import javax.servlet.ServletContext; import org.apache.hadoop.classification.VisibleForTesting; -import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.security.authentication.server.AuthenticationFilter; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/conf/Configuration.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/conf/Configuration.java index 5f720841d7689..d8ceb58aba72c 100755 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/conf/Configuration.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/conf/Configuration.java @@ -774,7 +774,7 @@ private void updatePropertiesWithDeprecatedKeys( private void handleDeprecation() { LOG.debug("Handling deprecation for all properties in config..."); DeprecationContext deprecations = deprecationContext.get(); - Set keys = new HashSet(); + Set keys = new HashSet<>(); keys.addAll(getProps().keySet()); for (Object item: keys) { LOG.debug("Handling deprecation for " + (String)item); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/KeyProviderCryptoExtension.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/KeyProviderCryptoExtension.java index d706e5ef100c0..f1bb314582038 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/KeyProviderCryptoExtension.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/KeyProviderCryptoExtension.java @@ -25,10 +25,6 @@ import java.util.List; import java.util.ListIterator; -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - import org.apache.hadoop.util.Preconditions; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.crypto.CryptoCodec; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/kms/LoadBalancingKMSClientProvider.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/kms/LoadBalancingKMSClientProvider.java index f46da1f318664..f9cc3f4524ff5 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/kms/LoadBalancingKMSClientProvider.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/kms/LoadBalancingKMSClientProvider.java @@ -31,7 +31,6 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.SSLException; -import javax.net.ssl.SSLHandshakeException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.crypto.key.KeyProvider; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/BufferedFSInputStream.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/BufferedFSInputStream.java index 59345f5d25caf..7f3171235c8f4 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/BufferedFSInputStream.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/BufferedFSInputStream.java @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -22,6 +22,9 @@ import java.io.FileDescriptor; import java.io.IOException; import java.util.StringJoiner; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.function.IntFunction; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; @@ -158,8 +161,24 @@ public IOStatistics getIOStatistics() { @Override public String toString() { return new StringJoiner(", ", - BufferedFSInputStream.class.getSimpleName() + "[", "]") - .add("in=" + in) - .toString(); + BufferedFSInputStream.class.getSimpleName() + "[", "]") + .add("in=" + in) + .toString(); + } + + @Override + public int minSeekForVectorReads() { + return ((PositionedReadable) in).minSeekForVectorReads(); + } + + @Override + public int maxReadSizeForVectorReads() { + return ((PositionedReadable) in).maxReadSizeForVectorReads(); + } + + @Override + public void readVectored(List ranges, + IntFunction allocate) throws IOException { + ((PositionedReadable) in).readVectored(ranges, allocate); } } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/ChecksumFileSystem.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/ChecksumFileSystem.java index 0efcdc8022f7b..1cca9fe2bfdb1 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/ChecksumFileSystem.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/ChecksumFileSystem.java @@ -22,17 +22,24 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.IntFunction; +import java.util.zip.CRC32; import org.apache.hadoop.util.Preconditions; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.impl.AbstractFSBuilderImpl; +import org.apache.hadoop.fs.impl.CombinedFileRange; import org.apache.hadoop.fs.impl.FutureDataInputStreamBuilderImpl; import org.apache.hadoop.fs.impl.OpenFileParameters; import org.apache.hadoop.fs.permission.AclEntry; @@ -47,6 +54,7 @@ import static org.apache.hadoop.fs.Options.OpenFileOptions.FS_OPTION_OPENFILE_STANDARD_OPTIONS; import static org.apache.hadoop.fs.impl.PathCapabilitiesSupport.validatePathCapabilityArgs; import static org.apache.hadoop.fs.impl.StoreImplementationUtils.isProbeForSyncable; +import static org.apache.hadoop.fs.VectoredReadUtils.sortRanges; /**************************************************************** * Abstract Checksumed FileSystem. @@ -66,7 +74,7 @@ public abstract class ChecksumFileSystem extends FilterFileSystem { public static double getApproxChkSumLength(long size) { return ChecksumFSOutputSummer.CHKSUM_AS_FRACTION * size; } - + public ChecksumFileSystem(FileSystem fs) { super(fs); } @@ -82,7 +90,7 @@ public void setConf(Configuration conf) { bytesPerChecksum); } } - + /** * Set whether to verify checksum. */ @@ -95,7 +103,7 @@ public void setVerifyChecksum(boolean verifyChecksum) { public void setWriteChecksum(boolean writeChecksum) { this.writeChecksum = writeChecksum; } - + /** get the raw file system */ @Override public FileSystem getRawFileSystem() { @@ -158,22 +166,22 @@ private int getSumBufferSize(int bytesPerSum, int bufferSize) { * It verifies that data matches checksums. *******************************************************/ private static class ChecksumFSInputChecker extends FSInputChecker implements - IOStatisticsSource { + IOStatisticsSource, StreamCapabilities { private ChecksumFileSystem fs; private FSDataInputStream datas; private FSDataInputStream sums; - + private static final int HEADER_LENGTH = 8; - + private int bytesPerSum = 1; - + public ChecksumFSInputChecker(ChecksumFileSystem fs, Path file) throws IOException { this(fs, file, fs.getConf().getInt( - LocalFileSystemConfigKeys.LOCAL_FS_STREAM_BUFFER_SIZE_KEY, + LocalFileSystemConfigKeys.LOCAL_FS_STREAM_BUFFER_SIZE_KEY, LocalFileSystemConfigKeys.LOCAL_FS_STREAM_BUFFER_SIZE_DEFAULT)); } - + public ChecksumFSInputChecker(ChecksumFileSystem fs, Path file, int bufferSize) throws IOException { super( file, fs.getFileStatus(file).getReplication() ); @@ -189,7 +197,8 @@ public ChecksumFSInputChecker(ChecksumFileSystem fs, Path file, int bufferSize) if (!Arrays.equals(version, CHECKSUM_VERSION)) throw new IOException("Not a checksum file: "+sumFile); this.bytesPerSum = sums.readInt(); - set(fs.verifyChecksum, DataChecksum.newCrc32(), bytesPerSum, 4); + set(fs.verifyChecksum, DataChecksum.newCrc32(), bytesPerSum, + FSInputChecker.CHECKSUM_SIZE); } catch (IOException e) { // mincing the message is terrible, but java throws permission // exceptions as FNF because that's all the method signatures allow! @@ -201,21 +210,21 @@ public ChecksumFSInputChecker(ChecksumFileSystem fs, Path file, int bufferSize) set(fs.verifyChecksum, null, 1, 0); } } - + private long getChecksumFilePos( long dataPos ) { - return HEADER_LENGTH + 4*(dataPos/bytesPerSum); + return HEADER_LENGTH + FSInputChecker.CHECKSUM_SIZE*(dataPos/bytesPerSum); } - + @Override protected long getChunkPosition( long dataPos ) { return dataPos/bytesPerSum*bytesPerSum; } - + @Override public int available() throws IOException { return datas.available() + super.available(); } - + @Override public int read(long position, byte[] b, int off, int len) throws IOException { @@ -233,7 +242,7 @@ public int read(long position, byte[] b, int off, int len) } return nread; } - + @Override public void close() throws IOException { datas.close(); @@ -242,7 +251,7 @@ public void close() throws IOException { } set(fs.verifyChecksum, null, 1, 0); } - + @Override public boolean seekToNewSource(long targetPos) throws IOException { @@ -265,7 +274,7 @@ protected int readChunk(long pos, byte[] buf, int offset, int len, final int checksumsToRead = Math.min( len/bytesPerSum, // number of checksums based on len to read checksum.length / CHECKSUM_SIZE); // size of checksum buffer - long checksumPos = getChecksumFilePos(pos); + long checksumPos = getChecksumFilePos(pos); if(checksumPos != sums.getPos()) { sums.seek(checksumPos); } @@ -305,8 +314,134 @@ protected int readChunk(long pos, byte[] buf, int offset, int len, public IOStatistics getIOStatistics() { return IOStatisticsSupport.retrieveIOStatistics(datas); } + + public static long findChecksumOffset(long dataOffset, + int bytesPerSum) { + return HEADER_LENGTH + (dataOffset/bytesPerSum) * FSInputChecker.CHECKSUM_SIZE; + } + + /** + * Find the checksum ranges that correspond to the given data ranges. + * @param dataRanges the input data ranges, which are assumed to be sorted + * and non-overlapping + * @return a list of AsyncReaderUtils.CombinedFileRange that correspond to + * the checksum ranges + */ + public static List findChecksumRanges( + List dataRanges, + int bytesPerSum, + int minSeek, + int maxSize) { + List result = new ArrayList<>(); + CombinedFileRange currentCrc = null; + for(FileRange range: dataRanges) { + long crcOffset = findChecksumOffset(range.getOffset(), bytesPerSum); + long crcEnd = findChecksumOffset(range.getOffset() + range.getLength() + + bytesPerSum - 1, bytesPerSum); + if (currentCrc == null || + !currentCrc.merge(crcOffset, crcEnd, range, minSeek, maxSize)) { + currentCrc = new CombinedFileRange(crcOffset, crcEnd, range); + result.add(currentCrc); + } + } + return result; + } + + /** + * Check the data against the checksums. + * @param sumsBytes the checksum data + * @param sumsOffset where from the checksum file this buffer started + * @param data the file data + * @param dataOffset where the file data started (must be a multiple of + * bytesPerSum) + * @param bytesPerSum how many bytes per a checksum + * @param file the path of the filename + * @return the data buffer + * @throws CompletionException if the checksums don't match + */ + static ByteBuffer checkBytes(ByteBuffer sumsBytes, + long sumsOffset, + ByteBuffer data, + long dataOffset, + int bytesPerSum, + Path file) { + // determine how many bytes we need to skip at the start of the sums + int offset = + (int) (findChecksumOffset(dataOffset, bytesPerSum) - sumsOffset); + IntBuffer sums = sumsBytes.asIntBuffer(); + sums.position(offset / FSInputChecker.CHECKSUM_SIZE); + ByteBuffer current = data.duplicate(); + int numChunks = data.remaining() / bytesPerSum; + CRC32 crc = new CRC32(); + // check each chunk to ensure they match + for(int c = 0; c < numChunks; ++c) { + // set the buffer position and the limit + current.limit((c + 1) * bytesPerSum); + current.position(c * bytesPerSum); + // compute the crc + crc.reset(); + crc.update(current); + int expected = sums.get(); + int calculated = (int) crc.getValue(); + + if (calculated != expected) { + // cast of c added to silence findbugs + long errPosn = dataOffset + (long) c * bytesPerSum; + throw new CompletionException(new ChecksumException( + "Checksum error: " + file + " at " + errPosn + + " exp: " + expected + " got: " + calculated, errPosn)); + } + } + // if everything matches, we return the data + return data; + } + + @Override + public void readVectored(List ranges, + IntFunction allocate) throws IOException { + // If the stream doesn't have checksums, just delegate. + VectoredReadUtils.validateVectoredReadRanges(ranges); + if (sums == null) { + datas.readVectored(ranges, allocate); + return; + } + int minSeek = minSeekForVectorReads(); + int maxSize = maxReadSizeForVectorReads(); + List dataRanges = + VectoredReadUtils.mergeSortedRanges(Arrays.asList(sortRanges(ranges)), bytesPerSum, + minSeek, maxReadSizeForVectorReads()); + List checksumRanges = findChecksumRanges(dataRanges, + bytesPerSum, minSeek, maxSize); + sums.readVectored(checksumRanges, allocate); + datas.readVectored(dataRanges, allocate); + // Data read is correct. I have verified content of dataRanges. + // There is some bug below here as test (testVectoredReadMultipleRanges) + // is failing, should be + // somewhere while slicing the merged data into smaller user ranges. + // Spend some time figuring out but it is a complex code. + for(CombinedFileRange checksumRange: checksumRanges) { + for(FileRange dataRange: checksumRange.getUnderlying()) { + // when we have both the ranges, validate the checksum + CompletableFuture result = + checksumRange.getData().thenCombineAsync(dataRange.getData(), + (sumBuffer, dataBuffer) -> + checkBytes(sumBuffer, checksumRange.getOffset(), + dataBuffer, dataRange.getOffset(), bytesPerSum, file)); + // Now, slice the read data range to the user's ranges + for(FileRange original: ((CombinedFileRange) dataRange).getUnderlying()) { + original.setData(result.thenApply( + (b) -> VectoredReadUtils.sliceTo(b, dataRange.getOffset(), original))); + } + } + } + } + + @Override + public boolean hasCapability(String capability) { + return datas.hasCapability(capability); + } } - + private static class FSDataBoundedInputStream extends FSDataInputStream { private FileSystem fs; private Path file; @@ -317,12 +452,12 @@ private static class FSDataBoundedInputStream extends FSDataInputStream { this.fs = fs; this.file = file; } - + @Override public boolean markSupported() { return false; } - + /* Return the file length */ private long getFileLength() throws IOException { if( fileLen==-1L ) { @@ -330,7 +465,7 @@ private long getFileLength() throws IOException { } return fileLen; } - + /** * Skips over and discards n bytes of data from the * input stream. @@ -354,11 +489,11 @@ public synchronized long skip(long n) throws IOException { } return super.skip(n); } - + /** * Seek to the given position in the stream. * The next read() will be from that position. - * + * *

This method does not allow seek past the end of the file. * This produces IOException. * @@ -424,22 +559,22 @@ public void concat(final Path f, final Path[] psrcs) throws IOException { */ public static long getChecksumLength(long size, int bytesPerSum) { //the checksum length is equal to size passed divided by bytesPerSum + - //bytes written in the beginning of the checksum file. - return ((size + bytesPerSum - 1) / bytesPerSum) * 4 + - CHECKSUM_VERSION.length + 4; + //bytes written in the beginning of the checksum file. + return ((size + bytesPerSum - 1) / bytesPerSum) * FSInputChecker.CHECKSUM_SIZE + + ChecksumFSInputChecker.HEADER_LENGTH; } /** This class provides an output stream for a checksummed file. * It generates checksums for data. */ private static class ChecksumFSOutputSummer extends FSOutputSummer implements IOStatisticsSource, StreamCapabilities { - private FSDataOutputStream datas; + private FSDataOutputStream datas; private FSDataOutputStream sums; private static final float CHKSUM_AS_FRACTION = 0.01f; private boolean isClosed = false; - - public ChecksumFSOutputSummer(ChecksumFileSystem fs, - Path file, + + ChecksumFSOutputSummer(ChecksumFileSystem fs, + Path file, boolean overwrite, int bufferSize, short replication, @@ -460,7 +595,7 @@ public ChecksumFSOutputSummer(ChecksumFileSystem fs, sums.write(CHECKSUM_VERSION, 0, CHECKSUM_VERSION.length); sums.writeInt(bytesPerSum); } - + @Override public void close() throws IOException { try { @@ -471,7 +606,7 @@ public void close() throws IOException { isClosed = true; } } - + @Override protected void writeChunk(byte[] b, int offset, int len, byte[] checksum, int ckoff, int cklen) @@ -727,7 +862,7 @@ public boolean rename(Path src, Path dst) throws IOException { value = fs.rename(srcCheckFile, dstCheckFile); } else if (fs.exists(dstCheckFile)) { // no src checksum, so remove dst checksum - value = fs.delete(dstCheckFile, true); + value = fs.delete(dstCheckFile, true); } return value; @@ -759,7 +894,7 @@ public boolean delete(Path f, boolean recursive) throws IOException{ return fs.delete(f, true); } } - + final private static PathFilter DEFAULT_FILTER = new PathFilter() { @Override public boolean accept(Path file) { @@ -770,7 +905,7 @@ public boolean accept(Path file) { /** * List the statuses of the files/directories in the given path if the path is * a directory. - * + * * @param f * given path * @return the statuses of the files/directories in the given path @@ -791,7 +926,7 @@ public RemoteIterator listStatusIterator(final Path p) /** * List the statuses of the files/directories in the given path if the path is * a directory. - * + * * @param f * given path * @return the statuses of the files/directories in the given patch @@ -802,7 +937,7 @@ public RemoteIterator listLocatedStatus(Path f) throws IOException { return fs.listLocatedStatus(f, DEFAULT_FILTER); } - + @Override public boolean mkdirs(Path f) throws IOException { return fs.mkdirs(f); @@ -856,7 +991,7 @@ public void copyToLocalFile(Path src, Path dst, boolean copyCrc) } else { FileStatus[] srcs = listStatus(src); for (FileStatus srcFile : srcs) { - copyToLocalFile(srcFile.getPath(), + copyToLocalFile(srcFile.getPath(), new Path(dst, srcFile.getPath().getName()), copyCrc); } } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeysPublic.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeysPublic.java index 5225236509294..7a458e8f3fccd 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeysPublic.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeysPublic.java @@ -1054,5 +1054,13 @@ public class CommonConfigurationKeysPublic { public static final String HADOOP_HTTP_IDLE_TIMEOUT_MS_KEY = "hadoop.http.idle_timeout.ms"; public static final int HADOOP_HTTP_IDLE_TIMEOUT_MS_DEFAULT = 60000; + + /** + * To configure scheduling of server metrics update thread. This config is used to indicate + * initial delay and delay between each execution of the metric update runnable thread. + */ + public static final String IPC_SERVER_METRICS_UPDATE_RUNNER_INTERVAL = + "ipc.server.metrics.update.runner.interval"; + public static final int IPC_SERVER_METRICS_UPDATE_RUNNER_INTERVAL_DEFAULT = 5000; } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FSDataInputStream.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FSDataInputStream.java index b143a4cb63d19..52644402ca459 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FSDataInputStream.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FSDataInputStream.java @@ -1,4 +1,4 @@ -/** +/* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -26,6 +26,8 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.util.EnumSet; +import java.util.List; +import java.util.function.IntFunction; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; @@ -51,7 +53,7 @@ public class FSDataInputStream extends DataInputStream */ private final IdentityHashStore extendedReadBuffers - = new IdentityHashStore(0); + = new IdentityHashStore<>(0); public FSDataInputStream(InputStream in) { super(in); @@ -279,4 +281,20 @@ public void readFully(long position, ByteBuffer buf) throws IOException { public IOStatistics getIOStatistics() { return IOStatisticsSupport.retrieveIOStatistics(in); } + + @Override + public int minSeekForVectorReads() { + return ((PositionedReadable) in).minSeekForVectorReads(); + } + + @Override + public int maxReadSizeForVectorReads() { + return ((PositionedReadable) in).maxReadSizeForVectorReads(); + } + + @Override + public void readVectored(List ranges, + IntFunction allocate) throws IOException { + ((PositionedReadable) in).readVectored(ranges, allocate); + } } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FileContext.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FileContext.java index 298570bb55fe8..22ac2ecbd7949 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FileContext.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FileContext.java @@ -2372,8 +2372,7 @@ public FileStatus next(final AbstractFileSystem fs, final Path p) Set resolveAbstractFileSystems(final Path f) throws IOException { final Path absF = fixRelativePart(f); - final HashSet result - = new HashSet(); + final HashSet result = new HashSet<>(); new FSLinkResolver() { @Override public Void next(final AbstractFileSystem fs, final Path p) diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FileRange.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FileRange.java new file mode 100644 index 0000000000000..e55696e96507e --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FileRange.java @@ -0,0 +1,67 @@ +/* + * 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; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; + +import org.apache.hadoop.fs.impl.FileRangeImpl; + +/** + * A byte range of a file. + * This is used for the asynchronous gather read API of + * {@link PositionedReadable#readVectored}. + */ +public interface FileRange { + + /** + * Get the starting offset of the range. + * @return the byte offset of the start + */ + long getOffset(); + + /** + * Get the length of the range. + * @return the number of bytes in the range. + */ + int getLength(); + + /** + * Get the future data for this range. + * @return the future for the {@link ByteBuffer} that contains the data + */ + CompletableFuture getData(); + + /** + * Set a future for this range's data. + * This method is called by {@link PositionedReadable#readVectored} to store the + * data for the user to pick up later via {@link #getData}. + * @param data the future of the ByteBuffer that will have the data + */ + void setData(CompletableFuture data); + + /** + * Factory method to create a FileRange object. + * @param offset starting offset of the range. + * @param length length of the range. + * @return a new instance of FileRangeImpl. + */ + static FileRange createFileRange(long offset, int length) { + return new FileRangeImpl(offset, length); + } +} diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/PositionedReadable.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/PositionedReadable.java index 6744d17a72666..de76090512705 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/PositionedReadable.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/PositionedReadable.java @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -17,7 +17,11 @@ */ package org.apache.hadoop.fs; -import java.io.*; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.function.IntFunction; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; @@ -85,4 +89,37 @@ void readFully(long position, byte[] buffer, int offset, int length) * the read operation completed */ void readFully(long position, byte[] buffer) throws IOException; + + /** + * What is the smallest reasonable seek? + * @return the minimum number of bytes + */ + default int minSeekForVectorReads() { + return 4 * 1024; + } + + /** + * What is the largest size that we should group ranges together as? + * @return the number of bytes to read at once + */ + default int maxReadSizeForVectorReads() { + return 1024 * 1024; + } + + /** + * Read fully a list of file ranges asynchronously from this file. + * The default iterates through the ranges to read each synchronously, but + * the intent is that FSDataInputStream subclasses can make more efficient + * readers. + * As a result of the call, each range will have FileRange.setData(CompletableFuture) + * called with a future that when complete will have a ByteBuffer with the + * data from the file's range. + * @param ranges the byte ranges to read + * @param allocate the function to allocate ByteBuffer + * @throws IOException any IOE. + */ + default void readVectored(List ranges, + IntFunction allocate) throws IOException { + VectoredReadUtils.readVectored(this, ranges, allocate); + } } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/RawLocalFileSystem.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/RawLocalFileSystem.java index 468b37a885d23..f525c3cba78fe 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/RawLocalFileSystem.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/RawLocalFileSystem.java @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -33,8 +33,11 @@ import java.io.FileDescriptor; import java.net.URI; import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousFileChannel; +import java.nio.channels.CompletionHandler; import java.nio.file.Files; import java.nio.file.NoSuchFileException; +import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.FileTime; @@ -44,6 +47,9 @@ import java.util.Optional; import java.util.StringTokenizer; import java.util.concurrent.atomic.AtomicLong; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.IntFunction; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; @@ -61,6 +67,7 @@ import org.apache.hadoop.util.StringUtils; import static org.apache.hadoop.fs.impl.PathCapabilitiesSupport.validatePathCapabilityArgs; +import static org.apache.hadoop.fs.VectoredReadUtils.sortRanges; import static org.apache.hadoop.fs.statistics.StreamStatisticNames.STREAM_READ_BYTES; import static org.apache.hadoop.fs.statistics.StreamStatisticNames.STREAM_READ_EXCEPTIONS; import static org.apache.hadoop.fs.statistics.StreamStatisticNames.STREAM_READ_SEEK_OPERATIONS; @@ -130,7 +137,9 @@ public void initialize(URI uri, Configuration conf) throws IOException { class LocalFSFileInputStream extends FSInputStream implements HasFileDescriptor, IOStatisticsSource, StreamCapabilities { private FileInputStream fis; + private final File name; private long position; + private AsynchronousFileChannel asyncChannel = null; /** * Minimal set of counters. @@ -148,7 +157,8 @@ class LocalFSFileInputStream extends FSInputStream implements private final AtomicLong bytesRead; public LocalFSFileInputStream(Path f) throws IOException { - fis = new FileInputStream(pathToFile(f)); + name = pathToFile(f); + fis = new FileInputStream(name); bytesRead = ioStatistics.getCounterReference( STREAM_READ_BYTES); } @@ -179,10 +189,16 @@ public boolean seekToNewSource(long targetPos) throws IOException { @Override public int available() throws IOException { return fis.available(); } @Override - public void close() throws IOException { fis.close(); } - @Override public boolean markSupported() { return false; } - + + @Override + public void close() throws IOException { + fis.close(); + if (asyncChannel != null) { + asyncChannel.close(); + } + } + @Override public int read() throws IOException { try { @@ -262,6 +278,7 @@ public boolean hasCapability(String capability) { // new capabilities. switch (capability.toLowerCase(Locale.ENGLISH)) { case StreamCapabilities.IOSTATISTICS: + case StreamCapabilities.VECTOREDIO: return true; default: return false; @@ -272,8 +289,89 @@ public boolean hasCapability(String capability) { public IOStatistics getIOStatistics() { return ioStatistics; } + + AsynchronousFileChannel getAsyncChannel() throws IOException { + if (asyncChannel == null) { + synchronized (this) { + asyncChannel = AsynchronousFileChannel.open(name.toPath(), + StandardOpenOption.READ); + } + } + return asyncChannel; + } + + @Override + public void readVectored(List ranges, + IntFunction allocate) throws IOException { + + List sortedRanges = Arrays.asList(sortRanges(ranges)); + // Set up all of the futures, so that we can use them if things fail + for(FileRange range: sortedRanges) { + VectoredReadUtils.validateRangeRequest(range); + range.setData(new CompletableFuture<>()); + } + try { + AsynchronousFileChannel channel = getAsyncChannel(); + ByteBuffer[] buffers = new ByteBuffer[sortedRanges.size()]; + AsyncHandler asyncHandler = new AsyncHandler(channel, sortedRanges, buffers); + for(int i = 0; i < sortedRanges.size(); ++i) { + FileRange range = sortedRanges.get(i); + buffers[i] = allocate.apply(range.getLength()); + channel.read(buffers[i], range.getOffset(), i, asyncHandler); + } + } catch (IOException ioe) { + LOG.debug("Exception occurred during vectored read ", ioe); + for(FileRange range: sortedRanges) { + range.getData().completeExceptionally(ioe); + } + } + } } - + + /** + * A CompletionHandler that implements readFully and translates back + * into the form of CompletionHandler that our users expect. + */ + static class AsyncHandler implements CompletionHandler { + private final AsynchronousFileChannel channel; + private final List ranges; + private final ByteBuffer[] buffers; + + AsyncHandler(AsynchronousFileChannel channel, + List ranges, + ByteBuffer[] buffers) { + this.channel = channel; + this.ranges = ranges; + this.buffers = buffers; + } + + @Override + public void completed(Integer result, Integer r) { + FileRange range = ranges.get(r); + ByteBuffer buffer = buffers[r]; + if (result == -1) { + failed(new EOFException("Read past End of File"), r); + } else { + if (buffer.remaining() > 0) { + // issue a read for the rest of the buffer + // QQ: What if this fails? It has the same handler. + channel.read(buffer, range.getOffset() + buffer.position(), r, this); + } else { + // QQ: Why is this required? I think because we don't want the + // user to read data beyond limit. + buffer.flip(); + range.getData().complete(buffer); + } + } + } + + @Override + public void failed(Throwable exc, Integer r) { + LOG.debug("Failed while reading range {} ", r, exc); + ranges.get(r).getData().completeExceptionally(exc); + } + } + @Override public FSDataInputStream open(Path f, int bufferSize) throws IOException { getFileStatus(f); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/StreamCapabilities.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/StreamCapabilities.java index 861178019505e..d68ef505dc3fe 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/StreamCapabilities.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/StreamCapabilities.java @@ -80,6 +80,12 @@ public interface StreamCapabilities { */ String IOSTATISTICS = "iostatistics"; + /** + * Support for vectored IO api. + * See {@code PositionedReadable#readVectored(List, IntFunction)}. + */ + String VECTOREDIO = "readvectored"; + /** * Stream abort() capability implemented by {@link Abortable#abort()}. * This matches the Path Capability diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/VectoredReadUtils.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/VectoredReadUtils.java new file mode 100644 index 0000000000000..64107f1a18f89 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/VectoredReadUtils.java @@ -0,0 +1,292 @@ +/* + * 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; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.IntFunction; + +import org.apache.hadoop.fs.impl.CombinedFileRange; +import org.apache.hadoop.util.Preconditions; + +/** + * Utility class which implements helper methods used + * in vectored IO implementation. + */ +public final class VectoredReadUtils { + + /** + * Validate a single range. + * @param range file range. + * @throws EOFException any EOF Exception. + */ + public static void validateRangeRequest(FileRange range) + throws EOFException { + + Preconditions.checkArgument(range.getLength() >= 0, "length is negative"); + if (range.getOffset() < 0) { + throw new EOFException("position is negative"); + } + } + + /** + * Validate a list of vectored read ranges. + * @param ranges list of ranges. + * @throws EOFException any EOF exception. + */ + public static void validateVectoredReadRanges(List ranges) + throws EOFException { + for (FileRange range : ranges) { + validateRangeRequest(range); + } + } + + + + /** + * This is the default implementation which iterates through the ranges + * to read each synchronously, but the intent is that subclasses + * can make more efficient readers. + * The data or exceptions are pushed into {@link FileRange#getData()}. + * @param stream the stream to read the data from + * @param ranges the byte ranges to read + * @param allocate the byte buffer allocation + */ + public static void readVectored(PositionedReadable stream, + List ranges, + IntFunction allocate) { + for (FileRange range: ranges) { + range.setData(readRangeFrom(stream, range, allocate)); + } + } + + /** + * Synchronously reads a range from the stream dealing with the combinations + * of ByteBuffers buffers and PositionedReadable streams. + * @param stream the stream to read from + * @param range the range to read + * @param allocate the function to allocate ByteBuffers + * @return the CompletableFuture that contains the read data + */ + public static CompletableFuture readRangeFrom(PositionedReadable stream, + FileRange range, + IntFunction allocate) { + CompletableFuture result = new CompletableFuture<>(); + try { + ByteBuffer buffer = allocate.apply(range.getLength()); + if (stream instanceof ByteBufferPositionedReadable) { + ((ByteBufferPositionedReadable) stream).readFully(range.getOffset(), + buffer); + buffer.flip(); + } else { + readNonByteBufferPositionedReadable(stream, range, buffer); + } + result.complete(buffer); + } catch (IOException ioe) { + result.completeExceptionally(ioe); + } + return result; + } + + private static void readNonByteBufferPositionedReadable(PositionedReadable stream, + FileRange range, + ByteBuffer buffer) throws IOException { + if (buffer.isDirect()) { + buffer.put(readInDirectBuffer(stream, range)); + buffer.flip(); + } else { + stream.readFully(range.getOffset(), buffer.array(), + buffer.arrayOffset(), range.getLength()); + } + } + + private static byte[] readInDirectBuffer(PositionedReadable stream, + FileRange range) throws IOException { + // if we need to read data from a direct buffer and the stream doesn't + // support it, we allocate a byte array to use. + byte[] tmp = new byte[range.getLength()]; + stream.readFully(range.getOffset(), tmp, 0, tmp.length); + return tmp; + } + + /** + * Is the given input list. + *

    + *
  • already sorted by offset
  • + *
  • each range is more than minimumSeek apart
  • + *
  • the start and end of each range is a multiple of chunkSize
  • + *
+ * + * @param input the list of input ranges. + * @param chunkSize the size of the chunks that the offset and end must align to. + * @param minimumSeek the minimum distance between ranges. + * @return true if we can use the input list as is. + */ + public static boolean isOrderedDisjoint(List input, + int chunkSize, + int minimumSeek) { + long previous = -minimumSeek; + for (FileRange range: input) { + long offset = range.getOffset(); + long end = range.getOffset() + range.getLength(); + if (offset % chunkSize != 0 || + end % chunkSize != 0 || + (offset - previous < minimumSeek)) { + return false; + } + previous = end; + } + return true; + } + + /** + * Calculates floor value of offset based on chunk size. + * @param offset file offset. + * @param chunkSize file chunk size. + * @return floor value. + */ + public static long roundDown(long offset, int chunkSize) { + if (chunkSize > 1) { + return offset - (offset % chunkSize); + } else { + return offset; + } + } + + /** + * Calculates the ceil value of offset based on chunk size. + * @param offset file offset. + * @param chunkSize file chunk size. + * @return ceil value. + */ + public static long roundUp(long offset, int chunkSize) { + if (chunkSize > 1) { + long next = offset + chunkSize - 1; + return next - (next % chunkSize); + } else { + return offset; + } + } + + /** + * Check if the input ranges are overlapping in nature. + * We call two ranges to be overlapping when start offset + * of second is less than the end offset of first. + * End offset is calculated as start offset + length. + * @param input list if input ranges. + * @return true/false based on logic explained above. + */ + public static List validateNonOverlappingAndReturnSortedRanges( + List input) { + + if (input.size() <= 1) { + return input; + } + FileRange[] sortedRanges = sortRanges(input); + FileRange prev = sortedRanges[0]; + for (int i=1; i input) { + FileRange[] sortedRanges = input.toArray(new FileRange[0]); + Arrays.sort(sortedRanges, Comparator.comparingLong(FileRange::getOffset)); + return sortedRanges; + } + + /** + * Merge sorted ranges to optimize the access from the underlying file + * system. + * The motivations are that: + *
    + *
  • Upper layers want to pass down logical file ranges.
  • + *
  • Fewer reads have better performance.
  • + *
  • Applications want callbacks as ranges are read.
  • + *
  • Some file systems want to round ranges to be at checksum boundaries.
  • + *
+ * + * @param sortedRanges already sorted list of ranges based on offset. + * @param chunkSize round the start and end points to multiples of chunkSize + * @param minimumSeek the smallest gap that we should seek over in bytes + * @param maxSize the largest combined file range in bytes + * @return the list of sorted CombinedFileRanges that cover the input + */ + public static List mergeSortedRanges(List sortedRanges, + int chunkSize, + int minimumSeek, + int maxSize) { + + CombinedFileRange current = null; + List result = new ArrayList<>(sortedRanges.size()); + + // now merge together the ones that merge + for (FileRange range: sortedRanges) { + long start = roundDown(range.getOffset(), chunkSize); + long end = roundUp(range.getOffset() + range.getLength(), chunkSize); + if (current == null || !current.merge(start, end, range, minimumSeek, maxSize)) { + current = new CombinedFileRange(start, end, range); + result.add(current); + } + } + return result; + } + + /** + * Slice the data that was read to the user's request. + * This function assumes that the user's request is completely subsumed by the + * read data. This always creates a new buffer pointing to the same underlying + * data but with its own mark and position fields such that reading one buffer + * can't effect other's mark and position. + * @param readData the buffer with the readData + * @param readOffset the offset in the file for the readData + * @param request the user's request + * @return the readData buffer that is sliced to the user's request + */ + public static ByteBuffer sliceTo(ByteBuffer readData, long readOffset, + FileRange request) { + int offsetChange = (int) (request.getOffset() - readOffset); + int requestLength = request.getLength(); + readData = readData.slice(); + readData.position(offsetChange); + readData.limit(offsetChange + requestLength); + return readData; + } + + /** + * private constructor. + */ + private VectoredReadUtils() { + throw new UnsupportedOperationException(); + } +} diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/audit/AuditConstants.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/audit/AuditConstants.java index d9629e388b384..0929c2be03acf 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/audit/AuditConstants.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/audit/AuditConstants.java @@ -90,6 +90,11 @@ private AuditConstants() { */ public static final String PARAM_PROCESS = "ps"; + /** + * Task Attempt ID query header: {@value}. + */ + public static final String PARAM_TASK_ATTEMPT_ID = "ta"; + /** * Thread 0: the thread which created a span {@value}. */ diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/audit/CommonAuditContext.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/audit/CommonAuditContext.java index e188e168e5313..2dcd4f8b3f570 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/audit/CommonAuditContext.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/audit/CommonAuditContext.java @@ -124,11 +124,15 @@ private CommonAuditContext() { /** * Put a context entry. * @param key key - * @param value new value + * @param value new value., If null, triggers removal. * @return old value or null */ public Supplier put(String key, String value) { - return evaluatedEntries.put(key, () -> value); + if (value != null) { + return evaluatedEntries.put(key, () -> value); + } else { + return evaluatedEntries.remove(key); + } } /** diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/impl/CombinedFileRange.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/impl/CombinedFileRange.java new file mode 100644 index 0000000000000..516bbb2c70c76 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/impl/CombinedFileRange.java @@ -0,0 +1,70 @@ +/* + * 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.impl; + +import org.apache.hadoop.fs.FileRange; + +import java.util.ArrayList; +import java.util.List; + +/** + * A file range that represents a set of underlying file ranges. + * This is used when we combine the user's FileRange objects + * together into a single read for efficiency. + */ +public class CombinedFileRange extends FileRangeImpl { + private ArrayList underlying = new ArrayList<>(); + + public CombinedFileRange(long offset, long end, FileRange original) { + super(offset, (int) (end - offset)); + this.underlying.add(original); + } + + /** + * Get the list of ranges that were merged together to form this one. + * @return the list of input ranges + */ + public List getUnderlying() { + return underlying; + } + + /** + * Merge this input range into the current one, if it is compatible. + * It is assumed that otherOffset is greater or equal the current offset, + * which typically happens by sorting the input ranges on offset. + * @param otherOffset the offset to consider merging + * @param otherEnd the end to consider merging + * @param other the underlying FileRange to add if we merge + * @param minSeek the minimum distance that we'll seek without merging the + * ranges together + * @param maxSize the maximum size that we'll merge into a single range + * @return true if we have merged the range into this one + */ + public boolean merge(long otherOffset, long otherEnd, FileRange other, + int minSeek, int maxSize) { + long end = this.getOffset() + this.getLength(); + long newEnd = Math.max(end, otherEnd); + if (otherOffset - end >= minSeek || newEnd - this.getOffset() > maxSize) { + return false; + } + this.setLength((int) (newEnd - this.getOffset())); + underlying.add(other); + return true; + } +} diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/impl/FileRangeImpl.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/impl/FileRangeImpl.java new file mode 100644 index 0000000000000..041e5f0a8d2d7 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/impl/FileRangeImpl.java @@ -0,0 +1,74 @@ +/* + * 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.impl; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.fs.FileRange; + +/** + * A range of bytes from a file with an optional buffer to read those bytes + * for zero copy. This shouldn't be created directly via constructor rather + * factory defined in {@code FileRange#createFileRange} should be used. + */ +@InterfaceAudience.Private +public class FileRangeImpl implements FileRange { + private long offset; + private int length; + private CompletableFuture reader; + + public FileRangeImpl(long offset, int length) { + this.offset = offset; + this.length = length; + } + + @Override + public String toString() { + return "range[" + offset + "," + (offset + length) + ")"; + } + + @Override + public long getOffset() { + return offset; + } + + @Override + public int getLength() { + return length; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public void setLength(int length) { + this.length = length; + } + + @Override + public void setData(CompletableFuture pReader) { + this.reader = pReader; + } + + @Override + public CompletableFuture getData() { + return reader; + } +} diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/sftp/SFTPConnectionPool.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/sftp/SFTPConnectionPool.java index de86bab6d3324..eace6417dcd68 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/sftp/SFTPConnectionPool.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/sftp/SFTPConnectionPool.java @@ -76,7 +76,7 @@ synchronized void returnToPool(ChannelSftp channel) { ConnectionInfo info = con2infoMap.get(channel); HashSet cons = idleConnections.get(info); if (cons == null) { - cons = new HashSet(); + cons = new HashSet<>(); idleConnections.put(info, cons); } cons.add(channel); @@ -94,7 +94,7 @@ synchronized void shutdown() { Set cons = con2infoMap.keySet(); if (cons != null && cons.size() > 0) { // make a copy since we need to modify the underlying Map - Set copy = new HashSet(cons); + Set copy = new HashSet<>(cons); // Initiate disconnect from all outstanding connections for (ChannelSftp con : copy) { try { diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/shell/CommandFormat.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/shell/CommandFormat.java index 4dd20d108428e..1228f76d846ab 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/shell/CommandFormat.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/shell/CommandFormat.java @@ -165,7 +165,7 @@ public String getOptValue(String option) { * @return Set{@literal <}String{@literal >} of the enabled options */ public Set getOpts() { - Set optSet = new HashSet(); + Set optSet = new HashSet<>(); for (Map.Entry entry : options.entrySet()) { if (entry.getValue()) { optSet.add(entry.getKey()); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/shell/find/Find.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/shell/find/Find.java index 199038a751226..07baea89dd604 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/shell/find/Find.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/shell/find/Find.java @@ -96,7 +96,7 @@ private static void addExpression(Class clazz) { private Expression rootExpression; /** Set of path items returning a {@link Result#STOP} result. */ - private HashSet stopPaths = new HashSet(); + private HashSet stopPaths = new HashSet<>(); /** Register the expressions with the expression factory. */ private static void registerExpressions(ExpressionFactory factory) { diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/statistics/StoreStatisticNames.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/statistics/StoreStatisticNames.java index c458269c3510d..c04c1bb47fcea 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/statistics/StoreStatisticNames.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/statistics/StoreStatisticNames.java @@ -53,6 +53,9 @@ public final class StoreStatisticNames { /** {@value}. */ public static final String OP_CREATE = "op_create"; + /** {@value}. */ + public static final String OP_CREATE_FILE = "op_createfile"; + /** {@value}. */ public static final String OP_CREATE_NON_RECURSIVE = "op_create_non_recursive"; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/viewfs/ViewFileSystem.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/viewfs/ViewFileSystem.java index da3955b125e84..e31a701a6eaa7 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/viewfs/ViewFileSystem.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/viewfs/ViewFileSystem.java @@ -1037,7 +1037,7 @@ public FileSystem[] getChildFileSystems() { List> mountPoints = fsState.getMountPoints(); Map fsMap = initializeMountedFileSystems(mountPoints); - Set children = new HashSet(); + Set children = new HashSet<>(); for (InodeTree.MountPoint mountPoint : mountPoints) { FileSystem targetFs = fsMap.get(mountPoint.src); children.addAll(Arrays.asList(targetFs.getChildFileSystems())); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/ByteBufferPool.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/ByteBufferPool.java index aa5f8731c54a7..b30e7cfb9c5f0 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/ByteBufferPool.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/ByteBufferPool.java @@ -45,4 +45,9 @@ public interface ByteBufferPool { * @param buffer a direct bytebuffer */ void putBuffer(ByteBuffer buffer); + + /** + * Clear the buffer pool thus releasing all the buffers. + */ + default void release() { } } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/ElasticByteBufferPool.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/ElasticByteBufferPool.java index 6a162c3ff2087..c4c2940622729 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/ElasticByteBufferPool.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/ElasticByteBufferPool.java @@ -36,8 +36,8 @@ */ @InterfaceAudience.Public @InterfaceStability.Stable -public final class ElasticByteBufferPool implements ByteBufferPool { - private static final class Key implements Comparable { +public class ElasticByteBufferPool implements ByteBufferPool { + protected static final class Key implements Comparable { private final int capacity; private final long insertionTime; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/WeakReferencedElasticByteBufferPool.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/WeakReferencedElasticByteBufferPool.java new file mode 100644 index 0000000000000..c71c44e798a65 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/WeakReferencedElasticByteBufferPool.java @@ -0,0 +1,155 @@ +/** + * 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.io; + +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.classification.VisibleForTesting; + +/** + * Buffer pool implementation which uses weak references to store + * buffers in the pool, such that they are garbage collected when + * there are no references to the buffer during a gc run. This is + * important as direct buffers don't get garbage collected automatically + * during a gc run as they are not stored on heap memory. + * Also the buffers are stored in a tree map which helps in returning + * smallest buffer whose size is just greater than requested length. + * This is a thread safe implementation. + */ +@InterfaceAudience.Private +@InterfaceStability.Unstable +public final class WeakReferencedElasticByteBufferPool extends ElasticByteBufferPool { + + /** + * Map to store direct byte buffers of different sizes in the pool. + * Used tree map such that we can return next greater than capacity + * buffer if buffer with exact capacity is unavailable. + * This must be accessed in synchronized blocks. + */ + private final TreeMap> directBuffers = + new TreeMap<>(); + + /** + * Map to store heap based byte buffers of different sizes in the pool. + * Used tree map such that we can return next greater than capacity + * buffer if buffer with exact capacity is unavailable. + * This must be accessed in synchronized blocks. + */ + private final TreeMap> heapBuffers = + new TreeMap<>(); + + /** + * Method to get desired buffer tree. + * @param isDirect whether the buffer is heap based or direct. + * @return corresponding buffer tree. + */ + private TreeMap> getBufferTree(boolean isDirect) { + return isDirect + ? directBuffers + : heapBuffers; + } + + /** + * {@inheritDoc} + * + * @param direct whether we want a direct byte buffer or a heap one. + * @param length length of requested buffer. + * @return returns equal or next greater than capacity buffer from + * pool if already available and not garbage collected else creates + * a new buffer and return it. + */ + @Override + public synchronized ByteBuffer getBuffer(boolean direct, int length) { + TreeMap> buffersTree = getBufferTree(direct); + + // Scan the entire tree and remove all weak null references. + buffersTree.entrySet().removeIf(next -> next.getValue().get() == null); + + Map.Entry> entry = + buffersTree.ceilingEntry(new Key(length, 0)); + // If there is no buffer present in the pool with desired size. + if (entry == null) { + return direct ? ByteBuffer.allocateDirect(length) : + ByteBuffer.allocate(length); + } + // buffer is available in the pool and not garbage collected. + WeakReference bufferInPool = entry.getValue(); + buffersTree.remove(entry.getKey()); + ByteBuffer buffer = bufferInPool.get(); + if (buffer != null) { + return buffer; + } + // buffer was in pool but already got garbage collected. + return direct + ? ByteBuffer.allocateDirect(length) + : ByteBuffer.allocate(length); + } + + /** + * Return buffer to the pool. + * @param buffer buffer to be returned. + */ + @Override + public synchronized void putBuffer(ByteBuffer buffer) { + buffer.clear(); + TreeMap> buffersTree = getBufferTree(buffer.isDirect()); + // Buffers are indexed by (capacity, time). + // If our key is not unique on the first try, we try again, since the + // time will be different. Since we use nanoseconds, it's pretty + // unlikely that we'll loop even once, unless the system clock has a + // poor granularity or multi-socket systems have clocks slightly out + // of sync. + while (true) { + Key keyToInsert = new Key(buffer.capacity(), System.nanoTime()); + if (!buffersTree.containsKey(keyToInsert)) { + buffersTree.put(keyToInsert, new WeakReference<>(buffer)); + return; + } + } + } + + /** + * Clear the buffer pool thus releasing all the buffers. + * The caller must remove all references of + * existing buffers before calling this method to avoid + * memory leaks. + */ + @Override + public synchronized void release() { + heapBuffers.clear(); + directBuffers.clear(); + } + + /** + * Get current buffers count in the pool. + * @param isDirect whether we want to count the heap or direct buffers. + * @return count of buffers. + */ + @VisibleForTesting + public synchronized int getCurrentBuffersCount(boolean isDirect) { + return isDirect + ? directBuffers.size() + : heapBuffers.size(); + } +} diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/compress/CodecPool.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/compress/CodecPool.java index 69e8c99a1f4da..1f095c6c6736e 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/compress/CodecPool.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/compress/CodecPool.java @@ -109,7 +109,7 @@ private static boolean payback(Map, Set> pool, T codec) { synchronized (pool) { codecSet = pool.get(codecClass); if (codecSet == null) { - codecSet = new HashSet(); + codecSet = new HashSet<>(); pool.put(codecClass, codecSet); } } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/compress/DefaultCodec.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/compress/DefaultCodec.java index d2ffb22eaafb3..b407ddb11046c 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/compress/DefaultCodec.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/compress/DefaultCodec.java @@ -26,7 +26,6 @@ import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.conf.Configurable; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.io.compress.zlib.ZlibDecompressor; import org.apache.hadoop.io.compress.zlib.ZlibFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/erasurecode/codec/ErasureCodec.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/erasurecode/codec/ErasureCodec.java index c75eaead83d01..22ab632a49512 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/erasurecode/codec/ErasureCodec.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/erasurecode/codec/ErasureCodec.java @@ -19,7 +19,6 @@ import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.io.erasurecode.CodecUtil; import org.apache.hadoop.io.erasurecode.ECSchema; import org.apache.hadoop.io.erasurecode.ErasureCodecOptions; import org.apache.hadoop.io.erasurecode.ErasureCoderOptions; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/retry/AsyncCallHandler.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/retry/AsyncCallHandler.java index 3ebbcd912dc71..60210ccd920c2 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/retry/AsyncCallHandler.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/retry/AsyncCallHandler.java @@ -28,7 +28,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.InterruptedIOException; import java.lang.reflect.Method; import java.util.Iterator; import java.util.Queue; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/retry/RetryPolicies.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/retry/RetryPolicies.java index 0b66347f1f90f..d7693f868eb30 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/retry/RetryPolicies.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/retry/RetryPolicies.java @@ -41,7 +41,6 @@ import org.apache.hadoop.net.ConnectTimeoutException; import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.token.SecretManager.InvalidToken; -import org.ietf.jgss.GSSException; import org.apache.hadoop.classification.VisibleForTesting; import org.slf4j.Logger; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/serializer/avro/AvroReflectSerialization.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/serializer/avro/AvroReflectSerialization.java index cfbc60d10452b..544958e682a50 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/serializer/avro/AvroReflectSerialization.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/io/serializer/avro/AvroReflectSerialization.java @@ -64,7 +64,7 @@ public synchronized boolean accept(Class c) { private void getPackages() { String[] pkgList = getConf().getStrings(AVRO_REFLECT_PACKAGES); - packages = new HashSet(); + packages = new HashSet<>(); if (pkgList != null) { for (String pkg : pkgList) { packages.add(pkg.trim()); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/ProtocolProxy.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/ProtocolProxy.java index 49029f97b3d29..f5f212b29276d 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/ProtocolProxy.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/ProtocolProxy.java @@ -69,7 +69,7 @@ private void fetchServerMethods(Method method) throws IOException { } int[] serverMethodsCodes = serverInfo.getMethods(); if (serverMethodsCodes != null) { - serverMethods = new HashSet(serverMethodsCodes.length); + serverMethods = new HashSet<>(serverMethodsCodes.length); for (int m : serverMethodsCodes) { this.serverMethods.add(Integer.valueOf(m)); } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/Server.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/Server.java index 90f730d38836d..e79612f7a5a0f 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/Server.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/Server.java @@ -65,9 +65,12 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; import java.util.stream.Collectors; import javax.security.sasl.Sasl; @@ -127,6 +130,8 @@ import org.apache.hadoop.tracing.TraceUtils; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hadoop.classification.VisibleForTesting; + +import org.apache.hadoop.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; import org.apache.hadoop.thirdparty.protobuf.ByteString; import org.apache.hadoop.thirdparty.protobuf.CodedOutputStream; import org.apache.hadoop.thirdparty.protobuf.Message; @@ -500,6 +505,11 @@ protected ResponseBuffer initialValue() { private Responder responder = null; private Handler[] handlers = null; private final AtomicInteger numInProcessHandler = new AtomicInteger(); + private final LongAdder totalRequests = new LongAdder(); + private long lastSeenTotalRequests = 0; + private long totalRequestsPerSecond = 0; + private final long metricsUpdaterInterval; + private final ScheduledExecutorService scheduledExecutorService; private boolean logSlowRPC = false; @@ -515,6 +525,14 @@ public int getNumInProcessHandler() { return numInProcessHandler.get(); } + public long getTotalRequests() { + return totalRequests.sum(); + } + + public long getTotalRequestsPerSecond() { + return totalRequestsPerSecond; + } + /** * Sets slow RPC flag. * @param logSlowRPCFlag input logSlowRPCFlag. @@ -578,6 +596,7 @@ void logSlowRpcCalls(String methodName, Call call, } void updateMetrics(Call call, long startTime, boolean connDropped) { + totalRequests.increment(); // delta = handler + processing + response long deltaNanos = Time.monotonicNowNanos() - startTime; long timestampNanos = call.timestampNanos; @@ -3304,6 +3323,14 @@ protected Server(String bindAddress, int port, this.exceptionsHandler.addTerseLoggingExceptions(StandbyException.class); this.exceptionsHandler.addTerseLoggingExceptions( HealthCheckFailedException.class); + this.metricsUpdaterInterval = + conf.getLong(CommonConfigurationKeysPublic.IPC_SERVER_METRICS_UPDATE_RUNNER_INTERVAL, + CommonConfigurationKeysPublic.IPC_SERVER_METRICS_UPDATE_RUNNER_INTERVAL_DEFAULT); + this.scheduledExecutorService = new ScheduledThreadPoolExecutor(1, + new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Hadoop-Metrics-Updater-%d") + .build()); + this.scheduledExecutorService.scheduleWithFixedDelay(new MetricsUpdateRunner(), + metricsUpdaterInterval, metricsUpdaterInterval, TimeUnit.MILLISECONDS); } public synchronized void addAuxiliaryListener(int auxiliaryPort) @@ -3598,10 +3625,25 @@ public synchronized void stop() { } responder.interrupt(); notifyAll(); + shutdownMetricsUpdaterExecutor(); this.rpcMetrics.shutdown(); this.rpcDetailedMetrics.shutdown(); } + private void shutdownMetricsUpdaterExecutor() { + this.scheduledExecutorService.shutdown(); + try { + boolean isExecutorShutdown = + this.scheduledExecutorService.awaitTermination(3, TimeUnit.SECONDS); + if (!isExecutorShutdown) { + LOG.info("Hadoop Metrics Updater executor could not be shutdown."); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.info("Hadoop Metrics Updater executor shutdown interrupted.", e); + } + } + /** * Wait for the server to be stopped. * Does not wait for all subthreads to finish. @@ -4061,4 +4103,32 @@ protected int getMaxIdleTime() { public String getServerName() { return serverName; } + + /** + * Server metrics updater thread, used to update some metrics on a regular basis. + * For instance, requests per second. + */ + private class MetricsUpdateRunner implements Runnable { + + private long lastExecuted = 0; + + @Override + public synchronized void run() { + long currentTime = Time.monotonicNow(); + if (lastExecuted == 0) { + lastExecuted = currentTime - metricsUpdaterInterval; + } + long currentTotalRequests = totalRequests.sum(); + long totalRequestsDiff = currentTotalRequests - lastSeenTotalRequests; + lastSeenTotalRequests = currentTotalRequests; + if ((currentTime - lastExecuted) > 0) { + double totalRequestsPerSecInDouble = + (double) totalRequestsDiff / TimeUnit.MILLISECONDS.toSeconds( + currentTime - lastExecuted); + totalRequestsPerSecond = ((long) totalRequestsPerSecInDouble); + } + lastExecuted = currentTime; + } + } + } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/metrics/RpcMetrics.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/metrics/RpcMetrics.java index a67530b3c97b2..bf21e3865fa8a 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/metrics/RpcMetrics.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/ipc/metrics/RpcMetrics.java @@ -151,6 +151,16 @@ public String numOpenConnectionsPerUser() { return server.getNumDroppedConnections(); } + @Metric("Number of total requests") + public long getTotalRequests() { + return server.getTotalRequests(); + } + + @Metric("Number of total requests per second") + public long getTotalRequestsPerSecond() { + return server.getTotalRequestsPerSecond(); + } + public TimeUnit getMetricsTimeUnit() { return metricsTimeUnit; } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/lib/MutableRates.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/lib/MutableRates.java index 19696bd839400..90b5da01c062f 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/lib/MutableRates.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/lib/MutableRates.java @@ -19,11 +19,10 @@ package org.apache.hadoop.metrics2.lib; import java.lang.reflect.Method; +import java.util.HashSet; import java.util.Set; import static org.apache.hadoop.util.Preconditions.*; -import org.apache.hadoop.util.Sets; - import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.metrics2.MetricsRecordBuilder; @@ -44,7 +43,7 @@ public class MutableRates extends MutableMetric { static final Logger LOG = LoggerFactory.getLogger(MutableRates.class); private final MetricsRegistry registry; - private final Set> protocolCache = Sets.newHashSet(); + private final Set> protocolCache = new HashSet<>(); MutableRates(MetricsRegistry registry) { this.registry = checkNotNull(registry, "metrics registry"); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/lib/MutableRatesWithAggregation.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/lib/MutableRatesWithAggregation.java index dc37f96f4f449..4c5f0a844aaab 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/lib/MutableRatesWithAggregation.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/metrics2/lib/MutableRatesWithAggregation.java @@ -18,9 +18,9 @@ package org.apache.hadoop.metrics2.lib; -import org.apache.hadoop.util.Sets; import java.lang.ref.WeakReference; import java.lang.reflect.Method; +import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; @@ -52,7 +52,7 @@ public class MutableRatesWithAggregation extends MutableMetric { LoggerFactory.getLogger(MutableRatesWithAggregation.class); private final Map globalMetrics = new ConcurrentHashMap<>(); - private final Set> protocolCache = Sets.newHashSet(); + private final Set> protocolCache = new HashSet<>(); private final ConcurrentLinkedDeque>> weakReferenceQueue = new ConcurrentLinkedDeque<>(); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/net/AbstractDNSToSwitchMapping.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/net/AbstractDNSToSwitchMapping.java index f050219398721..5a13b00098a44 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/net/AbstractDNSToSwitchMapping.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/net/AbstractDNSToSwitchMapping.java @@ -115,7 +115,7 @@ public String dumpTopology() { builder.append("Mapping: ").append(toString()).append("\n"); if (rack != null) { builder.append("Map:\n"); - Set switches = new HashSet(); + Set switches = new HashSet<>(); for (Map.Entry entry : rack.entrySet()) { builder.append(" ") .append(entry.getKey()) diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/net/NetworkTopology.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/net/NetworkTopology.java index 6644b3911b844..ebb354e7db3cb 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/net/NetworkTopology.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/net/NetworkTopology.java @@ -1086,7 +1086,7 @@ private void interAddNodeWithEmptyRack(Node node) { String rackname = node.getNetworkLocation(); Set nodes = rackMap.get(rackname); if (nodes == null) { - nodes = new HashSet(); + nodes = new HashSet<>(); } if (!decommissionNodes.contains(node.getName())) { nodes.add(node.getName()); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/CompositeGroupsMapping.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/CompositeGroupsMapping.java index 6f799c1542095..deca6f1152ba4 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/CompositeGroupsMapping.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/CompositeGroupsMapping.java @@ -109,7 +109,7 @@ public void cacheGroupsAdd(List groups) throws IOException { @Override public synchronized Set getGroupsSet(String user) throws IOException { - Set groupSet = new HashSet(); + Set groupSet = new HashSet<>(); Set groups = null; for (GroupMappingServiceProvider provider : providersList) { diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/IdMappingServiceProvider.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/IdMappingServiceProvider.java index 86edab7de7097..08cacdc248fa4 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/IdMappingServiceProvider.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/IdMappingServiceProvider.java @@ -18,11 +18,9 @@ package org.apache.hadoop.security; import java.io.IOException; -import java.util.List; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.fs.CommonConfigurationKeysPublic; /** * An interface for the implementation of {@literal <}userId, diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/NetgroupCache.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/NetgroupCache.java index aa06c59a64814..5e466033fb713 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/NetgroupCache.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/NetgroupCache.java @@ -65,7 +65,7 @@ public static List getNetgroupNames() { } private static Set getGroups() { - Set allGroups = new HashSet (); + Set allGroups = new HashSet<>(); for (Set userGroups : userToNetgroupsMap.values()) { allGroups.addAll(userGroups); } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/authorize/AccessControlList.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/authorize/AccessControlList.java index 39dc29a79e1f6..6fabbfb47b9f8 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/authorize/AccessControlList.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/authorize/AccessControlList.java @@ -105,8 +105,8 @@ public AccessControlList(String users, String groups) { * @param userGroupStrings build ACL from array of Strings */ private void buildACL(String[] userGroupStrings) { - users = new HashSet(); - groups = new HashSet(); + users = new HashSet<>(); + groups = new HashSet<>(); for (String aclPart : userGroupStrings) { if (aclPart != null && isWildCardACLValue(aclPart)) { allAllowed = true; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/authorize/ProxyServers.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/authorize/ProxyServers.java index 410e25f583966..6f5283074dca6 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/authorize/ProxyServers.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/authorize/ProxyServers.java @@ -33,7 +33,7 @@ public static void refresh() { } public static void refresh(Configuration conf){ - Collection tempServers = new HashSet(); + Collection tempServers = new HashSet<>(); // trusted proxy servers such as http proxies for (String host : conf.getTrimmedStrings(CONF_HADOOP_PROXYSERVERS)) { InetSocketAddress addr = new InetSocketAddress(host, 0); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/CrossOriginFilter.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/CrossOriginFilter.java index 059cdc4b653de..ef342f257a937 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/CrossOriginFilter.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/CrossOriginFilter.java @@ -22,7 +22,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.Filter; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/RestCsrfPreventionFilter.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/RestCsrfPreventionFilter.java index b81ed8e90155e..7363ca0ba6450 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/RestCsrfPreventionFilter.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/RestCsrfPreventionFilter.java @@ -94,7 +94,7 @@ public void init(FilterConfig filterConfig) throws ServletException { void parseBrowserUserAgents(String userAgents) { String[] agentsArray = userAgents.split(","); - browserUserAgents = new HashSet(); + browserUserAgents = new HashSet<>(); for (String patternString : agentsArray) { browserUserAgents.add(Pattern.compile(patternString)); } @@ -102,7 +102,7 @@ void parseBrowserUserAgents(String userAgents) { void parseMethodsToIgnore(String mti) { String[] methods = mti.split(","); - methodsToIgnore = new HashSet(); + methodsToIgnore = new HashSet<>(); for (int i = 0; i < methods.length; i++) { methodsToIgnore.add(methods[i]); } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/ReloadingX509TrustManager.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/ReloadingX509TrustManager.java index c6049a91b5a51..3dc5017ba6377 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/ReloadingX509TrustManager.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ssl/ReloadingX509TrustManager.java @@ -27,7 +27,6 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenSecretManager.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenSecretManager.java index c85595e922279..d0c0fac6e88df 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenSecretManager.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/AbstractDelegationTokenSecretManager.java @@ -518,9 +518,9 @@ protected DelegationTokenInformation checkToken(TokenIdent identifier) } long now = Time.now(); if (info.getRenewDate() < now) { - err = - "Token has" + identifier.getRealUser() + "expired, current time: " + Time.formatTime(now) - + " expected renewal time: " + Time.formatTime(info.getRenewDate()); + err = "Token " + identifier.getRealUser() + " has expired, current time: " + + Time.formatTime(now) + " expected renewal time: " + Time + .formatTime(info.getRenewDate()); LOG.info("{}, Token={}", err, formatTokenId(identifier)); throw new InvalidToken(err); } @@ -716,7 +716,7 @@ public String getTrackingId() { /** Remove expired delegation tokens from cache */ private void removeExpiredToken() throws IOException { long now = Time.now(); - Set expiredTokens = new HashSet(); + Set expiredTokens = new HashSet<>(); synchronized (this) { Iterator> i = currentTokens.entrySet().iterator(); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/DelegationTokenAuthenticationHandler.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/DelegationTokenAuthenticationHandler.java index b55214451ec25..f4ede6f35edb0 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/DelegationTokenAuthenticationHandler.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/DelegationTokenAuthenticationHandler.java @@ -89,7 +89,7 @@ public abstract class DelegationTokenAuthenticationHandler public static final String TOKEN_KIND = PREFIX + "token-kind"; - private static final Set DELEGATION_TOKEN_OPS = new HashSet(); + private static final Set DELEGATION_TOKEN_OPS = new HashSet<>(); public static final String DELEGATION_TOKEN_UGI_ATTRIBUTE = "hadoop.security.delegation-token.ugi"; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/HttpUserGroupInformation.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/HttpUserGroupInformation.java index 614c0d3b36bcf..1f18b1c2f6ae0 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/HttpUserGroupInformation.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/HttpUserGroupInformation.java @@ -20,8 +20,6 @@ import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.security.UserGroupInformation; -import javax.servlet.http.HttpServletRequest; - /** * Util class that returns the remote {@link UserGroupInformation} in scope * for the HTTP request. diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/FileBasedIPList.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/FileBasedIPList.java index 47aa9cc71a12e..31dfe594207be 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/FileBasedIPList.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/FileBasedIPList.java @@ -58,7 +58,7 @@ public FileBasedIPList(String fileName) { lines = null; } if (lines != null) { - addressList = new MachineList(new HashSet(Arrays.asList(lines))); + addressList = new MachineList(new HashSet<>(Arrays.asList(lines))); } else { addressList = null; } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/FindClass.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/FindClass.java index e51f7b14a60e0..268b4e166e8a3 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/FindClass.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/FindClass.java @@ -20,7 +20,6 @@ import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configured; -import org.apache.hadoop.util.StringUtils; import org.apache.hadoop.util.Tool; import org.apache.hadoop.util.ToolRunner; import java.io.IOException; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/HostsFileReader.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/HostsFileReader.java index 5141740a3d23e..d94668356e261 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/HostsFileReader.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/HostsFileReader.java @@ -135,7 +135,7 @@ public static void readFileToMapWithFileInputStream(String type, if (xmlInput) { readXmlFileToMapWithFileInputStream(type, filename, inputStream, map); } else { - HashSet nodes = new HashSet(); + HashSet nodes = new HashSet<>(); readFileToSetWithFileInputStream(type, filename, inputStream, nodes); for (String node : nodes) { map.put(node, null); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/JsonSerialization.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/JsonSerialization.java index 0bba79fd77f14..52c6c4505226a 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/JsonSerialization.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/JsonSerialization.java @@ -297,11 +297,12 @@ public void save(FileSystem fs, Path path, T instance, } /** - * Write the JSON as bytes, then close the file. + * Write the JSON as bytes, then close the stream. + * @param instance instance to write * @param dataOutputStream an output stream that will always be closed * @throws IOException on any failure */ - private void writeJsonAsBytes(T instance, + public void writeJsonAsBytes(T instance, OutputStream dataOutputStream) throws IOException { try { dataOutputStream.write(toBytes(instance)); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ReflectionUtils.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ReflectionUtils.java index 2438b714ffcd3..155c4f9c5f498 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ReflectionUtils.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ReflectionUtils.java @@ -21,7 +21,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; -import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ShutdownHookManager.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ShutdownHookManager.java index fbdd33331b62b..e85f850514b16 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ShutdownHookManager.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/ShutdownHookManager.java @@ -249,7 +249,7 @@ TimeUnit getTimeUnit() { } private final Set hooks = - Collections.synchronizedSet(new HashSet()); + Collections.synchronizedSet(new HashSet<>()); private AtomicBoolean shutdownInProgress = new AtomicBoolean(false); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/functional/CloseableTaskPoolSubmitter.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/functional/CloseableTaskPoolSubmitter.java index 26b687a3c5610..695da7e932279 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/functional/CloseableTaskPoolSubmitter.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/functional/CloseableTaskPoolSubmitter.java @@ -34,7 +34,7 @@ */ @InterfaceAudience.Public @InterfaceStability.Unstable -public final class CloseableTaskPoolSubmitter implements TaskPool.Submitter, +public class CloseableTaskPoolSubmitter implements TaskPool.Submitter, Closeable { /** Executors. */ diff --git a/hadoop-common-project/hadoop-common/src/site/markdown/Metrics.md b/hadoop-common-project/hadoop-common/src/site/markdown/Metrics.md index 47786e473a52f..04cbd9fedf83c 100644 --- a/hadoop-common-project/hadoop-common/src/site/markdown/Metrics.md +++ b/hadoop-common-project/hadoop-common/src/site/markdown/Metrics.md @@ -104,6 +104,8 @@ The default timeunit used for RPC metrics is milliseconds (as per the below desc | `rpcLockWaitTime`*num*`s90thPercentileLatency` | Shows the 90th percentile of RPC lock wait time in milliseconds (*num* seconds granularity) if `rpc.metrics.quantile.enable` is set to true. *num* is specified by `rpc.metrics.percentiles.intervals`. | | `rpcLockWaitTime`*num*`s95thPercentileLatency` | Shows the 95th percentile of RPC lock wait time in milliseconds (*num* seconds granularity) if `rpc.metrics.quantile.enable` is set to true. *num* is specified by `rpc.metrics.percentiles.intervals`. | | `rpcLockWaitTime`*num*`s99thPercentileLatency` | Shows the 99th percentile of RPC lock wait time in milliseconds (*num* seconds granularity) if `rpc.metrics.quantile.enable` is set to true. *num* is specified by `rpc.metrics.percentiles.intervals`. | +| `TotalRequests` | Total num of requests served by the RPC server. | +| `TotalRequestsPerSeconds` | Total num of requests per second served by the RPC server. | RetryCache/NameNodeRetryCache ----------------------------- diff --git a/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdatainputstream.md b/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdatainputstream.md index 090696483be34..197b999c81f66 100644 --- a/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdatainputstream.md +++ b/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdatainputstream.md @@ -443,6 +443,45 @@ The semantics of this are exactly equivalent to That is, the buffer is filled entirely with the contents of the input source from position `position` +### `default void readVectored(List ranges, IntFunction allocate)` + +Read fully data for a list of ranges asynchronously. The default implementation +iterates through the ranges, tries to coalesce the ranges based on values of +`minSeekForVectorReads` and `maxReadSizeForVectorReads` and then read each merged +ranges synchronously, but the intent is sub classes can implement efficient +implementation. Reading in both direct and heap byte buffers are supported. +Also, clients are encouraged to use `WeakReferencedElasticByteBufferPool` for +allocating buffers such that even direct buffers are garbage collected when +they are no longer referenced. + +Note: Don't use direct buffers for reading from ChecksumFileSystem as that may +lead to memory fragmentation explained in HADOOP-18296. + + +#### Preconditions + +For each requested range: + + range.getOffset >= 0 else raise IllegalArgumentException + range.getLength >= 0 else raise EOFException + +#### Postconditions + +For each requested range: + + range.getData() returns CompletableFuture which will have data + from range.getOffset to range.getLength. + +### `minSeekForVectorReads()` + +The smallest reasonable seek. Two ranges won't be merged together if the difference between +end of first and start of next range is more than this value. + +### `maxReadSizeForVectorReads()` + +Maximum number of bytes which can be read in one go after merging the ranges. +Two ranges won't be merged if the combined data to be read is more than this value. +Essentially setting this to 0 will disable the merging of ranges. ## Consistency diff --git a/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdatainputstreambuilder.md b/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdatainputstreambuilder.md index db630e05c22d4..16a14150ef949 100644 --- a/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdatainputstreambuilder.md +++ b/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdatainputstreambuilder.md @@ -230,7 +230,7 @@ Note: some operations on the input stream, such as `seek()` may not attempt any at all. Such operations MAY NOT raise exceotions when interacting with nonexistent/unreadable files. -## Standard `openFile()` options since Hadoop 3.3.3 +## Standard `openFile()` options since hadoop branch-3.3 These are options which `FileSystem` and `FileContext` implementation MUST recognise and MAY support by changing the behavior of diff --git a/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdataoutputstreambuilder.md b/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdataoutputstreambuilder.md index 64dda2df8c63c..59a93c5887a1f 100644 --- a/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdataoutputstreambuilder.md +++ b/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/fsdataoutputstreambuilder.md @@ -26,7 +26,7 @@ create a new file or open an existing file on `FileSystem` for write. ## Invariants The `FSDataOutputStreamBuilder` interface does not validate parameters -and modify the state of `FileSystem` until [`build()`](#Builder.build) is +and modify the state of `FileSystem` until `build()` is invoked. ## Implementation-agnostic parameters. @@ -110,7 +110,7 @@ of `FileSystem`. #### Implementation Notes The concrete `FileSystem` and/or `FSDataOutputStreamBuilder` implementation -MUST verify that implementation-agnostic parameters (i.e., "syncable") or +MUST verify that implementation-agnostic parameters (i.e., "syncable`) or implementation-specific parameters (i.e., "foofs:cache") are supported. `FileSystem` will satisfy optional parameters (via `opt(key, ...)`) on best effort. If the mandatory parameters (via `must(key, ...)`) can not be satisfied @@ -182,3 +182,58 @@ see `FileSystem#create(path, ...)` and `FileSystem#append()`. result = FSDataOutputStream The result is `FSDataOutputStream` to be used to write data to filesystem. + + +## S3A-specific options + +Here are the custom options which the S3A Connector supports. + +| Name | Type | Meaning | +|-----------------------------|-----------|----------------------------------------| +| `fs.s3a.create.performance` | `boolean` | create a file with maximum performance | +| `fs.s3a.create.header` | `string` | prefix for user supplied headers | + +### `fs.s3a.create.performance` + +Prioritize file creation performance over safety checks for filesystem consistency. + +This: +1. Skips the `LIST` call which makes sure a file is being created over a directory. + Risk: a file is created over a directory. +1. Ignores the overwrite flag. +1. Never issues a `DELETE` call to delete parent directory markers. + +It is possible to probe an S3A Filesystem instance for this capability through +the `hasPathCapability(path, "fs.s3a.create.performance")` check. + +Creating files with this option over existing directories is likely +to make S3A filesystem clients behave inconsistently. + +Operations optimized for directories (e.g. listing calls) are likely +to see the directory tree not the file; operations optimized for +files (`getFileStatus()`, `isFile()`) more likely to see the file. +The exact form of the inconsistencies, and which operations/parameters +trigger this are undefined and may change between even minor releases. + +Using this option is the equivalent of pressing and holding down the +"Electronic Stability Control" +button on a rear-wheel drive car for five seconds: the safety checks are off. +Things wil be faster if the driver knew what they were doing. +If they didn't, the fact they had held the button down will +be used as evidence at the inquest as proof that they made a +conscious decision to choose speed over safety and +that the outcome was their own fault. + +Accordingly: *Use if and only if you are confident that the conditions are met.* + +### `fs.s3a.create.header` User-supplied header support + +Options with the prefix `fs.s3a.create.header.` will be added to to the +S3 object metadata as "user defined metadata". +This metadata is visible to all applications. It can also be retrieved through the +FileSystem/FileContext `listXAttrs()` and `getXAttrs()` API calls with the prefix `header.` + +When an object is renamed, the metadata is propagated the copy created. + +It is possible to probe an S3A Filesystem instance for this capability through +the `hasPathCapability(path, "fs.s3a.create.header")` check. \ No newline at end of file diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/cli/util/CommandExecutor.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/cli/util/CommandExecutor.java index 5ef129cdc87ed..2ccfcfebb27e3 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/cli/util/CommandExecutor.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/cli/util/CommandExecutor.java @@ -23,7 +23,6 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.PrintStream; -import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.ArrayList; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java index 9fcf4a5eb55a2..c31229ba9fcf1 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java @@ -21,21 +21,17 @@ import java.util.HashSet; import org.apache.hadoop.crypto.key.kms.KMSClientProvider; -import org.apache.hadoop.fs.AbstractFileSystem; import org.apache.hadoop.fs.CommonConfigurationKeys; import org.apache.hadoop.fs.CommonConfigurationKeysPublic; import org.apache.hadoop.fs.ftp.FtpConfigKeys; import org.apache.hadoop.fs.local.LocalConfigKeys; import org.apache.hadoop.ha.SshFenceByTcpPort; import org.apache.hadoop.ha.ZKFailoverController; -import org.apache.hadoop.http.HttpServer2; import org.apache.hadoop.io.erasurecode.CodecUtil; -import org.apache.hadoop.io.nativeio.NativeIO; import org.apache.hadoop.security.CompositeGroupsMapping; import org.apache.hadoop.security.HttpCrossOriginFilterInitializer; import org.apache.hadoop.security.LdapGroupsMapping; import org.apache.hadoop.security.RuleBasedLdapGroupsMapping; -import org.apache.hadoop.security.http.CrossOriginFilter; import org.apache.hadoop.security.ssl.SSLFactory; /** @@ -80,9 +76,9 @@ public void initializeMemberVariables() { }; // Initialize used variables - xmlPropsToSkipCompare = new HashSet(); - xmlPrefixToSkipCompare = new HashSet(); - configurationPropsToSkipCompare = new HashSet(); + xmlPropsToSkipCompare = new HashSet<>(); + xmlPrefixToSkipCompare = new HashSet<>(); + configurationPropsToSkipCompare = new HashSet<>(); // Set error modes errorIfMissingConfigProps = true; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestConfigurationDeprecation.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestConfigurationDeprecation.java index 2c0d6025f2688..83837862ac47e 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestConfigurationDeprecation.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestConfigurationDeprecation.java @@ -30,7 +30,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/TestCryptoStreamsForLocalFS.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/TestCryptoStreamsForLocalFS.java index 8453889b53a5a..072baf188de72 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/TestCryptoStreamsForLocalFS.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/TestCryptoStreamsForLocalFS.java @@ -25,7 +25,6 @@ import java.io.OutputStream; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.CommonConfigurationKeysPublic; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FileUtil; import org.apache.hadoop.fs.LocalFileSystem; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/key/TestValueQueue.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/key/TestValueQueue.java index 5da973c6a761d..4805fca1d49f4 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/key/TestValueQueue.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/key/TestValueQueue.java @@ -18,6 +18,8 @@ package org.apache.hadoop.crypto.key; import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeoutException; @@ -32,7 +34,6 @@ import org.junit.Assert; import org.junit.Test; -import org.apache.hadoop.util.Sets; public class TestValueQueue { Logger LOG = LoggerFactory.getLogger(TestValueQueue.class); @@ -103,10 +104,10 @@ public void testWarmUp() throws Exception { Assert.assertEquals(5, fillInfos[0].num); Assert.assertEquals(5, fillInfos[1].num); Assert.assertEquals(5, fillInfos[2].num); - Assert.assertEquals(Sets.newHashSet("k1", "k2", "k3"), - Sets.newHashSet(fillInfos[0].key, + Assert.assertEquals(new HashSet<>(Arrays.asList("k1", "k2", "k3")), + new HashSet<>(Arrays.asList(fillInfos[0].key, fillInfos[1].key, - fillInfos[2].key)); + fillInfos[2].key))); vq.shutdown(); } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/key/kms/TestLoadBalancingKMSClientProvider.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/key/kms/TestLoadBalancingKMSClientProvider.java index 886297b745c0f..3bc96c3e2fce0 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/key/kms/TestLoadBalancingKMSClientProvider.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/key/kms/TestLoadBalancingKMSClientProvider.java @@ -39,6 +39,8 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivilegedExceptionAction; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.concurrent.TimeUnit; @@ -65,7 +67,6 @@ import org.junit.rules.Timeout; import org.mockito.Mockito; -import org.apache.hadoop.util.Sets; public class TestLoadBalancingKMSClientProvider { @@ -86,8 +87,8 @@ public void testCreation() throws Exception { KMSClientProvider[] providers = ((LoadBalancingKMSClientProvider) kp).getProviders(); assertEquals(1, providers.length); - assertEquals(Sets.newHashSet("http://host1:9600/kms/foo/v1/"), - Sets.newHashSet(providers[0].getKMSUrl())); + assertEquals(new HashSet<>(Collections.singleton("http://host1:9600/kms/foo/v1/")), + new HashSet<>(Collections.singleton(providers[0].getKMSUrl()))); kp = new KMSClientProvider.Factory().createProvider(new URI( "kms://http@host1;host2;host3:9600/kms/foo"), conf); @@ -95,12 +96,12 @@ public void testCreation() throws Exception { providers = ((LoadBalancingKMSClientProvider) kp).getProviders(); assertEquals(3, providers.length); - assertEquals(Sets.newHashSet("http://host1:9600/kms/foo/v1/", + assertEquals(new HashSet<>(Arrays.asList("http://host1:9600/kms/foo/v1/", "http://host2:9600/kms/foo/v1/", - "http://host3:9600/kms/foo/v1/"), - Sets.newHashSet(providers[0].getKMSUrl(), + "http://host3:9600/kms/foo/v1/")), + new HashSet<>(Arrays.asList(providers[0].getKMSUrl(), providers[1].getKMSUrl(), - providers[2].getKMSUrl())); + providers[2].getKMSUrl()))); kp = new KMSClientProvider.Factory().createProvider(new URI( "kms://http@host1;host2;host3:9600/kms/foo"), conf); @@ -108,12 +109,12 @@ public void testCreation() throws Exception { providers = ((LoadBalancingKMSClientProvider) kp).getProviders(); assertEquals(3, providers.length); - assertEquals(Sets.newHashSet("http://host1:9600/kms/foo/v1/", + assertEquals(new HashSet<>(Arrays.asList("http://host1:9600/kms/foo/v1/", "http://host2:9600/kms/foo/v1/", - "http://host3:9600/kms/foo/v1/"), - Sets.newHashSet(providers[0].getKMSUrl(), + "http://host3:9600/kms/foo/v1/")), + new HashSet<>(Arrays.asList(providers[0].getKMSUrl(), providers[1].getKMSUrl(), - providers[2].getKMSUrl())); + providers[2].getKMSUrl()))); } @Test diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/random/TestOsSecureRandom.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/random/TestOsSecureRandom.java index 2ea45231a13a1..6448a9a2fba73 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/random/TestOsSecureRandom.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/random/TestOsSecureRandom.java @@ -22,7 +22,7 @@ import org.apache.commons.lang3.SystemUtils; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.util.Shell.ShellCommandExecutor; + import org.junit.Assume; import org.junit.Test; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestCommandFormat.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestCommandFormat.java index 4b855c4940440..084c6a0aef83d 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestCommandFormat.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestCommandFormat.java @@ -43,9 +43,9 @@ public class TestCommandFormat { @Before public void setUp() { - args = new ArrayList(); - expectedOpts = new HashSet(); - expectedArgs = new ArrayList(); + args = new ArrayList<>(); + expectedOpts = new HashSet<>(); + expectedArgs = new ArrayList<>(); } @Test @@ -205,6 +205,6 @@ private static List listOf(String ... objects) { } private static Set setOf(String ... objects) { - return new HashSet(listOf(objects)); + return new HashSet<>(listOf(objects)); } } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestHarFileSystemBasics.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestHarFileSystemBasics.java index 6415df6310fc2..471d2458f4f46 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestHarFileSystemBasics.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestHarFileSystemBasics.java @@ -246,7 +246,7 @@ public void testListLocatedStatus() throws Exception { // test.har has the following contents: // dir1/1.txt // dir1/2.txt - Set expectedFileNames = new HashSet(); + Set expectedFileNames = new HashSet<>(); expectedFileNames.add("1.txt"); expectedFileNames.add("2.txt"); diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestListFiles.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestListFiles.java index 44308ea6fc5ea..dce3b956d47ef 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestListFiles.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestListFiles.java @@ -152,7 +152,7 @@ public void testDirectory() throws IOException { writeFile(fs, FILE1, FILE_LEN); writeFile(fs, FILE3, FILE_LEN); - Set filesToFind = new HashSet(); + Set filesToFind = new HashSet<>(); filesToFind.add(fs.makeQualified(FILE1)); filesToFind.add(fs.makeQualified(FILE2)); filesToFind.add(fs.makeQualified(FILE3)); diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestTrash.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestTrash.java index 72287782baac6..5b8c10b3fa6f9 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestTrash.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestTrash.java @@ -747,7 +747,7 @@ public void testTrashEmptier() throws Exception { Path myPath = new Path(TEST_DIR, "test/mkdirs"); mkdir(fs, myPath); int fileIndex = 0; - Set checkpoints = new HashSet(); + Set checkpoints = new HashSet<>(); while (true) { // Create a file with a new name Path myFile = new Path(TEST_DIR, "test/mkdirs/myFile" + fileIndex++); diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestVectoredReadUtils.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestVectoredReadUtils.java new file mode 100644 index 0000000000000..5d08b02e113d5 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestVectoredReadUtils.java @@ -0,0 +1,371 @@ +/* + * 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; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.IntFunction; + +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; + +import org.apache.hadoop.fs.impl.CombinedFileRange; +import org.apache.hadoop.test.HadoopTestBase; + +import static org.apache.hadoop.fs.VectoredReadUtils.sortRanges; +import static org.apache.hadoop.test.MoreAsserts.assertFutureCompletedSuccessfully; +import static org.apache.hadoop.test.MoreAsserts.assertFutureFailedExceptionally; + +/** + * Test behavior of {@link VectoredReadUtils}. + */ +public class TestVectoredReadUtils extends HadoopTestBase { + + @Test + public void testSliceTo() { + final int size = 64 * 1024; + ByteBuffer buffer = ByteBuffer.allocate(size); + // fill the buffer with data + IntBuffer intBuffer = buffer.asIntBuffer(); + for(int i=0; i < size / Integer.BYTES; ++i) { + intBuffer.put(i); + } + // ensure we don't make unnecessary slices + ByteBuffer slice = VectoredReadUtils.sliceTo(buffer, 100, + FileRange.createFileRange(100, size)); + Assertions.assertThat(buffer) + .describedAs("Slicing on the same offset shouldn't " + + "create a new buffer") + .isEqualTo(slice); + + // try slicing a range + final int offset = 100; + final int sliceStart = 1024; + final int sliceLength = 16 * 1024; + slice = VectoredReadUtils.sliceTo(buffer, offset, + FileRange.createFileRange(offset + sliceStart, sliceLength)); + // make sure they aren't the same, but use the same backing data + Assertions.assertThat(buffer) + .describedAs("Slicing on new offset should " + + "create a new buffer") + .isNotEqualTo(slice); + Assertions.assertThat(buffer.array()) + .describedAs("Slicing should use the same underlying " + + "data") + .isEqualTo(slice.array()); + // test the contents of the slice + intBuffer = slice.asIntBuffer(); + for(int i=0; i < sliceLength / Integer.BYTES; ++i) { + assertEquals("i = " + i, i + sliceStart / Integer.BYTES, intBuffer.get()); + } + } + + @Test + public void testRounding() { + for(int i=5; i < 10; ++i) { + assertEquals("i = "+ i, 5, VectoredReadUtils.roundDown(i, 5)); + assertEquals("i = "+ i, 10, VectoredReadUtils.roundUp(i+1, 5)); + } + assertEquals("Error while roundDown", 13, VectoredReadUtils.roundDown(13, 1)); + assertEquals("Error while roundUp", 13, VectoredReadUtils.roundUp(13, 1)); + } + + @Test + public void testMerge() { + FileRange base = FileRange.createFileRange(2000, 1000); + CombinedFileRange mergeBase = new CombinedFileRange(2000, 3000, base); + + // test when the gap between is too big + assertFalse("Large gap ranges shouldn't get merged", mergeBase.merge(5000, 6000, + FileRange.createFileRange(5000, 1000), 2000, 4000)); + assertEquals("Number of ranges in merged range shouldn't increase", + 1, mergeBase.getUnderlying().size()); + assertEquals("post merge offset", 2000, mergeBase.getOffset()); + assertEquals("post merge length", 1000, mergeBase.getLength()); + + // test when the total size gets exceeded + assertFalse("Large size ranges shouldn't get merged", mergeBase.merge(5000, 6000, + FileRange.createFileRange(5000, 1000), 2001, 3999)); + assertEquals("Number of ranges in merged range shouldn't increase", + 1, mergeBase.getUnderlying().size()); + assertEquals("post merge offset", 2000, mergeBase.getOffset()); + assertEquals("post merge length", 1000, mergeBase.getLength()); + + // test when the merge works + assertTrue("ranges should get merged ", mergeBase.merge(5000, 6000, + FileRange.createFileRange(5000, 1000), 2001, 4000)); + assertEquals("post merge size", 2, mergeBase.getUnderlying().size()); + assertEquals("post merge offset", 2000, mergeBase.getOffset()); + assertEquals("post merge length", 4000, mergeBase.getLength()); + + // reset the mergeBase and test with a 10:1 reduction + mergeBase = new CombinedFileRange(200, 300, base); + assertEquals(200, mergeBase.getOffset()); + assertEquals(100, mergeBase.getLength()); + assertTrue("ranges should get merged ", mergeBase.merge(500, 600, + FileRange.createFileRange(5000, 1000), 201, 400)); + assertEquals("post merge size", 2, mergeBase.getUnderlying().size()); + assertEquals("post merge offset", 200, mergeBase.getOffset()); + assertEquals("post merge length", 400, mergeBase.getLength()); + } + + @Test + public void testSortAndMerge() { + List input = Arrays.asList( + FileRange.createFileRange(3000, 100), + FileRange.createFileRange(2100, 100), + FileRange.createFileRange(1000, 100) + ); + assertFalse("Ranges are non disjoint", VectoredReadUtils.isOrderedDisjoint(input, 100, 800)); + List outputList = VectoredReadUtils.mergeSortedRanges( + Arrays.asList(sortRanges(input)), 100, 1001, 2500); + Assertions.assertThat(outputList) + .describedAs("merged range size") + .hasSize(1); + CombinedFileRange output = outputList.get(0); + Assertions.assertThat(output.getUnderlying()) + .describedAs("merged range underlying size") + .hasSize(3); + assertEquals("range[1000,3100)", output.toString()); + assertTrue("merged output ranges are disjoint", + VectoredReadUtils.isOrderedDisjoint(outputList, 100, 800)); + + // the minSeek doesn't allow the first two to merge + assertFalse("Ranges are non disjoint", + VectoredReadUtils.isOrderedDisjoint(input, 100, 1000)); + outputList = VectoredReadUtils.mergeSortedRanges(Arrays.asList(sortRanges(input)), + 100, 1000, 2100); + Assertions.assertThat(outputList) + .describedAs("merged range size") + .hasSize(2); + assertEquals("range[1000,1100)", outputList.get(0).toString()); + assertEquals("range[2100,3100)", outputList.get(1).toString()); + assertTrue("merged output ranges are disjoint", + VectoredReadUtils.isOrderedDisjoint(outputList, 100, 1000)); + + // the maxSize doesn't allow the third range to merge + assertFalse("Ranges are non disjoint", + VectoredReadUtils.isOrderedDisjoint(input, 100, 800)); + outputList = VectoredReadUtils.mergeSortedRanges(Arrays.asList(sortRanges(input)), + 100, 1001, 2099); + Assertions.assertThat(outputList) + .describedAs("merged range size") + .hasSize(2); + assertEquals("range[1000,2200)", outputList.get(0).toString()); + assertEquals("range[3000,3100)", outputList.get(1).toString()); + assertTrue("merged output ranges are disjoint", + VectoredReadUtils.isOrderedDisjoint(outputList, 100, 800)); + + // test the round up and round down (the maxSize doesn't allow any merges) + assertFalse("Ranges are non disjoint", + VectoredReadUtils.isOrderedDisjoint(input, 16, 700)); + outputList = VectoredReadUtils.mergeSortedRanges(Arrays.asList(sortRanges(input)), + 16, 1001, 100); + Assertions.assertThat(outputList) + .describedAs("merged range size") + .hasSize(3); + assertEquals("range[992,1104)", outputList.get(0).toString()); + assertEquals("range[2096,2208)", outputList.get(1).toString()); + assertEquals("range[2992,3104)", outputList.get(2).toString()); + assertTrue("merged output ranges are disjoint", + VectoredReadUtils.isOrderedDisjoint(outputList, 16, 700)); + } + + @Test + public void testSortAndMergeMoreCases() throws Exception { + List input = Arrays.asList( + FileRange.createFileRange(3000, 110), + FileRange.createFileRange(3000, 100), + FileRange.createFileRange(2100, 100), + FileRange.createFileRange(1000, 100) + ); + assertFalse("Ranges are non disjoint", + VectoredReadUtils.isOrderedDisjoint(input, 100, 800)); + List outputList = VectoredReadUtils.mergeSortedRanges( + Arrays.asList(sortRanges(input)), 1, 1001, 2500); + Assertions.assertThat(outputList) + .describedAs("merged range size") + .hasSize(1); + CombinedFileRange output = outputList.get(0); + Assertions.assertThat(output.getUnderlying()) + .describedAs("merged range underlying size") + .hasSize(4); + assertEquals("range[1000,3110)", output.toString()); + assertTrue("merged output ranges are disjoint", + VectoredReadUtils.isOrderedDisjoint(outputList, 1, 800)); + + outputList = VectoredReadUtils.mergeSortedRanges( + Arrays.asList(sortRanges(input)), 100, 1001, 2500); + Assertions.assertThat(outputList) + .describedAs("merged range size") + .hasSize(1); + output = outputList.get(0); + Assertions.assertThat(output.getUnderlying()) + .describedAs("merged range underlying size") + .hasSize(4); + assertEquals("range[1000,3200)", output.toString()); + assertTrue("merged output ranges are disjoint", + VectoredReadUtils.isOrderedDisjoint(outputList, 1, 800)); + + } + + @Test + public void testMaxSizeZeroDisablesMering() throws Exception { + List randomRanges = Arrays.asList( + FileRange.createFileRange(3000, 110), + FileRange.createFileRange(3000, 100), + FileRange.createFileRange(2100, 100) + ); + assertEqualRangeCountsAfterMerging(randomRanges, 1, 1, 0); + assertEqualRangeCountsAfterMerging(randomRanges, 1, 0, 0); + assertEqualRangeCountsAfterMerging(randomRanges, 1, 100, 0); + } + + private void assertEqualRangeCountsAfterMerging(List inputRanges, + int chunkSize, + int minimumSeek, + int maxSize) { + List combinedFileRanges = VectoredReadUtils + .mergeSortedRanges(inputRanges, chunkSize, minimumSeek, maxSize); + Assertions.assertThat(combinedFileRanges) + .describedAs("Mismatch in number of ranges post merging") + .hasSize(inputRanges.size()); + } + + interface Stream extends PositionedReadable, ByteBufferPositionedReadable { + // nothing + } + + static void fillBuffer(ByteBuffer buffer) { + byte b = 0; + while (buffer.remaining() > 0) { + buffer.put(b++); + } + } + + @Test + public void testReadRangeFromByteBufferPositionedReadable() throws Exception { + Stream stream = Mockito.mock(Stream.class); + Mockito.doAnswer(invocation -> { + fillBuffer(invocation.getArgument(1)); + return null; + }).when(stream).readFully(ArgumentMatchers.anyLong(), + ArgumentMatchers.any(ByteBuffer.class)); + CompletableFuture result = + VectoredReadUtils.readRangeFrom(stream, FileRange.createFileRange(1000, 100), + ByteBuffer::allocate); + assertFutureCompletedSuccessfully(result); + ByteBuffer buffer = result.get(); + assertEquals("Size of result buffer", 100, buffer.remaining()); + byte b = 0; + while (buffer.remaining() > 0) { + assertEquals("remain = " + buffer.remaining(), b++, buffer.get()); + } + + // test an IOException + Mockito.reset(stream); + Mockito.doThrow(new IOException("foo")) + .when(stream).readFully(ArgumentMatchers.anyLong(), + ArgumentMatchers.any(ByteBuffer.class)); + result = + VectoredReadUtils.readRangeFrom(stream, FileRange.createFileRange(1000, 100), + ByteBuffer::allocate); + assertFutureFailedExceptionally(result); + } + + static void runReadRangeFromPositionedReadable(IntFunction allocate) + throws Exception { + PositionedReadable stream = Mockito.mock(PositionedReadable.class); + Mockito.doAnswer(invocation -> { + byte b=0; + byte[] buffer = invocation.getArgument(1); + for(int i=0; i < buffer.length; ++i) { + buffer[i] = b++; + } + return null; + }).when(stream).readFully(ArgumentMatchers.anyLong(), + ArgumentMatchers.any(), ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt()); + CompletableFuture result = + VectoredReadUtils.readRangeFrom(stream, FileRange.createFileRange(1000, 100), + allocate); + assertFutureCompletedSuccessfully(result); + ByteBuffer buffer = result.get(); + assertEquals("Size of result buffer", 100, buffer.remaining()); + byte b = 0; + while (buffer.remaining() > 0) { + assertEquals("remain = " + buffer.remaining(), b++, buffer.get()); + } + + // test an IOException + Mockito.reset(stream); + Mockito.doThrow(new IOException("foo")) + .when(stream).readFully(ArgumentMatchers.anyLong(), + ArgumentMatchers.any(), ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt()); + result = + VectoredReadUtils.readRangeFrom(stream, FileRange.createFileRange(1000, 100), + ByteBuffer::allocate); + assertFutureFailedExceptionally(result); + } + + @Test + public void testReadRangeArray() throws Exception { + runReadRangeFromPositionedReadable(ByteBuffer::allocate); + } + + @Test + public void testReadRangeDirect() throws Exception { + runReadRangeFromPositionedReadable(ByteBuffer::allocateDirect); + } + + static void validateBuffer(String message, ByteBuffer buffer, int start) { + byte expected = (byte) start; + while (buffer.remaining() > 0) { + assertEquals(message + " remain: " + buffer.remaining(), expected++, + buffer.get()); + } + } + + @Test + public void testReadVectored() throws Exception { + List input = Arrays.asList(FileRange.createFileRange(0, 100), + FileRange.createFileRange(100_000, 100), + FileRange.createFileRange(200_000, 100)); + Stream stream = Mockito.mock(Stream.class); + Mockito.doAnswer(invocation -> { + fillBuffer(invocation.getArgument(1)); + return null; + }).when(stream).readFully(ArgumentMatchers.anyLong(), + ArgumentMatchers.any(ByteBuffer.class)); + // should not merge the ranges + VectoredReadUtils.readVectored(stream, input, ByteBuffer::allocate); + Mockito.verify(stream, Mockito.times(3)) + .readFully(ArgumentMatchers.anyLong(), ArgumentMatchers.any(ByteBuffer.class)); + for(int b=0; b < input.size(); ++b) { + validateBuffer("buffer " + b, input.get(b).getData().get(), 0); + } + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractDeleteTest.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractDeleteTest.java index 08df1d4d883a6..605ea45649a16 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractDeleteTest.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractDeleteTest.java @@ -19,7 +19,7 @@ package org.apache.hadoop.fs.contract; import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.FileSystem; + import org.junit.Test; import java.io.IOException; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractVectoredReadTest.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractVectoredReadTest.java new file mode 100644 index 0000000000000..77bcc496ff4a2 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractVectoredReadTest.java @@ -0,0 +1,406 @@ +/* + * 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.contract; + +import java.io.EOFException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.IntFunction; + +import org.assertj.core.api.Assertions; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FileRange; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.StreamCapabilities; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.impl.FutureIOSupport; +import org.apache.hadoop.io.WeakReferencedElasticByteBufferPool; +import org.apache.hadoop.test.LambdaTestUtils; + +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertCapabilities; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertDatasetEquals; +import static org.apache.hadoop.fs.contract.ContractTestUtils.createFile; +import static org.apache.hadoop.fs.contract.ContractTestUtils.returnBuffersToPoolPostRead; +import static org.apache.hadoop.fs.contract.ContractTestUtils.validateVectoredReadResult; + +@RunWith(Parameterized.class) +public abstract class AbstractContractVectoredReadTest extends AbstractFSContractTestBase { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractContractVectoredReadTest.class); + + public static final int DATASET_LEN = 64 * 1024; + protected static final byte[] DATASET = ContractTestUtils.dataset(DATASET_LEN, 'a', 32); + protected static final String VECTORED_READ_FILE_NAME = "vectored_file.txt"; + + private final IntFunction allocate; + + private final WeakReferencedElasticByteBufferPool pool = + new WeakReferencedElasticByteBufferPool(); + + private final String bufferType; + + @Parameterized.Parameters(name = "Buffer type : {0}") + public static List params() { + return Arrays.asList("direct", "array"); + } + + public AbstractContractVectoredReadTest(String bufferType) { + this.bufferType = bufferType; + this.allocate = value -> { + boolean isDirect = !"array".equals(bufferType); + return pool.getBuffer(isDirect, value); + }; + } + + public IntFunction getAllocate() { + return allocate; + } + + @Override + public void setup() throws Exception { + super.setup(); + Path path = path(VECTORED_READ_FILE_NAME); + FileSystem fs = getFileSystem(); + createFile(fs, path, true, DATASET); + } + + @Override + public void teardown() throws Exception { + super.teardown(); + pool.release(); + } + + @Test + public void testVectoredReadCapability() throws Exception { + FileSystem fs = getFileSystem(); + String[] vectoredReadCapability = new String[]{StreamCapabilities.VECTOREDIO}; + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))) { + assertCapabilities(in, vectoredReadCapability, null); + } + } + + @Test + public void testVectoredReadMultipleRanges() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + FileRange fileRange = FileRange.createFileRange(i * 100, 100); + fileRanges.add(fileRange); + } + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))) { + in.readVectored(fileRanges, allocate); + CompletableFuture[] completableFutures = new CompletableFuture[fileRanges.size()]; + int i = 0; + for (FileRange res : fileRanges) { + completableFutures[i++] = res.getData(); + } + CompletableFuture combinedFuture = CompletableFuture.allOf(completableFutures); + combinedFuture.get(); + + validateVectoredReadResult(fileRanges, DATASET); + returnBuffersToPoolPostRead(fileRanges, pool); + } + } + + @Test + public void testVectoredReadAndReadFully() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(100, 100)); + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))) { + in.readVectored(fileRanges, allocate); + byte[] readFullRes = new byte[100]; + in.readFully(100, readFullRes); + ByteBuffer vecRes = FutureIOSupport.awaitFuture(fileRanges.get(0).getData()); + Assertions.assertThat(vecRes) + .describedAs("Result from vectored read and readFully must match") + .isEqualByComparingTo(ByteBuffer.wrap(readFullRes)); + returnBuffersToPoolPostRead(fileRanges, pool); + } + } + + /** + * As the minimum seek value is 4*1024,none of the below ranges + * will get merged. + */ + @Test + public void testDisjointRanges() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(0, 100)); + fileRanges.add(FileRange.createFileRange(4_000 + 101, 100)); + fileRanges.add(FileRange.createFileRange(16_000 + 101, 100)); + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))) { + in.readVectored(fileRanges, allocate); + validateVectoredReadResult(fileRanges, DATASET); + returnBuffersToPoolPostRead(fileRanges, pool); + } + } + + /** + * As the minimum seek value is 4*1024, all the below ranges + * will get merged into one. + */ + @Test + public void testAllRangesMergedIntoOne() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(0, 100)); + fileRanges.add(FileRange.createFileRange(4_000 - 101, 100)); + fileRanges.add(FileRange.createFileRange(8_000 - 101, 100)); + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))) { + in.readVectored(fileRanges, allocate); + validateVectoredReadResult(fileRanges, DATASET); + returnBuffersToPoolPostRead(fileRanges, pool); + } + } + + /** + * As the minimum seek value is 4*1024, the first three ranges will be + * merged into and other two will remain as it is. + */ + @Test + public void testSomeRangesMergedSomeUnmerged() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(8 * 1024, 100)); + fileRanges.add(FileRange.createFileRange(14 * 1024, 100)); + fileRanges.add(FileRange.createFileRange(10 * 1024, 100)); + fileRanges.add(FileRange.createFileRange(2 * 1024 - 101, 100)); + fileRanges.add(FileRange.createFileRange(40 * 1024, 1024)); + FileStatus fileStatus = fs.getFileStatus(path(VECTORED_READ_FILE_NAME)); + CompletableFuture builder = + fs.openFile(path(VECTORED_READ_FILE_NAME)) + .withFileStatus(fileStatus) + .build(); + try (FSDataInputStream in = builder.get()) { + in.readVectored(fileRanges, allocate); + validateVectoredReadResult(fileRanges, DATASET); + returnBuffersToPoolPostRead(fileRanges, pool); + } + } + + @Test + public void testOverlappingRanges() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = getSampleOverlappingRanges(); + FileStatus fileStatus = fs.getFileStatus(path(VECTORED_READ_FILE_NAME)); + CompletableFuture builder = + fs.openFile(path(VECTORED_READ_FILE_NAME)) + .withFileStatus(fileStatus) + .build(); + try (FSDataInputStream in = builder.get()) { + in.readVectored(fileRanges, allocate); + validateVectoredReadResult(fileRanges, DATASET); + returnBuffersToPoolPostRead(fileRanges, pool); + } + } + + @Test + public void testSameRanges() throws Exception { + // Same ranges are special case of overlapping only. + FileSystem fs = getFileSystem(); + List fileRanges = getSampleSameRanges(); + CompletableFuture builder = + fs.openFile(path(VECTORED_READ_FILE_NAME)) + .build(); + try (FSDataInputStream in = builder.get()) { + in.readVectored(fileRanges, allocate); + validateVectoredReadResult(fileRanges, DATASET); + returnBuffersToPoolPostRead(fileRanges, pool); + } + } + + @Test + public void testSomeRandomNonOverlappingRanges() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(500, 100)); + fileRanges.add(FileRange.createFileRange(1000, 200)); + fileRanges.add(FileRange.createFileRange(50, 10)); + fileRanges.add(FileRange.createFileRange(10, 5)); + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))) { + in.readVectored(fileRanges, allocate); + validateVectoredReadResult(fileRanges, DATASET); + returnBuffersToPoolPostRead(fileRanges, pool); + } + } + + @Test + public void testConsecutiveRanges() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(500, 100)); + fileRanges.add(FileRange.createFileRange(600, 200)); + fileRanges.add(FileRange.createFileRange(800, 100)); + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))) { + in.readVectored(fileRanges, allocate); + validateVectoredReadResult(fileRanges, DATASET); + returnBuffersToPoolPostRead(fileRanges, pool); + } + } + + @Test + public void testEOFRanges() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(DATASET_LEN, 100)); + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))) { + in.readVectored(fileRanges, allocate); + for (FileRange res : fileRanges) { + CompletableFuture data = res.getData(); + try { + ByteBuffer buffer = data.get(); + // Shouldn't reach here. + Assert.fail("EOFException must be thrown while reading EOF"); + } catch (ExecutionException ex) { + // ignore as expected. + } catch (Exception ex) { + LOG.error("Exception while running vectored read ", ex); + Assert.fail("Exception while running vectored read " + ex); + } + } + } + } + + @Test + public void testNegativeLengthRange() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(0, -50)); + verifyExceptionalVectoredRead(fs, fileRanges, IllegalArgumentException.class); + } + + @Test + public void testNegativeOffsetRange() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(-1, 50)); + verifyExceptionalVectoredRead(fs, fileRanges, EOFException.class); + } + + @Test + public void testNormalReadAfterVectoredRead() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = createSampleNonOverlappingRanges(); + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))) { + in.readVectored(fileRanges, allocate); + // read starting 200 bytes + byte[] res = new byte[200]; + in.read(res, 0, 200); + ByteBuffer buffer = ByteBuffer.wrap(res); + assertDatasetEquals(0, "normal_read", buffer, 200, DATASET); + Assertions.assertThat(in.getPos()) + .describedAs("Vectored read shouldn't change file pointer.") + .isEqualTo(200); + validateVectoredReadResult(fileRanges, DATASET); + returnBuffersToPoolPostRead(fileRanges, pool); + } + } + + @Test + public void testVectoredReadAfterNormalRead() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = createSampleNonOverlappingRanges(); + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))) { + // read starting 200 bytes + byte[] res = new byte[200]; + in.read(res, 0, 200); + ByteBuffer buffer = ByteBuffer.wrap(res); + assertDatasetEquals(0, "normal_read", buffer, 200, DATASET); + Assertions.assertThat(in.getPos()) + .describedAs("Vectored read shouldn't change file pointer.") + .isEqualTo(200); + in.readVectored(fileRanges, allocate); + validateVectoredReadResult(fileRanges, DATASET); + returnBuffersToPoolPostRead(fileRanges, pool); + } + } + + @Test + public void testMultipleVectoredReads() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges1 = createSampleNonOverlappingRanges(); + List fileRanges2 = createSampleNonOverlappingRanges(); + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))) { + in.readVectored(fileRanges1, allocate); + in.readVectored(fileRanges2, allocate); + validateVectoredReadResult(fileRanges2, DATASET); + validateVectoredReadResult(fileRanges1, DATASET); + returnBuffersToPoolPostRead(fileRanges1, pool); + returnBuffersToPoolPostRead(fileRanges2, pool); + } + } + + protected List createSampleNonOverlappingRanges() { + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(0, 100)); + fileRanges.add(FileRange.createFileRange(110, 50)); + return fileRanges; + } + + protected List getSampleSameRanges() { + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(8_000, 1000)); + fileRanges.add(FileRange.createFileRange(8_000, 1000)); + fileRanges.add(FileRange.createFileRange(8_000, 1000)); + return fileRanges; + } + + protected List getSampleOverlappingRanges() { + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(100, 500)); + fileRanges.add(FileRange.createFileRange(400, 500)); + return fileRanges; + } + + /** + * Validate that exceptions must be thrown during a vectored + * read operation with specific input ranges. + * @param fs FileSystem instance. + * @param fileRanges input file ranges. + * @param clazz type of exception expected. + * @throws Exception any other IOE. + */ + protected void verifyExceptionalVectoredRead( + FileSystem fs, + List fileRanges, + Class clazz) throws Exception { + + CompletableFuture builder = + fs.openFile(path(VECTORED_READ_FILE_NAME)) + .build(); + try (FSDataInputStream in = builder.get()) { + LambdaTestUtils.intercept(clazz, + () -> in.readVectored(fileRanges, allocate)); + } + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ContractTestUtils.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ContractTestUtils.java index eb56d957d9a1a..b61abddd43426 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ContractTestUtils.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ContractTestUtils.java @@ -21,6 +21,7 @@ import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileContext; +import org.apache.hadoop.fs.FileRange; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.LocatedFileStatus; @@ -28,7 +29,11 @@ import org.apache.hadoop.fs.PathCapabilities; import org.apache.hadoop.fs.RemoteIterator; import org.apache.hadoop.fs.StreamCapabilities; +import org.apache.hadoop.io.ByteBufferPool; import org.apache.hadoop.io.IOUtils; +import org.apache.hadoop.util.functional.RemoteIterators; +import org.apache.hadoop.util.functional.FutureIO; + import org.junit.Assert; import org.junit.AssumptionViolatedException; import org.slf4j.Logger; @@ -39,6 +44,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -49,6 +55,9 @@ import java.util.Properties; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.IO_FILE_BUFFER_SIZE_DEFAULT; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.IO_FILE_BUFFER_SIZE_KEY; @@ -68,6 +77,11 @@ public class ContractTestUtils extends Assert { public static final String IO_CHUNK_MODULUS_SIZE = "io.chunk.modulus.size"; public static final int DEFAULT_IO_CHUNK_MODULUS_SIZE = 128; + /** + * Timeout in seconds for vectored read operation in tests : {@value}. + */ + public static final int VECTORED_READ_OPERATION_TEST_TIMEOUT_SECONDS = 5 * 60; + /** * Assert that a property in the property set matches the expected value. * @param props property set @@ -1095,6 +1109,78 @@ public static void validateFileContent(byte[] concat, byte[][] bytes) { mismatch); } + /** + * Utility to validate vectored read results. + * @param fileRanges input ranges. + * @param originalData original data. + * @throws IOException any ioe. + */ + public static void validateVectoredReadResult(List fileRanges, + byte[] originalData) + throws IOException, TimeoutException { + CompletableFuture[] completableFutures = new CompletableFuture[fileRanges.size()]; + int i = 0; + for (FileRange res : fileRanges) { + completableFutures[i++] = res.getData(); + } + CompletableFuture combinedFuture = CompletableFuture.allOf(completableFutures); + FutureIO.awaitFuture(combinedFuture, + VECTORED_READ_OPERATION_TEST_TIMEOUT_SECONDS, + TimeUnit.SECONDS); + + for (FileRange res : fileRanges) { + CompletableFuture data = res.getData(); + ByteBuffer buffer = FutureIO.awaitFuture(data, + VECTORED_READ_OPERATION_TEST_TIMEOUT_SECONDS, + TimeUnit.SECONDS); + assertDatasetEquals((int) res.getOffset(), "vecRead", + buffer, res.getLength(), originalData); + } + } + + /** + * Utility to return buffers back to the pool once all + * data has been read for each file range. + * @param fileRanges list of file range. + * @param pool buffer pool. + * @throws IOException any IOE + * @throws TimeoutException ideally this should never occur. + */ + public static void returnBuffersToPoolPostRead(List fileRanges, + ByteBufferPool pool) + throws IOException, TimeoutException { + for (FileRange range : fileRanges) { + ByteBuffer buffer = FutureIO.awaitFuture(range.getData(), + VECTORED_READ_OPERATION_TEST_TIMEOUT_SECONDS, + TimeUnit.SECONDS); + pool.putBuffer(buffer); + } + } + + + /** + * Assert that the data read matches the dataset at the given offset. + * This helps verify that the seek process is moving the read pointer + * to the correct location in the file. + * @param readOffset the offset in the file where the read began. + * @param operation operation name for the assertion. + * @param data data read in. + * @param length length of data to check. + * @param originalData original data. + */ + public static void assertDatasetEquals( + final int readOffset, + final String operation, + final ByteBuffer data, + int length, byte[] originalData) { + for (int i = 0; i < length; i++) { + int o = readOffset + i; + assertEquals(operation + " with read offset " + readOffset + + ": data[" + i + "] != DATASET[" + o + "]", + originalData[o], data.get()); + } + } + /** * Receives test data from the given input file and checks the size of the * data as well as the pattern inside the received data. @@ -1446,11 +1532,7 @@ public static TreeScanResults treeWalk(FileSystem fs, Path path) */ public static List toList( RemoteIterator iterator) throws IOException { - ArrayList list = new ArrayList<>(); - while (iterator.hasNext()) { - list.add(iterator.next()); - } - return list; + return RemoteIterators.toList(iterator); } /** @@ -1464,11 +1546,7 @@ public static List toList( */ public static List iteratorToList( RemoteIterator iterator) throws IOException { - List list = new ArrayList<>(); - while (iterator.hasNext()) { - list.add(iterator.next()); - } - return list; + return RemoteIterators.toList(iterator); } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ftp/FTPContract.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ftp/FTPContract.java index 1efd7fc4e95d4..62648ec58bcc7 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ftp/FTPContract.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ftp/FTPContract.java @@ -22,7 +22,6 @@ import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.contract.AbstractBondedFSContract; -import org.junit.Assert; import java.net.URI; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/localfs/TestLocalFSContractVectoredRead.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/localfs/TestLocalFSContractVectoredRead.java new file mode 100644 index 0000000000000..5d6ca3f8f0c90 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/localfs/TestLocalFSContractVectoredRead.java @@ -0,0 +1,86 @@ +/* + * 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.contract.localfs; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.ChecksumException; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileRange; +import org.apache.hadoop.fs.LocalFileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.contract.AbstractContractVectoredReadTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; +import org.apache.hadoop.fs.contract.ContractTestUtils; + +import static org.apache.hadoop.fs.contract.ContractTestUtils.validateVectoredReadResult; +import static org.apache.hadoop.test.LambdaTestUtils.intercept; + +public class TestLocalFSContractVectoredRead extends AbstractContractVectoredReadTest { + + public TestLocalFSContractVectoredRead(String bufferType) { + super(bufferType); + } + + @Override + protected AbstractFSContract createContract(Configuration conf) { + return new LocalFSContract(conf); + } + + @Test + public void testChecksumValidationDuringVectoredRead() throws Exception { + Path testPath = path("big_range_checksum"); + LocalFileSystem localFs = (LocalFileSystem) getFileSystem(); + final byte[] datasetCorrect = ContractTestUtils.dataset(DATASET_LEN, 'a', 32); + try (FSDataOutputStream out = localFs.create(testPath, true)){ + out.write(datasetCorrect); + } + Path checksumPath = localFs.getChecksumFile(testPath); + Assertions.assertThat(localFs.exists(checksumPath)) + .describedAs("Checksum file should be present") + .isTrue(); + CompletableFuture fis = localFs.openFile(testPath).build(); + List someRandomRanges = new ArrayList<>(); + someRandomRanges.add(FileRange.createFileRange(10, 1024)); + someRandomRanges.add(FileRange.createFileRange(1025, 1024)); + try (FSDataInputStream in = fis.get()){ + in.readVectored(someRandomRanges, getAllocate()); + validateVectoredReadResult(someRandomRanges, datasetCorrect); + } + final byte[] datasetCorrupted = ContractTestUtils.dataset(DATASET_LEN, 'a', 64); + try (FSDataOutputStream out = localFs.getRaw().create(testPath, true)){ + out.write(datasetCorrupted); + } + CompletableFuture fisN = localFs.openFile(testPath).build(); + try (FSDataInputStream in = fisN.get()){ + in.readVectored(someRandomRanges, getAllocate()); + // Expect checksum exception when data is updated directly through + // raw local fs instance. + intercept(ChecksumException.class, + () -> validateVectoredReadResult(someRandomRanges, datasetCorrupted)); + } + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/rawlocal/TestRawLocalContractUnderlyingFileBehavior.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/rawlocal/TestRawLocalContractUnderlyingFileBehavior.java index 2cb5414caa4c7..6eb24985f4ff3 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/rawlocal/TestRawLocalContractUnderlyingFileBehavior.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/rawlocal/TestRawLocalContractUnderlyingFileBehavior.java @@ -19,7 +19,7 @@ package org.apache.hadoop.fs.contract.rawlocal; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.contract.ContractTestUtils; + import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/rawlocal/TestRawLocalContractVectoredRead.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/rawlocal/TestRawLocalContractVectoredRead.java new file mode 100644 index 0000000000000..cbb31ffe27a59 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/rawlocal/TestRawLocalContractVectoredRead.java @@ -0,0 +1,35 @@ +/* + * 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.contract.rawlocal; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractVectoredReadTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; + +public class TestRawLocalContractVectoredRead extends AbstractContractVectoredReadTest { + + public TestRawLocalContractVectoredRead(String bufferType) { + super(bufferType); + } + + @Override + protected AbstractFSContract createContract(Configuration conf) { + return new RawlocalFSContract(conf); + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/rawlocal/TestRawlocalContractPathHandle.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/rawlocal/TestRawlocalContractPathHandle.java index 3c088d278e536..c34269708ddcb 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/rawlocal/TestRawlocalContractPathHandle.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/rawlocal/TestRawlocalContractPathHandle.java @@ -21,7 +21,6 @@ import org.apache.hadoop.fs.Options; import org.apache.hadoop.fs.contract.AbstractContractPathHandleTest; import org.apache.hadoop.fs.contract.AbstractFSContract; -import org.apache.hadoop.fs.contract.localfs.LocalFSContract; import org.apache.hadoop.fs.contract.rawlocal.RawlocalFSContract; public class TestRawlocalContractPathHandle diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/shell/TestPathData.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/shell/TestPathData.java index 921fc18131a5c..130ee5edee768 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/shell/TestPathData.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/shell/TestPathData.java @@ -24,7 +24,6 @@ import java.io.File; import java.io.IOException; -import java.net.URI; import java.util.Arrays; import org.apache.hadoop.conf.Configuration; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/shell/TestTextCommand.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/shell/TestTextCommand.java index c99b97e6e4021..4eb1d433bee45 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/shell/TestTextCommand.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/shell/TestTextCommand.java @@ -25,15 +25,12 @@ import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; -import java.lang.reflect.Method; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Paths; import org.apache.commons.io.IOUtils; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.Path; import org.apache.hadoop.test.GenericTestUtils; import org.junit.Test; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/TestViewFileSystemWithAuthorityLocalFileSystem.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/TestViewFileSystemWithAuthorityLocalFileSystem.java index 9223338f34bf5..f2452279bc7fc 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/TestViewFileSystemWithAuthorityLocalFileSystem.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/TestViewFileSystemWithAuthorityLocalFileSystem.java @@ -22,7 +22,6 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.FileSystemTestHelper; import org.apache.hadoop.fs.FsConstants; import org.apache.hadoop.fs.Path; import static org.apache.hadoop.fs.FileSystem.TRASH_PREFIX; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFileSystemTestSetup.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFileSystemTestSetup.java index 866c03ecda9d2..5713f532be7e8 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFileSystemTestSetup.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFileSystemTestSetup.java @@ -20,7 +20,6 @@ import java.net.URI; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileContext; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FileSystemTestHelper; import org.apache.hadoop.fs.FsConstants; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServerLifecycle.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServerLifecycle.java index 757ea0c05e7c0..4ae1190abd5af 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServerLifecycle.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServerLifecycle.java @@ -17,8 +17,6 @@ */ package org.apache.hadoop.http; -import org.apache.hadoop.test.GenericTestUtils; -import org.apache.log4j.Logger; import org.junit.Test; public class TestHttpServerLifecycle extends HttpServerFunctionalTest { diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/TestIOUtils.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/TestIOUtils.java index fca72d9c65a6a..51f207f97ad29 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/TestIOUtils.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/TestIOUtils.java @@ -275,7 +275,7 @@ public void testListDirectory() throws IOException { File dir = new File("testListDirectory"); Files.createDirectory(dir.toPath()); try { - Set entries = new HashSet(); + Set entries = new HashSet<>(); entries.add("entry1"); entries.add("entry2"); entries.add("entry3"); diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/TestMoreWeakReferencedElasticByteBufferPool.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/TestMoreWeakReferencedElasticByteBufferPool.java new file mode 100644 index 0000000000000..6ca380ef0e46b --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/TestMoreWeakReferencedElasticByteBufferPool.java @@ -0,0 +1,97 @@ +/** + * 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.io; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import org.apache.hadoop.test.HadoopTestBase; + +import static org.apache.hadoop.test.LambdaTestUtils.intercept; + +/** + * Non parameterized tests for {@code WeakReferencedElasticByteBufferPool}. + */ +public class TestMoreWeakReferencedElasticByteBufferPool + extends HadoopTestBase { + + @Test + public void testMixedBuffersInPool() { + WeakReferencedElasticByteBufferPool pool = new WeakReferencedElasticByteBufferPool(); + ByteBuffer buffer1 = pool.getBuffer(true, 5); + ByteBuffer buffer2 = pool.getBuffer(true, 10); + ByteBuffer buffer3 = pool.getBuffer(false, 5); + ByteBuffer buffer4 = pool.getBuffer(false, 10); + ByteBuffer buffer5 = pool.getBuffer(true, 15); + + assertBufferCounts(pool, 0, 0); + pool.putBuffer(buffer1); + pool.putBuffer(buffer2); + assertBufferCounts(pool, 2, 0); + pool.putBuffer(buffer3); + assertBufferCounts(pool, 2, 1); + pool.putBuffer(buffer5); + assertBufferCounts(pool, 3, 1); + pool.putBuffer(buffer4); + assertBufferCounts(pool, 3, 2); + pool.release(); + assertBufferCounts(pool, 0, 0); + + } + + @Test + public void testUnexpectedBufferSizes() throws Exception { + WeakReferencedElasticByteBufferPool pool = new WeakReferencedElasticByteBufferPool(); + ByteBuffer buffer1 = pool.getBuffer(true, 0); + + // try writing a random byte in a 0 length buffer. + // Expected exception as buffer requested is of size 0. + intercept(BufferOverflowException.class, + () -> buffer1.put(new byte[1])); + + // Expected IllegalArgumentException as negative length buffer is requested. + intercept(IllegalArgumentException.class, + () -> pool.getBuffer(true, -5)); + + // test returning null buffer to the pool. + intercept(NullPointerException.class, + () -> pool.putBuffer(null)); + } + + /** + * Utility method to assert counts of direct and heap buffers in + * the given buffer pool. + * @param pool buffer pool. + * @param numDirectBuffersExpected expected number of direct buffers. + * @param numHeapBuffersExpected expected number of heap buffers. + */ + private void assertBufferCounts(WeakReferencedElasticByteBufferPool pool, + int numDirectBuffersExpected, + int numHeapBuffersExpected) { + Assertions.assertThat(pool.getCurrentBuffersCount(true)) + .describedAs("Number of direct buffers in pool") + .isEqualTo(numDirectBuffersExpected); + Assertions.assertThat(pool.getCurrentBuffersCount(false)) + .describedAs("Number of heap buffers in pool") + .isEqualTo(numHeapBuffersExpected); + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/TestWeakReferencedElasticByteBufferPool.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/TestWeakReferencedElasticByteBufferPool.java new file mode 100644 index 0000000000000..1434010ffa652 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/TestWeakReferencedElasticByteBufferPool.java @@ -0,0 +1,232 @@ +/** + * 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.io; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.hadoop.test.HadoopTestBase; + +/** + * Unit tests for {@code WeakReferencedElasticByteBufferPool}. + */ +@RunWith(Parameterized.class) +public class TestWeakReferencedElasticByteBufferPool + extends HadoopTestBase { + + private final boolean isDirect; + + private final String type; + + @Parameterized.Parameters(name = "Buffer type : {0}") + public static List params() { + return Arrays.asList("direct", "array"); + } + + public TestWeakReferencedElasticByteBufferPool(String type) { + this.type = type; + this.isDirect = !"array".equals(type); + } + + @Test + public void testGetAndPutBasic() { + WeakReferencedElasticByteBufferPool pool = new WeakReferencedElasticByteBufferPool(); + int bufferSize = 5; + ByteBuffer buffer = pool.getBuffer(isDirect, bufferSize); + Assertions.assertThat(buffer.isDirect()) + .describedAs("Buffered returned should be of correct type {}", type) + .isEqualTo(isDirect); + Assertions.assertThat(buffer.capacity()) + .describedAs("Initial capacity of returned buffer from pool") + .isEqualTo(bufferSize); + Assertions.assertThat(buffer.position()) + .describedAs("Initial position of returned buffer from pool") + .isEqualTo(0); + + byte[] arr = createByteArray(bufferSize); + buffer.put(arr, 0, arr.length); + buffer.flip(); + validateBufferContent(buffer, arr); + Assertions.assertThat(buffer.position()) + .describedAs("Buffer's position after filling bytes in it") + .isEqualTo(bufferSize); + // releasing buffer to the pool. + pool.putBuffer(buffer); + Assertions.assertThat(buffer.position()) + .describedAs("Position should be reset to 0 after returning buffer to the pool") + .isEqualTo(0); + + } + + @Test + public void testPoolingWithDifferentSizes() { + WeakReferencedElasticByteBufferPool pool = new WeakReferencedElasticByteBufferPool(); + ByteBuffer buffer = pool.getBuffer(isDirect, 5); + ByteBuffer buffer1 = pool.getBuffer(isDirect, 10); + ByteBuffer buffer2 = pool.getBuffer(isDirect, 15); + + Assertions.assertThat(pool.getCurrentBuffersCount(isDirect)) + .describedAs("Number of buffers in the pool") + .isEqualTo(0); + + pool.putBuffer(buffer1); + pool.putBuffer(buffer2); + Assertions.assertThat(pool.getCurrentBuffersCount(isDirect)) + .describedAs("Number of buffers in the pool") + .isEqualTo(2); + ByteBuffer buffer3 = pool.getBuffer(isDirect, 12); + Assertions.assertThat(buffer3.capacity()) + .describedAs("Pooled buffer should have older capacity") + .isEqualTo(15); + Assertions.assertThat(pool.getCurrentBuffersCount(isDirect)) + .describedAs("Number of buffers in the pool") + .isEqualTo(1); + pool.putBuffer(buffer); + ByteBuffer buffer4 = pool.getBuffer(isDirect, 6); + Assertions.assertThat(buffer4.capacity()) + .describedAs("Pooled buffer should have older capacity") + .isEqualTo(10); + Assertions.assertThat(pool.getCurrentBuffersCount(isDirect)) + .describedAs("Number of buffers in the pool") + .isEqualTo(1); + + pool.release(); + Assertions.assertThat(pool.getCurrentBuffersCount(isDirect)) + .describedAs("Number of buffers in the pool post release") + .isEqualTo(0); + } + + @Test + public void testPoolingWithDifferentInsertionTime() { + WeakReferencedElasticByteBufferPool pool = new WeakReferencedElasticByteBufferPool(); + ByteBuffer buffer = pool.getBuffer(isDirect, 10); + ByteBuffer buffer1 = pool.getBuffer(isDirect, 10); + ByteBuffer buffer2 = pool.getBuffer(isDirect, 10); + + Assertions.assertThat(pool.getCurrentBuffersCount(isDirect)) + .describedAs("Number of buffers in the pool") + .isEqualTo(0); + + pool.putBuffer(buffer1); + pool.putBuffer(buffer2); + Assertions.assertThat(pool.getCurrentBuffersCount(isDirect)) + .describedAs("Number of buffers in the pool") + .isEqualTo(2); + ByteBuffer buffer3 = pool.getBuffer(isDirect, 10); + // As buffer1 is returned to the pool before buffer2, it should + // be returned when buffer of same size is asked again from + // the pool. Memory references must match not just content + // that is why {@code Assertions.isSameAs} is used here rather + // than usual {@code Assertions.isEqualTo}. + Assertions.assertThat(buffer3) + .describedAs("Buffers should be returned in order of their " + + "insertion time") + .isSameAs(buffer1); + pool.putBuffer(buffer); + ByteBuffer buffer4 = pool.getBuffer(isDirect, 10); + Assertions.assertThat(buffer4) + .describedAs("Buffers should be returned in order of their " + + "insertion time") + .isSameAs(buffer2); + } + + @Test + public void testGarbageCollection() { + WeakReferencedElasticByteBufferPool pool = new WeakReferencedElasticByteBufferPool(); + ByteBuffer buffer = pool.getBuffer(isDirect, 5); + ByteBuffer buffer1 = pool.getBuffer(isDirect, 10); + ByteBuffer buffer2 = pool.getBuffer(isDirect, 15); + Assertions.assertThat(pool.getCurrentBuffersCount(isDirect)) + .describedAs("Number of buffers in the pool") + .isEqualTo(0); + pool.putBuffer(buffer1); + pool.putBuffer(buffer2); + Assertions.assertThat(pool.getCurrentBuffersCount(isDirect)) + .describedAs("Number of buffers in the pool") + .isEqualTo(2); + // Before GC. + ByteBuffer buffer4 = pool.getBuffer(isDirect, 12); + Assertions.assertThat(buffer4.capacity()) + .describedAs("Pooled buffer should have older capacity") + .isEqualTo(15); + pool.putBuffer(buffer4); + // Removing the references + buffer1 = null; + buffer2 = null; + buffer4 = null; + System.gc(); + ByteBuffer buffer3 = pool.getBuffer(isDirect, 12); + Assertions.assertThat(buffer3.capacity()) + .describedAs("After garbage collection new buffer should be " + + "returned with fixed capacity") + .isEqualTo(12); + } + + @Test + public void testWeakReferencesPruning() { + WeakReferencedElasticByteBufferPool pool = new WeakReferencedElasticByteBufferPool(); + ByteBuffer buffer1 = pool.getBuffer(isDirect, 5); + ByteBuffer buffer2 = pool.getBuffer(isDirect, 10); + ByteBuffer buffer3 = pool.getBuffer(isDirect, 15); + + pool.putBuffer(buffer2); + pool.putBuffer(buffer3); + Assertions.assertThat(pool.getCurrentBuffersCount(isDirect)) + .describedAs("Number of buffers in the pool") + .isEqualTo(2); + + // marking only buffer2 to be garbage collected. + buffer2 = null; + System.gc(); + ByteBuffer buffer4 = pool.getBuffer(isDirect, 10); + // Number of buffers in the pool is 0 as one got garbage + // collected and other got returned in above call. + Assertions.assertThat(pool.getCurrentBuffersCount(isDirect)) + .describedAs("Number of buffers in the pool") + .isEqualTo(0); + Assertions.assertThat(buffer4.capacity()) + .describedAs("After gc, pool should return next greater than " + + "available buffer") + .isEqualTo(15); + + } + + private void validateBufferContent(ByteBuffer buffer, byte[] arr) { + for (int i=0; i compressors = new HashSet(); + Set compressors = new HashSet<>(); for (int i = 0; i < 10; ++i) { compressors.add(CodecPool.getCompressor(codec)); } @@ -180,7 +180,7 @@ public void testDecompressorNotReturnSameInstance() { Decompressor decomp = CodecPool.getDecompressor(codec); CodecPool.returnDecompressor(decomp); CodecPool.returnDecompressor(decomp); - Set decompressors = new HashSet(); + Set decompressors = new HashSet<>(); for (int i = 0; i < 10; ++i) { decompressors.add(CodecPool.getDecompressor(codec)); } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/compress/bzip2/TestBzip2CompressorDecompressor.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/compress/bzip2/TestBzip2CompressorDecompressor.java index c585a463e46b1..fae5ce6de40a4 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/compress/bzip2/TestBzip2CompressorDecompressor.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/compress/bzip2/TestBzip2CompressorDecompressor.java @@ -18,9 +18,6 @@ package org.apache.hadoop.io.compress.bzip2; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.io.DataInputBuffer; -import org.apache.hadoop.io.DataOutputBuffer; -import org.apache.hadoop.io.compress.*; import org.apache.hadoop.io.compress.bzip2.Bzip2Compressor; import org.apache.hadoop.io.compress.bzip2.Bzip2Decompressor; import org.apache.hadoop.test.MultithreadedTestUtil; @@ -32,7 +29,6 @@ import static org.junit.Assert.*; import static org.junit.Assume.*; -import static org.junit.Assume.assumeTrue; public class TestBzip2CompressorDecompressor { diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/compress/zlib/TestZlibCompressorDecompressor.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/compress/zlib/TestZlibCompressorDecompressor.java index ac9ea5e8a8468..25da4fe2375ed 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/compress/zlib/TestZlibCompressorDecompressor.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/compress/zlib/TestZlibCompressorDecompressor.java @@ -28,7 +28,6 @@ import java.util.zip.DeflaterOutputStream; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.CommonConfigurationKeys; import org.apache.hadoop.io.DataInputBuffer; import org.apache.hadoop.io.compress.CompressDecompressTester; import org.apache.hadoop.io.compress.Compressor; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestCompression.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestCompression.java index b1bf0774974da..6b4c698551359 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestCompression.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/io/file/tfile/TestCompression.java @@ -17,15 +17,12 @@ */ package org.apache.hadoop.io.file.tfile; -import org.apache.hadoop.io.compress.CompressionCodec; import org.apache.hadoop.test.LambdaTestUtils; import org.junit.*; import java.io.IOException; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; public class TestCompression { diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/MiniRPCBenchmark.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/MiniRPCBenchmark.java index 2290270bfba1a..70ae639091421 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/MiniRPCBenchmark.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/MiniRPCBenchmark.java @@ -36,7 +36,6 @@ import org.apache.hadoop.security.SecurityUtil; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.authorize.DefaultImpersonationProvider; -import org.apache.hadoop.security.authorize.ProxyUsers; import org.apache.hadoop.security.token.Token; import org.apache.hadoop.security.token.TokenInfo; import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenSelector; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/TestRPC.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/TestRPC.java index 5fc9cb5410b30..5caabd22a88c6 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/TestRPC.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/ipc/TestRPC.java @@ -19,6 +19,8 @@ package org.apache.hadoop.ipc; import org.apache.hadoop.ipc.metrics.RpcMetrics; + +import org.apache.hadoop.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; import org.apache.hadoop.thirdparty.protobuf.ServiceException; import org.apache.hadoop.HadoopIllegalArgumentException; import org.apache.hadoop.conf.Configuration; @@ -84,6 +86,7 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; @@ -1697,6 +1700,61 @@ public void testRpcMetricsInNanos() throws Exception { } } + @Test + public void testNumTotalRequestsMetrics() throws Exception { + UserGroupInformation ugi = UserGroupInformation. + createUserForTesting("userXyz", new String[0]); + + final Server server = setupTestServer(conf, 1); + + ExecutorService executorService = null; + try { + RpcMetrics rpcMetrics = server.getRpcMetrics(); + assertEquals(0, rpcMetrics.getTotalRequests()); + assertEquals(0, rpcMetrics.getTotalRequestsPerSecond()); + + List> externalCallList = new ArrayList<>(); + + executorService = Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder().setDaemon(true).setNameFormat("testNumTotalRequestsMetrics") + .build()); + AtomicInteger rps = new AtomicInteger(0); + CountDownLatch countDownLatch = new CountDownLatch(1); + executorService.submit(() -> { + while (true) { + int numRps = (int) rpcMetrics.getTotalRequestsPerSecond(); + rps.getAndSet(numRps); + if (rps.get() > 0) { + countDownLatch.countDown(); + break; + } + } + }); + + for (int i = 0; i < 100000; i++) { + externalCallList.add(newExtCall(ugi, () -> null)); + } + for (ExternalCall externalCall : externalCallList) { + server.queueCall(externalCall); + } + for (ExternalCall externalCall : externalCallList) { + externalCall.get(); + } + + assertEquals(100000, rpcMetrics.getTotalRequests()); + if (countDownLatch.await(10, TimeUnit.SECONDS)) { + assertTrue(rps.get() > 10); + } else { + throw new AssertionError("total requests per seconds are still 0"); + } + } finally { + if (executorService != null) { + executorService.shutdown(); + } + server.stop(); + } + } + public static void main(String[] args) throws Exception { new TestRPC().testCallsInternal(conf); diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/net/TestDNS.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/net/TestDNS.java index 0e1ac1deb9612..2504a6401a8d9 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/net/TestDNS.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/net/TestDNS.java @@ -18,8 +18,6 @@ package org.apache.hadoop.net; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/net/TestTableMapping.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/net/TestTableMapping.java index 86870e1257119..697b0bad43757 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/net/TestTableMapping.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/net/TestTableMapping.java @@ -30,7 +30,7 @@ import java.util.List; import org.apache.hadoop.conf.Configuration; -import org.junit.Before; + import org.junit.Test; public class TestTableMapping { diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestKDiagNoKDC.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestKDiagNoKDC.java index 2e266bba1f97a..03d953b5f3cc3 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestKDiagNoKDC.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestKDiagNoKDC.java @@ -19,8 +19,7 @@ package org.apache.hadoop.security; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.minikdc.MiniKdc; -import org.junit.AfterClass; + import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; @@ -31,20 +30,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.util.Properties; import java.util.concurrent.TimeUnit; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_TOKEN_FILES; -import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION; import static org.apache.hadoop.security.KDiag.ARG_KEYLEN; -import static org.apache.hadoop.security.KDiag.ARG_KEYTAB; import static org.apache.hadoop.security.KDiag.ARG_NOFAIL; import static org.apache.hadoop.security.KDiag.ARG_NOLOGIN; -import static org.apache.hadoop.security.KDiag.ARG_PRINCIPAL; -import static org.apache.hadoop.security.KDiag.ARG_SECURE; -import static org.apache.hadoop.security.KDiag.CAT_CONFIG; -import static org.apache.hadoop.security.KDiag.CAT_KERBEROS; import static org.apache.hadoop.security.KDiag.CAT_LOGIN; import static org.apache.hadoop.security.KDiag.CAT_TOKEN; import static org.apache.hadoop.security.KDiag.KerberosDiagsFailure; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGIWithMiniKdc.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGIWithMiniKdc.java index de74d17863668..f04fbe1e08926 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGIWithMiniKdc.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestUGIWithMiniKdc.java @@ -19,23 +19,18 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.minikdc.MiniKdc; -import org.apache.hadoop.security.authentication.util.KerberosUtil; import org.apache.hadoop.test.GenericTestUtils; import org.apache.hadoop.test.LambdaTestUtils; -import org.apache.hadoop.util.PlatformName; + import org.junit.After; import org.junit.Test; import org.slf4j.event.Level; -import javax.security.auth.Subject; import javax.security.auth.kerberos.KerberosPrincipal; -import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.LoginContext; import java.io.File; import java.security.Principal; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Properties; import java.util.Set; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/http/TestCrossOriginFilter.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/http/TestCrossOriginFilter.java index b9662b8c6a328..0b396be48f983 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/http/TestCrossOriginFilter.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/http/TestCrossOriginFilter.java @@ -36,9 +36,6 @@ import org.junit.Test; import org.mockito.Mockito; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - public class TestCrossOriginFilter { @Test diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLauncher.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLauncher.java index f40051b0d178f..72757e4b1c182 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLauncher.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/TestServiceLauncher.java @@ -29,8 +29,6 @@ import org.apache.hadoop.service.launcher.testservices.StoppingInStartLaunchableService; import org.apache.hadoop.service.launcher.testservices.StringConstructorOnlyService; -import static org.apache.hadoop.service.launcher.LauncherArguments.*; - import static org.apache.hadoop.test.GenericTestUtils.*; import static org.apache.hadoop.service.launcher.testservices.ExceptionInExecuteLaunchableService.*; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/NoArgsAllowedService.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/NoArgsAllowedService.java index 602cb157ed5d8..9245b1844f792 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/NoArgsAllowedService.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/service/launcher/testservices/NoArgsAllowedService.java @@ -26,7 +26,6 @@ import org.slf4j.LoggerFactory; import java.util.List; -import java.util.Map; /** * service that does not allow any arguments. diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/GenericTestUtils.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/GenericTestUtils.java index f1bf4bb91e668..61d5938494c22 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/GenericTestUtils.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/GenericTestUtils.java @@ -39,6 +39,7 @@ import java.util.Random; import java.util.Set; import java.util.Enumeration; +import java.util.TreeSet; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -72,7 +73,6 @@ import org.slf4j.LoggerFactory; import org.apache.hadoop.thirdparty.com.google.common.base.Joiner; -import org.apache.hadoop.util.Sets; import static org.apache.hadoop.fs.contract.ContractTestUtils.createFile; import static org.apache.hadoop.util.functional.CommonCallableSupplier.submit; @@ -344,13 +344,13 @@ public static void assertExists(File f) { public static void assertGlobEquals(File dir, String pattern, String ... expectedMatches) throws IOException { - Set found = Sets.newTreeSet(); + Set found = new TreeSet<>(); for (File f : FileUtil.listFiles(dir)) { if (f.getName().matches(pattern)) { found.add(f.getName()); } } - Set expectedSet = Sets.newTreeSet( + Set expectedSet = new TreeSet<>( Arrays.asList(expectedMatches)); Assert.assertEquals("Bad files matching " + pattern + " in " + dir, Joiner.on(",").join(expectedSet), diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/MoreAsserts.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/MoreAsserts.java index 142669b78682e..f6e6055d78e2c 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/MoreAsserts.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/MoreAsserts.java @@ -19,6 +19,9 @@ package org.apache.hadoop.test; import java.util.Iterator; +import java.util.concurrent.CompletableFuture; + +import org.assertj.core.api.Assertions; import org.junit.Assert; /** @@ -28,17 +31,18 @@ public class MoreAsserts { /** * Assert equivalence for array and iterable - * @param the type of the elements - * @param s the name/message for the collection - * @param expected the expected array of elements - * @param actual the actual iterable of elements + * + * @param the type of the elements + * @param s the name/message for the collection + * @param expected the expected array of elements + * @param actual the actual iterable of elements */ public static void assertEquals(String s, T[] expected, Iterable actual) { Iterator it = actual.iterator(); int i = 0; for (; i < expected.length && it.hasNext(); ++i) { - Assert.assertEquals("Element "+ i +" for "+ s, expected[i], it.next()); + Assert.assertEquals("Element " + i + " for " + s, expected[i], it.next()); } Assert.assertTrue("Expected more elements", i == expected.length); Assert.assertTrue("Expected less elements", !it.hasNext()); @@ -46,7 +50,8 @@ public static void assertEquals(String s, T[] expected, /** * Assert equality for two iterables - * @param the type of the elements + * + * @param the type of the elements * @param s * @param expected * @param actual @@ -57,10 +62,40 @@ public static void assertEquals(String s, Iterable expected, Iterator ita = actual.iterator(); int i = 0; while (ite.hasNext() && ita.hasNext()) { - Assert.assertEquals("Element "+ i +" for "+s, ite.next(), ita.next()); + Assert.assertEquals("Element " + i + " for " + s, ite.next(), ita.next()); } Assert.assertTrue("Expected more elements", !ite.hasNext()); Assert.assertTrue("Expected less elements", !ita.hasNext()); } + + public static void assertFutureCompletedSuccessfully(CompletableFuture future) { + Assertions.assertThat(future.isDone()) + .describedAs("This future is supposed to be " + + "completed successfully") + .isTrue(); + Assertions.assertThat(future.isCompletedExceptionally()) + .describedAs("This future is supposed to be " + + "completed successfully") + .isFalse(); + } + + public static void assertFutureFailedExceptionally(CompletableFuture future) { + Assertions.assertThat(future.isCompletedExceptionally()) + .describedAs("This future is supposed to be " + + "completed exceptionally") + .isTrue(); + } + + /** + * Assert two same type of values. + * @param actual actual value. + * @param expected expected value. + * @param message error message to print in case of mismatch. + */ + public static void assertEqual(T actual, T expected, String message) { + Assertions.assertThat(actual) + .describedAs("Mismatch in %s", message) + .isEqualTo(expected); + } } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/MultithreadedTestUtil.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/MultithreadedTestUtil.java index 217c2f84eba4b..e270ee68000eb 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/MultithreadedTestUtil.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/MultithreadedTestUtil.java @@ -70,8 +70,8 @@ public abstract class MultithreadedTestUtil { public static class TestContext { private Throwable err = null; private boolean stopped = false; - private Set testThreads = new HashSet(); - private Set finishedThreads = new HashSet(); + private Set testThreads = new HashSet<>(); + private Set finishedThreads = new HashSet<>(); /** * Check if the context can run threads. diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/TestTimedOutTestsListener.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/TestTimedOutTestsListener.java index 1334f1c95f407..42ed8c8775570 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/TestTimedOutTestsListener.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/TestTimedOutTestsListener.java @@ -19,7 +19,6 @@ import java.io.PrintWriter; import java.io.StringWriter; -import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/tools/TestCommandShell.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/tools/TestCommandShell.java index 606791801fe13..e9c5950b729c6 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/tools/TestCommandShell.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/tools/TestCommandShell.java @@ -24,7 +24,6 @@ import org.apache.hadoop.tools.CommandShell; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Before; import org.junit.Test; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestDiskCheckerWithDiskIo.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestDiskCheckerWithDiskIo.java index 082672ccd33d2..552d1319312c6 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestDiskCheckerWithDiskIo.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestDiskCheckerWithDiskIo.java @@ -23,15 +23,12 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestShell.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestShell.java index 68e70ebb79a5c..9ae52ff95cb91 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestShell.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestShell.java @@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import org.apache.commons.io.FileUtils; -import org.apache.hadoop.security.alias.AbstractJavaKeyStoreProvider; + import org.junit.Assert; import java.io.BufferedReader; diff --git a/hadoop-common-project/hadoop-kms/src/main/java/org/apache/hadoop/crypto/key/kms/server/KMSAudit.java b/hadoop-common-project/hadoop-kms/src/main/java/org/apache/hadoop/crypto/key/kms/server/KMSAudit.java index 14c2ae907b14d..d71172e1b93ef 100644 --- a/hadoop-common-project/hadoop-kms/src/main/java/org/apache/hadoop/crypto/key/kms/server/KMSAudit.java +++ b/hadoop-common-project/hadoop-kms/src/main/java/org/apache/hadoop/crypto/key/kms/server/KMSAudit.java @@ -36,9 +36,9 @@ import org.apache.hadoop.thirdparty.com.google.common.cache.CacheBuilder; import org.apache.hadoop.thirdparty.com.google.common.cache.RemovalListener; import org.apache.hadoop.thirdparty.com.google.common.cache.RemovalNotification; -import org.apache.hadoop.util.Sets; import org.apache.hadoop.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.Arrays; import java.util.HashSet; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -56,10 +56,10 @@ */ public class KMSAudit { @VisibleForTesting - static final Set AGGREGATE_OPS_WHITELIST = Sets.newHashSet( + static final Set AGGREGATE_OPS_WHITELIST = new HashSet<>(Arrays.asList( KMS.KMSOp.GET_KEY_VERSION, KMS.KMSOp.GET_CURRENT_KEY, KMS.KMSOp.DECRYPT_EEK, KMS.KMSOp.GENERATE_EEK, KMS.KMSOp.REENCRYPT_EEK - ); + )); private Cache cache; diff --git a/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMS.java b/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMS.java index a0a58ff3567f5..f4c7fbe0b3c3c 100644 --- a/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMS.java +++ b/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMS.java @@ -17,6 +17,7 @@ */ package org.apache.hadoop.crypto.key.kms.server; +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.hadoop.thirdparty.com.google.common.cache.LoadingCache; import org.apache.curator.test.TestingServer; import org.apache.hadoop.conf.Configuration; @@ -48,7 +49,6 @@ import org.apache.hadoop.security.token.TokenIdentifier; import org.apache.hadoop.security.token.delegation.web.DelegationTokenIdentifier; import org.apache.hadoop.test.GenericTestUtils; -import org.apache.hadoop.test.Whitebox; import org.apache.hadoop.util.Time; import org.apache.http.client.utils.URIBuilder; import org.junit.After; @@ -929,6 +929,7 @@ public Void call() throws Exception { } @Test + @SuppressWarnings("unchecked") public void testKMSProviderCaching() throws Exception { Configuration conf = new Configuration(); File confDir = getTestDir(); @@ -946,11 +947,12 @@ public Void call() throws Exception { KMSClientProvider kmscp = createKMSClientProvider(uri, conf); // get the reference to the internal cache, to test invalidation. - ValueQueue vq = - (ValueQueue) Whitebox.getInternalState(kmscp, "encKeyVersionQueue"); + ValueQueue vq = (ValueQueue) FieldUtils.getField(KMSClientProvider.class, + "encKeyVersionQueue", true).get(kmscp); LoadingCache> kq = - ((LoadingCache>) - Whitebox.getInternalState(vq, "keyQueues")); + (LoadingCache>) + FieldUtils.getField(ValueQueue.class, "keyQueues", true).get(vq); + EncryptedKeyVersion mockEKV = Mockito.mock(EncryptedKeyVersion.class); when(mockEKV.getEncryptionKeyName()).thenReturn(keyName); when(mockEKV.getEncryptionKeyVersionName()).thenReturn(mockVersionName); diff --git a/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMSAudit.java b/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMSAudit.java index 2f47ed794ac84..3d0fd7de6428d 100644 --- a/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMSAudit.java +++ b/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMSAudit.java @@ -24,13 +24,14 @@ import java.io.OutputStream; import java.io.PrintStream; import java.util.List; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.crypto.key.kms.server.KMS.KMSOp; import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.test.GenericTestUtils; -import org.apache.hadoop.test.Whitebox; import org.apache.hadoop.util.ThreadUtil; import org.apache.log4j.LogManager; import org.apache.log4j.PropertyConfigurator; @@ -63,7 +64,7 @@ public void setOutputStream(OutputStream out) { } @Rule - public final Timeout testTimeout = new Timeout(180000); + public final Timeout testTimeout = new Timeout(180000L, TimeUnit.MILLISECONDS); @Before public void setUp() throws IOException { @@ -207,8 +208,9 @@ public void testAuditLogFormat() throws Exception { @Test public void testInitAuditLoggers() throws Exception { // Default should be the simple logger - List loggers = (List) Whitebox - .getInternalState(kmsAudit, "auditLoggers"); + List loggers = (List) FieldUtils. + getField(KMSAudit.class, "auditLoggers", true).get(kmsAudit); + Assert.assertEquals(1, loggers.size()); Assert.assertEquals(SimpleKMSAuditLogger.class, loggers.get(0).getClass()); @@ -218,8 +220,8 @@ public void testInitAuditLoggers() throws Exception { SimpleKMSAuditLogger.class.getName() + ", " + SimpleKMSAuditLogger.class.getName()); final KMSAudit audit = new KMSAudit(conf); - loggers = - (List) Whitebox.getInternalState(audit, "auditLoggers"); + loggers = (List) FieldUtils. + getField(KMSAudit.class, "auditLoggers", true).get(kmsAudit); Assert.assertEquals(1, loggers.size()); Assert.assertEquals(SimpleKMSAuditLogger.class, loggers.get(0).getClass()); diff --git a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/FSINFO3Response.java b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/FSINFO3Response.java index ebd54fe08cdbe..4ec4cec610732 100644 --- a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/FSINFO3Response.java +++ b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/FSINFO3Response.java @@ -18,7 +18,6 @@ package org.apache.hadoop.nfs.nfs3.response; import org.apache.hadoop.nfs.NfsTime; -import org.apache.hadoop.nfs.nfs3.FileHandle; import org.apache.hadoop.nfs.nfs3.Nfs3FileAttributes; import org.apache.hadoop.nfs.nfs3.Nfs3Status; import org.apache.hadoop.oncrpc.XDR; diff --git a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/FSSTAT3Response.java b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/FSSTAT3Response.java index c0d1a8a38ef06..bcc95861a9608 100644 --- a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/FSSTAT3Response.java +++ b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/FSSTAT3Response.java @@ -17,7 +17,6 @@ */ package org.apache.hadoop.nfs.nfs3.response; -import org.apache.hadoop.nfs.nfs3.FileHandle; import org.apache.hadoop.nfs.nfs3.Nfs3FileAttributes; import org.apache.hadoop.nfs.nfs3.Nfs3Status; import org.apache.hadoop.oncrpc.XDR; diff --git a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/LINK3Response.java b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/LINK3Response.java index 3893aa10b2f3e..c810030c2616e 100644 --- a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/LINK3Response.java +++ b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/LINK3Response.java @@ -17,8 +17,6 @@ */ package org.apache.hadoop.nfs.nfs3.response; -import org.apache.hadoop.nfs.nfs3.Nfs3FileAttributes; -import org.apache.hadoop.nfs.nfs3.Nfs3Status; import org.apache.hadoop.oncrpc.XDR; import org.apache.hadoop.oncrpc.security.Verifier; diff --git a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/READDIR3Response.java b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/READDIR3Response.java index 5802b7544e6a8..f47acce57f435 100644 --- a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/READDIR3Response.java +++ b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/READDIR3Response.java @@ -17,7 +17,6 @@ */ package org.apache.hadoop.nfs.nfs3.response; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; diff --git a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/READDIRPLUS3Response.java b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/READDIRPLUS3Response.java index f1bfd56fd3908..cc27397d77c1d 100644 --- a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/READDIRPLUS3Response.java +++ b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/READDIRPLUS3Response.java @@ -25,8 +25,6 @@ import org.apache.hadoop.nfs.nfs3.FileHandle; import org.apache.hadoop.nfs.nfs3.Nfs3FileAttributes; import org.apache.hadoop.nfs.nfs3.Nfs3Status; -import org.apache.hadoop.nfs.nfs3.response.READDIR3Response.DirList3; -import org.apache.hadoop.nfs.nfs3.response.READDIR3Response.Entry3; import org.apache.hadoop.oncrpc.XDR; import org.apache.hadoop.oncrpc.security.Verifier; diff --git a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/REMOVE3Response.java b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/REMOVE3Response.java index f0fcb3d705c5d..a0b51111a6368 100644 --- a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/REMOVE3Response.java +++ b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/REMOVE3Response.java @@ -17,8 +17,6 @@ */ package org.apache.hadoop.nfs.nfs3.response; -import org.apache.hadoop.nfs.nfs3.Nfs3FileAttributes; -import org.apache.hadoop.nfs.nfs3.Nfs3Status; import org.apache.hadoop.oncrpc.XDR; import org.apache.hadoop.oncrpc.security.Verifier; diff --git a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/WRITE3Response.java b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/WRITE3Response.java index 8d4b4d909f640..3f2552a8eca2a 100644 --- a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/WRITE3Response.java +++ b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/nfs/nfs3/response/WRITE3Response.java @@ -17,9 +17,7 @@ */ package org.apache.hadoop.nfs.nfs3.response; -import org.apache.hadoop.nfs.nfs3.FileHandle; import org.apache.hadoop.nfs.nfs3.Nfs3Constant; -import org.apache.hadoop.nfs.nfs3.Nfs3FileAttributes; import org.apache.hadoop.nfs.nfs3.Nfs3Status; import org.apache.hadoop.nfs.nfs3.Nfs3Constant.WriteStableHow; import org.apache.hadoop.oncrpc.XDR; diff --git a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/oncrpc/SimpleTcpClientHandler.java b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/oncrpc/SimpleTcpClientHandler.java index 1acefc857f830..163e26b823d04 100644 --- a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/oncrpc/SimpleTcpClientHandler.java +++ b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/oncrpc/SimpleTcpClientHandler.java @@ -18,11 +18,8 @@ package org.apache.hadoop.oncrpc; import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.util.ReferenceCountUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/oncrpc/security/SysSecurityHandler.java b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/oncrpc/security/SysSecurityHandler.java index 884bebc97561a..1ca4dbe2cca0c 100644 --- a/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/oncrpc/security/SysSecurityHandler.java +++ b/hadoop-common-project/hadoop-nfs/src/main/java/org/apache/hadoop/oncrpc/security/SysSecurityHandler.java @@ -17,7 +17,6 @@ */ package org.apache.hadoop.oncrpc.security; -import org.apache.hadoop.nfs.nfs3.Nfs3Constant; import org.apache.hadoop.oncrpc.RpcCall; import org.apache.hadoop.security.IdMappingConstant; import org.apache.hadoop.security.IdMappingServiceProvider; diff --git a/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/binding/RegistryPathUtils.java b/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/binding/RegistryPathUtils.java index 9fa4b8d84d85e..b0b30bdea5faa 100644 --- a/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/binding/RegistryPathUtils.java +++ b/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/binding/RegistryPathUtils.java @@ -24,7 +24,6 @@ import org.apache.hadoop.fs.PathNotFoundException; import org.apache.hadoop.registry.client.exceptions.InvalidPathnameException; import org.apache.hadoop.registry.client.impl.zk.RegistryInternalConstants; -import org.apache.hadoop.registry.server.dns.BaseServiceRecordProcessor; import org.apache.zookeeper.common.PathUtils; import java.net.IDN; diff --git a/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/binding/RegistryUtils.java b/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/binding/RegistryUtils.java index cff70a613783a..f451792355c50 100644 --- a/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/binding/RegistryUtils.java +++ b/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/binding/RegistryUtils.java @@ -43,7 +43,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; /** diff --git a/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/impl/zk/RegistryOperationsService.java b/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/impl/zk/RegistryOperationsService.java index 49ea16bba7d62..96cf911fd9541 100644 --- a/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/impl/zk/RegistryOperationsService.java +++ b/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/impl/zk/RegistryOperationsService.java @@ -28,7 +28,6 @@ import org.apache.hadoop.registry.client.binding.RegistryUtils; import org.apache.hadoop.registry.client.binding.RegistryPathUtils; import org.apache.hadoop.registry.client.exceptions.InvalidPathnameException; -import org.apache.hadoop.registry.client.exceptions.NoRecordException; import org.apache.hadoop.registry.client.types.RegistryPathStatus; import org.apache.hadoop.registry.client.types.ServiceRecord; import org.apache.zookeeper.CreateMode; diff --git a/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/impl/zk/RegistrySecurity.java b/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/impl/zk/RegistrySecurity.java index bcca604182bfd..e500ba6617b5f 100644 --- a/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/impl/zk/RegistrySecurity.java +++ b/hadoop-common-project/hadoop-registry/src/main/java/org/apache/hadoop/registry/client/impl/zk/RegistrySecurity.java @@ -33,7 +33,6 @@ import org.apache.zookeeper.Environment; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.client.ZKClientConfig; -import org.apache.zookeeper.client.ZooKeeperSaslClient; import org.apache.zookeeper.data.ACL; import org.apache.zookeeper.data.Id; import org.apache.zookeeper.server.auth.DigestAuthenticationProvider; diff --git a/hadoop-common-project/pom.xml b/hadoop-common-project/pom.xml index b36dbf30610ff..f167a079a9b0c 100644 --- a/hadoop-common-project/pom.xml +++ b/hadoop-common-project/pom.xml @@ -56,5 +56,4 @@ - diff --git a/hadoop-hdfs-project/hadoop-hdfs-rbf/src/main/java/org/apache/hadoop/hdfs/server/federation/resolver/MountTableResolver.java b/hadoop-hdfs-project/hadoop-hdfs-rbf/src/main/java/org/apache/hadoop/hdfs/server/federation/resolver/MountTableResolver.java index 9c0f33763f208..2888d8cc501a2 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-rbf/src/main/java/org/apache/hadoop/hdfs/server/federation/resolver/MountTableResolver.java +++ b/hadoop-hdfs-project/hadoop-hdfs-rbf/src/main/java/org/apache/hadoop/hdfs/server/federation/resolver/MountTableResolver.java @@ -359,7 +359,7 @@ public static boolean isTrashPath(String path) throws IOException { public static String getTrashRoot() throws IOException { // Gets the Trash directory for the current user. return FileSystem.USER_HOME_PREFIX + "/" + - RouterRpcServer.getRemoteUser().getUserName() + "/" + + RouterRpcServer.getRemoteUser().getShortUserName() + "/" + FileSystem.TRASH_PREFIX; } diff --git a/hadoop-hdfs-project/hadoop-hdfs-rbf/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouterTrash.java b/hadoop-hdfs-project/hadoop-hdfs-rbf/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouterTrash.java index acd7b87a14b4f..dfb8c33c72d4b 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-rbf/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouterTrash.java +++ b/hadoop-hdfs-project/hadoop-hdfs-rbf/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouterTrash.java @@ -189,6 +189,44 @@ public void testMoveToTrashNoMountPoint() throws IOException, assertEquals(2, fileStatuses.length); } + @Test + public void testMoveToTrashWithKerberosUser() throws IOException, + URISyntaxException, InterruptedException { + //Constructs the structure of the KerBoers user name + String kerberosUser = "randomUser/dev@HADOOP.COM"; + UserGroupInformation ugi = UserGroupInformation.createRemoteUser(kerberosUser); + MountTable addEntry = MountTable.newInstance(MOUNT_POINT, + Collections.singletonMap(ns1, MOUNT_POINT)); + assertTrue(addMountTable(addEntry)); + // current user client + MiniRouterDFSCluster.NamenodeContext nn1Context = cluster.getNamenode(ns1, null); + DFSClient currentUserClientNs0 = nnContext.getClient(); + DFSClient currentUserClientNs1 = nn1Context.getClient(); + + currentUserClientNs0.setOwner("/", ugi.getShortUserName(), ugi.getShortUserName()); + currentUserClientNs1.setOwner("/", ugi.getShortUserName(), ugi.getShortUserName()); + + // test user client + DFSClient testUserClientNs1 = nn1Context.getClient(ugi); + testUserClientNs1.mkdirs(MOUNT_POINT, new FsPermission("777"), true); + assertTrue(testUserClientNs1.exists(MOUNT_POINT)); + // create test file + testUserClientNs1.create(FILE, true); + Path filePath = new Path(FILE); + + FileStatus[] fileStatuses = routerFs.listStatus(filePath); + assertEquals(1, fileStatuses.length); + assertEquals(ugi.getShortUserName(), fileStatuses[0].getOwner()); + // move to Trash + Configuration routerConf = routerContext.getConf(); + FileSystem fs = DFSTestUtil.getFileSystemAs(ugi, routerConf); + Trash trash = new Trash(fs, routerConf); + assertTrue(trash.moveToTrash(filePath)); + fileStatuses = fs.listStatus( + new Path("/user/" + ugi.getShortUserName() + "/.Trash/Current" + MOUNT_POINT)); + assertEquals(1, fileStatuses.length); + } + @Test public void testDeleteToTrashExistMountPoint() throws IOException, URISyntaxException, InterruptedException { diff --git a/hadoop-hdfs-project/hadoop-hdfs-rbf/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouterUserMappings.java b/hadoop-hdfs-project/hadoop-hdfs-rbf/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouterUserMappings.java index 707a2f7baa348..2be7b7b1f49c2 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-rbf/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouterUserMappings.java +++ b/hadoop-hdfs-project/hadoop-hdfs-rbf/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouterUserMappings.java @@ -39,7 +39,6 @@ import org.apache.hadoop.test.GenericTestUtils; import org.apache.hadoop.test.LambdaTestUtils; import org.apache.hadoop.tools.GetUserMappingsProtocol; -import org.apache.hadoop.util.Sets; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -57,6 +56,8 @@ import java.net.URL; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -120,7 +121,7 @@ public Set getGroupsSet(String user) throws IOException { LOG.info("Getting groups in MockUnixGroupsMapping"); String g1 = user + (10 * i + 1); String g2 = user + (10 * i + 2); - Set s = Sets.newHashSet(g1, g2); + Set s = new HashSet<>(Arrays.asList(g1, g2)); i++; return s; } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSUtil.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSUtil.java index 50c947b05941a..7237489e7bfef 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSUtil.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSUtil.java @@ -76,7 +76,6 @@ import org.apache.hadoop.net.DomainNameResolverFactory; import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.util.Lists; -import org.apache.hadoop.util.Sets; import org.apache.hadoop.thirdparty.com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -725,8 +724,9 @@ private static Collection getParentNameServices(Configuration conf) } else { // Ensure that the internal service is indeed in the list of all available // nameservices. - Set availableNameServices = Sets.newHashSet(conf - .getTrimmedStringCollection(DFSConfigKeys.DFS_NAMESERVICES)); + Collection namespaces = conf + .getTrimmedStringCollection(DFSConfigKeys.DFS_NAMESERVICES); + Set availableNameServices = new HashSet<>(namespaces); for (String nsId : parentNameServices) { if (!availableNameServices.contains(nsId)) { throw new IOException("Unknown nameservice: " + nsId); diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/qjournal/server/JournalNodeSyncer.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/qjournal/server/JournalNodeSyncer.java index ff46aa751d7ba..fd29c849dfcb3 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/qjournal/server/JournalNodeSyncer.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/qjournal/server/JournalNodeSyncer.java @@ -39,7 +39,6 @@ import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.util.Daemon; import org.apache.hadoop.util.Lists; -import org.apache.hadoop.util.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +50,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.security.PrivilegedExceptionAction; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -273,7 +273,7 @@ private List getOtherJournalNodeAddrs() { } if (uriStr == null || uriStr.isEmpty()) { - HashSet sharedEditsUri = Sets.newHashSet(); + HashSet sharedEditsUri = new HashSet<>(); if (nameServiceId != null) { Collection nnIds = DFSUtilClient.getNameNodeIds( conf, nameServiceId); @@ -315,7 +315,7 @@ private List getJournalAddrList(String uriStr) throws IOException { URI uri = new URI(uriStr); return Util.getLoggerAddresses(uri, - Sets.newHashSet(jn.getBoundIpcAddress()), conf); + new HashSet<>(Arrays.asList(jn.getBoundIpcAddress())), conf); } private void getMissingLogSegments(List thisJournalEditLogs, diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/BlockManager.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/BlockManager.java index c522e2604e70f..51e12ec43372c 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/BlockManager.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/BlockManager.java @@ -1916,23 +1916,29 @@ private void markBlockAsCorrupt(BlockToMarkCorrupt b, b.getReasonCode(), b.getStored().isStriped()); NumberReplicas numberOfReplicas = countNodes(b.getStored()); - boolean hasEnoughLiveReplicas = numberOfReplicas.liveReplicas() >= + final int numUsableReplicas = numberOfReplicas.liveReplicas() + + numberOfReplicas.decommissioning() + + numberOfReplicas.liveEnteringMaintenanceReplicas(); + boolean hasEnoughLiveReplicas = numUsableReplicas >= expectedRedundancies; boolean minReplicationSatisfied = hasMinStorage(b.getStored(), - numberOfReplicas.liveReplicas()); + numUsableReplicas); boolean hasMoreCorruptReplicas = minReplicationSatisfied && (numberOfReplicas.liveReplicas() + numberOfReplicas.corruptReplicas()) > expectedRedundancies; boolean corruptedDuringWrite = minReplicationSatisfied && b.isCorruptedDuringWrite(); - // case 1: have enough number of live replicas - // case 2: corrupted replicas + live replicas > Replication factor + // case 1: have enough number of usable replicas + // case 2: corrupted replicas + usable replicas > Replication factor // case 3: Block is marked corrupt due to failure while writing. In this // case genstamp will be different than that of valid block. // In all these cases we can delete the replica. - // In case of 3, rbw block will be deleted and valid block can be replicated + // In case 3, rbw block will be deleted and valid block can be replicated. + // Note NN only becomes aware of corrupt blocks when the block report is sent, + // this means that by default it can take up to 6 hours for a corrupt block to + // be invalidated, after which the valid block can be replicated. if (hasEnoughLiveReplicas || hasMoreCorruptReplicas || corruptedDuringWrite) { if (b.getStored().isStriped()) { @@ -3656,7 +3662,7 @@ private Block addStoredBlock(final BlockInfo block, ". blockMap has {} but corrupt replicas map has {}", storedBlock, numCorruptNodes, corruptReplicasCount); } - if ((corruptReplicasCount > 0) && (numLiveReplicas >= fileRedundancy)) { + if ((corruptReplicasCount > 0) && (numUsableReplicas >= fileRedundancy)) { invalidateCorruptReplicas(storedBlock, reportedBlock, num); } return storedBlock; diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/DatanodeManager.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/DatanodeManager.java index e75caeffef2cb..237daed0960ff 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/DatanodeManager.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/DatanodeManager.java @@ -1790,8 +1790,8 @@ private void addCacheCommands(String blockPoolId, DatanodeDescriptor nodeinfo, /** Handle heartbeat from datanodes. */ public DatanodeCommand[] handleHeartbeat(DatanodeRegistration nodeReg, StorageReport[] reports, final String blockPoolId, - long cacheCapacity, long cacheUsed, int xceiverCount, - int maxTransfers, int failedVolumes, + long cacheCapacity, long cacheUsed, int xceiverCount, + int xmitsInProgress, int failedVolumes, VolumeFailureSummary volumeFailureSummary, @Nonnull SlowPeerReports slowPeers, @Nonnull SlowDiskReports slowDisks) throws IOException { @@ -1835,6 +1835,14 @@ public DatanodeCommand[] handleHeartbeat(DatanodeRegistration nodeReg, int totalECBlocks = nodeinfo.getNumberOfBlocksToBeErasureCoded(); int totalBlocks = totalReplicateBlocks + totalECBlocks; if (totalBlocks > 0) { + int maxTransfers; + if (nodeinfo.isDecommissionInProgress()) { + maxTransfers = blockManager.getReplicationStreamsHardLimit() + - xmitsInProgress; + } else { + maxTransfers = blockManager.getMaxReplicationStreams() + - xmitsInProgress; + } int numReplicationTasks = (int) Math.ceil( (double) (totalReplicateBlocks * maxTransfers) / totalBlocks); int numECTasks = (int) Math.ceil( @@ -2248,5 +2256,9 @@ public DatanodeStorageReport[] getDatanodeStorageReport( public Map getDatanodeMap() { return datanodeMap; } -} + public void setMaxSlowPeersToReport(int maxSlowPeersToReport) { + Preconditions.checkNotNull(slowPeerTracker, "slowPeerTracker should not be un-assigned"); + slowPeerTracker.setMaxSlowPeersToReport(maxSlowPeersToReport); + } +} diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/SlowPeerTracker.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/SlowPeerTracker.java index ec47b6941ef34..e4feb4815eee4 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/SlowPeerTracker.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/blockmanagement/SlowPeerTracker.java @@ -80,7 +80,7 @@ public class SlowPeerTracker { * Number of nodes to include in JSON report. We will return nodes with * the highest number of votes from peers. */ - private final int maxNodesToReport; + private volatile int maxNodesToReport; /** * Information about peers that have reported a node as being slow. @@ -104,9 +104,8 @@ public SlowPeerTracker(Configuration conf, Timer timer) { DFSConfigKeys.DFS_DATANODE_OUTLIERS_REPORT_INTERVAL_KEY, DFSConfigKeys.DFS_DATANODE_OUTLIERS_REPORT_INTERVAL_DEFAULT, TimeUnit.MILLISECONDS) * 3; - this.maxNodesToReport = conf.getInt( - DFSConfigKeys.DFS_DATANODE_MAX_NODES_TO_REPORT_KEY, - DFSConfigKeys.DFS_DATANODE_MAX_NODES_TO_REPORT_DEFAULT); + this.setMaxSlowPeersToReport(conf.getInt(DFSConfigKeys.DFS_DATANODE_MAX_NODES_TO_REPORT_KEY, + DFSConfigKeys.DFS_DATANODE_MAX_NODES_TO_REPORT_DEFAULT)); } /** @@ -282,6 +281,10 @@ long getReportValidityMs() { return reportValidityMs; } + public synchronized void setMaxSlowPeersToReport(int maxSlowPeersToReport) { + this.maxNodesToReport = maxSlowPeersToReport; + } + private static class LatencyWithLastReportTime { private final Long time; private final OutlierMetrics latency; diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BPOfferService.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BPOfferService.java index 51a6f115ccdea..a990e1915de6f 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BPOfferService.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BPOfferService.java @@ -40,6 +40,7 @@ import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; @@ -143,11 +144,11 @@ void writeUnlock() { void refreshNNList(String serviceId, List nnIds, ArrayList addrs, ArrayList lifelineAddrs) throws IOException { - Set oldAddrs = Sets.newHashSet(); + Set oldAddrs = new HashSet<>(); for (BPServiceActor actor : bpServices) { oldAddrs.add(actor.getNNSocketAddress()); } - Set newAddrs = Sets.newHashSet(addrs); + Set newAddrs = new HashSet<>(addrs); // Process added NNs Set addedNNs = Sets.difference(newAddrs, oldAddrs); diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockPoolManager.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockPoolManager.java index 2ce81f593e33a..073576546c790 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockPoolManager.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockPoolManager.java @@ -196,8 +196,8 @@ private void doRefreshNamenodes( // Step 2. Any nameservices we currently have but are no longer present // need to be removed. - toRemove = Sets.newHashSet(Sets.difference( - bpByNameserviceId.keySet(), addrMap.keySet())); + toRemove = Sets.difference( + bpByNameserviceId.keySet(), addrMap.keySet()); assert toRefresh.size() + toAdd.size() == addrMap.size() : diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockReceiver.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockReceiver.java index 9b3a899323642..77e0be6c7b32e 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockReceiver.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/BlockReceiver.java @@ -307,6 +307,17 @@ Replica getReplica() { return replicaInfo; } + public void releaseAnyRemainingReservedSpace() { + if (replicaInfo != null) { + if (replicaInfo.getReplicaInfo().getBytesReserved() > 0) { + LOG.warn("Block {} has not released the reserved bytes. " + + "Releasing {} bytes as part of close.", replicaInfo.getBlockId(), + replicaInfo.getReplicaInfo().getBytesReserved()); + replicaInfo.releaseAllBytesReserved(); + } + } + } + /** * close files and release volume reference. */ diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/DataXceiver.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/DataXceiver.java index 9ad3e7cf32613..770410230162a 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/DataXceiver.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/DataXceiver.java @@ -951,6 +951,9 @@ public void writeBlock(final ExtendedBlock block, IOUtils.closeStream(mirrorIn); IOUtils.closeStream(replyOut); IOUtils.closeSocket(mirrorSock); + if (blockReceiver != null) { + blockReceiver.releaseAnyRemainingReservedSpace(); + } IOUtils.closeStream(blockReceiver); setCurrentBlockReceiver(null); } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/LocalReplicaInPipeline.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/LocalReplicaInPipeline.java index 99d2fc8e04ea8..24b6bd550e7b5 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/LocalReplicaInPipeline.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/LocalReplicaInPipeline.java @@ -174,6 +174,10 @@ public void releaseAllBytesReserved() { getVolume().releaseLockedMemory(bytesReserved); bytesReserved = 0; } + @Override + public void releaseReplicaInfoBytesReserved() { + bytesReserved = 0; + } @Override public void setLastChecksumAndDataLen(long dataLength, byte[] checksum) { diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/ReplicaInPipeline.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/ReplicaInPipeline.java index 174827b5a20eb..65da42d3a205a 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/ReplicaInPipeline.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/ReplicaInPipeline.java @@ -51,6 +51,11 @@ public interface ReplicaInPipeline extends Replica { */ public void releaseAllBytesReserved(); + /** + * Release the reserved space from the ReplicaInfo. + */ + void releaseReplicaInfoBytesReserved(); + /** * store the checksum for the last chunk along with the data length * @param dataLength number of bytes on disk diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/FsDatasetImpl.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/FsDatasetImpl.java index 6d049f98bb674..633eeab03e9cc 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/FsDatasetImpl.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/FsDatasetImpl.java @@ -121,7 +121,6 @@ import org.apache.hadoop.util.DiskChecker.DiskOutOfSpaceException; import org.apache.hadoop.util.Lists; import org.apache.hadoop.util.ReflectionUtils; -import org.apache.hadoop.util.Sets; import org.apache.hadoop.util.Time; import org.apache.hadoop.util.Timer; @@ -404,11 +403,7 @@ public LengthInputStream getMetaDataInputStream(ExtendedBlock b) */ private static List getInitialVolumeFailureInfos( Collection dataLocations, DataStorage storage) { - Set failedLocationSet = Sets.newHashSetWithExpectedSize( - dataLocations.size()); - for (StorageLocation sl: dataLocations) { - failedLocationSet.add(sl); - } + Set failedLocationSet = new HashSet<>(dataLocations); for (Iterator it = storage.dirIterator(); it.hasNext(); ) { Storage.StorageDirectory sd = it.next(); @@ -2022,6 +2017,9 @@ private ReplicaInfo finalizeReplica(String bpid, ReplicaInfo replicaInfo) newReplicaInfo = v.addFinalizedBlock( bpid, replicaInfo, replicaInfo, replicaInfo.getBytesReserved()); + if (replicaInfo instanceof ReplicaInPipeline) { + ((ReplicaInPipeline) replicaInfo).releaseReplicaInfoBytesReserved(); + } if (v.isTransientStorage()) { releaseLockedMemory( replicaInfo.getOriginalBytesReserved() @@ -3528,28 +3526,32 @@ public void evictBlocks(long bytesNeeded) throws IOException { ReplicaInfo replicaInfo, newReplicaInfo; final String bpid = replicaState.getBlockPoolId(); + final FsVolumeImpl lazyPersistVolume = replicaState.getLazyPersistVolume(); - try (AutoCloseableLock lock = lockManager.writeLock(LockLevel.BLOCK_POOl, bpid)) { + try (AutoCloseableLock lock = lockManager.readLock(LockLevel.BLOCK_POOl, bpid)) { replicaInfo = getReplicaInfo(replicaState.getBlockPoolId(), replicaState.getBlockId()); Preconditions.checkState(replicaInfo.getVolume().isTransientStorage()); ramDiskReplicaTracker.discardReplica(replicaState.getBlockPoolId(), replicaState.getBlockId(), false); - // Move the replica from lazyPersist/ to finalized/ on - // the target volume - newReplicaInfo = - replicaState.getLazyPersistVolume().activateSavedReplica(bpid, - replicaInfo, replicaState); - // Update the volumeMap entry. - volumeMap.add(bpid, newReplicaInfo); - - // Update metrics - datanode.getMetrics().incrRamDiskBlocksEvicted(); - datanode.getMetrics().addRamDiskBlocksEvictionWindowMs( - Time.monotonicNow() - replicaState.getCreationTime()); - if (replicaState.getNumReads() == 0) { - datanode.getMetrics().incrRamDiskBlocksEvictedWithoutRead(); + try (AutoCloseableLock lock1 = lockManager.writeLock(LockLevel.VOLUME, + bpid, lazyPersistVolume.getStorageID())) { + // Move the replica from lazyPersist/ to finalized/ on + // the target volume + newReplicaInfo = + replicaState.getLazyPersistVolume().activateSavedReplica(bpid, + replicaInfo, replicaState); + // Update the volumeMap entry. + volumeMap.add(bpid, newReplicaInfo); + + // Update metrics + datanode.getMetrics().incrRamDiskBlocksEvicted(); + datanode.getMetrics().addRamDiskBlocksEvictionWindowMs( + Time.monotonicNow() - replicaState.getCreationTime()); + if (replicaState.getNumReads() == 0) { + datanode.getMetrics().incrRamDiskBlocksEvictedWithoutRead(); + } } // Delete the block+meta files from RAM disk and release locked diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSNamesystem.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSNamesystem.java index ab3c49fc2641f..13894b4fecf8c 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSNamesystem.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/FSNamesystem.java @@ -4393,11 +4393,9 @@ HeartbeatResponse handleHeartbeat(DatanodeRegistration nodeReg, readLock(); try { //get datanode commands - final int maxTransfer = blockManager.getMaxReplicationStreams() - - xmitsInProgress; DatanodeCommand[] cmds = blockManager.getDatanodeManager().handleHeartbeat( nodeReg, reports, getBlockPoolId(), cacheCapacity, cacheUsed, - xceiverCount, maxTransfer, failedVolumes, volumeFailureSummary, + xceiverCount, xmitsInProgress, failedVolumes, volumeFailureSummary, slowPeers, slowDisks); long blockReportLeaseId = 0; if (requestFullBlockReportLease) { diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/JournalSet.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/JournalSet.java index 4729cd99f305d..19efc0d631120 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/JournalSet.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/JournalSet.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.PriorityQueue; import java.util.SortedSet; +import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArrayList; import org.slf4j.Logger; @@ -42,7 +43,6 @@ import org.apache.hadoop.hdfs.server.protocol.RemoteEditLog; import org.apache.hadoop.hdfs.server.protocol.RemoteEditLogManifest; import org.apache.hadoop.util.Lists; -import org.apache.hadoop.util.Sets; import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.util.Preconditions; @@ -677,7 +677,7 @@ public synchronized RemoteEditLogManifest getEditLogManifest(long fromTxId) { // storage directory with ancient logs. Clear out any logs we've // accumulated so far, and then skip to the next segment of logs // after the gap. - SortedSet startTxIds = Sets.newTreeSet(logsByStartTxId.keySet()); + SortedSet startTxIds = new TreeSet<>(logsByStartTxId.keySet()); startTxIds = startTxIds.tailSet(curStartTxId); if (startTxIds.isEmpty()) { break; diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/NameNode.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/NameNode.java index f57135a7fc664..c3371eefacb04 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/NameNode.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/NameNode.java @@ -124,6 +124,8 @@ import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_TRASH_INTERVAL_DEFAULT; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_TRASH_INTERVAL_KEY; import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_BLOCK_INVALIDATE_LIMIT_KEY; +import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATANODE_MAX_NODES_TO_REPORT_DEFAULT; +import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATANODE_MAX_NODES_TO_REPORT_KEY; import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATANODE_PEER_STATS_ENABLED_DEFAULT; import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATANODE_PEER_STATS_ENABLED_KEY; import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_HA_NN_NOT_BECOME_ACTIVE_IN_SAFEMODE; @@ -344,7 +346,8 @@ public enum OperationCategory { DFS_NAMENODE_BLOCKPLACEMENTPOLICY_EXCLUDE_SLOW_NODES_ENABLED_KEY, DFS_NAMENODE_MAX_SLOWPEER_COLLECT_NODES_KEY, DFS_BLOCK_INVALIDATE_LIMIT_KEY, - DFS_DATANODE_PEER_STATS_ENABLED_KEY)); + DFS_DATANODE_PEER_STATS_ENABLED_KEY, + DFS_DATANODE_MAX_NODES_TO_REPORT_KEY)); private static final String USAGE = "Usage: hdfs namenode [" + StartupOption.BACKUP.getName() + "] | \n\t[" @@ -2216,7 +2219,8 @@ protected String reconfigurePropertyImpl(String property, String newVal) } else if (property.equals(DFS_NAMENODE_AVOID_SLOW_DATANODE_FOR_READ_KEY) || (property.equals( DFS_NAMENODE_BLOCKPLACEMENTPOLICY_EXCLUDE_SLOW_NODES_ENABLED_KEY)) || (property.equals( DFS_NAMENODE_MAX_SLOWPEER_COLLECT_NODES_KEY)) || (property.equals( - DFS_DATANODE_PEER_STATS_ENABLED_KEY))) { + DFS_DATANODE_PEER_STATS_ENABLED_KEY)) || property.equals( + DFS_DATANODE_MAX_NODES_TO_REPORT_KEY)) { return reconfigureSlowNodesParameters(datanodeManager, property, newVal); } else if (property.equals(DFS_BLOCK_INVALIDATE_LIMIT_KEY)) { return reconfigureBlockInvalidateLimit(datanodeManager, property, newVal); @@ -2450,6 +2454,13 @@ String reconfigureSlowNodesParameters(final DatanodeManager datanodeManager, datanodeManager.initSlowPeerTracker(getConf(), timer, peerStatsEnabled); break; } + case DFS_DATANODE_MAX_NODES_TO_REPORT_KEY: { + int maxSlowPeersToReport = (newVal == null + ? DFS_DATANODE_MAX_NODES_TO_REPORT_DEFAULT : Integer.parseInt(newVal)); + result = Integer.toString(maxSlowPeersToReport); + datanodeManager.setMaxSlowPeersToReport(maxSlowPeersToReport); + break; + } default: { throw new IllegalArgumentException( "Unexpected property " + property + " in reconfigureSlowNodesParameters"); diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/startupprogress/package-info.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/startupprogress/package-info.java index d7d7b3d754d63..1ba0b8332d183 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/startupprogress/package-info.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/startupprogress/package-info.java @@ -17,7 +17,7 @@ */ /** - * This package provides a mechanism for tracking {@link NameNode} startup + * This package provides a mechanism for tracking NameNode startup * progress. The package models NameNode startup as a series of {@link Phase}s, * with each phase further sub-divided into multiple {@link Step}s. All phases * are coarse-grained and typically known in advance, implied by the structure of diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/sps/metrics/ExternalSPSBeanMetrics.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/sps/metrics/ExternalSPSBeanMetrics.java index 75546386f9da3..adab0e40328a9 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/sps/metrics/ExternalSPSBeanMetrics.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/sps/metrics/ExternalSPSBeanMetrics.java @@ -95,6 +95,7 @@ public int getAttemptedItemsCount() { @VisibleForTesting public void updateAttemptedItemsCount() { storagePolicySatisfier.getAttemptedItemsMonitor().getStorageMovementAttemptedItems() - .add(new StoragePolicySatisfier.AttemptedItemInfo(0, 1, 1, new HashSet<>(), 1)); + .add(new StoragePolicySatisfier.AttemptedItemInfo(0, 1, + 1, new HashSet<>(), 1)); } } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/tools/DFSAdmin.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/tools/DFSAdmin.java index a9eb552213354..1d3e8da77a3a9 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/tools/DFSAdmin.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/tools/DFSAdmin.java @@ -35,7 +35,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.TreeSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -1648,40 +1647,45 @@ public int metaSave(String[] argv, int idx) throws IOException { * @throws IOException If an error while getting datanode report */ public int printTopology() throws IOException { - DistributedFileSystem dfs = getDFS(); - final DatanodeInfo[] report = dfs.getDataNodeStats(); - - // Build a map of rack -> nodes from the datanode report - HashMap > tree = new HashMap>(); - for(DatanodeInfo dni : report) { - String location = dni.getNetworkLocation(); - String name = dni.getName(); - - if(!tree.containsKey(location)) { - tree.put(location, new TreeSet()); - } + DistributedFileSystem dfs = getDFS(); + final DatanodeInfo[] report = dfs.getDataNodeStats(); + + // Build a map of rack -> nodes from the datanode report + Map> map = new HashMap<>(); + for(DatanodeInfo dni : report) { + String location = dni.getNetworkLocation(); + String name = dni.getName(); + String dnState = dni.getAdminState().toString(); - tree.get(location).add(name); + if(!map.containsKey(location)) { + map.put(location, new HashMap<>()); } + + Map node = map.get(location); + node.put(name, dnState); + } - // Sort the racks (and nodes) alphabetically, display in order - ArrayList racks = new ArrayList(tree.keySet()); - Collections.sort(racks); + // Sort the racks (and nodes) alphabetically, display in order + List racks = new ArrayList<>(map.keySet()); + Collections.sort(racks); - for(String r : racks) { - System.out.println("Rack: " + r); - TreeSet nodes = tree.get(r); - - for(String n : nodes) { - System.out.print(" " + n); - String hostname = NetUtils.getHostNameOfIP(n); - if(hostname != null) - System.out.print(" (" + hostname + ")"); - System.out.println(); + for(String r : racks) { + System.out.println("Rack: " + r); + Map nodes = map.get(r); + + for(Map.Entry entry : nodes.entrySet()) { + String n = entry.getKey(); + System.out.print(" " + n); + String hostname = NetUtils.getHostNameOfIP(n); + if(hostname != null) { + System.out.print(" (" + hostname + ")"); } - + System.out.print(" " + entry.getValue()); System.out.println(); } + + System.out.println(); + } return 0; } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/MiniDFSCluster.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/MiniDFSCluster.java index d8d633f2c861e..484958e3c302c 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/MiniDFSCluster.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/MiniDFSCluster.java @@ -71,6 +71,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -87,7 +88,6 @@ import org.apache.hadoop.http.HttpConfig; import org.apache.hadoop.security.ssl.KeyStoreTestUtil; import org.apache.hadoop.util.Lists; -import org.apache.hadoop.util.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.hadoop.classification.InterfaceAudience; @@ -653,7 +653,7 @@ public DataNode getDatanode() { private boolean federation; private boolean checkExitOnShutdown = true; protected final int storagesPerDatanode; - private Set fileSystems = Sets.newHashSet(); + private Set fileSystems = new HashSet<>(); private List storageCap = Lists.newLinkedList(); diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestDFSUtil.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestDFSUtil.java index 9a024c3084586..e6ce29316c5b6 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestDFSUtil.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestDFSUtil.java @@ -53,6 +53,7 @@ import java.net.URISyntaxException; import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -80,7 +81,6 @@ import org.apache.hadoop.security.alias.JavaKeyStoreProvider; import org.apache.hadoop.test.GenericTestUtils; import org.apache.hadoop.test.LambdaTestUtils; -import org.apache.hadoop.util.Sets; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -1042,10 +1042,10 @@ public void testGetNNServiceRpcAddressesForNsIds() throws IOException { { Collection internal = DFSUtil.getInternalNameServices(conf); - assertEquals(Sets.newHashSet("nn1"), internal); + assertEquals(new HashSet<>(Arrays.asList("nn1")), internal); Collection all = DFSUtilClient.getNameServiceIds(conf); - assertEquals(Sets.newHashSet("nn1", "nn2"), all); + assertEquals(new HashSet<>(Arrays.asList("nn1", "nn2")), all); } Map> nnMap = DFSUtil diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestDecommission.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestDecommission.java index 670ca5fd9a6fe..0133d3aec37b1 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestDecommission.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestDecommission.java @@ -52,6 +52,7 @@ import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.BatchedRemoteIterator.BatchedEntries; +import org.apache.hadoop.hdfs.client.HdfsClientConfigKeys; import org.apache.hadoop.hdfs.client.HdfsDataInputStream; import org.apache.hadoop.hdfs.client.HdfsDataOutputStream; import org.apache.hadoop.hdfs.protocol.Block; @@ -1902,4 +1903,285 @@ private void createClusterWithDeadNodesDecommissionInProgress(final int numLiveN !BlockManagerTestUtil.isNodeHealthyForDecommissionOrMaintenance(blockManager, node) && !node.isAlive()), 500, 20000); } + + /* + This test reproduces a scenario where an under-replicated block on a decommissioning node + cannot be replicated to some datanodes because they have a corrupt replica of the block. + The test ensures that the corrupt replicas are eventually invalidated so that the + under-replicated block can be replicated to sufficient datanodes & the decommissioning + node can be decommissioned. + */ + @Test(timeout = 60000) + public void testDeleteCorruptReplicaForUnderReplicatedBlock() throws Exception { + // Constants + final Path file = new Path("/test-file"); + final int numDatanode = 3; + final short replicationFactor = 2; + final int numStoppedNodes = 2; + final int numDecommNodes = 1; + assertEquals(numDatanode, numStoppedNodes + numDecommNodes); + + // Run monitor every 5 seconds to speed up decommissioning & make the test faster + final int datanodeAdminMonitorFixedRateSeconds = 5; + getConf().setInt(MiniDFSCluster.DFS_NAMENODE_DECOMMISSION_INTERVAL_TESTING_KEY, + datanodeAdminMonitorFixedRateSeconds); + // Set block report interval to 6 hours to avoid unexpected block reports. + // The default block report interval is different for a MiniDFSCluster + getConf().setLong(DFSConfigKeys.DFS_BLOCKREPORT_INTERVAL_MSEC_KEY, + DFSConfigKeys.DFS_BLOCKREPORT_INTERVAL_MSEC_DEFAULT); + // Run the BlockManager RedundancyMonitor every 3 seconds such that the Namenode + // sends under-replication blocks for replication frequently + getConf().setLong(DFSConfigKeys.DFS_NAMENODE_REDUNDANCY_INTERVAL_SECONDS_KEY, + DFSConfigKeys.DFS_NAMENODE_REDUNDANCY_INTERVAL_SECONDS_DEFAULT); + // Ensure that the DataStreamer client will replace the bad datanode on append failure + getConf().set(HdfsClientConfigKeys.BlockWrite.ReplaceDatanodeOnFailure.POLICY_KEY, "ALWAYS"); + // Avoid having the DataStreamer client fail the append operation if datanode replacement fails + getConf() + .setBoolean(HdfsClientConfigKeys.BlockWrite.ReplaceDatanodeOnFailure.BEST_EFFORT_KEY, true); + + // References to datanodes in the cluster + // - 2 datanode will be stopped to generate corrupt block replicas & then + // restarted later to validate the corrupt replicas are invalidated + // - 1 datanode will start decommissioning to make the block under replicated + final List allNodes = new ArrayList<>(); + final List stoppedNodes = new ArrayList<>(); + final DatanodeDescriptor decommNode; + + // Create MiniDFSCluster + startCluster(1, numDatanode); + getCluster().waitActive(); + final FSNamesystem namesystem = getCluster().getNamesystem(); + final BlockManager blockManager = namesystem.getBlockManager(); + final DatanodeManager datanodeManager = blockManager.getDatanodeManager(); + final DatanodeAdminManager decomManager = datanodeManager.getDatanodeAdminManager(); + final FileSystem fs = getCluster().getFileSystem(); + + // Get DatanodeDescriptors + for (final DataNode node : getCluster().getDataNodes()) { + allNodes.add(getDatanodeDesriptor(namesystem, node.getDatanodeUuid())); + } + + // Create block with 2 FINALIZED replicas + // Note that: + // - calling hflush leaves block in state ReplicaBeingWritten + // - calling close leaves the block in state FINALIZED + // - amount of data is kept small because flush is not synchronous + LOG.info("Creating Initial Block with {} FINALIZED replicas", replicationFactor); + FSDataOutputStream out = fs.create(file, replicationFactor); + for (int i = 0; i < 512; i++) { + out.write(i); + } + out.close(); + + // Validate the block exists with expected number of replicas + assertEquals(1, blockManager.getTotalBlocks()); + BlockLocation[] blocksInFile = fs.getFileBlockLocations(file, 0, 0); + assertEquals(1, blocksInFile.length); + List replicasInBlock = Arrays.asList(blocksInFile[0].getNames()); + assertEquals(replicationFactor, replicasInBlock.size()); + + // Identify the DatanodeDescriptors associated with the 2 nodes with replicas. + // Each of nodes with a replica will be stopped later to corrupt the replica + DatanodeDescriptor decommNodeTmp = null; + for (DatanodeDescriptor node : allNodes) { + if (replicasInBlock.contains(node.getName())) { + stoppedNodes.add(node); + } else { + decommNodeTmp = node; + } + } + assertEquals(numStoppedNodes, stoppedNodes.size()); + assertNotNull(decommNodeTmp); + decommNode = decommNodeTmp; + final DatanodeDescriptor firstStoppedNode = stoppedNodes.get(0); + final DatanodeDescriptor secondStoppedNode = stoppedNodes.get(1); + LOG.info("Detected 2 nodes with replicas : {} , {}", firstStoppedNode.getXferAddr(), + secondStoppedNode.getXferAddr()); + LOG.info("Detected 1 node without replica : {}", decommNode.getXferAddr()); + + // Stop firstStoppedNode & the append to the block pipeline such that DataStreamer client: + // - detects firstStoppedNode as bad link in block pipeline + // - replaces the firstStoppedNode with decommNode in block pipeline + // The result is that: + // - secondStoppedNode & decommNode have a live block replica + // - firstStoppedNode has a corrupt replica (corrupt because of old GenStamp) + LOG.info("Stopping first node with replica {}", firstStoppedNode.getXferAddr()); + final List stoppedNodeProps = new ArrayList<>(); + MiniDFSCluster.DataNodeProperties stoppedNodeProp = + getCluster().stopDataNode(firstStoppedNode.getXferAddr()); + stoppedNodeProps.add(stoppedNodeProp); + firstStoppedNode.setLastUpdate(213); // Set last heartbeat to be in the past + // Wait for NN to detect the datanode as dead + GenericTestUtils.waitFor( + () -> 2 == datanodeManager.getNumLiveDataNodes() && 1 == datanodeManager + .getNumDeadDataNodes(), 500, 30000); + // Append to block pipeline + appendBlock(fs, file, 2); + + // Stop secondStoppedNode & the append to the block pipeline such that DataStreamer client: + // - detects secondStoppedNode as bad link in block pipeline + // - attempts to replace secondStoppedNode but cannot because there are no more live nodes + // - appends to the block pipeline containing just decommNode + // The result is that: + // - decommNode has a live block replica + // - firstStoppedNode & secondStoppedNode both have a corrupt replica + LOG.info("Stopping second node with replica {}", secondStoppedNode.getXferAddr()); + stoppedNodeProp = getCluster().stopDataNode(secondStoppedNode.getXferAddr()); + stoppedNodeProps.add(stoppedNodeProp); + secondStoppedNode.setLastUpdate(213); // Set last heartbeat to be in the past + // Wait for NN to detect the datanode as dead + GenericTestUtils.waitFor(() -> numDecommNodes == datanodeManager.getNumLiveDataNodes() + && numStoppedNodes == datanodeManager.getNumDeadDataNodes(), 500, 30000); + // Append to block pipeline + appendBlock(fs, file, 1); + + // Validate block replica locations + blocksInFile = fs.getFileBlockLocations(file, 0, 0); + assertEquals(1, blocksInFile.length); + replicasInBlock = Arrays.asList(blocksInFile[0].getNames()); + assertEquals(numDecommNodes, replicasInBlock.size()); + assertTrue(replicasInBlock.contains(decommNode.getName())); + LOG.info("Block now has 2 corrupt replicas on [{} , {}] and 1 live replica on {}", + firstStoppedNode.getXferAddr(), secondStoppedNode.getXferAddr(), decommNode.getXferAddr()); + + LOG.info("Decommission node {} with the live replica", decommNode.getXferAddr()); + final ArrayList decommissionedNodes = new ArrayList<>(); + takeNodeOutofService(0, decommNode.getDatanodeUuid(), 0, decommissionedNodes, + AdminStates.DECOMMISSION_INPROGRESS); + + // Wait for the datanode to start decommissioning + try { + GenericTestUtils.waitFor(() -> decomManager.getNumTrackedNodes() == 0 + && decomManager.getNumPendingNodes() == numDecommNodes && decommNode.getAdminState() + .equals(AdminStates.DECOMMISSION_INPROGRESS), 500, 30000); + } catch (Exception e) { + blocksInFile = fs.getFileBlockLocations(file, 0, 0); + assertEquals(1, blocksInFile.length); + replicasInBlock = Arrays.asList(blocksInFile[0].getNames()); + String errMsg = String.format("Node %s failed to start decommissioning." + + " numTrackedNodes=%d , numPendingNodes=%d , adminState=%s , nodesWithReplica=[%s]", + decommNode.getXferAddr(), decomManager.getNumTrackedNodes(), + decomManager.getNumPendingNodes(), decommNode.getAdminState(), + String.join(", ", replicasInBlock)); + LOG.error(errMsg); // Do not log generic timeout exception + fail(errMsg); + } + + // Validate block replica locations + blocksInFile = fs.getFileBlockLocations(file, 0, 0); + assertEquals(1, blocksInFile.length); + replicasInBlock = Arrays.asList(blocksInFile[0].getNames()); + assertEquals(numDecommNodes, replicasInBlock.size()); + assertEquals(replicasInBlock.get(0), decommNode.getName()); + LOG.info("Block now has 2 corrupt replicas on [{} , {}] and 1 decommissioning replica on {}", + firstStoppedNode.getXferAddr(), secondStoppedNode.getXferAddr(), decommNode.getXferAddr()); + + // Restart the 2 stopped datanodes + LOG.info("Restarting stopped nodes {} , {}", firstStoppedNode.getXferAddr(), + secondStoppedNode.getXferAddr()); + for (final MiniDFSCluster.DataNodeProperties stoppedNode : stoppedNodeProps) { + assertTrue(getCluster().restartDataNode(stoppedNode)); + } + for (final MiniDFSCluster.DataNodeProperties stoppedNode : stoppedNodeProps) { + try { + getCluster().waitDatanodeFullyStarted(stoppedNode.getDatanode(), 30000); + LOG.info("Node {} Restarted", stoppedNode.getDatanode().getXferAddress()); + } catch (Exception e) { + String errMsg = String.format("Node %s Failed to Restart within 30 seconds", + stoppedNode.getDatanode().getXferAddress()); + LOG.error(errMsg); // Do not log generic timeout exception + fail(errMsg); + } + } + + // Trigger block reports for the 2 restarted nodes to ensure their corrupt + // block replicas are identified by the namenode + for (MiniDFSCluster.DataNodeProperties dnProps : stoppedNodeProps) { + DataNodeTestUtils.triggerBlockReport(dnProps.getDatanode()); + } + + // Validate the datanode is eventually decommissioned + // Some changes are needed to ensure replication/decommissioning occur in a timely manner: + // - if the namenode sends a DNA_TRANSFER before sending the DNA_INVALIDATE's then: + // - the block will enter the pendingReconstruction queue + // - this prevent the block from being sent for transfer again for some time + // - solution is to call "clearQueues" so that DNA_TRANSFER is sent again after DNA_INVALIDATE + // - need to run the check less frequently than DatanodeAdminMonitor + // such that in between "clearQueues" calls 2 things can occur: + // - DatanodeAdminMonitor runs which sets the block as neededReplication + // - datanode heartbeat is received which sends the DNA_TRANSFER to the node + final int checkEveryMillis = datanodeAdminMonitorFixedRateSeconds * 2 * 1000; + try { + GenericTestUtils.waitFor(() -> { + blockManager.clearQueues(); // Clear pendingReconstruction queue + return decomManager.getNumTrackedNodes() == 0 && decomManager.getNumPendingNodes() == 0 + && decommNode.getAdminState().equals(AdminStates.DECOMMISSIONED); + }, checkEveryMillis, 40000); + } catch (Exception e) { + blocksInFile = fs.getFileBlockLocations(file, 0, 0); + assertEquals(1, blocksInFile.length); + replicasInBlock = Arrays.asList(blocksInFile[0].getNames()); + String errMsg = String.format("Node %s failed to complete decommissioning." + + " numTrackedNodes=%d , numPendingNodes=%d , adminState=%s , nodesWithReplica=[%s]", + decommNode.getXferAddr(), decomManager.getNumTrackedNodes(), + decomManager.getNumPendingNodes(), decommNode.getAdminState(), + String.join(", ", replicasInBlock)); + LOG.error(errMsg); // Do not log generic timeout exception + fail(errMsg); + } + + // Validate block replica locations. + // Note that in order for decommissioning to complete the block must be + // replicated to both of the restarted datanodes; this implies that the + // corrupt replicas were invalidated on both of the restarted datanodes. + blocksInFile = fs.getFileBlockLocations(file, 0, 0); + assertEquals(1, blocksInFile.length); + replicasInBlock = Arrays.asList(blocksInFile[0].getNames()); + assertEquals(numDatanode, replicasInBlock.size()); + assertTrue(replicasInBlock.contains(decommNode.getName())); + for (final DatanodeDescriptor node : stoppedNodes) { + assertTrue(replicasInBlock.contains(node.getName())); + } + LOG.info("Block now has 2 live replicas on [{} , {}] and 1 decommissioned replica on {}", + firstStoppedNode.getXferAddr(), secondStoppedNode.getXferAddr(), decommNode.getXferAddr()); + } + + void appendBlock(final FileSystem fs, final Path file, int expectedReplicas) throws IOException { + LOG.info("Appending to the block pipeline"); + boolean failed = false; + Exception failedReason = null; + try { + FSDataOutputStream out = fs.append(file); + for (int i = 0; i < 512; i++) { + out.write(i); + } + out.close(); + } catch (Exception e) { + failed = true; + failedReason = e; + } finally { + BlockLocation[] blocksInFile = fs.getFileBlockLocations(file, 0, 0); + assertEquals(1, blocksInFile.length); + List replicasInBlock = Arrays.asList(blocksInFile[0].getNames()); + if (failed) { + String errMsg = String.format( + "Unexpected exception appending to the block pipeline." + + " nodesWithReplica=[%s]", String.join(", ", replicasInBlock)); + LOG.error(errMsg, failedReason); // Do not swallow the exception + fail(errMsg); + } else if (expectedReplicas != replicasInBlock.size()) { + String errMsg = String.format("Expecting %d replicas in block pipeline," + + " unexpectedly found %d replicas. nodesWithReplica=[%s]", expectedReplicas, + replicasInBlock.size(), String.join(", ", replicasInBlock)); + LOG.error(errMsg); + fail(errMsg); + } else { + String infoMsg = String.format( + "Successfully appended block pipeline with %d replicas." + + " nodesWithReplica=[%s]", + replicasInBlock.size(), String.join(", ", replicasInBlock)); + LOG.info(infoMsg); + } + } + } } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/net/TestDFSNetworkTopology.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/net/TestDFSNetworkTopology.java index 1d0024d44472e..6681b2e627d78 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/net/TestDFSNetworkTopology.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/net/TestDFSNetworkTopology.java @@ -26,7 +26,6 @@ import org.apache.hadoop.hdfs.server.blockmanagement.DatanodeDescriptor; import org.apache.hadoop.hdfs.server.blockmanagement.DatanodeStorageInfo; import org.apache.hadoop.net.Node; -import org.apache.hadoop.util.Sets; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -34,6 +33,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Arrays; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; @@ -309,11 +309,11 @@ public void testChooseRandomWithStorageType() throws Exception { // test the choose random can return desired storage type nodes without // exclude Set diskUnderL1 = - Sets.newHashSet("host2", "host4", "host5", "host6"); - Set archiveUnderL1 = Sets.newHashSet("host1", "host3"); - Set ramdiskUnderL1 = Sets.newHashSet("host7"); - Set ssdUnderL1 = Sets.newHashSet("host8"); - Set nvdimmUnderL1 = Sets.newHashSet("host9"); + new HashSet<>(Arrays.asList("host2", "host4", "host5", "host6")); + Set archiveUnderL1 = new HashSet<>(Arrays.asList("host1", "host3")); + Set ramdiskUnderL1 = new HashSet<>(Arrays.asList("host7")); + Set ssdUnderL1 = new HashSet<>(Arrays.asList("host8")); + Set nvdimmUnderL1 = new HashSet<>(Arrays.asList("host9")); for (int i = 0; i < 10; i++) { n = CLUSTER.chooseRandomWithStorageType("/l1", null, null, StorageType.DISK); @@ -396,7 +396,7 @@ public void testChooseRandomWithStorageTypeWithExcluded() throws Exception { assertEquals("host6", dd.getHostName()); // exclude the host on r4 (since there is only one host, no randomness here) excluded.add(n); - Set expectedSet = Sets.newHashSet("host4", "host5"); + Set expectedSet = new HashSet<>(Arrays.asList("host4", "host5")); for (int i = 0; i < 10; i++) { // under l1, there are four hosts with DISK: // /l1/d1/r1/host2, /l1/d1/r2/host4, /l1/d1/r2/host5 and /l1/d2/r3/host6 diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/qjournal/client/TestQJMWithFaults.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/qjournal/client/TestQJMWithFaults.java index 519d94af10afa..c6e9fb7aa034c 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/qjournal/client/TestQJMWithFaults.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/qjournal/client/TestQJMWithFaults.java @@ -34,6 +34,7 @@ import java.util.Map; import java.util.Random; import java.util.SortedSet; +import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; @@ -51,7 +52,6 @@ import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.ipc.ProtobufRpcEngine2; import org.apache.hadoop.test.GenericTestUtils; -import org.apache.hadoop.util.Sets; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -108,7 +108,7 @@ private static long determineMaxIpcNumber() throws Exception { qjm.format(FAKE_NSINFO, false); doWorkload(cluster, qjm); - SortedSet ipcCounts = Sets.newTreeSet(); + SortedSet ipcCounts = new TreeSet<>(); for (AsyncLogger l : qjm.getLoggerSetForTests().getLoggersForTests()) { InvocationCountingChannel ch = (InvocationCountingChannel)l; ch.waitForAllPendingCalls(); diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/qjournal/server/TestJournalNodeSync.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/qjournal/server/TestJournalNodeSync.java index bc4cf3a6ee7b1..1564e41031aba 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/qjournal/server/TestJournalNodeSync.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/qjournal/server/TestJournalNodeSync.java @@ -33,6 +33,8 @@ import org.apache.hadoop.hdfs.server.namenode.FileJournalManager.EditLogFile; import static org.apache.hadoop.hdfs.server.namenode.FileJournalManager .getLogFile; +import static org.assertj.core.api.Assertions.assertThat; + import org.apache.hadoop.hdfs.server.protocol.NamespaceInfo; import org.apache.hadoop.test.GenericTestUtils; import org.apache.hadoop.util.Lists; @@ -106,6 +108,8 @@ public void testJournalNodeSync() throws Exception { File firstJournalDir = jCluster.getJournalDir(0, jid); File firstJournalCurrentDir = new StorageDirectory(firstJournalDir) .getCurrentDir(); + assertThat(jCluster.getJournalNode(0).getRpcServer().getRpcServer().getRpcMetrics() + .getTotalRequests()).isGreaterThan(20); // Generate some edit logs and delete one. long firstTxId = generateEditLog(); diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/blockmanagement/TestDatanodeManager.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/blockmanagement/TestDatanodeManager.java index 232424d4404ec..35ff36a856ba8 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/blockmanagement/TestDatanodeManager.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/blockmanagement/TestDatanodeManager.java @@ -86,6 +86,10 @@ public class TestDatanodeManager { private static DatanodeManager mockDatanodeManager( FSNamesystem fsn, Configuration conf) throws IOException { BlockManager bm = Mockito.mock(BlockManager.class); + Mockito.when(bm.getMaxReplicationStreams()).thenReturn( + conf.getInt(DFSConfigKeys.DFS_NAMENODE_REPLICATION_MAX_STREAMS_KEY, 2)); + Mockito.when(bm.getReplicationStreamsHardLimit()).thenReturn( + conf.getInt(DFSConfigKeys.DFS_NAMENODE_REPLICATION_STREAMS_HARD_LIMIT_KEY, 2)); BlockReportLeaseManager blm = new BlockReportLeaseManager(conf); Mockito.when(bm.getBlockReportLeaseManager()).thenReturn(blm); DatanodeManager dm = new DatanodeManager(bm, fsn, conf); @@ -965,25 +969,33 @@ public void testRemoveIncludedNode() throws IOException { * @param numReplicationBlocks the number of replication blocks in the queue. * @param numECBlocks number of EC blocks in the queue. * @param maxTransfers the maxTransfer value. + * @param maxTransfersHardLimit the maxTransfer hard limit value. * @param numReplicationTasks the number of replication tasks polled from * the queue. * @param numECTasks the number of EC tasks polled from the queue. + * @param isDecommissioning if the node is in the decommissioning process. * * @throws IOException */ private void verifyPendingRecoveryTasks( int numReplicationBlocks, int numECBlocks, - int maxTransfers, int numReplicationTasks, int numECTasks) + int maxTransfers, int maxTransfersHardLimit, + int numReplicationTasks, int numECTasks, boolean isDecommissioning) throws IOException { FSNamesystem fsn = Mockito.mock(FSNamesystem.class); Mockito.when(fsn.hasWriteLock()).thenReturn(true); Configuration conf = new Configuration(); + conf.setInt(DFSConfigKeys.DFS_NAMENODE_REPLICATION_MAX_STREAMS_KEY, maxTransfers); + conf.setInt(DFSConfigKeys.DFS_NAMENODE_REPLICATION_STREAMS_HARD_LIMIT_KEY, + maxTransfersHardLimit); DatanodeManager dm = Mockito.spy(mockDatanodeManager(fsn, conf)); DatanodeDescriptor nodeInfo = Mockito.mock(DatanodeDescriptor.class); Mockito.when(nodeInfo.isRegistered()).thenReturn(true); Mockito.when(nodeInfo.getStorageInfos()) .thenReturn(new DatanodeStorageInfo[0]); + Mockito.when(nodeInfo.isDecommissionInProgress()) + .thenReturn(isDecommissioning); if (numReplicationBlocks > 0) { Mockito.when(nodeInfo.getNumberOfReplicateBlocks()) @@ -1010,7 +1022,7 @@ private void verifyPendingRecoveryTasks( DatanodeRegistration dnReg = Mockito.mock(DatanodeRegistration.class); Mockito.when(dm.getDatanode(dnReg)).thenReturn(nodeInfo); DatanodeCommand[] cmds = dm.handleHeartbeat( - dnReg, new StorageReport[1], "bp-123", 0, 0, 10, maxTransfers, 0, null, + dnReg, new StorageReport[1], "bp-123", 0, 0, 10, 0, 0, null, SlowPeerReports.EMPTY_REPORT, SlowDiskReports.EMPTY_REPORT); long expectedNumCmds = Arrays.stream( @@ -1042,11 +1054,14 @@ private void verifyPendingRecoveryTasks( @Test public void testPendingRecoveryTasks() throws IOException { // Tasks are slitted according to the ratio between queue lengths. - verifyPendingRecoveryTasks(20, 20, 20, 10, 10); - verifyPendingRecoveryTasks(40, 10, 20, 16, 4); + verifyPendingRecoveryTasks(20, 20, 20, 30, 10, 10, false); + verifyPendingRecoveryTasks(40, 10, 20, 30, 16, 4, false); // Approximately load tasks if the ratio between queue length is large. - verifyPendingRecoveryTasks(400, 1, 20, 20, 1); + verifyPendingRecoveryTasks(400, 1, 20, 30, 20, 1, false); + + // Tasks use dfs.namenode.replication.max-streams-hard-limit for decommissioning node + verifyPendingRecoveryTasks(30, 30, 20, 30, 15, 15, true); } @Test diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/SimulatedFSDataset.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/SimulatedFSDataset.java index 29eb051cb0210..e66b62e4e51fe 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/SimulatedFSDataset.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/SimulatedFSDataset.java @@ -356,6 +356,10 @@ synchronized public void setBytesAcked(long bytesAcked) { public void releaseAllBytesReserved() { } + @Override + public void releaseReplicaInfoBytesReserved() { + } + @Override synchronized public long getBytesOnDisk() { if (finalized) { @@ -418,7 +422,6 @@ public void waitForMinLength(long minLength, long time, TimeUnit unit) } while (deadLine > System.currentTimeMillis()); throw new IOException("Minimum length was not achieved within timeout"); } - @Override public FsVolumeSpi getVolume() { return getStorage(theBlock).getVolume(); diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/TestRefreshNamenodes.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/TestRefreshNamenodes.java index 60d4cca059ac9..1f18b7bde0f4f 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/TestRefreshNamenodes.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/TestRefreshNamenodes.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.net.InetSocketAddress; +import java.util.HashSet; import java.util.Set; import org.apache.hadoop.conf.Configuration; @@ -73,13 +74,13 @@ public void testRefreshNamenodes() throws IOException { // Ensure a BPOfferService in the datanodes corresponds to // a namenode in the cluster - Set nnAddrsFromCluster = Sets.newHashSet(); + Set nnAddrsFromCluster = new HashSet<>(); for (int i = 0; i < 4; i++) { assertTrue(nnAddrsFromCluster.add( cluster.getNameNode(i).getNameNodeAddress())); } - Set nnAddrsFromDN = Sets.newHashSet(); + Set nnAddrsFromDN = new HashSet<>(); for (BPOfferService bpos : dn.getAllBpOs()) { for (BPServiceActor bpsa : bpos.getBPServiceActors()) { assertTrue(nnAddrsFromDN.add(bpsa.getNNSocketAddress())); diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/extdataset/ExternalReplicaInPipeline.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/extdataset/ExternalReplicaInPipeline.java index 084caf038c3db..460d1c1eb7d4e 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/extdataset/ExternalReplicaInPipeline.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/extdataset/ExternalReplicaInPipeline.java @@ -45,6 +45,10 @@ public long getBytesAcked() { public void setBytesAcked(long bytesAcked) { } + @Override + public void releaseReplicaInfoBytesReserved() { + } + @Override public void releaseAllBytesReserved() { } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/TestSpaceReservation.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/TestSpaceReservation.java index a702cec7cb0ca..de4d236617374 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/TestSpaceReservation.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/TestSpaceReservation.java @@ -18,8 +18,14 @@ package org.apache.hadoop.hdfs.server.datanode.fsdataset.impl; +import java.util.Collection; +import java.util.EnumSet; import java.util.function.Supplier; +import org.apache.hadoop.fs.CreateFlag; +import org.apache.hadoop.fs.permission.FsPermission; +import org.apache.hadoop.hdfs.protocol.ExtendedBlock; +import org.apache.hadoop.hdfs.server.datanode.ReplicaInfo; import org.apache.hadoop.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -746,4 +752,48 @@ public Boolean get() { }, 500, 30000); checkReservedSpace(0); } + + /** + * Ensure that bytes reserved of ReplicaInfo gets cleared + * during finalize. + * + * @throws IOException + */ + @Test(timeout = 300000) + public void testReplicaInfoBytesReservedReleasedOnFinalize() throws IOException { + short replication = 3; + int bufferLength = 4096; + startCluster(BLOCK_SIZE, replication, -1); + + String methodName = GenericTestUtils.getMethodName(); + Path path = new Path("/" + methodName + ".01.dat"); + + FSDataOutputStream fos = + fs.create(path, FsPermission.getFileDefault(), EnumSet.of(CreateFlag.CREATE), bufferLength, + replication, BLOCK_SIZE, null); + // Allocate a block. + fos.write(new byte[bufferLength]); + fos.hsync(); + + DataNode dataNode = cluster.getDataNodes().get(0); + FsDatasetImpl fsDataSetImpl = (FsDatasetImpl) dataNode.getFSDataset(); + long expectedReservedSpace = BLOCK_SIZE - bufferLength; + + String bpid = cluster.getNamesystem().getBlockPoolId(); + Collection replicas = FsDatasetTestUtil.getReplicas(fsDataSetImpl, bpid); + ReplicaInfo r = replicas.iterator().next(); + + // Verify Initial Bytes Reserved for Replica and Volume are correct + assertEquals(fsDataSetImpl.getVolumeList().get(0).getReservedForReplicas(), + expectedReservedSpace); + assertEquals(r.getBytesReserved(), expectedReservedSpace); + + // Verify Bytes Reserved for Replica and Volume are correct after finalize + fsDataSetImpl.finalizeNewReplica(r, new ExtendedBlock(bpid, r)); + + assertEquals(fsDataSetImpl.getVolumeList().get(0).getReservedForReplicas(), 0L); + assertEquals(r.getBytesReserved(), 0L); + + fos.close(); + } } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/FSImageTestUtil.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/FSImageTestUtil.java index 7f4a0ce54197d..0334cb8a45f37 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/FSImageTestUtil.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/FSImageTestUtil.java @@ -416,7 +416,7 @@ public static void assertFileContentsSame(File... files) throws Exception { if (files.length < 2) return; Map md5s = getFileMD5s(files); - if (Sets.newHashSet(md5s.values()).size() > 1) { + if (new HashSet<>(md5s.values()).size() > 1) { fail("File contents differed:\n " + Joiner.on("\n ") .withKeyValueSeparator("=") @@ -433,7 +433,8 @@ public static void assertFileContentsDifferent( File... files) throws Exception { Map md5s = getFileMD5s(files); - if (Sets.newHashSet(md5s.values()).size() != expectedUniqueHashes) { + int uniqueHashes = new HashSet<>(md5s.values()).size(); + if (uniqueHashes != expectedUniqueHashes) { fail("Expected " + expectedUniqueHashes + " different hashes, got:\n " + Joiner.on("\n ") .withKeyValueSeparator("=") diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestFsck.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestFsck.java index ae1021da0db4d..7bb288809dea9 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestFsck.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestFsck.java @@ -50,6 +50,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; @@ -118,7 +119,6 @@ import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.test.GenericTestUtils; -import org.apache.hadoop.util.Sets; import org.apache.hadoop.util.ToolRunner; import org.apache.log4j.Level; import org.apache.log4j.Logger; @@ -386,15 +386,15 @@ public void testFsckMove() throws Exception { cluster.getNameNodePort()), conf); String[] fileNames = util.getFileNames(topDir); CorruptedTestFile[] ctFiles = new CorruptedTestFile[]{ - new CorruptedTestFile(fileNames[0], Sets.newHashSet(0), + new CorruptedTestFile(fileNames[0], new HashSet<>(Arrays.asList(0)), dfsClient, numDatanodes, dfsBlockSize), - new CorruptedTestFile(fileNames[1], Sets.newHashSet(2, 3), + new CorruptedTestFile(fileNames[1], new HashSet<>(Arrays.asList(2, 3)), dfsClient, numDatanodes, dfsBlockSize), - new CorruptedTestFile(fileNames[2], Sets.newHashSet(4), + new CorruptedTestFile(fileNames[2], new HashSet<>(Arrays.asList(4)), dfsClient, numDatanodes, dfsBlockSize), - new CorruptedTestFile(fileNames[3], Sets.newHashSet(0, 1, 2, 3), + new CorruptedTestFile(fileNames[3], new HashSet<>(Arrays.asList(0, 1, 2, 3)), dfsClient, numDatanodes, dfsBlockSize), - new CorruptedTestFile(fileNames[4], Sets.newHashSet(1, 2, 3, 4), + new CorruptedTestFile(fileNames[4], new HashSet<>(Arrays.asList(1, 2, 3, 4)), dfsClient, numDatanodes, dfsBlockSize) }; int totalMissingBlocks = 0; @@ -2215,7 +2215,7 @@ public void testFsckMoveAfterCorruption() throws Exception { new InetSocketAddress("localhost", cluster.getNameNodePort()), conf); final String blockFileToCorrupt = fileNames[0]; final CorruptedTestFile ctf = new CorruptedTestFile(blockFileToCorrupt, - Sets.newHashSet(0), dfsClient, numDatanodes, dfsBlockSize); + new HashSet<>(Arrays.asList(0)), dfsClient, numDatanodes, dfsBlockSize); ctf.corruptBlocks(cluster); // Wait for fsck to discover all the missing blocks diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNameNodeReconfigure.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNameNodeReconfigure.java index 7a3b9910553ab..d048429814656 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNameNodeReconfigure.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNameNodeReconfigure.java @@ -19,11 +19,14 @@ package org.apache.hadoop.hdfs.server.namenode; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import org.junit.Test; import org.junit.Before; import org.junit.After; +import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATANODE_MAX_NODES_TO_REPORT_KEY; import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_IMAGE_PARALLEL_LOAD_KEY; import static org.junit.Assert.*; @@ -40,7 +43,9 @@ import org.apache.hadoop.hdfs.HdfsConfiguration; import org.apache.hadoop.hdfs.server.blockmanagement.DatanodeManager; import org.apache.hadoop.hdfs.server.blockmanagement.BlockManager; +import org.apache.hadoop.hdfs.server.blockmanagement.SlowPeerTracker; import org.apache.hadoop.hdfs.server.namenode.sps.StoragePolicySatisfyManager; +import org.apache.hadoop.hdfs.server.protocol.OutlierMetrics; import org.apache.hadoop.ipc.RemoteException; import org.apache.hadoop.test.GenericTestUtils; @@ -513,6 +518,55 @@ public void testSlowPeerTrackerEnabled() throws Exception { } + @Test + public void testSlowPeerMaxNodesToReportReconf() throws Exception { + final NameNode nameNode = cluster.getNameNode(); + final DatanodeManager datanodeManager = nameNode.namesystem.getBlockManager() + .getDatanodeManager(); + nameNode.reconfigurePropertyImpl(DFS_DATANODE_PEER_STATS_ENABLED_KEY, "true"); + assertTrue("SlowNode tracker is still disabled. Reconfiguration could not be successful", + datanodeManager.getSlowPeerTracker().isSlowPeerTrackerEnabled()); + + SlowPeerTracker tracker = datanodeManager.getSlowPeerTracker(); + + OutlierMetrics outlierMetrics1 = new OutlierMetrics(0.0, 0.0, 0.0, 1.1); + tracker.addReport("node1", "node70", outlierMetrics1); + OutlierMetrics outlierMetrics2 = new OutlierMetrics(0.0, 0.0, 0.0, 1.23); + tracker.addReport("node2", "node71", outlierMetrics2); + OutlierMetrics outlierMetrics3 = new OutlierMetrics(0.0, 0.0, 0.0, 2.13); + tracker.addReport("node3", "node72", outlierMetrics3); + OutlierMetrics outlierMetrics4 = new OutlierMetrics(0.0, 0.0, 0.0, 1.244); + tracker.addReport("node4", "node73", outlierMetrics4); + OutlierMetrics outlierMetrics5 = new OutlierMetrics(0.0, 0.0, 0.0, 0.2); + tracker.addReport("node5", "node74", outlierMetrics4); + OutlierMetrics outlierMetrics6 = new OutlierMetrics(0.0, 0.0, 0.0, 1.244); + tracker.addReport("node6", "node75", outlierMetrics4); + + String jsonReport = tracker.getJson(); + LOG.info("Retrieved slow peer json report: {}", jsonReport); + + List containReport = validatePeerReport(jsonReport); + assertEquals(1, containReport.stream().filter(reportVal -> !reportVal).count()); + + nameNode.reconfigurePropertyImpl(DFS_DATANODE_MAX_NODES_TO_REPORT_KEY, "2"); + jsonReport = tracker.getJson(); + LOG.info("Retrieved slow peer json report: {}", jsonReport); + + containReport = validatePeerReport(jsonReport); + assertEquals(4, containReport.stream().filter(reportVal -> !reportVal).count()); + } + + private List validatePeerReport(String jsonReport) { + List containReport = new ArrayList<>(); + containReport.add(jsonReport.contains("node1")); + containReport.add(jsonReport.contains("node2")); + containReport.add(jsonReport.contains("node3")); + containReport.add(jsonReport.contains("node4")); + containReport.add(jsonReport.contains("node5")); + containReport.add(jsonReport.contains("node6")); + return containReport; + } + @After public void shutDown() throws IOException { if (cluster != null) { diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNameNodeRecovery.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNameNodeRecovery.java index 51389c8336f16..8a701a31b9fb9 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNameNodeRecovery.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNameNodeRecovery.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.io.RandomAccessFile; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -49,7 +50,6 @@ import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.test.GenericTestUtils; import org.apache.hadoop.test.PathUtils; -import org.apache.hadoop.util.Sets; import org.apache.hadoop.util.StringUtils; import org.junit.Test; import org.junit.runner.RunWith; @@ -299,7 +299,7 @@ public long getLastValidTxId() { @Override public Set getValidTxIds() { - return Sets.newHashSet(0L); + return new HashSet<>(Arrays.asList(0L)); } public int getMaxOpSize() { @@ -341,7 +341,7 @@ public long getLastValidTxId() { @Override public Set getValidTxIds() { - return Sets.newHashSet(0L); + return new HashSet<>(Arrays.asList(0L)); } } @@ -387,7 +387,7 @@ public long getLastValidTxId() { @Override public Set getValidTxIds() { - return Sets.newHashSet(1L , 2L, 3L, 5L, 6L, 7L, 8L, 9L, 10L); + return new HashSet<>(Arrays.asList(1L, 2L, 3L, 5L, 6L, 7L, 8L, 9L, 10L)); } } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/ha/TestConsistentReadsObserver.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/ha/TestConsistentReadsObserver.java index decf85c06a46f..ff6c2288b538b 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/ha/TestConsistentReadsObserver.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/ha/TestConsistentReadsObserver.java @@ -20,6 +20,7 @@ import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_NAMENODE_STATE_CONTEXT_ENABLED_KEY; import static org.apache.hadoop.test.MetricsAsserts.getLongCounter; import static org.apache.hadoop.test.MetricsAsserts.getMetrics; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -123,6 +124,7 @@ public void testRequeueCall() throws Exception { + CommonConfigurationKeys.IPC_BACKOFF_ENABLE, true); NameNodeAdapter.getRpcServer(nn).refreshCallQueue(configuration); + assertThat(NameNodeAdapter.getRpcServer(nn).getTotalRequests()).isGreaterThan(0); dfs.create(testPath, (short)1).close(); assertSentTo(0); @@ -132,6 +134,7 @@ public void testRequeueCall() throws Exception { // be triggered and client should retry active NN. dfs.getFileStatus(testPath); assertSentTo(0); + assertThat(NameNodeAdapter.getRpcServer(nn).getTotalRequests()).isGreaterThan(1); // reset the original call queue NameNodeAdapter.getRpcServer(nn).refreshCallQueue(originalConf); } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/tools/TestDFSAdmin.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/tools/TestDFSAdmin.java index b4ae9bcaab2d1..3df873a51ceae 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/tools/TestDFSAdmin.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/tools/TestDFSAdmin.java @@ -33,6 +33,7 @@ import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.IPC_CLIENT_CONNECT_MAX_RETRIES_KEY; import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_BLOCK_INVALIDATE_LIMIT_KEY; import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATANODE_DATA_DIR_KEY; +import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATANODE_MAX_NODES_TO_REPORT_KEY; import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_DATANODE_PEER_STATS_ENABLED_KEY; import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_HEARTBEAT_INTERVAL_DEFAULT; import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_HEARTBEAT_INTERVAL_KEY; @@ -70,6 +71,8 @@ import org.apache.hadoop.hdfs.protocol.SystemErasureCodingPolicies; import org.apache.hadoop.hdfs.server.blockmanagement.BlockManager; import org.apache.hadoop.hdfs.server.blockmanagement.BlockManagerTestUtil; +import org.apache.hadoop.hdfs.server.blockmanagement.DatanodeDescriptor; +import org.apache.hadoop.hdfs.server.blockmanagement.DatanodeManager; import org.apache.hadoop.hdfs.server.common.Storage; import org.apache.hadoop.hdfs.server.datanode.DataNode; import org.apache.hadoop.hdfs.server.datanode.StorageLocation; @@ -435,18 +438,19 @@ public void testNameNodeGetReconfigurableProperties() throws IOException, Interr final List outs = Lists.newArrayList(); final List errs = Lists.newArrayList(); getReconfigurableProperties("namenode", address, outs, errs); - assertEquals(18, outs.size()); + assertEquals(19, outs.size()); assertTrue(outs.get(0).contains("Reconfigurable properties:")); assertEquals(DFS_BLOCK_INVALIDATE_LIMIT_KEY, outs.get(1)); assertEquals(DFS_BLOCK_PLACEMENT_EC_CLASSNAME_KEY, outs.get(2)); assertEquals(DFS_BLOCK_REPLICATOR_CLASSNAME_KEY, outs.get(3)); - assertEquals(DFS_DATANODE_PEER_STATS_ENABLED_KEY, outs.get(4)); - assertEquals(DFS_HEARTBEAT_INTERVAL_KEY, outs.get(5)); - assertEquals(DFS_IMAGE_PARALLEL_LOAD_KEY, outs.get(6)); - assertEquals(DFS_NAMENODE_AVOID_SLOW_DATANODE_FOR_READ_KEY, outs.get(7)); - assertEquals(DFS_NAMENODE_BLOCKPLACEMENTPOLICY_EXCLUDE_SLOW_NODES_ENABLED_KEY, outs.get(8)); - assertEquals(DFS_NAMENODE_HEARTBEAT_RECHECK_INTERVAL_KEY, outs.get(9)); - assertEquals(DFS_NAMENODE_MAX_SLOWPEER_COLLECT_NODES_KEY, outs.get(10)); + assertEquals(DFS_DATANODE_MAX_NODES_TO_REPORT_KEY, outs.get(4)); + assertEquals(DFS_DATANODE_PEER_STATS_ENABLED_KEY, outs.get(5)); + assertEquals(DFS_HEARTBEAT_INTERVAL_KEY, outs.get(6)); + assertEquals(DFS_IMAGE_PARALLEL_LOAD_KEY, outs.get(7)); + assertEquals(DFS_NAMENODE_AVOID_SLOW_DATANODE_FOR_READ_KEY, outs.get(8)); + assertEquals(DFS_NAMENODE_BLOCKPLACEMENTPOLICY_EXCLUDE_SLOW_NODES_ENABLED_KEY, outs.get(9)); + assertEquals(DFS_NAMENODE_HEARTBEAT_RECHECK_INTERVAL_KEY, outs.get(10)); + assertEquals(DFS_NAMENODE_MAX_SLOWPEER_COLLECT_NODES_KEY, outs.get(11)); assertEquals(errs.size(), 0); } @@ -520,6 +524,52 @@ public void testPrintTopology() throws Exception { } } + @Test(timeout = 30000) + public void testPrintTopologyWithStatus() throws Exception { + redirectStream(); + final Configuration dfsConf = new HdfsConfiguration(); + final File baseDir = new File( + PathUtils.getTestDir(getClass()), + GenericTestUtils.getMethodName()); + dfsConf.set(MiniDFSCluster.HDFS_MINIDFS_BASEDIR, baseDir.getAbsolutePath()); + + final int numDn = 4; + final String[] racks = { + "/d1/r1", "/d1/r2", + "/d2/r1", "/d2/r2"}; + + try (MiniDFSCluster miniCluster = new MiniDFSCluster.Builder(dfsConf) + .numDataNodes(numDn).racks(racks).build()) { + miniCluster.waitActive(); + assertEquals(numDn, miniCluster.getDataNodes().size()); + + DatanodeManager dm = miniCluster.getNameNode().getNamesystem(). + getBlockManager().getDatanodeManager(); + DatanodeDescriptor maintenanceNode = dm.getDatanode( + miniCluster.getDataNodes().get(1).getDatanodeId()); + maintenanceNode.setInMaintenance(); + DatanodeDescriptor demissionNode = dm.getDatanode( + miniCluster.getDataNodes().get(2).getDatanodeId()); + demissionNode.setDecommissioned(); + + final DFSAdmin dfsAdmin = new DFSAdmin(dfsConf); + + resetStream(); + final int ret = ToolRunner.run(dfsAdmin, new String[] {"-printTopology"}); + + /* collect outputs */ + final List outs = Lists.newArrayList(); + scanIntoList(out, outs); + + /* verify results */ + assertEquals(0, ret); + assertTrue(outs.get(1).contains(DatanodeInfo.AdminStates.NORMAL.toString())); + assertTrue(outs.get(4).contains(DatanodeInfo.AdminStates.IN_MAINTENANCE.toString())); + assertTrue(outs.get(7).contains(DatanodeInfo.AdminStates.DECOMMISSIONED.toString())); + assertTrue(outs.get(10).contains(DatanodeInfo.AdminStates.NORMAL.toString())); + } + } + @Test(timeout = 30000) public void testNameNodeGetReconfigurationStatus() throws IOException, InterruptedException, TimeoutException { diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/security/TestRefreshUserMappings.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/security/TestRefreshUserMappings.java index d410d3b045500..6806b6715b159 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/security/TestRefreshUserMappings.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/security/TestRefreshUserMappings.java @@ -34,6 +34,8 @@ import java.net.URL; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -47,7 +49,6 @@ import org.apache.hadoop.security.authorize.DefaultImpersonationProvider; import org.apache.hadoop.security.authorize.ProxyUsers; import org.apache.hadoop.test.GenericTestUtils; -import org.apache.hadoop.util.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; @@ -93,7 +94,7 @@ public Set getGroupsSet(String user) { LOG.info("Getting groups in MockUnixGroupsMapping"); String g1 = user + (10 * i + 1); String g2 = user + (10 * i + 2); - Set s = Sets.newHashSet(g1, g2); + Set s = new HashSet<>(Arrays.asList(g1, g2)); i++; return s; } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/resources/testHDFSConf.xml b/hadoop-hdfs-project/hadoop-hdfs/src/test/resources/testHDFSConf.xml index 6a897aa609215..e6bf314a23d9c 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/resources/testHDFSConf.xml +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/resources/testHDFSConf.xml @@ -16723,19 +16723,19 @@ RegexpAcrossOutputComparator - ^Rack: \/rack1\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\) + ^Rack: \/rack1\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\sIn Service\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\sIn Service RegexpAcrossOutputComparator - Rack: \/rack2\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\) + Rack: \/rack2\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\sIn Service\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\sIn Service\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\sIn Service RegexpAcrossOutputComparator - Rack: \/rack3\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\) + Rack: \/rack3\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\sIn Service RegexpAcrossOutputComparator - Rack: \/rack4\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\) + Rack: \/rack4\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\sIn Service\s*127\.0\.0\.1:\d+\s\([-.a-zA-Z0-9]+\)\sIn Service diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapred/TaskAttemptListenerImpl.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapred/TaskAttemptListenerImpl.java index 7d151adea7d6c..5dffd735fdafd 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapred/TaskAttemptListenerImpl.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapred/TaskAttemptListenerImpl.java @@ -28,6 +28,10 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonConfigurationKeysPublic; import org.apache.hadoop.ipc.ProtocolSignature; @@ -50,8 +54,8 @@ import org.apache.hadoop.mapreduce.v2.app.job.event.TaskAttemptEvent; import org.apache.hadoop.mapreduce.v2.app.job.event.TaskAttemptEventType; import org.apache.hadoop.mapreduce.v2.app.job.event.TaskAttemptFailEvent; -import org.apache.hadoop.mapreduce.v2.app.job.event.TaskAttemptStatusUpdateEvent.TaskAttemptStatus; import org.apache.hadoop.mapreduce.v2.app.job.event.TaskAttemptStatusUpdateEvent; +import org.apache.hadoop.mapreduce.v2.app.job.event.TaskAttemptStatusUpdateEvent.TaskAttemptStatus; import org.apache.hadoop.mapreduce.v2.app.rm.RMHeartbeatHandler; import org.apache.hadoop.mapreduce.v2.app.rm.preemption.AMPreemptionPolicy; import org.apache.hadoop.mapreduce.v2.app.security.authorize.MRAMPolicyProvider; @@ -61,10 +65,6 @@ import org.apache.hadoop.util.StringInterner; import org.apache.hadoop.util.Time; import org.apache.hadoop.yarn.exceptions.YarnRuntimeException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.classification.VisibleForTesting; /** * This class is responsible for talking to the task umblical. @@ -409,6 +409,11 @@ public AMFeedback statusUpdate(TaskAttemptID taskAttemptID, if (LOG.isDebugEnabled()) { LOG.debug("Ping from " + taskAttemptID.toString()); } + // Consider ping from the tasks for liveliness check + if (getConfig().getBoolean(MRJobConfig.MR_TASK_ENABLE_PING_FOR_LIVELINESS_CHECK, + MRJobConfig.DEFAULT_MR_TASK_ENABLE_PING_FOR_LIVELINESS_CHECK)) { + taskHeartbeatHandler.progressing(yarnAttemptID); + } return feedback; } diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/test/java/org/apache/hadoop/mapred/TestTaskAttemptListenerImpl.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/test/java/org/apache/hadoop/mapred/TestTaskAttemptListenerImpl.java index f8b8c6ccdf1de..b5a7694e4cc6b 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/test/java/org/apache/hadoop/mapred/TestTaskAttemptListenerImpl.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/test/java/org/apache/hadoop/mapred/TestTaskAttemptListenerImpl.java @@ -17,23 +17,30 @@ */ package org.apache.hadoop.mapred; -import java.util.function.Supplier; -import org.apache.hadoop.mapred.Counters.Counter; -import org.apache.hadoop.mapreduce.checkpoint.EnumCounter; - import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; +import org.apache.hadoop.mapred.Counters.Counter; import org.apache.hadoop.mapreduce.MRJobConfig; import org.apache.hadoop.mapreduce.TaskType; import org.apache.hadoop.mapreduce.TypeConverter; import org.apache.hadoop.mapreduce.checkpoint.CheckpointID; +import org.apache.hadoop.mapreduce.checkpoint.EnumCounter; import org.apache.hadoop.mapreduce.checkpoint.FSCheckpointID; import org.apache.hadoop.mapreduce.checkpoint.TaskCheckpointID; import org.apache.hadoop.mapreduce.security.token.JobTokenSecretManager; @@ -48,9 +55,9 @@ import org.apache.hadoop.mapreduce.v2.app.job.Job; import org.apache.hadoop.mapreduce.v2.app.job.event.TaskAttemptStatusUpdateEvent; import org.apache.hadoop.mapreduce.v2.app.job.event.TaskAttemptStatusUpdateEvent.TaskAttemptStatus; +import org.apache.hadoop.mapreduce.v2.app.rm.RMHeartbeatHandler; import org.apache.hadoop.mapreduce.v2.app.rm.preemption.AMPreemptionPolicy; import org.apache.hadoop.mapreduce.v2.app.rm.preemption.CheckpointAMPreemptionPolicy; -import org.apache.hadoop.mapreduce.v2.app.rm.RMHeartbeatHandler; import org.apache.hadoop.mapreduce.v2.util.MRBuilderUtils; import org.apache.hadoop.test.GenericTestUtils; import org.apache.hadoop.yarn.event.Dispatcher; @@ -60,17 +67,22 @@ import org.apache.hadoop.yarn.factory.providers.RecordFactoryProvider; import org.apache.hadoop.yarn.util.ControlledClock; import org.apache.hadoop.yarn.util.SystemClock; -import org.junit.After; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Tests the behavior of TaskAttemptListenerImpl. @@ -417,6 +429,23 @@ public void testStatusUpdateProgress() verify(hbHandler).progressing(eq(attemptId)); } + @Test + public void testPingUpdateProgress() throws IOException, InterruptedException { + configureMocks(); + Configuration conf = new Configuration(); + conf.setBoolean(MRJobConfig.MR_TASK_ENABLE_PING_FOR_LIVELINESS_CHECK, true); + listener.init(conf); + listener.start(); + listener.registerPendingTask(task, wid); + listener.registerLaunchedTask(attemptId, wid); + verify(hbHandler).register(attemptId); + + // make sure a ping does report progress + AMFeedback feedback = listener.statusUpdate(attemptID, null); + assertTrue(feedback.getTaskFound()); + verify(hbHandler, times(1)).progressing(eq(attemptId)); + } + @Test public void testSingleStatusUpdate() throws IOException, InterruptedException { diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-common/src/test/java/org/apache/hadoop/mapred/TestLocalDistributedCacheManager.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-common/src/test/java/org/apache/hadoop/mapred/TestLocalDistributedCacheManager.java index 5d1c669e50083..50cc63094bd8c 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-common/src/test/java/org/apache/hadoop/mapred/TestLocalDistributedCacheManager.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-common/src/test/java/org/apache/hadoop/mapred/TestLocalDistributedCacheManager.java @@ -21,7 +21,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -31,9 +30,11 @@ import java.io.IOException; import java.net.URI; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.ArrayList; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -44,22 +45,31 @@ import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FilterFileSystem; +import org.apache.hadoop.fs.FutureDataInputStreamBuilder; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.PositionedReadable; import org.apache.hadoop.fs.Seekable; +import org.apache.hadoop.fs.impl.FutureDataInputStreamBuilderImpl; import org.apache.hadoop.fs.permission.FsPermission; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.MRConfig; import org.apache.hadoop.mapreduce.MRJobConfig; +import org.apache.hadoop.util.functional.CallableRaisingIOE; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +/** + * Test the LocalDistributedCacheManager using mocking. + * This suite is brittle to changes in the class under test. + */ @SuppressWarnings("deprecation") public class TestLocalDistributedCacheManager { + private static final byte[] TEST_DATA = "This is a test file\n".getBytes(); + private static FileSystem mockfs; public static class MockFileSystem extends FilterFileSystem { @@ -70,6 +80,14 @@ public MockFileSystem() { private File localDir; + /** + * Recursive delete of a path. + * For safety, paths of length under 5 are rejected. + * @param file path to delete. + * @throws IOException never, it is just "a dummy in the method signature" + * @throws IllegalArgumentException path too short + * @throws RuntimeException File.delete() failed. + */ private static void delete(File file) throws IOException { if (file.getAbsolutePath().length() < 5) { throw new IllegalArgumentException( @@ -109,9 +127,9 @@ public void cleanup() throws Exception { * Mock input stream based on a byte array so that it can be used by a * FSDataInputStream. */ - private static class MockInputStream extends ByteArrayInputStream + private static final class MockInputStream extends ByteArrayInputStream implements Seekable, PositionedReadable { - public MockInputStream(byte[] buf) { + private MockInputStream(byte[] buf) { super(buf); } @@ -134,47 +152,45 @@ public void testDownload() throws Exception { when(mockfs.getUri()).thenReturn(mockBase); Path working = new Path("mock://test-nn1/user/me/"); when(mockfs.getWorkingDirectory()).thenReturn(working); - when(mockfs.resolvePath(any(Path.class))).thenAnswer(new Answer() { - @Override - public Path answer(InvocationOnMock args) throws Throwable { - return (Path) args.getArguments()[0]; - } - }); + when(mockfs.resolvePath(any(Path.class))).thenAnswer( + (Answer) args -> (Path) args.getArguments()[0]); final URI file = new URI("mock://test-nn1/user/me/file.txt#link"); final Path filePath = new Path(file); File link = new File("link"); + // return a filestatus for the file "*/file.txt"; raise FNFE for anything else when(mockfs.getFileStatus(any(Path.class))).thenAnswer(new Answer() { @Override public FileStatus answer(InvocationOnMock args) throws Throwable { Path p = (Path)args.getArguments()[0]; if("file.txt".equals(p.getName())) { - return new FileStatus(201, false, 1, 500, 101, 101, - FsPermission.getDefault(), "me", "me", filePath); + return createMockTestFileStatus(filePath); } else { - throw new FileNotFoundException(p+" not supported by mocking"); + throw notMocked(p); } } }); when(mockfs.getConf()).thenReturn(conf); final FSDataInputStream in = - new FSDataInputStream(new MockInputStream("This is a test file\n".getBytes())); - when(mockfs.open(any(Path.class), anyInt())).thenAnswer(new Answer() { - @Override - public FSDataInputStream answer(InvocationOnMock args) throws Throwable { - Path src = (Path)args.getArguments()[0]; - if ("file.txt".equals(src.getName())) { - return in; - } else { - throw new FileNotFoundException(src+" not supported by mocking"); - } - } - }); + new FSDataInputStream(new MockInputStream(TEST_DATA)); + + // file.txt: return an openfile builder which will eventually return the data, + // anything else: FNFE + when(mockfs.openFile(any(Path.class))).thenAnswer( + (Answer) args -> { + Path src = (Path)args.getArguments()[0]; + if ("file.txt".equals(src.getName())) { + return new MockOpenFileBuilder(mockfs, src, + () -> CompletableFuture.completedFuture(in)); + } else { + throw notMocked(src); + } + }); Job.addCacheFile(file, conf); - Map policies = new HashMap(); + Map policies = new HashMap<>(); policies.put(file.toString(), true); Job.setFileSharedCacheUploadPolicies(conf, policies); conf.set(MRJobConfig.CACHE_FILE_TIMESTAMPS, "101"); @@ -191,6 +207,12 @@ public FSDataInputStream answer(InvocationOnMock args) throws Throwable { assertFalse(link.exists()); } + /** + * This test case sets the mock FS to raise FNFE + * on any getFileStatus/openFile calls. + * If the manager successfully starts up, it means that + * no files were probed for/opened. + */ @Test public void testEmptyDownload() throws Exception { JobID jobId = new JobID(); @@ -201,30 +223,21 @@ public void testEmptyDownload() throws Exception { when(mockfs.getUri()).thenReturn(mockBase); Path working = new Path("mock://test-nn1/user/me/"); when(mockfs.getWorkingDirectory()).thenReturn(working); - when(mockfs.resolvePath(any(Path.class))).thenAnswer(new Answer() { - @Override - public Path answer(InvocationOnMock args) throws Throwable { - return (Path) args.getArguments()[0]; - } - }); + when(mockfs.resolvePath(any(Path.class))).thenAnswer( + (Answer) args -> (Path) args.getArguments()[0]); - when(mockfs.getFileStatus(any(Path.class))).thenAnswer(new Answer() { - @Override - public FileStatus answer(InvocationOnMock args) throws Throwable { - Path p = (Path)args.getArguments()[0]; - throw new FileNotFoundException(p+" not supported by mocking"); - } - }); + when(mockfs.getFileStatus(any(Path.class))).thenAnswer( + (Answer) args -> { + Path p = (Path)args.getArguments()[0]; + throw notMocked(p); + }); when(mockfs.getConf()).thenReturn(conf); - when(mockfs.open(any(Path.class), anyInt())).thenAnswer(new Answer() { - @Override - public FSDataInputStream answer(InvocationOnMock args) throws Throwable { - Path src = (Path)args.getArguments()[0]; - throw new FileNotFoundException(src+" not supported by mocking"); - } - }); - + when(mockfs.openFile(any(Path.class))).thenAnswer( + (Answer) args -> { + Path src = (Path)args.getArguments()[0]; + throw notMocked(src); + }); conf.set(MRJobConfig.CACHE_FILES, ""); conf.set(MRConfig.LOCAL_DIR, localDir.getAbsolutePath()); LocalDistributedCacheManager manager = new LocalDistributedCacheManager(); @@ -236,6 +249,9 @@ public FSDataInputStream answer(InvocationOnMock args) throws Throwable { } + /** + * The same file can be added to the cache twice. + */ @Test public void testDuplicateDownload() throws Exception { JobID jobId = new JobID(); @@ -246,12 +262,8 @@ public void testDuplicateDownload() throws Exception { when(mockfs.getUri()).thenReturn(mockBase); Path working = new Path("mock://test-nn1/user/me/"); when(mockfs.getWorkingDirectory()).thenReturn(working); - when(mockfs.resolvePath(any(Path.class))).thenAnswer(new Answer() { - @Override - public Path answer(InvocationOnMock args) throws Throwable { - return (Path) args.getArguments()[0]; - } - }); + when(mockfs.resolvePath(any(Path.class))).thenAnswer( + (Answer) args -> (Path) args.getArguments()[0]); final URI file = new URI("mock://test-nn1/user/me/file.txt#link"); final Path filePath = new Path(file); @@ -262,32 +274,30 @@ public Path answer(InvocationOnMock args) throws Throwable { public FileStatus answer(InvocationOnMock args) throws Throwable { Path p = (Path)args.getArguments()[0]; if("file.txt".equals(p.getName())) { - return new FileStatus(201, false, 1, 500, 101, 101, - FsPermission.getDefault(), "me", "me", filePath); + return createMockTestFileStatus(filePath); } else { - throw new FileNotFoundException(p+" not supported by mocking"); + throw notMocked(p); } } }); when(mockfs.getConf()).thenReturn(conf); final FSDataInputStream in = - new FSDataInputStream(new MockInputStream("This is a test file\n".getBytes())); - when(mockfs.open(any(Path.class), anyInt())).thenAnswer(new Answer() { - @Override - public FSDataInputStream answer(InvocationOnMock args) throws Throwable { - Path src = (Path)args.getArguments()[0]; - if ("file.txt".equals(src.getName())) { - return in; - } else { - throw new FileNotFoundException(src+" not supported by mocking"); - } - } - }); + new FSDataInputStream(new MockInputStream(TEST_DATA)); + when(mockfs.openFile(any(Path.class))).thenAnswer( + (Answer) args -> { + Path src = (Path)args.getArguments()[0]; + if ("file.txt".equals(src.getName())) { + return new MockOpenFileBuilder(mockfs, src, + () -> CompletableFuture.completedFuture(in)); + } else { + throw notMocked(src); + } + }); Job.addCacheFile(file, conf); Job.addCacheFile(file, conf); - Map policies = new HashMap(); + Map policies = new HashMap<>(); policies.put(file.toString(), true); Job.setFileSharedCacheUploadPolicies(conf, policies); conf.set(MRJobConfig.CACHE_FILE_TIMESTAMPS, "101,101"); @@ -306,7 +316,7 @@ public FSDataInputStream answer(InvocationOnMock args) throws Throwable { /** * This test tries to replicate the issue with the previous version of - * {@ref LocalDistributedCacheManager} when the resulting timestamp is + * {@link LocalDistributedCacheManager} when the resulting timestamp is * identical as that in another process. Unfortunately, it is difficult * to mimic such behavior in a single process unit test. And mocking * the unique id (timestamp previously, UUID otherwise) won't prove the @@ -321,7 +331,7 @@ public void testMultipleCacheSetup() throws Exception { final int threadCount = 10; final CyclicBarrier barrier = new CyclicBarrier(threadCount); - ArrayList> setupCallable = new ArrayList<>(); + List> setupCallable = new ArrayList<>(); for (int i = 0; i < threadCount; ++i) { setupCallable.add(() -> { barrier.await(); @@ -340,4 +350,58 @@ public void testMultipleCacheSetup() throws Exception { manager.close(); } } + + /** + * Create test file status using test data as the length. + * @param filePath path to the file + * @return a file status. + */ + private FileStatus createMockTestFileStatus(final Path filePath) { + return new FileStatus(TEST_DATA.length, false, 1, 500, 101, 101, + FsPermission.getDefault(), "me", "me", filePath); + } + + /** + * Exception to throw on a not mocked path. + * @return a FileNotFoundException + */ + private FileNotFoundException notMocked(final Path p) { + return new FileNotFoundException(p + " not supported by mocking"); + } + + /** + * Openfile builder where the build operation is a l-expression + * supplied in the constructor. + */ + private static final class MockOpenFileBuilder extends + FutureDataInputStreamBuilderImpl { + + /** + * Operation to invoke to build the result. + */ + private final CallableRaisingIOE> + buildTheResult; + + /** + * Create the builder. the FS and path must be non-null. + * FileSystem.getConf() is the only method invoked of the FS by + * the superclass. + * @param fileSystem fs + * @param path path to open + * @param buildTheResult builder operation. + */ + private MockOpenFileBuilder(final FileSystem fileSystem, Path path, + final CallableRaisingIOE> buildTheResult) { + super(fileSystem, path); + this.buildTheResult = buildTheResult; + } + + @Override + public CompletableFuture build() + throws IllegalArgumentException, UnsupportedOperationException, + IOException { + return buildTheResult.apply(); + } + } + } diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/java/org/apache/hadoop/mapreduce/MRJobConfig.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/java/org/apache/hadoop/mapreduce/MRJobConfig.java index a90c58dd28b4c..15d57a6746b13 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/java/org/apache/hadoop/mapreduce/MRJobConfig.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/java/org/apache/hadoop/mapreduce/MRJobConfig.java @@ -919,6 +919,13 @@ public interface MRJobConfig { MR_AM_PREFIX + "scheduler.heartbeat.interval-ms"; public static final int DEFAULT_MR_AM_TO_RM_HEARTBEAT_INTERVAL_MS = 1000; + /** Whether to consider ping from tasks in liveliness check. */ + String MR_TASK_ENABLE_PING_FOR_LIVELINESS_CHECK = + "mapreduce.task.ping-for-liveliness-check.enabled"; + boolean DEFAULT_MR_TASK_ENABLE_PING_FOR_LIVELINESS_CHECK + = false; + + /** * If contact with RM is lost, the AM will wait MR_AM_TO_RM_WAIT_INTERVAL_MS * milliseconds before aborting. During this interval, AM will still try diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/java/org/apache/hadoop/mapreduce/lib/output/committer/manifest/files/AbstractManifestData.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/java/org/apache/hadoop/mapreduce/lib/output/committer/manifest/files/AbstractManifestData.java index 7020d5ca2d337..bf67281785c07 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/java/org/apache/hadoop/mapreduce/lib/output/committer/manifest/files/AbstractManifestData.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/java/org/apache/hadoop/mapreduce/lib/output/committer/manifest/files/AbstractManifestData.java @@ -70,7 +70,6 @@ public static Path unmarshallPath(String path) { throw new RuntimeException( "Failed to parse \"" + path + "\" : " + e, e); - } } diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/resources/mapred-default.xml b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/resources/mapred-default.xml index d315a00ba4a6c..848d33d92453b 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/resources/mapred-default.xml +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-core/src/main/resources/mapred-default.xml @@ -286,6 +286,13 @@ + + mapreduce.task.ping-for-liveliness-check.enabled + false + Whether to consider ping from tasks in liveliness check. + + + mapreduce.map.memory.mb -1 @@ -1715,7 +1722,7 @@ must specify the appropriate classpath for that archive, and the name of the archive must be present in the classpath. If mapreduce.app-submission.cross-platform is false, platform-specific - environment vairable expansion syntax would be used to construct the default + environment variable expansion syntax would be used to construct the default CLASSPATH entries. For Linux: $HADOOP_MAPRED_HOME/share/hadoop/mapreduce/*, diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-jobclient/src/test/java/org/apache/hadoop/mapreduce/security/TestJHSSecurity.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-jobclient/src/test/java/org/apache/hadoop/mapreduce/security/TestJHSSecurity.java index 6115c590d5fd6..9e58d460d1783 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-jobclient/src/test/java/org/apache/hadoop/mapreduce/security/TestJHSSecurity.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-jobclient/src/test/java/org/apache/hadoop/mapreduce/security/TestJHSSecurity.java @@ -18,7 +18,6 @@ package org.apache.hadoop.mapreduce.security; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; @@ -26,6 +25,8 @@ import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; +import org.apache.hadoop.security.token.SecretManager; +import org.apache.hadoop.test.LambdaTestUtils; import org.junit.Assert; import org.apache.hadoop.conf.Configuration; @@ -61,7 +62,7 @@ public class TestJHSSecurity { LoggerFactory.getLogger(TestJHSSecurity.class); @Test - public void testDelegationToken() throws IOException, InterruptedException { + public void testDelegationToken() throws Exception { org.apache.log4j.Logger rootLogger = LogManager.getRootLogger(); rootLogger.setLevel(Level.DEBUG); @@ -80,7 +81,7 @@ public void testDelegationToken() throws IOException, InterruptedException { final long renewInterval = 10000l; JobHistoryServer jobHistoryServer = null; - MRClientProtocol clientUsingDT = null; + MRClientProtocol clientUsingDT; long tokenFetchTime; try { jobHistoryServer = new JobHistoryServer() { @@ -155,14 +156,11 @@ protected JHSDelegationTokenSecretManager createJHSSecretManager( } Thread.sleep(50l); LOG.info("At time: " + System.currentTimeMillis() + ", token should be invalid"); - // Token should have expired. - try { - clientUsingDT.getJobReport(jobReportRequest); - fail("Should not have succeeded with an expired token"); - } catch (IOException e) { - assertTrue(e.getCause().getMessage().contains("is expired")); - } - + // Token should have expired. + final MRClientProtocol finalClientUsingDT = clientUsingDT; + LambdaTestUtils.intercept(SecretManager.InvalidToken.class, "has expired", + () -> finalClientUsingDT.getJobReport(jobReportRequest)); + // Test cancellation // Stop the existing proxy, start another. if (clientUsingDT != null) { diff --git a/hadoop-project/pom.xml b/hadoop-project/pom.xml index e27e74f835ce5..e8cb47efe4ba9 100644 --- a/hadoop-project/pom.xml +++ b/hadoop-project/pom.xml @@ -141,7 +141,7 @@ 2.0.6.1 5.2.0 2.2.21 - 2.8.9 + 2.9.0 3.2.4 3.10.6.Final 4.1.68.Final @@ -219,6 +219,7 @@ v12.22.1 v1.22.5 1.10.11 + 1.20 @@ -1589,6 +1590,16 @@ + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + org.apache.curator curator-test diff --git a/hadoop-tools/hadoop-aws/pom.xml b/hadoop-tools/hadoop-aws/pom.xml index 5583bb7ad05ec..6d9085b51a078 100644 --- a/hadoop-tools/hadoop-aws/pom.xml +++ b/hadoop-tools/hadoop-aws/pom.xml @@ -405,6 +405,39 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + + + banned-illegal-imports + process-sources + + enforce + + + + + false + Restrict mapreduce imports to committer code + + org.apache.hadoop.fs.s3a.commit.AbstractS3ACommitter + org.apache.hadoop.fs.s3a.commit.AbstractS3ACommitterFactory + org.apache.hadoop.fs.s3a.commit.S3ACommitterFactory + org.apache.hadoop.fs.s3a.commit.impl.* + org.apache.hadoop.fs.s3a.commit.magic.* + org.apache.hadoop.fs.s3a.commit.staging.* + + + org.apache.hadoop.mapreduce.** + org.apache.hadoop.mapred.** + + + + + + + diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java index 2e4f1e8361f4a..764a6adaca27d 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java @@ -1159,4 +1159,48 @@ private Constants() { * Require that all S3 access is made through Access Points. */ public static final String AWS_S3_ACCESSPOINT_REQUIRED = "fs.s3a.accesspoint.required"; + + /** + * Flag for create performance. + * This is *not* a configuration option; it is for use in the + * {code createFile()} builder. + * Value {@value}. + */ + public static final String FS_S3A_CREATE_PERFORMANCE = "fs.s3a.create.performance"; + + /** + * Prefix for adding a header to the object when created. + * The actual value must have a "." suffix and then the actual header. + * This is *not* a configuration option; it is only for use in the + * {code createFile()} builder. + * Value {@value}. + */ + public static final String FS_S3A_CREATE_HEADER = "fs.s3a.create.header"; + + /** + * What is the smallest reasonable seek in bytes such + * that we group ranges together during vectored read operation. + * Value : {@value}. + */ + public static final String AWS_S3_VECTOR_READS_MIN_SEEK_SIZE = + "fs.s3a.vectored.read.min.seek.size"; + + /** + * What is the largest merged read size in bytes such + * that we group ranges together during vectored read. + * Setting this value to 0 will disable merging of ranges. + * Value : {@value}. + */ + public static final String AWS_S3_VECTOR_READS_MAX_MERGED_READ_SIZE = + "fs.s3a.vectored.read.max.merged.size"; + + /** + * Default minimum seek in bytes during vectored reads : {@value}. + */ + public static final int DEFAULT_AWS_S3_VECTOR_READS_MIN_SEEK_SIZE = 4896; // 4K + + /** + * Default maximum read size in bytes during vectored reads : {@value}. + */ + public static final int DEFAULT_AWS_S3_VECTOR_READS_MAX_MERGED_READ_SIZE = 1253376; //1M } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ABlockOutputStream.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ABlockOutputStream.java index 6e6871751d7e4..8b1865c77c9eb 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ABlockOutputStream.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ABlockOutputStream.java @@ -40,6 +40,7 @@ import com.amazonaws.services.s3.model.PutObjectResult; import com.amazonaws.services.s3.model.UploadPartRequest; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; import org.apache.hadoop.util.Preconditions; import org.apache.hadoop.thirdparty.com.google.common.util.concurrent.Futures; import org.apache.hadoop.thirdparty.com.google.common.util.concurrent.ListenableFuture; @@ -68,6 +69,7 @@ import static org.apache.hadoop.fs.s3a.S3AUtils.*; import static org.apache.hadoop.fs.s3a.Statistic.*; import static org.apache.hadoop.fs.s3a.statistics.impl.EmptyS3AStatisticsContext.EMPTY_BLOCK_OUTPUT_STREAM_STATISTICS; +import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.trackDuration; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.trackDurationOfInvocation; import static org.apache.hadoop.io.IOUtils.cleanupWithLogger; @@ -103,6 +105,11 @@ class S3ABlockOutputStream extends OutputStream implements /** IO Statistics. */ private final IOStatistics iostatistics; + /** + * The options this instance was created with. + */ + private final BlockOutputStreamBuilder builder; + /** Total bytes for uploads submitted so far. */ private long bytesSubmitted; @@ -167,6 +174,7 @@ class S3ABlockOutputStream extends OutputStream implements S3ABlockOutputStream(BlockOutputStreamBuilder builder) throws IOException { builder.validate(); + this.builder = builder; this.key = builder.key; this.blockFactory = builder.blockFactory; this.blockSize = (int) builder.blockSize; @@ -332,6 +340,7 @@ public synchronized void write(byte[] source, int offset, int len) * initializing the upload, or if a previous operation * has failed. */ + @Retries.RetryTranslated private synchronized void uploadCurrentBlock(boolean isLast) throws IOException { Preconditions.checkState(hasActiveBlock(), "No active block"); @@ -353,6 +362,7 @@ private synchronized void uploadCurrentBlock(boolean isLast) * can take time and potentially fail. * @throws IOException failure to initialize the upload */ + @Retries.RetryTranslated private void initMultipartUpload() throws IOException { if (multiPartUpload == null) { LOG.debug("Initiating Multipart upload"); @@ -546,9 +556,15 @@ private int putObject() throws IOException { int size = block.dataSize(); final S3ADataBlocks.BlockUploadData uploadData = block.startUpload(); final PutObjectRequest putObjectRequest = uploadData.hasFile() ? - writeOperationHelper.createPutObjectRequest(key, uploadData.getFile()) - : writeOperationHelper.createPutObjectRequest(key, - uploadData.getUploadStream(), size, null); + writeOperationHelper.createPutObjectRequest( + key, + uploadData.getFile(), + builder.putOptions) + : writeOperationHelper.createPutObjectRequest( + key, + uploadData.getUploadStream(), + size, + builder.putOptions); BlockUploadProgress callback = new BlockUploadProgress( block, progressListener, now()); @@ -559,7 +575,7 @@ private int putObject() throws IOException { try { // the putObject call automatically closes the input // stream afterwards. - return writeOperationHelper.putObject(putObjectRequest); + return writeOperationHelper.putObject(putObjectRequest, builder.putOptions); } finally { cleanupWithLogger(LOG, uploadData, block); } @@ -702,8 +718,21 @@ private class MultiPartUpload { */ private IOException blockUploadFailure; + /** + * Constructor. + * Initiates the MPU request against S3. + * @param key upload destination + * @throws IOException failure + */ + + @Retries.RetryTranslated MultiPartUpload(String key) throws IOException { - this.uploadId = writeOperationHelper.initiateMultiPartUpload(key); + this.uploadId = trackDuration(statistics, + OBJECT_MULTIPART_UPLOAD_INITIATED.getSymbol(), + () -> writeOperationHelper.initiateMultiPartUpload( + key, + builder.putOptions)); + this.partETagsFutures = new ArrayList<>(2); LOG.debug("Initiated multi-part upload for {} with " + "id '{}'", writeOperationHelper, uploadId); @@ -887,7 +916,8 @@ private void complete(List partETags) uploadId, partETags, bytesSubmitted, - errorCount); + errorCount, + builder.putOptions); }); } finally { statistics.exceptionInMultipartComplete(errorCount.get()); @@ -1057,6 +1087,11 @@ public static final class BlockOutputStreamBuilder { /** is Client side Encryption enabled? */ private boolean isCSEEnabled; + /** + * Put object options. + */ + private PutObjectOptions putOptions; + private BlockOutputStreamBuilder() { } @@ -1070,6 +1105,7 @@ public void validate() { requireNonNull(statistics, "null statistics"); requireNonNull(writeOperations, "null writeOperationHelper"); requireNonNull(putTracker, "null putTracker"); + requireNonNull(putOptions, "null putOptions"); Preconditions.checkArgument(blockSize >= Constants.MULTIPART_MIN_SIZE, "Block size is too small: %s", blockSize); } @@ -1182,5 +1218,16 @@ public BlockOutputStreamBuilder withCSEEnabled(boolean value) { isCSEEnabled = value; return this; } + + /** + * Set builder value. + * @param value new value + * @return the builder + */ + public BlockOutputStreamBuilder withPutOptions( + final PutObjectOptions value) { + putOptions = value; + return this; + } } } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AFileSystem.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AFileSystem.java index 831770f4a37d1..40671e0d334cc 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AFileSystem.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AFileSystem.java @@ -22,6 +22,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InterruptedIOException; +import java.io.UncheckedIOException; import java.net.URI; import java.nio.file.AccessDeniedException; import java.text.DateFormat; @@ -91,6 +92,7 @@ import org.apache.hadoop.fs.CreateFlag; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FSDataOutputStreamBuilder; import org.apache.hadoop.fs.Globber; import org.apache.hadoop.fs.Options; import org.apache.hadoop.fs.impl.OpenFileParameters; @@ -104,6 +106,7 @@ import org.apache.hadoop.fs.s3a.impl.ContextAccessors; import org.apache.hadoop.fs.s3a.impl.CopyFromLocalOperation; import org.apache.hadoop.fs.s3a.impl.CopyOutcome; +import org.apache.hadoop.fs.s3a.impl.CreateFileBuilder; import org.apache.hadoop.fs.s3a.impl.DeleteOperation; import org.apache.hadoop.fs.s3a.impl.DirectoryPolicy; import org.apache.hadoop.fs.s3a.impl.DirectoryPolicyImpl; @@ -114,6 +117,7 @@ import org.apache.hadoop.fs.s3a.impl.MkdirOperation; import org.apache.hadoop.fs.s3a.impl.OpenFileSupport; import org.apache.hadoop.fs.s3a.impl.OperationCallbacks; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; import org.apache.hadoop.fs.s3a.impl.RenameOperation; import org.apache.hadoop.fs.s3a.impl.RequestFactoryImpl; import org.apache.hadoop.fs.s3a.impl.S3AMultipartUploaderBuilder; @@ -207,7 +211,8 @@ import static org.apache.hadoop.fs.s3a.commit.CommitConstants.FS_S3A_COMMITTER_ABORT_PENDING_UPLOADS; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.FS_S3A_COMMITTER_STAGING_ABORT_PENDING_UPLOADS; import static org.apache.hadoop.fs.s3a.impl.CallableSupplier.submit; -import static org.apache.hadoop.fs.s3a.impl.CallableSupplier.waitForCompletionIgnoringExceptions; +import static org.apache.hadoop.fs.s3a.impl.CreateFileBuilder.OPTIONS_CREATE_FILE_NO_OVERWRITE; +import static org.apache.hadoop.fs.s3a.impl.CreateFileBuilder.OPTIONS_CREATE_FILE_OVERWRITE; import static org.apache.hadoop.fs.s3a.impl.ErrorTranslation.isObjectNotFound; import static org.apache.hadoop.fs.s3a.impl.ErrorTranslation.isUnknownBucket; import static org.apache.hadoop.fs.s3a.impl.InternalConstants.AP_INACCESSIBLE; @@ -308,6 +313,10 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, * {@code openFile()}. */ private S3AInputPolicy inputPolicy; + /** Vectored IO context. */ + private VectoredIOContext vectoredIOContext; + + private long readAhead; private ChangeDetectionPolicy changeDetectionPolicy; private final AtomicBoolean closed = new AtomicBoolean(false); private volatile boolean isClosed = false; @@ -579,6 +588,7 @@ public void initialize(URI name, Configuration originalConf) longBytesOption(conf, ASYNC_DRAIN_THRESHOLD, DEFAULT_ASYNC_DRAIN_THRESHOLD, 0), inputPolicy); + vectoredIOContext = populateVectoredIOContext(conf); } catch (AmazonClientException e) { // amazon client exception: stop all services then throw the translation cleanupWithLogger(LOG, span); @@ -592,6 +602,23 @@ public void initialize(URI name, Configuration originalConf) } } + /** + * Populates the configurations related to vectored IO operation + * in the context which has to passed down to input streams. + * @param conf configuration object. + * @return VectoredIOContext. + */ + private VectoredIOContext populateVectoredIOContext(Configuration conf) { + final int minSeekVectored = (int) longBytesOption(conf, AWS_S3_VECTOR_READS_MIN_SEEK_SIZE, + DEFAULT_AWS_S3_VECTOR_READS_MIN_SEEK_SIZE, 0); + final int maxReadSizeVectored = (int) longBytesOption(conf, AWS_S3_VECTOR_READS_MAX_MERGED_READ_SIZE, + DEFAULT_AWS_S3_VECTOR_READS_MAX_MERGED_READ_SIZE, 0); + return new VectoredIOContext() + .setMinSeekForVectoredReads(minSeekVectored) + .setMaxReadSizeForVectoredReads(maxReadSizeVectored) + .build(); + } + /** * Set the client side encryption gauge to 0 or 1, indicating if CSE is * enabled through the gauge or not. @@ -1458,11 +1485,12 @@ private FSDataInputStream executeOpen( fileInformation.applyOptions(readContext); LOG.debug("Opening '{}'", readContext); return new FSDataInputStream( - new S3AInputStream( - readContext.build(), - createObjectAttributes(path, fileStatus), - createInputStreamCallbacks(auditSpan), - inputStreamStats)); + new S3AInputStream( + readContext.build(), + createObjectAttributes(path, fileStatus), + createInputStreamCallbacks(auditSpan), + inputStreamStats, + unboundedThreadPool)); } /** @@ -1546,7 +1574,8 @@ protected S3AReadOpContext createReadContext( invoker, statistics, statisticsContext, - fileStatus) + fileStatus, + vectoredIOContext) .withAuditSpan(auditSpan); openFileHelper.applyDefaultOptions(roc); return roc.build(); @@ -1614,10 +1643,15 @@ public FSDataOutputStream create(Path f, FsPermission permission, boolean overwrite, int bufferSize, short replication, long blockSize, Progressable progress) throws IOException { final Path path = qualify(f); + // the span will be picked up inside the output stream return trackDurationAndSpan(INVOCATION_CREATE, path, () -> - innerCreateFile(path, permission, overwrite, bufferSize, replication, - blockSize, progress)); + innerCreateFile(path, + progress, + getActiveAuditSpan(), + overwrite + ? OPTIONS_CREATE_FILE_OVERWRITE + : OPTIONS_CREATE_FILE_NO_OVERWRITE)); } /** @@ -1625,58 +1659,68 @@ public FSDataOutputStream create(Path f, FsPermission permission, * reporting; in the active span. * Retry policy: retrying, translated on the getFileStatus() probe. * No data is uploaded to S3 in this call, so no retry issues related to that. + * The "performance" flag disables safety checks for the path being a file, + * parent directory existing, and doesn't attempt to delete + * dir markers, irrespective of FS settings. + * If true, this method call does no IO at all. * @param path the file name to open - * @param permission the permission to set. - * @param overwrite if a file with this name already exists, then if true, - * the file will be overwritten, and if false an error will be thrown. - * @param bufferSize the size of the buffer to be used. - * @param replication required block replication for the file. - * @param blockSize the requested block size. * @param progress the progress reporter. + * @param auditSpan audit span + * @param options options for the file * @throws IOException in the event of IO related errors. - * @see #setPermission(Path, FsPermission) */ @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") @Retries.RetryTranslated - private FSDataOutputStream innerCreateFile(Path path, - FsPermission permission, - boolean overwrite, - int bufferSize, - short replication, - long blockSize, - Progressable progress) throws IOException { + private FSDataOutputStream innerCreateFile( + final Path path, + final Progressable progress, + final AuditSpan auditSpan, + final CreateFileBuilder.CreateFileOptions options) throws IOException { + auditSpan.activate(); String key = pathToKey(path); - FileStatus status = null; - try { - // get the status or throw an FNFE. - // when overwriting, there is no need to look for any existing file, - // and attempting to do so can poison the load balancers with 404 - // entries. - status = innerGetFileStatus(path, false, - overwrite - ? StatusProbeEnum.DIRECTORIES - : StatusProbeEnum.ALL); - - // if the thread reaches here, there is something at the path - if (status.isDirectory()) { - // path references a directory: automatic error - throw new FileAlreadyExistsException(path + " is a directory"); - } - if (!overwrite) { - // path references a file and overwrite is disabled - throw new FileAlreadyExistsException(path + " already exists"); + EnumSet flags = options.getFlags(); + boolean overwrite = flags.contains(CreateFlag.OVERWRITE); + boolean performance = options.isPerformance(); + boolean skipProbes = performance || isUnderMagicCommitPath(path); + if (skipProbes) { + LOG.debug("Skipping existence/overwrite checks"); + } else { + try { + // get the status or throw an FNFE. + // when overwriting, there is no need to look for any existing file, + // just a directory (for safety) + FileStatus status = innerGetFileStatus(path, false, + overwrite + ? StatusProbeEnum.DIRECTORIES + : StatusProbeEnum.ALL); + + // if the thread reaches here, there is something at the path + if (status.isDirectory()) { + // path references a directory: automatic error + throw new FileAlreadyExistsException(path + " is a directory"); + } + if (!overwrite) { + // path references a file and overwrite is disabled + throw new FileAlreadyExistsException(path + " already exists"); + } + LOG.debug("Overwriting file {}", path); + } catch (FileNotFoundException e) { + // this means there is nothing at the path; all good. } - LOG.debug("Overwriting file {}", path); - } catch (FileNotFoundException e) { - // this means the file is not found - } instrumentation.fileCreated(); - PutTracker putTracker = - committerIntegration.createTracker(path, key); - String destKey = putTracker.getDestKey(); final BlockOutputStreamStatistics outputStreamStatistics = statisticsContext.newOutputStreamStatistics(); + PutTracker putTracker = + committerIntegration.createTracker(path, key, outputStreamStatistics); + String destKey = putTracker.getDestKey(); + + // put options are derived from the path and the + // option builder. + boolean keep = performance || keepDirectoryMarkers(path); + final PutObjectOptions putOptions = + new PutObjectOptions(keep, null, options.getHeaders()); + final S3ABlockOutputStream.BlockOutputStreamBuilder builder = S3ABlockOutputStream.builder() .withKey(destKey) @@ -1686,7 +1730,7 @@ private FSDataOutputStream innerCreateFile(Path path, .withProgress(progress) .withPutTracker(putTracker) .withWriteOperations( - createWriteOperationHelper(getActiveAuditSpan())) + createWriteOperationHelper(auditSpan)) .withExecutorService( new SemaphoredDelegatingExecutor( boundedThreadPool, @@ -1697,12 +1741,12 @@ private FSDataOutputStream innerCreateFile(Path path, getConf().getBoolean( DOWNGRADE_SYNCABLE_EXCEPTIONS, DOWNGRADE_SYNCABLE_EXCEPTIONS_DEFAULT)) - .withCSEEnabled(isCSEEnabled); + .withCSEEnabled(isCSEEnabled) + .withPutOptions(putOptions); return new FSDataOutputStream( new S3ABlockOutputStream(builder), null); } - /** * Create a Write Operation Helper with the current active span. * All operations made through this helper will activate the @@ -1735,10 +1779,65 @@ public WriteOperationHelper createWriteOperationHelper(AuditSpan auditSpan) { auditSpan); } + /** + * Create instance of an FSDataOutputStreamBuilder for + * creating a file at the given path. + * @param path path to create + * @return a builder. + * @throws UncheckedIOException for problems creating the audit span + */ + @Override + @AuditEntryPoint + public FSDataOutputStreamBuilder createFile(final Path path) { + try { + final Path qualified = qualify(path); + final AuditSpan span = entryPoint(INVOCATION_CREATE_FILE, + pathToKey(qualified), + null); + return new CreateFileBuilder(this, + qualified, + new CreateFileBuilderCallbacksImpl(INVOCATION_CREATE_FILE, span)) + .create() + .overwrite(true); + } catch (IOException e) { + // catch any IOEs raised in span creation and convert to + // an UncheckedIOException + throw new UncheckedIOException(e); + } + } + + /** + * Callback for create file operations. + */ + private final class CreateFileBuilderCallbacksImpl implements + CreateFileBuilder.CreateFileBuilderCallbacks { + + private final Statistic statistic; + /** span for operations. */ + private final AuditSpan span; + + private CreateFileBuilderCallbacksImpl( + final Statistic statistic, + final AuditSpan span) { + this.statistic = statistic; + this.span = span; + } + + @Override + public FSDataOutputStream createFileFromBuilder( + final Path path, + final Progressable progress, + final CreateFileBuilder.CreateFileOptions options) throws IOException { + // the span will be picked up inside the output stream + return trackDuration(getDurationTrackerFactory(), statistic.getSymbol(), () -> + innerCreateFile(path, progress, span, options)); + } + } + /** * {@inheritDoc} - * @throws FileNotFoundException if the parent directory is not present -or - * is not a directory. + * The S3A implementations downgrades to the recursive creation, to avoid + * any race conditions with parent entries "disappearing". */ @Override @AuditEntryPoint @@ -1750,30 +1849,23 @@ public FSDataOutputStream createNonRecursive(Path p, long blockSize, Progressable progress) throws IOException { final Path path = makeQualified(p); - // this span is passed into the stream. - try (AuditSpan span = entryPoint(INVOCATION_CREATE_NON_RECURSIVE, path)) { - Path parent = path.getParent(); - // expect this to raise an exception if there is no parent dir - if (parent != null && !parent.isRoot()) { - S3AFileStatus status; - try { - // optimize for the directory existing: Call list first - status = innerGetFileStatus(parent, false, - StatusProbeEnum.DIRECTORIES); - } catch (FileNotFoundException e) { - // no dir, fall back to looking for a file - // (failure condition if true) - status = innerGetFileStatus(parent, false, - StatusProbeEnum.HEAD_ONLY); - } - if (!status.isDirectory()) { - throw new FileAlreadyExistsException("Not a directory: " + parent); - } - } - return innerCreateFile(path, permission, - flags.contains(CreateFlag.OVERWRITE), bufferSize, - replication, blockSize, progress); + + // span is created and passed in to the callbacks. + final AuditSpan span = entryPoint(INVOCATION_CREATE_NON_RECURSIVE, + pathToKey(path), + null); + // uses the CreateFileBuilder, filling it in with the relevant arguments. + final CreateFileBuilder builder = new CreateFileBuilder(this, + path, + new CreateFileBuilderCallbacksImpl(INVOCATION_CREATE_NON_RECURSIVE, span)) + .create() + .withFlags(flags) + .blockSize(blockSize) + .bufferSize(bufferSize); + if (progress != null) { + builder.progress(progress); } + return builder.build(); } /** @@ -2671,7 +2763,7 @@ private DeleteObjectsResult deleteObjects(DeleteObjectsRequest deleteRequest) */ public PutObjectRequest newPutObjectRequest(String key, ObjectMetadata metadata, File srcfile) { - return requestFactory.newPutObjectRequest(key, metadata, srcfile); + return requestFactory.newPutObjectRequest(key, metadata, null, srcfile); } /** @@ -2721,12 +2813,14 @@ public UploadInfo putObject(PutObjectRequest putObjectRequest) { * Auditing: must be inside an audit span. * Important: this call will close any input stream in the request. * @param putObjectRequest the request + * @param putOptions put object options * @return the upload initiated * @throws AmazonClientException on problems */ @VisibleForTesting - @Retries.OnceRaw("For PUT; post-PUT actions are RetryTranslated") - PutObjectResult putObjectDirect(PutObjectRequest putObjectRequest) + @Retries.OnceRaw("For PUT; post-PUT actions are RetryExceptionsSwallowed") + PutObjectResult putObjectDirect(PutObjectRequest putObjectRequest, + PutObjectOptions putOptions) throws AmazonClientException { long len = getPutRequestLength(putObjectRequest); LOG.debug("PUT {} bytes to {}", len, putObjectRequest.getKey()); @@ -2737,9 +2831,10 @@ PutObjectResult putObjectDirect(PutObjectRequest putObjectRequest) OBJECT_PUT_REQUESTS.getSymbol(), () -> s3.putObject(putObjectRequest)); incrementPutCompletedStatistics(true, len); - // update metadata + // apply any post-write actions. finishedWrite(putObjectRequest.getKey(), len, - result.getETag(), result.getVersionId()); + result.getETag(), result.getVersionId(), + putOptions); return result; } catch (SdkBaseException e) { incrementPutCompletedStatistics(false, len); @@ -3011,7 +3106,7 @@ private void createFakeDirectoryIfNecessary(Path f) // is mostly harmless to create a new one. if (!key.isEmpty() && !s3Exists(f, StatusProbeEnum.DIRECTORIES)) { LOG.debug("Creating new fake directory at {}", f); - createFakeDirectory(key); + createFakeDirectory(key, putOptionsForPath(f)); } } @@ -3026,7 +3121,7 @@ private void createFakeDirectoryIfNecessary(Path f) protected void maybeCreateFakeParentDirectory(Path path) throws IOException, AmazonClientException { Path parent = path.getParent(); - if (parent != null && !parent.isRoot()) { + if (parent != null && !parent.isRoot() && !isUnderMagicCommitPath(parent)) { createFakeDirectoryIfNecessary(parent); } } @@ -3197,6 +3292,11 @@ public UserGroupInformation getOwner() { * Make the given path and all non-existent parents into * directories. Has the semantics of Unix {@code 'mkdir -p'}. * Existence of the directory hierarchy is not an error. + * Parent elements are scanned to see if any are a file, + * except under __magic paths. + * There the FS assumes that the destination directory creation + * did that scan and that paths in job/task attempts are all + * "well formed" * @param p path to create * @param permission to apply to path * @return true if a directory was created or already existed @@ -3214,7 +3314,8 @@ public boolean mkdirs(Path p, FsPermission permission) throws IOException, new MkdirOperation( createStoreContext(), path, - createMkdirOperationCallbacks())); + createMkdirOperationCallbacks(), + isMagicCommitPath(path))); } /** @@ -3240,9 +3341,13 @@ public S3AFileStatus probePathStatus(final Path path, } @Override - public void createFakeDirectory(final String key) + public void createFakeDirectory(final Path dir, final boolean keepMarkers) throws IOException { - S3AFileSystem.this.createEmptyObject(key); + S3AFileSystem.this.createFakeDirectory( + pathToKey(dir), + keepMarkers + ? PutObjectOptions.keepingDirs() + : putOptionsForPath(dir)); } } @@ -3608,7 +3713,7 @@ public void copyLocalFileFromTo(File file, Path from, Path to) throws IOExceptio S3AFileSystem.this.invoker.retry( "putObject(" + "" + ")", to.toString(), true, - () -> executePut(putObjectRequest, progress)); + () -> executePut(putObjectRequest, progress, putOptionsForPath(to))); return null; }); @@ -3627,7 +3732,7 @@ public boolean createEmptyDir(Path path, StoreContext storeContext) new MkdirOperation( storeContext, path, - createMkdirOperationCallbacks())); + createMkdirOperationCallbacks(), false)); } } @@ -3637,14 +3742,18 @@ public boolean createEmptyDir(Path path, StoreContext storeContext) * aborted before an {@code InterruptedIOException} is thrown. * @param putObjectRequest request * @param progress optional progress callback + * @param putOptions put object options * @return the upload result * @throws InterruptedIOException if the blocking was interrupted. */ - @Retries.OnceRaw("For PUT; post-PUT actions are RetryTranslated") - UploadResult executePut(PutObjectRequest putObjectRequest, - Progressable progress) + @Retries.OnceRaw("For PUT; post-PUT actions are RetrySwallowed") + UploadResult executePut( + final PutObjectRequest putObjectRequest, + final Progressable progress, + final PutObjectOptions putOptions) throws InterruptedIOException { String key = putObjectRequest.getKey(); + long len = getPutRequestLength(putObjectRequest); UploadInfo info = putObject(putObjectRequest); Upload upload = info.getUpload(); ProgressableProgressListener listener = new ProgressableProgressListener( @@ -3652,9 +3761,10 @@ UploadResult executePut(PutObjectRequest putObjectRequest, upload.addProgressListener(listener); UploadResult result = waitForUploadCompletion(key, info); listener.uploadCompleted(); + // post-write actions - finishedWrite(key, info.getLength(), - result.getETag(), result.getVersionId()); + finishedWrite(key, len, + result.getETag(), result.getVersionId(), putOptions); return result; } @@ -3663,7 +3773,9 @@ UploadResult executePut(PutObjectRequest putObjectRequest, * If the waiting for completion is interrupted, the upload will be * aborted before an {@code InterruptedIOException} is thrown. * If the upload (or its result collection) failed, this is where - * the failure is raised as an AWS exception + * the failure is raised as an AWS exception. + * Calls {@link #incrementPutCompletedStatistics(boolean, long)} + * to update the statistics. * @param key destination key * @param uploadInfo upload to wait for * @return the upload result @@ -3985,63 +4097,64 @@ InitiateMultipartUploadResult initiateMultipartUpload( /** * Perform post-write actions. - *

+ *

* This operation MUST be called after any PUT/multipart PUT completes * successfully. - *

- * The actions include: - *
    - *
  1. - * Calling - * {@link #deleteUnnecessaryFakeDirectories(Path)} - * if directory markers are not being retained. - *
  2. - *
  3. - * Updating any metadata store with details on the newly created - * object. - *
  4. - *
+ *

+ * The actions include calling + * {@link #deleteUnnecessaryFakeDirectories(Path)} + * if directory markers are not being retained. * @param key key written to * @param length total length of file written * @param eTag eTag of the written object * @param versionId S3 object versionId of the written object + * @param putOptions put object options */ @InterfaceAudience.Private - @Retries.RetryTranslated("Except if failOnMetadataWriteError=false, in which" - + " case RetryExceptionsSwallowed") - void finishedWrite(String key, long length, String eTag, String versionId) { + @Retries.RetryExceptionsSwallowed + void finishedWrite( + String key, + long length, + String eTag, + String versionId, + PutObjectOptions putOptions) { LOG.debug("Finished write to {}, len {}. etag {}, version {}", key, length, eTag, versionId); - Path p = keyToQualifiedPath(key); Preconditions.checkArgument(length >= 0, "content length is negative"); - // kick off an async delete - CompletableFuture deletion; - if (!keepDirectoryMarkers(p)) { - deletion = submit( - unboundedThreadPool, getActiveAuditSpan(), - () -> { - deleteUnnecessaryFakeDirectories( - p.getParent() - ); - return null; - }); - } else { - deletion = null; + if (!putOptions.isKeepMarkers()) { + Path p = keyToQualifiedPath(key); + deleteUnnecessaryFakeDirectories(p.getParent()); } - - // catch up with any delete operation. - waitForCompletionIgnoringExceptions(deletion); } /** * Should we keep directory markers under the path being created * by mkdir/file creation/rename? + * This is done if marker retention is enabled for the path, + * or it is under a magic path where we are saving IOPs + * knowing that all committers are on the same code version and + * therefore marker aware. * @param path path to probe * @return true if the markers MAY be retained, * false if they MUST be deleted */ private boolean keepDirectoryMarkers(Path path) { - return directoryPolicy.keepDirectoryMarkers(path); + return directoryPolicy.keepDirectoryMarkers(path) + || isUnderMagicCommitPath(path); + } + + /** + * Should we keep directory markers under the path being created + * by mkdir/file creation/rename? + * See {@link #keepDirectoryMarkers(Path)} for the policy. + * + * @param path path to probe + * @return the options to use with the put request + */ + private PutObjectOptions putOptionsForPath(Path path) { + return keepDirectoryMarkers(path) + ? PutObjectOptions.keepingDirs() + : PutObjectOptions.deletingDirs(); } /** @@ -4078,27 +4191,32 @@ private void deleteUnnecessaryFakeDirectories(Path path) { * Create a fake directory, always ending in "/". * Retry policy: retrying; translated. * @param objectName name of directory object. + * @param putOptions put object options * @throws IOException IO failure */ @Retries.RetryTranslated - private void createFakeDirectory(final String objectName) + private void createFakeDirectory(final String objectName, + final PutObjectOptions putOptions) throws IOException { - createEmptyObject(objectName); + createEmptyObject(objectName, putOptions); } /** * Used to create an empty file that represents an empty directory. + * The policy for deleting parent dirs depends on the path, dir + * status and the putOptions value. * Retry policy: retrying; translated. * @param objectName object to create + * @param putOptions put object options * @throws IOException IO failure */ @Retries.RetryTranslated - private void createEmptyObject(final String objectName) + private void createEmptyObject(final String objectName, PutObjectOptions putOptions) throws IOException { invoker.retry("PUT 0-byte object ", objectName, true, () -> putObjectDirect(getRequestFactory() - .newDirectoryMarkerRequest(objectName))); + .newDirectoryMarkerRequest(objectName), putOptions)); incrementPutProgressStatistics(objectName, 0); instrumentation.directoryCreated(); } @@ -4207,14 +4325,26 @@ public boolean isMagicCommitEnabled() { /** * Predicate: is a path a magic commit path? - * True if magic commit is enabled and the path qualifies as special. + * True if magic commit is enabled and the path qualifies as special, + * and is not a a .pending or .pendingset file, * @param path path to examine - * @return true if the path is or is under a magic directory + * @return true if writing a file to the path triggers a "magic" write. */ public boolean isMagicCommitPath(Path path) { return committerIntegration.isMagicCommitPath(path); } + /** + * Predicate: is a path under a magic commit path? + * True if magic commit is enabled and the path is under __magic, + * irrespective of file type. + * @param path path to examine + * @return true if the path is in a magic dir and the FS has magic writes enabled. + */ + private boolean isUnderMagicCommitPath(Path path) { + return committerIntegration.isUnderMagicPath(path); + } + /** * Increments the statistic {@link Statistic#INVOCATION_GLOB_STATUS}. * Override superclass so as to disable symlink resolution as symlinks @@ -4766,9 +4896,19 @@ public boolean hasPathCapability(final Path path, final String capability) case STORE_CAPABILITY_DIRECTORY_MARKER_POLICY_KEEP: case STORE_CAPABILITY_DIRECTORY_MARKER_POLICY_DELETE: case STORE_CAPABILITY_DIRECTORY_MARKER_POLICY_AUTHORITATIVE: + return getDirectoryMarkerPolicy().hasPathCapability(path, cap); + + // keep for a magic path or if the policy retains it case STORE_CAPABILITY_DIRECTORY_MARKER_ACTION_KEEP: + return keepDirectoryMarkers(path); + // delete is the opposite of keep case STORE_CAPABILITY_DIRECTORY_MARKER_ACTION_DELETE: - return getDirectoryMarkerPolicy().hasPathCapability(path, cap); + return !keepDirectoryMarkers(path); + + // create file options + case FS_S3A_CREATE_PERFORMANCE: + case FS_S3A_CREATE_HEADER: + return true; default: return super.hasPathCapability(p, cap); @@ -4810,9 +4950,8 @@ public AWSCredentialProviderList shareCredentials(final String purpose) { /** * This is a proof of concept of a select API. * @param source path to source data - * @param expression select expression * @param options request configuration from the builder. - * @param providedStatus any passed in status + * @param fileInformation any passed in information. * @return the stream of the results * @throws IOException IO failure */ diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInputStream.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInputStream.java index 6beeb2891eea7..3069f17289119 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInputStream.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInputStream.java @@ -19,38 +19,50 @@ package org.apache.hadoop.fs.s3a; import javax.annotation.Nullable; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.IntFunction; import com.amazonaws.services.s3.model.GetObjectRequest; import com.amazonaws.services.s3.model.S3Object; import com.amazonaws.services.s3.model.S3ObjectInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import org.apache.hadoop.classification.VisibleForTesting; -import org.apache.hadoop.util.Preconditions; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.fs.CanSetReadahead; import org.apache.hadoop.fs.CanUnbuffer; import org.apache.hadoop.fs.FSExceptionMessages; -import org.apache.hadoop.fs.s3a.statistics.S3AInputStreamStatistics; +import org.apache.hadoop.fs.FSInputStream; +import org.apache.hadoop.fs.FileRange; +import org.apache.hadoop.fs.PathIOException; +import org.apache.hadoop.fs.StreamCapabilities; +import org.apache.hadoop.fs.impl.CombinedFileRange; +import org.apache.hadoop.fs.VectoredReadUtils; import org.apache.hadoop.fs.s3a.impl.ChangeTracker; +import org.apache.hadoop.fs.s3a.statistics.S3AInputStreamStatistics; +import org.apache.hadoop.fs.statistics.DurationTracker; import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsSource; -import org.apache.hadoop.fs.PathIOException; -import org.apache.hadoop.fs.StreamCapabilities; -import org.apache.hadoop.fs.FSInputStream; +import org.apache.hadoop.io.IOUtils; +import org.apache.hadoop.util.Preconditions; import org.apache.hadoop.util.functional.CallableRaisingIOE; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Closeable; -import java.io.EOFException; -import java.io.IOException; -import java.net.SocketTimeoutException; -import java.util.concurrent.CompletableFuture; - import static java.util.Objects.requireNonNull; import static org.apache.commons.lang3.StringUtils.isNotEmpty; +import static org.apache.hadoop.fs.VectoredReadUtils.isOrderedDisjoint; +import static org.apache.hadoop.fs.VectoredReadUtils.mergeSortedRanges; +import static org.apache.hadoop.fs.VectoredReadUtils.validateNonOverlappingAndReturnSortedRanges; import static org.apache.hadoop.fs.s3a.Invoker.onceTrackingDuration; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.invokeTrackingDuration; import static org.apache.hadoop.util.StringUtils.toLowerCase; @@ -88,6 +100,20 @@ public class S3AInputStream extends FSInputStream implements CanSetReadahead, * size of a buffer to create when draining the stream. */ private static final int DRAIN_BUFFER_SIZE = 16384; + /** + * This is the maximum temporary buffer size we use while + * populating the data in direct byte buffers during a vectored IO + * operation. This is to ensure that when a big range of data is + * requested in direct byte buffer doesn't leads to OOM errors. + */ + private static final int TMP_BUFFER_MAX_SIZE = 64 * 1024; + + /** + * Atomic boolean variable to stop all ongoing vectored read operation + * for this input stream. This will be set to true when the stream is + * closed or unbuffer is called. + */ + private final AtomicBoolean stopVectoredIOOperations = new AtomicBoolean(false); /** * This is the public position; the one set in {@link #seek(long)} @@ -111,6 +137,11 @@ public class S3AInputStream extends FSInputStream implements CanSetReadahead, private S3ObjectInputStream wrappedStream; private final S3AReadOpContext context; private final InputStreamCallbacks client; + + /** + * Thread pool used for vectored IO operation. + */ + private final ThreadPoolExecutor unboundedThreadPool; private final String bucket; private final String key; private final String pathStr; @@ -122,6 +153,9 @@ public class S3AInputStream extends FSInputStream implements CanSetReadahead, private S3AInputPolicy inputPolicy; private long readahead = Constants.DEFAULT_READAHEAD_RANGE; + /** Vectored IO context. */ + private final VectoredIOContext vectoredIOContext; + /** * This is the actual position within the object, used by * lazy seek to decide whether to seek on the next read or not. @@ -160,12 +194,14 @@ public class S3AInputStream extends FSInputStream implements CanSetReadahead, * @param ctx operation context * @param s3Attributes object attributes * @param client S3 client to use - * @param streamStatistics statistics for this stream + * @param streamStatistics stream io stats. + * @param unboundedThreadPool thread pool to use. */ public S3AInputStream(S3AReadOpContext ctx, - S3ObjectAttributes s3Attributes, - InputStreamCallbacks client, - S3AInputStreamStatistics streamStatistics) { + S3ObjectAttributes s3Attributes, + InputStreamCallbacks client, + S3AInputStreamStatistics streamStatistics, + ThreadPoolExecutor unboundedThreadPool) { Preconditions.checkArgument(isNotEmpty(s3Attributes.getBucket()), "No Bucket"); Preconditions.checkArgument(isNotEmpty(s3Attributes.getKey()), "No Key"); @@ -187,6 +223,8 @@ public S3AInputStream(S3AReadOpContext ctx, setInputPolicy(ctx.getInputPolicy()); setReadahead(ctx.getReadahead()); this.asyncDrainThreshold = ctx.getAsyncDrainThreshold(); + this.unboundedThreadPool = unboundedThreadPool; + this.vectoredIOContext = context.getVectoredIOContext(); } /** @@ -559,6 +597,7 @@ public synchronized void close() throws IOException { if (!closed) { closed = true; try { + stopVectoredIOOperations.set(true); // close or abort the stream; blocking awaitFuture(closeStream("close() operation", false, true)); LOG.debug("Statistics of stream {}\n{}", key, streamStatistics); @@ -834,6 +873,7 @@ public String toString() { sb.append(" remainingInCurrentRequest=") .append(remainingInCurrentRequest()); sb.append(" ").append(changeTracker); + sb.append(" ").append(vectoredIOContext); sb.append('\n').append(s); sb.append('}'); return sb.toString(); @@ -880,6 +920,313 @@ public void readFully(long position, byte[] buffer, int offset, int length) } } + /** + * {@inheritDoc}. + */ + @Override + public int minSeekForVectorReads() { + return vectoredIOContext.getMinSeekForVectorReads(); + } + + /** + * {@inheritDoc}. + */ + @Override + public int maxReadSizeForVectorReads() { + return vectoredIOContext.getMaxReadSizeForVectorReads(); + } + + /** + * {@inheritDoc} + * Vectored read implementation for S3AInputStream. + * @param ranges the byte ranges to read. + * @param allocate the function to allocate ByteBuffer. + * @throws IOException IOE if any. + */ + @Override + public void readVectored(List ranges, + IntFunction allocate) throws IOException { + + LOG.debug("Starting vectored read on path {} for ranges {} ", pathStr, ranges); + checkNotClosed(); + if (stopVectoredIOOperations.getAndSet(false)) { + LOG.debug("Reinstating vectored read operation for path {} ", pathStr); + } + List sortedRanges = validateNonOverlappingAndReturnSortedRanges(ranges); + for (FileRange range : ranges) { + validateRangeRequest(range); + CompletableFuture result = new CompletableFuture<>(); + range.setData(result); + } + + if (isOrderedDisjoint(sortedRanges, 1, minSeekForVectorReads())) { + LOG.debug("Not merging the ranges as they are disjoint"); + for (FileRange range: sortedRanges) { + ByteBuffer buffer = allocate.apply(range.getLength()); + unboundedThreadPool.submit(() -> readSingleRange(range, buffer)); + } + } else { + LOG.debug("Trying to merge the ranges as they are not disjoint"); + List combinedFileRanges = mergeSortedRanges(sortedRanges, + 1, minSeekForVectorReads(), + maxReadSizeForVectorReads()); + LOG.debug("Number of original ranges size {} , Number of combined ranges {} ", + ranges.size(), combinedFileRanges.size()); + for (CombinedFileRange combinedFileRange: combinedFileRanges) { + unboundedThreadPool.submit( + () -> readCombinedRangeAndUpdateChildren(combinedFileRange, allocate)); + } + } + LOG.debug("Finished submitting vectored read to threadpool" + + " on path {} for ranges {} ", pathStr, ranges); + } + + /** + * Read the data from S3 for the bigger combined file range and update all the + * underlying ranges. + * @param combinedFileRange big combined file range. + * @param allocate method to create byte buffers to hold result data. + */ + private void readCombinedRangeAndUpdateChildren(CombinedFileRange combinedFileRange, + IntFunction allocate) { + LOG.debug("Start reading combined range {} from path {} ", combinedFileRange, pathStr); + // This reference is must be kept till all buffers are populated as this is a + // finalizable object which closes the internal stream when gc triggers. + S3Object objectRange = null; + S3ObjectInputStream objectContent = null; + try { + checkIfVectoredIOStopped(); + final String operationName = "readCombinedFileRange"; + objectRange = getS3Object(operationName, + combinedFileRange.getOffset(), + combinedFileRange.getLength()); + objectContent = objectRange.getObjectContent(); + if (objectContent == null) { + throw new PathIOException(uri, + "Null IO stream received during " + operationName); + } + populateChildBuffers(combinedFileRange, objectContent, allocate); + } catch (Exception ex) { + LOG.debug("Exception while reading a range {} from path {} ", combinedFileRange, pathStr, ex); + for(FileRange child : combinedFileRange.getUnderlying()) { + child.getData().completeExceptionally(ex); + } + } finally { + IOUtils.cleanupWithLogger(LOG, objectRange, objectContent); + } + LOG.debug("Finished reading range {} from path {} ", combinedFileRange, pathStr); + } + + /** + * Populate underlying buffers of the child ranges. + * @param combinedFileRange big combined file range. + * @param objectContent data from s3. + * @param allocate method to allocate child byte buffers. + * @throws IOException any IOE. + */ + private void populateChildBuffers(CombinedFileRange combinedFileRange, + S3ObjectInputStream objectContent, + IntFunction allocate) throws IOException { + // If the combined file range just contains a single child + // range, we only have to fill that one child buffer else + // we drain the intermediate data between consecutive ranges + // and fill the buffers one by one. + if (combinedFileRange.getUnderlying().size() == 1) { + FileRange child = combinedFileRange.getUnderlying().get(0); + ByteBuffer buffer = allocate.apply(child.getLength()); + populateBuffer(child.getLength(), buffer, objectContent); + child.getData().complete(buffer); + } else { + FileRange prev = null; + for (FileRange child : combinedFileRange.getUnderlying()) { + if (prev != null && prev.getOffset() + prev.getLength() < child.getOffset()) { + long drainQuantity = child.getOffset() - prev.getOffset() - prev.getLength(); + drainUnnecessaryData(objectContent, drainQuantity); + } + ByteBuffer buffer = allocate.apply(child.getLength()); + populateBuffer(child.getLength(), buffer, objectContent); + child.getData().complete(buffer); + prev = child; + } + } + } + + /** + * Drain unnecessary data in between ranges. + * @param objectContent s3 data stream. + * @param drainQuantity how many bytes to drain. + * @throws IOException any IOE. + */ + private void drainUnnecessaryData(S3ObjectInputStream objectContent, long drainQuantity) + throws IOException { + int drainBytes = 0; + int readCount; + while (drainBytes < drainQuantity) { + if (drainBytes + DRAIN_BUFFER_SIZE <= drainQuantity) { + byte[] drainBuffer = new byte[DRAIN_BUFFER_SIZE]; + readCount = objectContent.read(drainBuffer); + } else { + byte[] drainBuffer = new byte[(int) (drainQuantity - drainBytes)]; + readCount = objectContent.read(drainBuffer); + } + drainBytes += readCount; + } + LOG.debug("{} bytes drained from stream ", drainBytes); + } + + /** + * Validates range parameters. + * In case of S3 we already have contentLength from the first GET request + * during an open file operation so failing fast here. + * @param range requested range. + * @throws EOFException end of file exception. + */ + private void validateRangeRequest(FileRange range) throws EOFException { + VectoredReadUtils.validateRangeRequest(range); + if(range.getOffset() + range.getLength() > contentLength) { + LOG.warn("Requested range [{}, {}) is beyond EOF for path {}", + range.getOffset(), range.getLength(), pathStr); + throw new EOFException("Requested range [" + range.getOffset() +", " + + range.getLength() + ") is beyond EOF for path " + pathStr); + } + } + + /** + * Read data from S3 for this range and populate the buffer. + * @param range range of data to read. + * @param buffer buffer to fill. + */ + private void readSingleRange(FileRange range, ByteBuffer buffer) { + LOG.debug("Start reading range {} from path {} ", range, pathStr); + S3Object objectRange = null; + S3ObjectInputStream objectContent = null; + try { + checkIfVectoredIOStopped(); + long position = range.getOffset(); + int length = range.getLength(); + final String operationName = "readRange"; + objectRange = getS3Object(operationName, position, length); + objectContent = objectRange.getObjectContent(); + if (objectContent == null) { + throw new PathIOException(uri, + "Null IO stream received during " + operationName); + } + populateBuffer(length, buffer, objectContent); + range.getData().complete(buffer); + } catch (Exception ex) { + LOG.warn("Exception while reading a range {} from path {} ", range, pathStr, ex); + range.getData().completeExceptionally(ex); + } finally { + IOUtils.cleanupWithLogger(LOG, objectRange, objectContent); + } + LOG.debug("Finished reading range {} from path {} ", range, pathStr); + } + + /** + * Populates the buffer with data from objectContent + * till length. Handles both direct and heap byte buffers. + * @param length length of data to populate. + * @param buffer buffer to fill. + * @param objectContent result retrieved from S3 store. + * @throws IOException any IOE. + */ + private void populateBuffer(int length, + ByteBuffer buffer, + S3ObjectInputStream objectContent) throws IOException { + if (buffer.isDirect()) { + int readBytes = 0; + int offset = 0; + byte[] tmp = new byte[TMP_BUFFER_MAX_SIZE]; + while (readBytes < length) { + checkIfVectoredIOStopped(); + int currentLength = readBytes + TMP_BUFFER_MAX_SIZE < length ? + TMP_BUFFER_MAX_SIZE + : length - readBytes; + readByteArray(objectContent, tmp, 0, currentLength); + buffer.put(tmp, 0, currentLength); + offset = offset + currentLength; + readBytes = readBytes + currentLength; + } + buffer.flip(); + } else { + readByteArray(objectContent, buffer.array(), 0, length); + } + } + + /** + * Read data into destination buffer from s3 object content. + * @param objectContent result from S3. + * @param dest destination buffer. + * @param offset start offset of dest buffer. + * @param length number of bytes to fill in dest. + * @throws IOException any IOE. + */ + private void readByteArray(S3ObjectInputStream objectContent, + byte[] dest, + int offset, + int length) throws IOException { + int readBytes = 0; + while (readBytes < length) { + int readBytesCurr = objectContent.read(dest, + offset + readBytes, + length - readBytes); + readBytes +=readBytesCurr; + if (readBytesCurr < 0) { + throw new EOFException(FSExceptionMessages.EOF_IN_READ_FULLY); + } + } + } + + /** + * Read data from S3 using a http request with retries. + * This also handles if file has been changed while the + * http call is getting executed. If the file has been + * changed RemoteFileChangedException is thrown. + * @param operationName name of the operation for which get object on S3 is called. + * @param position position of the object to be read from S3. + * @param length length from position of the object to be read from S3. + * @return S3Object result s3 object. + * @throws IOException exception if any. + */ + private S3Object getS3Object(String operationName, long position, + int length) throws IOException { + final GetObjectRequest request = client.newGetRequest(key) + .withRange(position, position + length - 1); + changeTracker.maybeApplyConstraint(request); + DurationTracker tracker = streamStatistics.initiateGetRequest(); + S3Object objectRange; + Invoker invoker = context.getReadInvoker(); + try { + objectRange = invoker.retry(operationName, pathStr, true, + () -> { + checkIfVectoredIOStopped(); + return client.getObject(request); + }); + + } catch (IOException ex) { + tracker.failed(); + throw ex; + } finally { + tracker.close(); + } + changeTracker.processResponse(objectRange, operationName, + position); + return objectRange; + } + + /** + * Check if vectored io operation has been stooped. This happens + * when the stream is closed or unbuffer is called. + * @throws InterruptedIOException throw InterruptedIOException such + * that all running vectored io is + * terminated thus releasing resources. + */ + private void checkIfVectoredIOStopped() throws InterruptedIOException { + if (stopVectoredIOOperations.get()) { + throw new InterruptedIOException("Stream closed or unbuffer is called"); + } + } + /** * Access the input stream statistics. * This is for internal testing and may be removed without warning. @@ -965,10 +1312,15 @@ public static long validateReadahead(@Nullable Long readahead) { /** * Closes the underlying S3 stream, and merges the {@link #streamStatistics} * instance associated with the stream. + * Also sets the {@code stopVectoredIOOperations} flag to true such that + * active vectored read operations are terminated. However termination of + * old vectored reads are not guaranteed if a new vectored read operation + * is initiated after unbuffer is called. */ @Override public synchronized void unbuffer() { try { + stopVectoredIOOperations.set(true); closeStream("unbuffer()", false, false); } finally { streamStatistics.unbuffered(); @@ -981,6 +1333,7 @@ public boolean hasCapability(String capability) { case StreamCapabilities.IOSTATISTICS: case StreamCapabilities.READAHEAD: case StreamCapabilities.UNBUFFER: + case StreamCapabilities.VECTOREDIO: return true; default: return false; diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInstrumentation.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInstrumentation.java index eec636672010a..67734b7502976 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInstrumentation.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInstrumentation.java @@ -1308,8 +1308,18 @@ private void mergeOutputStreamStatistics( incrementCounter(STREAM_WRITE_EXCEPTIONS, source.lookupCounterValue( StreamStatisticNames.STREAM_WRITE_EXCEPTIONS)); + // merge in all the IOStatistics - this.getIOStatistics().aggregate(source.getIOStatistics()); + final IOStatisticsStore sourceIOStatistics = source.getIOStatistics(); + this.getIOStatistics().aggregate(sourceIOStatistics); + + // propagate any extra values into the FS-level stats. + incrementMutableCounter(OBJECT_PUT_REQUESTS.getSymbol(), + sourceIOStatistics.counters().get(OBJECT_PUT_REQUESTS.getSymbol())); + incrementMutableCounter( + COMMITTER_MAGIC_MARKER_PUT.getSymbol(), + sourceIOStatistics.counters().get(COMMITTER_MAGIC_MARKER_PUT.getSymbol())); + } /** @@ -1366,9 +1376,12 @@ private OutputStreamStatistics( STREAM_WRITE_BLOCK_UPLOADS_BYTES_PENDING.getSymbol()) .withDurationTracking( ACTION_EXECUTOR_ACQUIRED, + COMMITTER_MAGIC_MARKER_PUT.getSymbol(), INVOCATION_ABORT.getSymbol(), + MULTIPART_UPLOAD_COMPLETED.getSymbol(), OBJECT_MULTIPART_UPLOAD_ABORTED.getSymbol(), - MULTIPART_UPLOAD_COMPLETED.getSymbol()) + OBJECT_MULTIPART_UPLOAD_INITIATED.getSymbol(), + OBJECT_PUT_REQUESTS.getSymbol()) .build(); setIOStatistics(st); // these are extracted to avoid lookups on heavily used counters. @@ -1630,6 +1643,7 @@ private CommitterStatisticsImpl() { COMMITTER_TASKS_SUCCEEDED.getSymbol()) .withDurationTracking( COMMITTER_COMMIT_JOB.getSymbol(), + COMMITTER_LOAD_SINGLE_PENDING_FILE.getSymbol(), COMMITTER_MATERIALIZE_FILE.getSymbol(), COMMITTER_STAGE_FILE_UPLOAD.getSymbol()) .build(); diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AReadOpContext.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AReadOpContext.java index f416cf9485d12..803b7757d252b 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AReadOpContext.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AReadOpContext.java @@ -64,6 +64,12 @@ public class S3AReadOpContext extends S3AOpContext { */ private long asyncDrainThreshold; + /** + * Vectored IO context for vectored read api + * in {@code S3AInputStream#readVectored(List, IntFunction)}. + */ + private final VectoredIOContext vectoredIOContext; + /** * Instantiate. * @param path path of read @@ -71,17 +77,19 @@ public class S3AReadOpContext extends S3AOpContext { * @param stats Fileystem statistics (may be null) * @param instrumentation statistics context * @param dstFileStatus target file status + * @param vectoredIOContext context for vectored read operation. */ public S3AReadOpContext( final Path path, Invoker invoker, @Nullable FileSystem.Statistics stats, S3AStatisticsContext instrumentation, - FileStatus dstFileStatus) { - + FileStatus dstFileStatus, + VectoredIOContext vectoredIOContext) { super(invoker, stats, instrumentation, dstFileStatus); this.path = requireNonNull(path); + this.vectoredIOContext = requireNonNull(vectoredIOContext, "vectoredIOContext"); } /** @@ -199,6 +207,14 @@ public long getAsyncDrainThreshold() { return asyncDrainThreshold; } + /** + * Get Vectored IO context for this this read op. + * @return vectored IO context. + */ + public VectoredIOContext getVectoredIOContext() { + return vectoredIOContext; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder( diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AUtils.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AUtils.java index d644d3f47667c..e7e741d42c521 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AUtils.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AUtils.java @@ -88,6 +88,7 @@ import static org.apache.hadoop.fs.s3a.impl.InternalConstants.CSE_PADDING_LENGTH; import static org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteSupport.translateDeleteException; import static org.apache.hadoop.io.IOUtils.cleanupWithLogger; +import static org.apache.hadoop.util.functional.RemoteIterators.filteringRemoteIterator; /** * Utility methods for S3A code. @@ -1467,17 +1468,22 @@ public static List flatmapLocatedFiles( /** * List located files and filter them as a classic listFiles(path, filter) * would do. + * This will be incremental, fetching pages async. + * While it is rare for job to have many thousands of files, jobs + * against versioned buckets may return earlier if there are many + * non-visible objects. * @param fileSystem filesystem * @param path path to list * @param recursive recursive listing? * @param filter filter for the filename - * @return the filtered list of entries + * @return interator over the entries. * @throws IOException IO failure. */ - public static List listAndFilter(FileSystem fileSystem, + public static RemoteIterator listAndFilter(FileSystem fileSystem, Path path, boolean recursive, PathFilter filter) throws IOException { - return flatmapLocatedFiles(fileSystem.listFiles(path, recursive), - status -> maybe(filter.accept(status.getPath()), status)); + return filteringRemoteIterator( + fileSystem.listFiles(path, recursive), + status -> filter.accept(status.getPath())); } /** diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Statistic.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Statistic.java index 86cb18076cc6c..dfe9fdf2d8d37 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Statistic.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Statistic.java @@ -111,6 +111,10 @@ public enum Statistic { StoreStatisticNames.OP_CREATE, "Calls of create()", TYPE_DURATION), + INVOCATION_CREATE_FILE( + StoreStatisticNames.OP_CREATE_FILE, + "Calls of createFile()", + TYPE_DURATION), INVOCATION_CREATE_NON_RECURSIVE( StoreStatisticNames.OP_CREATE_NON_RECURSIVE, "Calls of createNonRecursive()", @@ -459,10 +463,19 @@ public enum Statistic { "committer_commits_reverted", "Count of commits reverted", TYPE_COUNTER), + COMMITTER_LOAD_SINGLE_PENDING_FILE( + "committer_load_single_pending_file", + "Duration to load a single pending file in task commit", + TYPE_DURATION), COMMITTER_MAGIC_FILES_CREATED( "committer_magic_files_created", "Count of files created under 'magic' paths", TYPE_COUNTER), + + COMMITTER_MAGIC_MARKER_PUT( + "committer_magic_marker_put", + "Duration Tracking of marker files created under 'magic' paths", + TYPE_DURATION), COMMITTER_MATERIALIZE_FILE( "committer_materialize_file", "Duration Tracking of time to materialize a file in job commit", diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/VectoredIOContext.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/VectoredIOContext.java new file mode 100644 index 0000000000000..31f0ae4cb5515 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/VectoredIOContext.java @@ -0,0 +1,78 @@ +/* + * 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.s3a; + +import java.util.List; +import java.util.function.IntFunction; + +/** + * Context related to vectored IO operation. + * See {@link S3AInputStream#readVectored(List, IntFunction)}. + */ +public class VectoredIOContext { + + /** + * What is the smallest reasonable seek that we should group + * ranges together during vectored read operation. + */ + private int minSeekForVectorReads; + + /** + * What is the largest size that we should group ranges + * together during vectored read operation. + * Setting this value 0 will disable merging of ranges. + */ + private int maxReadSizeForVectorReads; + + /** + * Default no arg constructor. + */ + public VectoredIOContext() { + } + + public VectoredIOContext setMinSeekForVectoredReads(int minSeek) { + this.minSeekForVectorReads = minSeek; + return this; + } + + public VectoredIOContext setMaxReadSizeForVectoredReads(int maxSize) { + this.maxReadSizeForVectorReads = maxSize; + return this; + } + + public VectoredIOContext build() { + return this; + } + + public int getMinSeekForVectorReads() { + return minSeekForVectorReads; + } + + public int getMaxReadSizeForVectorReads() { + return maxReadSizeForVectorReads; + } + + @Override + public String toString() { + return "VectoredIOContext{" + + "minSeekForVectorReads=" + minSeekForVectorReads + + ", maxReadSizeForVectorReads=" + maxReadSizeForVectorReads + + '}'; + } +} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperationHelper.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperationHelper.java index ee91bacfb0740..ce50f0b85ed73 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperationHelper.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperationHelper.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import com.amazonaws.services.s3.model.AmazonS3Exception; @@ -40,8 +39,6 @@ import com.amazonaws.services.s3.model.SelectObjectContentResult; import com.amazonaws.services.s3.model.UploadPartRequest; import com.amazonaws.services.s3.model.UploadPartResult; -import com.amazonaws.services.s3.transfer.model.UploadResult; -import org.apache.hadoop.util.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +48,7 @@ import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.PathIOException; import org.apache.hadoop.fs.s3a.api.RequestFactory; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; import org.apache.hadoop.fs.s3a.impl.StoreContext; import org.apache.hadoop.fs.s3a.statistics.S3AStatisticsContext; import org.apache.hadoop.fs.s3a.select.SelectBinding; @@ -58,6 +56,7 @@ import org.apache.hadoop.fs.store.audit.AuditSpanSource; import org.apache.hadoop.util.DurationInfo; import org.apache.hadoop.util.functional.CallableRaisingIOE; +import org.apache.hadoop.util.Preconditions; import static org.apache.hadoop.util.Preconditions.checkNotNull; import static org.apache.hadoop.fs.s3a.Invoker.*; @@ -234,22 +233,20 @@ private void deactivateAuditSpan() { * @param destKey destination key * @param inputStream source data. * @param length size, if known. Use -1 for not known - * @param headers optional map of custom headers. + * @param options options for the request * @return the request */ @Retries.OnceRaw public PutObjectRequest createPutObjectRequest(String destKey, InputStream inputStream, long length, - final Map headers) { + final PutObjectOptions options) { activateAuditSpan(); ObjectMetadata objectMetadata = newObjectMetadata(length); - if (headers != null) { - objectMetadata.setUserMetadata(headers); - } return getRequestFactory().newPutObjectRequest( destKey, objectMetadata, + options, inputStream); } @@ -257,18 +254,26 @@ public PutObjectRequest createPutObjectRequest(String destKey, * Create a {@link PutObjectRequest} request to upload a file. * @param dest key to PUT to. * @param sourceFile source file + * @param options options for the request * @return the request */ @Retries.OnceRaw - public PutObjectRequest createPutObjectRequest(String dest, - File sourceFile) { + public PutObjectRequest createPutObjectRequest( + String dest, + File sourceFile, + final PutObjectOptions options) { Preconditions.checkState(sourceFile.length() < Integer.MAX_VALUE, "File length is too big for a single PUT upload"); activateAuditSpan(); - return getRequestFactory(). + final ObjectMetadata objectMetadata = + newObjectMetadata((int) sourceFile.length()); + + PutObjectRequest putObjectRequest = getRequestFactory(). newPutObjectRequest(dest, - newObjectMetadata((int) sourceFile.length()), + objectMetadata, + options, sourceFile); + return putObjectRequest; } /** @@ -298,21 +303,20 @@ public ObjectMetadata newObjectMetadata(long length) { } /** - * Start the multipart upload process. - * Retry policy: retrying, translated. - * @param destKey destination of upload - * @return the upload result containing the ID - * @throws IOException IO problem + * {@inheritDoc} */ @Retries.RetryTranslated - public String initiateMultiPartUpload(String destKey) throws IOException { + public String initiateMultiPartUpload( + final String destKey, + final PutObjectOptions options) + throws IOException { LOG.debug("Initiating Multipart upload to {}", destKey); try (AuditSpan span = activateAuditSpan()) { return retry("initiate MultiPartUpload", destKey, true, () -> { final InitiateMultipartUploadRequest initiateMPURequest = getRequestFactory().newMultipartUploadRequest( - destKey); + destKey, options); return owner.initiateMultipartUpload(initiateMPURequest) .getUploadId(); }); @@ -322,13 +326,14 @@ public String initiateMultiPartUpload(String destKey) throws IOException { /** * Finalize a multipart PUT operation. * This completes the upload, and, if that works, calls - * {@link S3AFileSystem#finishedWrite(String, long, String, String)} + * {@link S3AFileSystem#finishedWrite(String, long, String, String, org.apache.hadoop.fs.s3a.impl.PutObjectOptions)} * to update the filesystem. * Retry policy: retrying, translated. * @param destKey destination of the commit * @param uploadId multipart operation Id * @param partETags list of partial uploads * @param length length of the upload + * @param putOptions put object options * @param retrying retrying callback * @return the result of the operation. * @throws IOException on problems. @@ -339,6 +344,7 @@ private CompleteMultipartUploadResult finalizeMultipartUpload( String uploadId, List partETags, long length, + PutObjectOptions putOptions, Retried retrying) throws IOException { if (partETags.isEmpty()) { throw new PathIOException(destKey, @@ -357,7 +363,8 @@ private CompleteMultipartUploadResult finalizeMultipartUpload( request); }); owner.finishedWrite(destKey, length, uploadResult.getETag(), - uploadResult.getVersionId()); + uploadResult.getVersionId(), + putOptions); return uploadResult; } } @@ -373,6 +380,7 @@ private CompleteMultipartUploadResult finalizeMultipartUpload( * @param length length of the upload * @param errorCount a counter incremented by 1 on every error; for * use in statistics + * @param putOptions put object options * @return the result of the operation. * @throws IOException if problems arose which could not be retried, or * the retry count was exceeded @@ -383,7 +391,8 @@ public CompleteMultipartUploadResult completeMPUwithRetries( String uploadId, List partETags, long length, - AtomicInteger errorCount) + AtomicInteger errorCount, + PutObjectOptions putOptions) throws IOException { checkNotNull(uploadId); checkNotNull(partETags); @@ -393,8 +402,8 @@ public CompleteMultipartUploadResult completeMPUwithRetries( uploadId, partETags, length, - (text, e, r, i) -> errorCount.incrementAndGet() - ); + putOptions, + (text, e, r, i) -> errorCount.incrementAndGet()); } /** @@ -550,37 +559,43 @@ public String toString() { * Byte length is calculated from the file length, or, if there is no * file, from the content length of the header. * @param putObjectRequest the request + * @param putOptions put object options * @return the upload initiated * @throws IOException on problems */ @Retries.RetryTranslated - public PutObjectResult putObject(PutObjectRequest putObjectRequest) + public PutObjectResult putObject(PutObjectRequest putObjectRequest, + PutObjectOptions putOptions) throws IOException { return retry("Writing Object", putObjectRequest.getKey(), true, withinAuditSpan(getAuditSpan(), () -> - owner.putObjectDirect(putObjectRequest))); + owner.putObjectDirect(putObjectRequest, putOptions))); } /** - * PUT an object via the transfer manager. + * PUT an object. + * * @param putObjectRequest the request - * @return the result of the operation + * @param putOptions put object options + * * @throws IOException on problems */ @Retries.RetryTranslated - public UploadResult uploadObject(PutObjectRequest putObjectRequest) + public void uploadObject(PutObjectRequest putObjectRequest, + PutObjectOptions putOptions) throws IOException { - // no retry; rely on xfer manager logic - return retry("Writing Object", + + retry("Writing Object", putObjectRequest.getKey(), true, withinAuditSpan(getAuditSpan(), () -> - owner.executePut(putObjectRequest, null))); + owner.putObjectDirect(putObjectRequest, putOptions))); } /** * Revert a commit by deleting the file. - * Relies on retry code in filesystem + * Relies on retry code in filesystem. + * Does not attempt to recreate the parent directory * @throws IOException on problems * @param destKey destination key */ @@ -591,13 +606,14 @@ public void revertCommit(String destKey) throws IOException { Path destPath = owner.keyToQualifiedPath(destKey); owner.deleteObjectAtPath(destPath, destKey, true); - owner.maybeCreateFakeParentDirectory(destPath); })); } /** * This completes a multipart upload to the destination key via * {@code finalizeMultipartUpload()}. + * Markers are never deleted on commit; this avoids having to + * issue many duplicate deletions. * Retry policy: retrying, translated. * Retries increment the {@code errorCount} counter. * @param destKey destination @@ -623,8 +639,8 @@ public CompleteMultipartUploadResult commitUpload( uploadId, partETags, length, - Invoker.NO_OP - ); + PutObjectOptions.keepingDirs(), + Invoker.NO_OP); } /** diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperations.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperations.java index 7604bbe46b51a..321390446f705 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperations.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperations.java @@ -25,7 +25,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import com.amazonaws.services.s3.model.CompleteMultipartUploadResult; @@ -38,11 +37,11 @@ import com.amazonaws.services.s3.model.SelectObjectContentResult; import com.amazonaws.services.s3.model.UploadPartRequest; import com.amazonaws.services.s3.model.UploadPartResult; -import com.amazonaws.services.s3.transfer.model.UploadResult; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.PathIOException; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; import org.apache.hadoop.fs.store.audit.AuditSpanSource; import org.apache.hadoop.util.functional.CallableRaisingIOE; @@ -79,22 +78,25 @@ T retry(String action, * @param destKey destination key * @param inputStream source data. * @param length size, if known. Use -1 for not known - * @param headers optional map of custom headers. + * @param options options for the request * @return the request */ PutObjectRequest createPutObjectRequest(String destKey, InputStream inputStream, long length, - @Nullable Map headers); + @Nullable PutObjectOptions options); /** * Create a {@link PutObjectRequest} request to upload a file. * @param dest key to PUT to. * @param sourceFile source file + * @param options options for the request * @return the request */ - PutObjectRequest createPutObjectRequest(String dest, - File sourceFile); + PutObjectRequest createPutObjectRequest( + String dest, + File sourceFile, + @Nullable PutObjectOptions options); /** * Callback on a successful write. @@ -121,11 +123,12 @@ PutObjectRequest createPutObjectRequest(String dest, * Start the multipart upload process. * Retry policy: retrying, translated. * @param destKey destination of upload + * @param options options for the put request * @return the upload result containing the ID * @throws IOException IO problem */ @Retries.RetryTranslated - String initiateMultiPartUpload(String destKey) throws IOException; + String initiateMultiPartUpload(String destKey, PutObjectOptions options) throws IOException; /** * This completes a multipart upload to the destination key via @@ -138,6 +141,7 @@ PutObjectRequest createPutObjectRequest(String dest, * @param length length of the upload * @param errorCount a counter incremented by 1 on every error; for * use in statistics + * @param putOptions put object options * @return the result of the operation. * @throws IOException if problems arose which could not be retried, or * the retry count was exceeded @@ -148,7 +152,8 @@ CompleteMultipartUploadResult completeMPUwithRetries( String uploadId, List partETags, long length, - AtomicInteger errorCount) + AtomicInteger errorCount, + PutObjectOptions putOptions) throws IOException; /** @@ -238,26 +243,32 @@ UploadPartRequest newUploadPartRequest( * Byte length is calculated from the file length, or, if there is no * file, from the content length of the header. * @param putObjectRequest the request + * @param putOptions put object options * @return the upload initiated * @throws IOException on problems */ @Retries.RetryTranslated - PutObjectResult putObject(PutObjectRequest putObjectRequest) + PutObjectResult putObject(PutObjectRequest putObjectRequest, + PutObjectOptions putOptions) throws IOException; /** * PUT an object via the transfer manager. + * * @param putObjectRequest the request - * @return the result of the operation + * @param putOptions put object options + * * @throws IOException on problems */ @Retries.RetryTranslated - UploadResult uploadObject(PutObjectRequest putObjectRequest) + void uploadObject(PutObjectRequest putObjectRequest, + PutObjectOptions putOptions) throws IOException; /** * Revert a commit by deleting the file. - * Relies on retry code in filesystem + * No attempt is made to probe for/recreate a parent dir marker + * Relies on retry code in filesystem. * @throws IOException on problems * @param destKey destination key */ diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/api/RequestFactory.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/api/RequestFactory.java index 20cd27225a88d..cae4d3ef034e8 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/api/RequestFactory.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/api/RequestFactory.java @@ -50,6 +50,7 @@ import org.apache.hadoop.fs.PathIOException; import org.apache.hadoop.fs.s3a.S3AEncryptionMethods; import org.apache.hadoop.fs.s3a.auth.delegation.EncryptionSecrets; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; /** * Factory for S3 objects. @@ -141,11 +142,12 @@ CopyObjectRequest newCopyObjectRequest(String srcKey, * Adds the ACL and metadata * @param key key of object * @param metadata metadata header + * @param options options for the request * @param srcfile source file * @return the request */ PutObjectRequest newPutObjectRequest(String key, - ObjectMetadata metadata, File srcfile); + ObjectMetadata metadata, PutObjectOptions options, File srcfile); /** * Create a {@link PutObjectRequest} request. @@ -153,11 +155,13 @@ PutObjectRequest newPutObjectRequest(String key, * operation. * @param key key of object * @param metadata metadata header + * @param options options for the request * @param inputStream source data. * @return the request */ PutObjectRequest newPutObjectRequest(String key, ObjectMetadata metadata, + PutObjectOptions options, InputStream inputStream); /** @@ -190,10 +194,12 @@ AbortMultipartUploadRequest newAbortMultipartUploadRequest( /** * Start a multipart upload. * @param destKey destination object key + * @param options options for the request * @return the request. */ InitiateMultipartUploadRequest newMultipartUploadRequest( - String destKey); + String destKey, + @Nullable PutObjectOptions options); /** * Complete a multipart upload. diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/AbstractS3ACommitter.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/AbstractS3ACommitter.java index f08d6448e4993..78b687cc6f19c 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/AbstractS3ACommitter.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/AbstractS3ACommitter.java @@ -26,32 +26,30 @@ import java.util.Date; import java.util.List; import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; import com.amazonaws.services.s3.model.MultipartUpload; - -import org.apache.hadoop.classification.VisibleForTesting; -import org.apache.hadoop.util.Preconditions; -import org.apache.hadoop.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileSystem; +import org.apache.hadoop.fs.RemoteIterator; import org.apache.hadoop.fs.audit.AuditConstants; -import org.apache.hadoop.fs.audit.CommonAuditContext; import org.apache.hadoop.fs.store.audit.AuditSpan; import org.apache.hadoop.fs.store.audit.AuditSpanSource; +import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.commit.files.PendingSet; +import org.apache.hadoop.fs.s3a.commit.files.PersistentCommitData; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; import org.apache.hadoop.fs.s3a.commit.files.SuccessData; +import org.apache.hadoop.fs.s3a.commit.impl.AuditContextUpdater; +import org.apache.hadoop.fs.s3a.commit.impl.CommitContext; +import org.apache.hadoop.fs.s3a.commit.impl.CommitOperations; import org.apache.hadoop.fs.s3a.statistics.CommitterStatistics; import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsSnapshot; @@ -62,24 +60,33 @@ import org.apache.hadoop.mapreduce.TaskAttemptContext; import org.apache.hadoop.mapreduce.TaskAttemptID; import org.apache.hadoop.mapreduce.lib.output.PathOutputCommitter; +import org.apache.hadoop.mapreduce.lib.output.committer.manifest.impl.ManifestStoreOperations; +import org.apache.hadoop.mapreduce.lib.output.committer.manifest.impl.ManifestStoreOperationsThroughFileSystem; import org.apache.hadoop.net.NetUtils; import org.apache.hadoop.util.DurationInfo; -import org.apache.hadoop.util.concurrent.HadoopExecutors; import org.apache.hadoop.util.functional.InvocationRaisingIOE; +import org.apache.hadoop.util.functional.TaskPool; -import static org.apache.hadoop.fs.s3a.Constants.THREAD_POOL_SHUTDOWN_DELAY_SECONDS; +import static java.util.Objects.requireNonNull; +import static org.apache.hadoop.fs.s3a.Constants.DEFAULT_MAXIMUM_CONNECTIONS; +import static org.apache.hadoop.fs.s3a.Constants.DEFAULT_MAX_TOTAL_TASKS; +import static org.apache.hadoop.fs.s3a.Constants.MAXIMUM_CONNECTIONS; +import static org.apache.hadoop.fs.s3a.Constants.MAX_TOTAL_TASKS; import static org.apache.hadoop.fs.s3a.Invoker.ignoreIOExceptions; import static org.apache.hadoop.fs.s3a.S3AUtils.*; import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_COMMIT_JOB; import static org.apache.hadoop.fs.audit.CommonAuditContext.currentAuditContext; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.*; import static org.apache.hadoop.fs.s3a.commit.CommitUtils.*; -import static org.apache.hadoop.fs.s3a.commit.CommitUtilsWithMR.*; import static org.apache.hadoop.fs.s3a.commit.InternalCommitterConstants.E_NO_SPARK_UUID; import static org.apache.hadoop.fs.s3a.commit.InternalCommitterConstants.FS_S3A_COMMITTER_UUID; import static org.apache.hadoop.fs.s3a.commit.InternalCommitterConstants.FS_S3A_COMMITTER_UUID_SOURCE; import static org.apache.hadoop.fs.s3a.commit.InternalCommitterConstants.SPARK_WRITE_UUID; +import static org.apache.hadoop.fs.s3a.commit.impl.CommitUtilsWithMR.*; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.trackDurationOfInvocation; +import static org.apache.hadoop.mapreduce.lib.output.committer.manifest.files.DiagnosticKeys.STAGE; +import static org.apache.hadoop.mapreduce.lib.output.committer.manifest.impl.ManifestCommitterSupport.createJobSummaryFilename; +import static org.apache.hadoop.util.functional.RemoteIterators.toList; /** * Abstract base class for S3A committers; allows for any commonality @@ -136,8 +143,6 @@ public abstract class AbstractS3ACommitter extends PathOutputCommitter */ private final JobUUIDSource uuidSource; - private final CommonAuditContext commonAuditContext; - /** * Has this instance been used for job setup? * If so then it is safe for a locally generated @@ -145,11 +150,6 @@ public abstract class AbstractS3ACommitter extends PathOutputCommitter */ private boolean jobSetup; - /** - * Thread pool for task execution. - */ - private ExecutorService threadPool; - /** Underlying commit operations. */ private final CommitOperations commitOperations; @@ -203,9 +203,8 @@ protected AbstractS3ACommitter( Path outputPath, TaskAttemptContext context) throws IOException { super(outputPath, context); - Preconditions.checkArgument(outputPath != null, "null output path"); - Preconditions.checkArgument(context != null, "null job context"); - this.jobContext = context; + setOutputPath(outputPath); + this.jobContext = requireNonNull(context, "null job context"); this.role = "Task committer " + context.getTaskAttemptID(); setConf(context.getConfiguration()); Pair id = buildJobUUID( @@ -219,17 +218,20 @@ protected AbstractS3ACommitter( S3AFileSystem fs = getDestS3AFS(); // set this thread's context with the job ID. // audit spans created in this thread will pick - // up this value. - this.commonAuditContext = currentAuditContext(); - updateCommonContext(); + // up this value., including the commit operations instance + // soon to be created. + new AuditContextUpdater(jobContext) + .updateCurrentAuditContext(); + // the filesystem is the span source, always. - auditSpanSource = fs.getAuditSpanSource(); + this.auditSpanSource = fs.getAuditSpanSource(); this.createJobMarker = context.getConfiguration().getBoolean( CREATE_SUCCESSFUL_JOB_OUTPUT_DIR_MARKER, DEFAULT_CREATE_SUCCESSFUL_JOB_DIR_MARKER); // the statistics are shared between this committer and its operations. this.committerStatistics = fs.newCommitterStatistics(); - this.commitOperations = new CommitOperations(fs, committerStatistics); + this.commitOperations = new CommitOperations(fs, committerStatistics, + outputPath.toString()); } /** @@ -267,8 +269,7 @@ public final Path getOutputPath() { * @param outputPath new value */ protected final void setOutputPath(Path outputPath) { - Preconditions.checkNotNull(outputPath, "Null output path"); - this.outputPath = outputPath; + this.outputPath = requireNonNull(outputPath, "Null output path"); } /** @@ -338,6 +339,12 @@ public Path getJobAttemptPath(JobContext context) { return getJobAttemptPath(getAppAttemptId(context)); } + /** + * Compute the path under which all job attempts will be placed. + * @return the path to store job attempt data. + */ + protected abstract Path getJobPath(); + /** * Compute the path where the output of a given job attempt will be placed. * @param appAttemptId the ID of the application attempt for this job. @@ -440,6 +447,7 @@ protected FileSystem getDestinationFS(Path out, Configuration config) protected boolean requiresDelayedCommitOutputInFileSystem() { return false; } + /** * Task recovery considered Unsupported: Warn and fail. * @param taskContext Context of the task whose output is being recovered @@ -450,7 +458,7 @@ public void recoverTask(TaskAttemptContext taskContext) throws IOException { LOG.warn("Cannot recover task {}", taskContext.getTaskAttemptID()); throw new PathCommitException(outputPath, String.format("Unable to recover task %s", - taskContext.getTaskAttemptID())); + taskContext.getTaskAttemptID())); } /** @@ -459,11 +467,16 @@ public void recoverTask(TaskAttemptContext taskContext) throws IOException { * * While the classic committers create a 0-byte file, the S3A committers * PUT up a the contents of a {@link SuccessData} file. + * * @param context job context * @param pending the pending commits + * + * @return the success data, even if the marker wasn't created + * * @throws IOException IO failure */ - protected void maybeCreateSuccessMarkerFromCommits(JobContext context, + protected SuccessData maybeCreateSuccessMarkerFromCommits( + JobContext context, ActiveCommit pending) throws IOException { List filenames = new ArrayList<>(pending.size()); // The list of committed objects in pending is size limited in @@ -472,41 +485,86 @@ protected void maybeCreateSuccessMarkerFromCommits(JobContext context, // load in all the pending statistics IOStatisticsSnapshot snapshot = new IOStatisticsSnapshot( pending.getIOStatistics()); + // and the current statistics snapshot.aggregate(getIOStatistics()); - maybeCreateSuccessMarker(context, filenames, snapshot); + return maybeCreateSuccessMarker(context, filenames, snapshot); } /** * if the job requires a success marker on a successful job, - * create the file {@link CommitConstants#_SUCCESS}. + * create the {@code _SUCCESS} file. * * While the classic committers create a 0-byte file, the S3A committers * PUT up a the contents of a {@link SuccessData} file. + * The file is returned, even if no marker is created. + * This is so it can be saved to a report directory. * @param context job context * @param filenames list of filenames. * @param ioStatistics any IO Statistics to include * @throws IOException IO failure + * @return the success data. */ - protected void maybeCreateSuccessMarker(JobContext context, - List filenames, + protected SuccessData maybeCreateSuccessMarker( + final JobContext context, + final List filenames, final IOStatisticsSnapshot ioStatistics) throws IOException { + + SuccessData successData = + createSuccessData(context, filenames, ioStatistics, + getDestFS().getConf()); if (createJobMarker) { - // create a success data structure and then save it - SuccessData successData = new SuccessData(); - successData.setCommitter(getName()); - successData.setJobId(uuid); - successData.setJobIdSource(uuidSource.getText()); - successData.setDescription(getRole()); - successData.setHostname(NetUtils.getLocalHostname()); - Date now = new Date(); - successData.setTimestamp(now.getTime()); - successData.setDate(now.toString()); - successData.setFilenames(filenames); - successData.getIOStatistics().aggregate(ioStatistics); + // save it to the job dest dir commitOperations.createSuccessMarker(getOutputPath(), successData, true); } + return successData; + } + + /** + * Create the success data structure from a job context. + * @param context job context. + * @param filenames short list of filenames; nullable + * @param ioStatistics IOStatistics snapshot + * @param destConf config of the dest fs, can be null + * @return the structure + * + */ + private SuccessData createSuccessData(final JobContext context, + final List filenames, + final IOStatisticsSnapshot ioStatistics, + final Configuration destConf) { + // create a success data structure + SuccessData successData = new SuccessData(); + successData.setCommitter(getName()); + successData.setJobId(uuid); + successData.setJobIdSource(uuidSource.getText()); + successData.setDescription(getRole()); + successData.setHostname(NetUtils.getLocalHostname()); + Date now = new Date(); + successData.setTimestamp(now.getTime()); + successData.setDate(now.toString()); + if (filenames != null) { + successData.setFilenames(filenames); + } + successData.getIOStatistics().aggregate(ioStatistics); + // attach some config options as diagnostics to assist + // in debugging performance issues. + + // commit thread pool size + successData.addDiagnostic(FS_S3A_COMMITTER_THREADS, + Integer.toString(getJobCommitThreadCount(context))); + + // and filesystem http connection and thread pool sizes + if (destConf != null) { + successData.addDiagnostic(MAXIMUM_CONNECTIONS, + destConf.get(MAXIMUM_CONNECTIONS, + Integer.toString(DEFAULT_MAXIMUM_CONNECTIONS))); + successData.addDiagnostic(MAX_TOTAL_TASKS, + destConf.get(MAX_TOTAL_TASKS, + Integer.toString(DEFAULT_MAX_TOTAL_TASKS))); + } + return successData; } /** @@ -556,7 +614,11 @@ public void setupJob(JobContext context) throws IOException { @Override public void setupTask(TaskAttemptContext context) throws IOException { TaskAttemptID attemptID = context.getTaskAttemptID(); - updateCommonContext(); + + // update the context so that task IO in the same thread has + // the relevant values. + new AuditContextUpdater(context) + .updateCurrentAuditContext(); try (DurationInfo d = new DurationInfo(LOG, "Setup Task %s", attemptID)) { @@ -572,6 +634,9 @@ && getUUIDSource() == JobUUIDSource.GeneratedLocally) { } Path taskAttemptPath = getTaskAttemptPath(context); FileSystem fs = taskAttemptPath.getFileSystem(getConf()); + // delete that ta path if somehow it was there + fs.delete(taskAttemptPath, true); + // create an empty directory fs.mkdirs(taskAttemptPath); } } @@ -596,25 +661,23 @@ protected FileSystem getTaskAttemptFilesystem(TaskAttemptContext context) * On a failure or abort of a single file's commit, all its uploads are * aborted. * The revert operation lists the files already committed and deletes them. - * @param context job context + * @param commitContext commit context * @param pending pending uploads * @throws IOException on any failure */ protected void commitPendingUploads( - final JobContext context, + final CommitContext commitContext, final ActiveCommit pending) throws IOException { if (pending.isEmpty()) { LOG.warn("{}: No pending uploads to commit", getRole()); } try (DurationInfo ignored = new DurationInfo(LOG, - "committing the output of %s task(s)", pending.size()); - CommitOperations.CommitContext commitContext - = initiateCommitOperation()) { + "committing the output of %s task(s)", pending.size())) { - Tasks.foreach(pending.getSourceFiles()) + TaskPool.foreach(pending.getSourceFiles()) .stopOnFailure() .suppressExceptions(false) - .executeWith(buildSubmitter(context)) + .executeWith(commitContext.getOuterSubmitter()) .abortWith(status -> loadAndAbort(commitContext, pending, status, true, false)) .revertWith(status -> @@ -629,35 +692,39 @@ protected void commitPendingUploads( * This check avoids the situation where the inability to read * a file only surfaces partway through the job commit, so * results in the destination being tainted. - * @param context job context + * @param commitContext commit context * @param pending the pending operations * @throws IOException any failure */ protected void precommitCheckPendingFiles( - final JobContext context, + final CommitContext commitContext, final ActiveCommit pending) throws IOException { FileSystem sourceFS = pending.getSourceFS(); try (DurationInfo ignored = new DurationInfo(LOG, "Preflight Load of pending files")) { - Tasks.foreach(pending.getSourceFiles()) + TaskPool.foreach(pending.getSourceFiles()) .stopOnFailure() .suppressExceptions(false) - .executeWith(buildSubmitter(context)) - .run(status -> PendingSet.load(sourceFS, status)); + .executeWith(commitContext.getOuterSubmitter()) + .run(status -> PersistentCommitData.load(sourceFS, status, + commitContext.getPendingSetSerializer())); } } /** * Load a pendingset file and commit all of its contents. + * Invoked within a parallel run; the commitContext thread + * pool is already busy/possibly full, so do not + * execute work through the same submitter. * @param commitContext context to commit through * @param activeCommit commit state * @param status file to load * @throws IOException failure */ private void loadAndCommit( - final CommitOperations.CommitContext commitContext, + final CommitContext commitContext, final ActiveCommit activeCommit, final FileStatus status) throws IOException { @@ -665,18 +732,20 @@ private void loadAndCommit( try (DurationInfo ignored = new DurationInfo(LOG, "Loading and committing files in pendingset %s", path)) { - PendingSet pendingSet = PendingSet.load(activeCommit.getSourceFS(), - status); + PendingSet pendingSet = PersistentCommitData.load( + activeCommit.getSourceFS(), + status, + commitContext.getPendingSetSerializer()); String jobId = pendingSet.getJobId(); if (!StringUtils.isEmpty(jobId) && !getUUID().equals(jobId)) { throw new PathCommitException(path, String.format("Mismatch in Job ID (%s) and commit job ID (%s)", getUUID(), jobId)); } - Tasks.foreach(pendingSet.getCommits()) + TaskPool.foreach(pendingSet.getCommits()) .stopOnFailure() .suppressExceptions(false) - .executeWith(singleThreadSubmitter()) + .executeWith(commitContext.getInnerSubmitter()) .onFailure((commit, exception) -> commitContext.abortSingleCommit(commit)) .abortWith(commitContext::abortSingleCommit) @@ -692,22 +761,27 @@ private void loadAndCommit( /** * Load a pendingset file and revert all of its contents. + * Invoked within a parallel run; the commitContext thread + * pool is already busy/possibly full, so do not + * execute work through the same submitter. * @param commitContext context to commit through * @param activeCommit commit state * @param status status of file to load * @throws IOException failure */ private void loadAndRevert( - final CommitOperations.CommitContext commitContext, + final CommitContext commitContext, final ActiveCommit activeCommit, final FileStatus status) throws IOException { final Path path = status.getPath(); try (DurationInfo ignored = new DurationInfo(LOG, false, "Committing %s", path)) { - PendingSet pendingSet = PendingSet.load(activeCommit.getSourceFS(), - status); - Tasks.foreach(pendingSet.getCommits()) + PendingSet pendingSet = PersistentCommitData.load( + activeCommit.getSourceFS(), + status, + commitContext.getPendingSetSerializer()); + TaskPool.foreach(pendingSet.getCommits()) .suppressExceptions(true) .run(commitContext::revertCommit); } @@ -715,6 +789,9 @@ private void loadAndRevert( /** * Load a pendingset file and abort all of its contents. + * Invoked within a parallel run; the commitContext thread + * pool is already busy/possibly full, so do not + * execute work through the same submitter. * @param commitContext context to commit through * @param activeCommit commit state * @param status status of file to load @@ -722,7 +799,7 @@ private void loadAndRevert( * @throws IOException failure */ private void loadAndAbort( - final CommitOperations.CommitContext commitContext, + final CommitContext commitContext, final ActiveCommit activeCommit, final FileStatus status, final boolean suppressExceptions, @@ -731,11 +808,13 @@ private void loadAndAbort( final Path path = status.getPath(); try (DurationInfo ignored = new DurationInfo(LOG, false, "Aborting %s", path)) { - PendingSet pendingSet = PendingSet.load(activeCommit.getSourceFS(), - status); + PendingSet pendingSet = PersistentCommitData.load( + activeCommit.getSourceFS(), + status, + commitContext.getPendingSetSerializer()); FileSystem fs = getDestFS(); - Tasks.foreach(pendingSet.getCommits()) - .executeWith(singleThreadSubmitter()) + TaskPool.foreach(pendingSet.getCommits()) + .executeWith(commitContext.getInnerSubmitter()) .suppressExceptions(suppressExceptions) .run(commit -> { try { @@ -752,28 +831,51 @@ private void loadAndAbort( } /** - * Start the final commit/abort commit operations. + * Start the final job commit/abort commit operations. + * @param context job context + * @return a commit context through which the operations can be invoked. + * @throws IOException failure. + */ + protected CommitContext initiateJobOperation( + final JobContext context) + throws IOException { + + return getCommitOperations().createCommitContext( + context, + getOutputPath(), + getJobCommitThreadCount(context)); + } + /** + * Start a ask commit/abort commit operations. + * This may have a different thread count. + * @param context job or task context * @return a commit context through which the operations can be invoked. * @throws IOException failure. */ - protected CommitOperations.CommitContext initiateCommitOperation() + protected CommitContext initiateTaskOperation( + final JobContext context) throws IOException { - return getCommitOperations().initiateCommitOperation(getOutputPath()); + + return getCommitOperations().createCommitContext( + context, + getOutputPath(), + getTaskCommitThreadCount(context)); } /** * Internal Job commit operation: where the S3 requests are made * (potentially in parallel). - * @param context job context + * @param commitContext commit context * @param pending pending commits * @throws IOException any failure */ - protected void commitJobInternal(JobContext context, - ActiveCommit pending) + protected void commitJobInternal( + final CommitContext commitContext, + final ActiveCommit pending) throws IOException { trackDurationOfInvocation(committerStatistics, COMMITTER_COMMIT_JOB.getSymbol(), - () -> commitPendingUploads(context, pending)); + () -> commitPendingUploads(commitContext, pending)); } @Override @@ -782,7 +884,9 @@ public void abortJob(JobContext context, JobStatus.State state) LOG.info("{}: aborting job {} in state {}", getRole(), jobIdString(context), state); // final cleanup operations - abortJobInternal(context, false); + try (CommitContext commitContext = initiateJobOperation(context)){ + abortJobInternal(commitContext, false); + } } @@ -790,30 +894,33 @@ public void abortJob(JobContext context, JobStatus.State state) * The internal job abort operation; can be overridden in tests. * This must clean up operations; it is called when a commit fails, as * well as in an {@link #abortJob(JobContext, JobStatus.State)} call. - * The base implementation calls {@link #cleanup(JobContext, boolean)} + * The base implementation calls {@link #cleanup(CommitContext, boolean)} * so cleans up the filesystems and destroys the thread pool. * Subclasses must always invoke this superclass method after their * own operations. - * @param context job context + * Creates and closes its own commit context. + * + * @param commitContext commit context * @param suppressExceptions should exceptions be suppressed? * @throws IOException any IO problem raised when suppressExceptions is false. */ - protected void abortJobInternal(JobContext context, + protected void abortJobInternal(CommitContext commitContext, boolean suppressExceptions) throws IOException { - cleanup(context, suppressExceptions); + cleanup(commitContext, suppressExceptions); } /** * Abort all pending uploads to the destination directory during * job cleanup operations. * Note: this instantiates the thread pool if required -so - * {@link #destroyThreadPool()} must be called after this. * @param suppressExceptions should exceptions be suppressed + * @param commitContext commit context * @throws IOException IO problem */ protected void abortPendingUploadsInCleanup( - boolean suppressExceptions) throws IOException { + boolean suppressExceptions, + CommitContext commitContext) throws IOException { // return early if aborting is disabled. if (!shouldAbortUploadsInCleanup()) { LOG.debug("Not cleanup up pending uploads to {} as {} is false ", @@ -824,9 +931,7 @@ protected void abortPendingUploadsInCleanup( Path dest = getOutputPath(); try (DurationInfo ignored = new DurationInfo(LOG, "Aborting all pending commits under %s", - dest); - CommitOperations.CommitContext commitContext - = initiateCommitOperation()) { + dest)) { CommitOperations ops = getCommitOperations(); List pending; try { @@ -840,8 +945,8 @@ protected void abortPendingUploadsInCleanup( LOG.warn("{} pending uploads were found -aborting", pending.size()); LOG.warn("If other tasks/jobs are writing to {}," + "this action may cause them to fail", dest); - Tasks.foreach(pending) - .executeWith(buildSubmitter(getJobContext())) + TaskPool.foreach(pending) + .executeWith(commitContext.getOuterSubmitter()) .suppressExceptions(suppressExceptions) .run(u -> commitContext.abortMultipartCommit( u.getKey(), u.getUploadId())); @@ -863,12 +968,12 @@ private boolean shouldAbortUploadsInCleanup() { * they can be loaded. * The Magic committer does not, because of the overhead of reading files * from S3 makes it too expensive. - * @param context job context + * @param commitContext commit context * @param pending the pending operations * @throws IOException any failure */ @VisibleForTesting - public void preCommitJob(JobContext context, + public void preCommitJob(CommitContext commitContext, ActiveCommit pending) throws IOException { } @@ -890,23 +995,55 @@ public void preCommitJob(JobContext context, @Override public void commitJob(JobContext context) throws IOException { String id = jobIdString(context); + // the commit context is created outside a try-with-resources block + // so it can be used in exception handling. + CommitContext commitContext = null; + + SuccessData successData = null; + IOException failure = null; + String stage = "preparing"; try (DurationInfo d = new DurationInfo(LOG, "%s: commitJob(%s)", getRole(), id)) { + commitContext = initiateJobOperation(context); ActiveCommit pending - = listPendingUploadsToCommit(context); - preCommitJob(context, pending); - commitJobInternal(context, pending); + = listPendingUploadsToCommit(commitContext); + stage = "precommit"; + preCommitJob(commitContext, pending); + stage = "commit"; + commitJobInternal(commitContext, pending); + stage = "completed"; jobCompleted(true); - maybeCreateSuccessMarkerFromCommits(context, pending); - cleanup(context, false); + stage = "marker"; + successData = maybeCreateSuccessMarkerFromCommits(context, pending); + stage = "cleanup"; + cleanup(commitContext, false); } catch (IOException e) { + // failure. record it for the summary + failure = e; LOG.warn("Commit failure for job {}", id, e); jobCompleted(false); - abortJobInternal(context, true); + abortJobInternal(commitContext, true); throw e; } finally { - resetCommonContext(); + // save the report summary, even on failure + if (commitContext != null) { + if (successData == null) { + // if the commit did not get as far as creating success data, create one. + successData = createSuccessData(context, null, null, + getDestFS().getConf()); + } + // save quietly, so no exceptions are raised + maybeSaveSummary(stage, + commitContext, + successData, + failure, + true, + true); + // and close that commit context + commitContext.close(); + } } + } /** @@ -925,30 +1062,28 @@ protected void jobCompleted(boolean success) { /** * Get the list of pending uploads for this job attempt. - * @param context job context + * @param commitContext commit context * @return a list of pending uploads. * @throws IOException Any IO failure */ protected abstract ActiveCommit listPendingUploadsToCommit( - JobContext context) + CommitContext commitContext) throws IOException; /** * Cleanup the job context, including aborting anything pending * and destroying the thread pool. - * @param context job context + * @param commitContext commit context * @param suppressExceptions should exceptions be suppressed? * @throws IOException any failure if exceptions were not suppressed. */ - protected void cleanup(JobContext context, + protected void cleanup(CommitContext commitContext, boolean suppressExceptions) throws IOException { try (DurationInfo d = new DurationInfo(LOG, - "Cleanup job %s", jobIdString(context))) { - abortPendingUploadsInCleanup(suppressExceptions); + "Cleanup job %s", jobIdString(commitContext.getJobContext()))) { + abortPendingUploadsInCleanup(suppressExceptions, commitContext); } finally { - destroyThreadPool(); cleanupStagingDirs(); - resetCommonContext(); } } @@ -958,8 +1093,9 @@ public void cleanupJob(JobContext context) throws IOException { String r = getRole(); String id = jobIdString(context); LOG.warn("{}: using deprecated cleanupJob call for {}", r, id); - try (DurationInfo d = new DurationInfo(LOG, "%s: cleanup Job %s", r, id)) { - cleanup(context, true); + try (DurationInfo d = new DurationInfo(LOG, "%s: cleanup Job %s", r, id); + CommitContext commitContext = initiateJobOperation(context)) { + cleanup(commitContext, true); } } @@ -1016,137 +1152,26 @@ protected String getRole() { return role; } - /** - * Returns an {@link Tasks.Submitter} for parallel tasks. The number of - * threads in the thread-pool is set by fs.s3a.committer.threads. - * If num-threads is 0, this will return null; - * this is used in Tasks as a cue - * to switch to single-threaded execution. - * - * @param context the JobContext for this commit - * @return a submitter or null - */ - protected Tasks.Submitter buildSubmitter( - JobContext context) { - if (getThreadCount(context) > 0) { - return new PoolSubmitter(context); - } else { - return null; - } - } - - /** - * Returns an {@link ExecutorService} for parallel tasks. The number of - * threads in the thread-pool is set by fs.s3a.committer.threads. - * If num-threads is 0, this will raise an exception. - * - * @param context the JobContext for this commit - * @param numThreads threads - * @return an {@link ExecutorService} for the number of threads - */ - private synchronized ExecutorService buildThreadPool( - JobContext context, int numThreads) { - Preconditions.checkArgument(numThreads > 0, - "Cannot create a thread pool with no threads"); - if (threadPool == null) { - LOG.debug("{}: creating thread pool of size {}", getRole(), numThreads); - threadPool = HadoopExecutors.newFixedThreadPool(numThreads, - new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat(THREAD_PREFIX + context.getJobID() + "-%d") - .build()); - } - return threadPool; - } - /** * Get the thread count for this job's commit operations. * @param context the JobContext for this commit * @return a possibly zero thread count. */ - private int getThreadCount(final JobContext context) { + private int getJobCommitThreadCount(final JobContext context) { return context.getConfiguration().getInt( FS_S3A_COMMITTER_THREADS, DEFAULT_COMMITTER_THREADS); } /** - * Submit a runnable. - * This will demand-create the thread pool if needed. - *

- * This is synchronized to ensure the thread pool is always valid when - * work is synchronized. See HADOOP-16798. + * Get the thread count for this task's commit operations. * @param context the JobContext for this commit - * @param task task to execute - * @return the future of the submitted task. - */ - private synchronized Future submitRunnable( - final JobContext context, - final Runnable task) { - return buildThreadPool(context, getThreadCount(context)).submit(task); - } - - /** - * The real task submitter, which hands off the work to - * the current thread pool. - */ - private final class PoolSubmitter implements Tasks.Submitter { - - private final JobContext context; - - private final int numThreads; - - private PoolSubmitter(final JobContext context) { - this.numThreads = getThreadCount(context); - Preconditions.checkArgument(numThreads > 0, - "Cannot create a thread pool with no threads"); - this.context = context; - } - - @Override - public Future submit(final Runnable task) { - return submitRunnable(context, task); - } - - } - - /** - * Destroy any thread pools; wait for that to finish, - * but don't overreact if it doesn't finish in time. - */ - protected void destroyThreadPool() { - ExecutorService pool; - // reset the thread pool in a sync block, then shut it down - // afterwards. This allows for other threads to create a - // new thread pool on demand. - synchronized(this) { - pool = this.threadPool; - threadPool = null; - } - if (pool != null) { - LOG.debug("Destroying thread pool"); - HadoopExecutors.shutdown(pool, LOG, - THREAD_POOL_SHUTDOWN_DELAY_SECONDS, TimeUnit.SECONDS); - } - } - - /** - * Get the thread pool for executing the single file commit/revert - * within the commit of all uploads of a single task. - * This is currently null; it is here to allow the Tasks class to - * provide the logic for execute/revert. - * @return null. always. - */ - protected final synchronized Tasks.Submitter singleThreadSubmitter() { - return null; - } - - /** - * Does this committer have a thread pool? - * @return true if a thread pool exists. + * @return a possibly zero thread count. */ - public synchronized boolean hasThreadPool() { - return threadPool != null; + private int getTaskCommitThreadCount(final JobContext context) { + return context.getConfiguration().getInt( + FS_S3A_COMMITTER_THREADS, + DEFAULT_COMMITTER_THREADS); } /** @@ -1164,24 +1189,23 @@ protected void deleteTaskAttemptPathQuietly(TaskAttemptContext context) { * Abort all pending uploads in the list. * This operation is used by the magic committer as part of its * rollback after a failure during task commit. - * @param context job context + * @param commitContext commit context * @param pending pending uploads * @param suppressExceptions should exceptions be suppressed * @throws IOException any exception raised */ - protected void abortPendingUploads(JobContext context, - List pending, - boolean suppressExceptions) + protected void abortPendingUploads( + final CommitContext commitContext, + final List pending, + final boolean suppressExceptions) throws IOException { if (pending == null || pending.isEmpty()) { LOG.info("{}: no pending commits to abort", getRole()); } else { try (DurationInfo d = new DurationInfo(LOG, - "Aborting %s uploads", pending.size()); - CommitOperations.CommitContext commitContext - = initiateCommitOperation()) { - Tasks.foreach(pending) - .executeWith(buildSubmitter(context)) + "Aborting %s uploads", pending.size())) { + TaskPool.foreach(pending) + .executeWith(commitContext.getOuterSubmitter()) .suppressExceptions(suppressExceptions) .run(commitContext::abortSingleCommit); } @@ -1190,27 +1214,24 @@ protected void abortPendingUploads(JobContext context, /** * Abort all pending uploads in the list. - * @param context job context + * @param commitContext commit context * @param pending pending uploads * @param suppressExceptions should exceptions be suppressed? * @param deleteRemoteFiles should remote files be deleted? * @throws IOException any exception raised */ protected void abortPendingUploads( - final JobContext context, + final CommitContext commitContext, final ActiveCommit pending, final boolean suppressExceptions, final boolean deleteRemoteFiles) throws IOException { - if (pending.isEmpty()) { LOG.info("{}: no pending commits to abort", getRole()); } else { try (DurationInfo d = new DurationInfo(LOG, - "Aborting %s uploads", pending.size()); - CommitOperations.CommitContext commitContext - = initiateCommitOperation()) { - Tasks.foreach(pending.getSourceFiles()) - .executeWith(buildSubmitter(context)) + "Aborting %s uploads", pending.size())) { + TaskPool.foreach(pending.getSourceFiles()) + .executeWith(commitContext.getOuterSubmitter()) .suppressExceptions(suppressExceptions) .run(path -> loadAndAbort(commitContext, @@ -1392,13 +1413,7 @@ public String toString() { */ protected final void updateCommonContext() { currentAuditContext().put(AuditConstants.PARAM_JOB_ID, uuid); - } - /** - * Remove JobID from the current thread's context. - */ - protected final void resetCommonContext() { - currentAuditContext().remove(AuditConstants.PARAM_JOB_ID); } protected AuditSpanSource getAuditSpanSource() { @@ -1424,6 +1439,77 @@ protected AuditSpan startOperation(String name, return getAuditSpanSource().createSpan(name, path1, path2); } + + /** + * Save a summary to the report dir if the config option + * is set. + * The report will be updated with the current active stage, + * and if {@code thrown} is non-null, it will be added to the + * diagnostics (and the job tagged as a failure). + * Static for testability. + * @param activeStage active stage + * @param context commit context. + * @param report summary file. + * @param thrown any exception indicting failure. + * @param quiet should exceptions be swallowed. + * @param overwrite should the existing file be overwritten + * @return the path of a file, if successfully saved + * @throws IOException if a failure occured and quiet==false + */ + private static Path maybeSaveSummary( + String activeStage, + CommitContext context, + SuccessData report, + Throwable thrown, + boolean quiet, + boolean overwrite) throws IOException { + Configuration conf = context.getConf(); + String reportDir = conf.getTrimmed(OPT_SUMMARY_REPORT_DIR, ""); + if (reportDir.isEmpty()) { + LOG.debug("No summary directory set in " + OPT_SUMMARY_REPORT_DIR); + return null; + } + LOG.debug("Summary directory set to {}", reportDir); + + Path reportDirPath = new Path(reportDir); + Path path = new Path(reportDirPath, + createJobSummaryFilename(context.getJobId())); + + if (thrown != null) { + report.recordJobFailure(thrown); + } + report.putDiagnostic(STAGE, activeStage); + // the store operations here is explicitly created for the FS where + // the reports go, which may not be the target FS of the job. + + final FileSystem fs = path.getFileSystem(conf); + try (ManifestStoreOperations operations = new ManifestStoreOperationsThroughFileSystem( + fs)) { + if (!overwrite) { + // check for file existence so there is no need to worry about + // precisely what exception is raised when overwrite=false and dest file + // exists + try { + FileStatus st = operations.getFileStatus(path); + // get here and the file exists + LOG.debug("Report already exists: {}", st); + return null; + } catch (FileNotFoundException ignored) { + } + } + report.save(fs, path, SuccessData.serializer()); + LOG.info("Job summary saved to {}", path); + return path; + } catch (IOException e) { + LOG.debug("Failed to save summary to {}", path, e); + if (quiet) { + return null; + } else { + throw e; + } + } + } + /** * State of the active commit operation. * @@ -1445,7 +1531,7 @@ protected AuditSpan startOperation(String name, * * */ - public static class ActiveCommit { + public static final class ActiveCommit { private static final AbstractS3ACommitter.ActiveCommit EMPTY = new ActiveCommit(null, new ArrayList<>()); @@ -1486,6 +1572,7 @@ public static class ActiveCommit { * @param sourceFS filesystem containing the list of pending files * @param sourceFiles .pendingset files to load and commit. */ + @SuppressWarnings("unchecked") public ActiveCommit( final FileSystem sourceFS, final List sourceFiles) { @@ -1496,13 +1583,14 @@ public ActiveCommit( /** * Create an active commit of the given pending files. * @param pendingFS source filesystem. - * @param statuses list of file status or subclass to use. + * @param statuses iterator of file status or subclass to use. * @return the commit + * @throws IOException if the iterator raises one. */ - public static ActiveCommit fromStatusList( + public static ActiveCommit fromStatusIterator( final FileSystem pendingFS, - final List statuses) { - return new ActiveCommit(pendingFS, statuses); + final RemoteIterator statuses) throws IOException { + return new ActiveCommit(pendingFS, toList(statuses)); } /** diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitConstants.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitConstants.java index bbc59f168f60d..b85ce276504ba 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitConstants.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitConstants.java @@ -22,7 +22,6 @@ import org.apache.hadoop.classification.InterfaceStability; import static org.apache.hadoop.fs.s3a.Constants.XA_HEADER_PREFIX; -import static org.apache.hadoop.mapreduce.lib.output.PathOutputCommitterFactory.COMMITTER_FACTORY_SCHEME_PATTERN; /** * Constants for working with committers. @@ -57,6 +56,12 @@ private CommitConstants() { */ public static final String PENDINGSET_SUFFIX = ".pendingset"; + + /** + * Prefix to use for config options: {@value}. + */ + public static final String OPT_PREFIX = "fs.s3a.committer."; + /** * Flag to indicate whether support for the Magic committer is enabled * in the filesystem. @@ -121,9 +126,8 @@ private CommitConstants() { /** * Temp data which is not auto-committed: {@value}. - * Uses a different name from normal just to make clear it is different. */ - public static final String TEMP_DATA = "__temp-data"; + public static final String TEMP_DATA = TEMPORARY; /** @@ -144,7 +148,7 @@ private CommitConstants() { * Key to set for the S3A schema to use the specific committer. */ public static final String S3A_COMMITTER_FACTORY_KEY = String.format( - COMMITTER_FACTORY_SCHEME_PATTERN, "s3a"); + "mapreduce.outputcommitter.factory.scheme.s3a"); /** * S3 Committer factory: {@value}. @@ -222,13 +226,19 @@ private CommitConstants() { /** * Number of threads in committers for parallel operations on files * (upload, commit, abort, delete...): {@value}. + * Two thread pools this size are created, one for the outer + * task-level parallelism, and one for parallel execution + * within tasks (POSTs to commit individual uploads) + * If the value is negative, it is inverted and then multiplied + * by the number of cores in the CPU. */ public static final String FS_S3A_COMMITTER_THREADS = "fs.s3a.committer.threads"; + /** * Default value for {@link #FS_S3A_COMMITTER_THREADS}: {@value}. */ - public static final int DEFAULT_COMMITTER_THREADS = 8; + public static final int DEFAULT_COMMITTER_THREADS = 32; /** * Path in the cluster filesystem for temporary data: {@value}. @@ -330,4 +340,18 @@ private CommitConstants() { public static final String XA_MAGIC_MARKER = XA_HEADER_PREFIX + X_HEADER_MAGIC_MARKER; + /** + * Task Attempt ID query header: {@value}. + */ + public static final String PARAM_TASK_ATTEMPT_ID = "ta"; + + /** + * Directory for saving job summary reports. + * These are the _SUCCESS files, but are saved even on + * job failures. + * Value: {@value}. + */ + public static final String OPT_SUMMARY_REPORT_DIR = + OPT_PREFIX + "summary.report.directory"; + } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitterStatisticNames.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitterStatisticNames.java new file mode 100644 index 0000000000000..20af292b3eb66 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitterStatisticNames.java @@ -0,0 +1,211 @@ +/* + * 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.s3a.commit; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.statistics.StoreStatisticNames; + +import static org.apache.hadoop.fs.statistics.StoreStatisticNames.OP_RENAME; + +/** + * Statistic names for committers. + * Please keep in sync with org.apache.hadoop.fs.s3a.Statistic + * so that S3A and manifest committers are in sync. + */ +@InterfaceAudience.Public +@InterfaceStability.Unstable +public final class CommitterStatisticNames { + + + /** Amount of data committed: {@value}. */ + public static final String COMMITTER_BYTES_COMMITTED_COUNT = + "committer_bytes_committed"; + + /** Duration Tracking of time to commit an entire job: {@value}. */ + public static final String COMMITTER_COMMIT_JOB = + "committer_commit_job"; + + /** Number of files committed: {@value}. */ + public static final String COMMITTER_FILES_COMMITTED_COUNT = + "committer_files_committed"; + /** "Count of successful tasks:: {@value}. */ + public static final String COMMITTER_TASKS_COMPLETED_COUNT = + "committer_tasks_completed"; + + /** Count of failed tasks: {@value}. */ + public static final String COMMITTER_TASKS_FAILED_COUNT = + "committer_tasks_failed"; + + /** Count of commits aborted: {@value}. */ + public static final String COMMITTER_COMMITS_ABORTED_COUNT = + "committer_commits_aborted"; + + /** Count of commits reverted: {@value}. */ + public static final String COMMITTER_COMMITS_REVERTED_COUNT = + "committer_commits_reverted"; + + /** Count of commits failed: {@value}. */ + public static final String COMMITTER_COMMITS_FAILED = + "committer_commits" + StoreStatisticNames.SUFFIX_FAILURES; + + /** + * The number of files in a task. This will be a MeanStatistic. + */ + public static final String COMMITTER_FILE_COUNT_MEAN = + "committer_task_file_count"; + + /** + * File Size. + */ + public static final String COMMITTER_FILE_SIZE_MEAN = + "committer_task_file_size"; + + /** + * What is a task attempt's directory count. + */ + public static final String COMMITTER_TASK_DIRECTORY_COUNT_MEAN = + "committer_task_directory_count"; + + /** + * What is the depth of a task attempt's directory tree. + */ + public static final String COMMITTER_TASK_DIRECTORY_DEPTH_MEAN = + "committer_task_directory_depth"; + + /** + * The number of files in a task. This will be a MeanStatistic. + */ + public static final String COMMITTER_TASK_FILE_COUNT_MEAN = + "committer_task_file_count"; + + /** + * The number of files in a task. This will be a MeanStatistic. + */ + public static final String COMMITTER_TASK_FILE_SIZE_MEAN = + "committer_task_file_size"; + + /** Directory creation {@value}. */ + public static final String OP_CREATE_DIRECTORIES = "op_create_directories"; + + /** Creating a single directory {@value}. */ + public static final String OP_CREATE_ONE_DIRECTORY = + "op_create_one_directory"; + + /** Directory scan {@value}. */ + public static final String OP_DIRECTORY_SCAN = "op_directory_scan"; + + /** + * Overall job commit {@value}. + */ + public static final String OP_STAGE_JOB_COMMIT = COMMITTER_COMMIT_JOB; + + /** {@value}. */ + public static final String OP_LOAD_ALL_MANIFESTS = "op_load_all_manifests"; + + /** + * Load a task manifest: {@value}. + */ + public static final String OP_LOAD_MANIFEST = "op_load_manifest"; + + /** Rename a file: {@value}. */ + public static final String OP_RENAME_FILE = OP_RENAME; + + /** + * Save a task manifest: {@value}. + */ + public static final String OP_SAVE_TASK_MANIFEST = + "op_task_stage_save_task_manifest"; + + /** + * Task abort: {@value}. + */ + public static final String OP_STAGE_TASK_ABORT_TASK + = "op_task_stage_abort_task"; + + /** + * Job abort: {@value}. + */ + public static final String OP_STAGE_JOB_ABORT = "op_job_stage_abort"; + + /** + * Job cleanup: {@value}. + */ + public static final String OP_STAGE_JOB_CLEANUP = "op_job_stage_cleanup"; + + /** + * Prepare Directories Stage: {@value}. + */ + public static final String OP_STAGE_JOB_CREATE_TARGET_DIRS = + "op_job_stage_create_target_dirs"; + + /** + * Load Manifest Stage: {@value}. + */ + public static final String OP_STAGE_JOB_LOAD_MANIFESTS = + "op_job_stage_load_manifests"; + + /** + * Rename files stage duration: {@value}. + */ + public static final String OP_STAGE_JOB_RENAME_FILES = + "op_job_stage_rename_files"; + + + /** + * Job Setup Stage: {@value}. + */ + public static final String OP_STAGE_JOB_SETUP = "op_job_stage_setup"; + + /** + * Job saving _SUCCESS marker Stage: {@value}. + */ + public static final String OP_STAGE_JOB_SAVE_SUCCESS = + "op_job_stage_save_success_marker"; + + /** + * Output Validation (within job commit) Stage: {@value}. + */ + public static final String OP_STAGE_JOB_VALIDATE_OUTPUT = + + "op_job_stage_optional_validate_output"; + /** + * Task saving manifest file Stage: {@value}. + */ + public static final String OP_STAGE_TASK_SAVE_MANIFEST = + "op_task_stage_save_manifest"; + + /** + * Task Setup Stage: {@value}. + */ + public static final String OP_STAGE_TASK_SETUP = "op_task_stage_setup"; + + + /** + * Task Commit Stage: {@value}. + */ + public static final String OP_STAGE_TASK_COMMIT = "op_stage_task_commit"; + + /** Task Scan directory Stage: {@value}. */ + public static final String OP_STAGE_TASK_SCAN_DIRECTORY + = "op_stage_task_scan_directory"; + + private CommitterStatisticNames() { + } +} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/InternalCommitterConstants.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/InternalCommitterConstants.java index fcafdd1ed1280..b2d4bfaeeabab 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/InternalCommitterConstants.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/InternalCommitterConstants.java @@ -35,6 +35,12 @@ @InterfaceStability.Unstable public final class InternalCommitterConstants { + /** + * How long threads in the thread pool stay alive when + * idle. Value in seconds: {@value}. + */ + public static final long THREAD_KEEP_ALIVE_TIME = 60L; + private InternalCommitterConstants() { } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/MagicCommitIntegration.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/MagicCommitIntegration.java index 41d36b2a8d7a0..e6524c91961dc 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/MagicCommitIntegration.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/MagicCommitIntegration.java @@ -28,6 +28,7 @@ import org.apache.hadoop.fs.s3a.Statistic; import org.apache.hadoop.fs.s3a.commit.magic.MagicCommitTracker; import org.apache.hadoop.fs.s3a.impl.AbstractStoreOperation; +import org.apache.hadoop.fs.s3a.statistics.PutTrackerStatistics; import static org.apache.hadoop.fs.s3a.commit.MagicCommitPaths.*; @@ -38,8 +39,9 @@ * in this case: *
    *
  1. {@link #isMagicCommitPath(Path)} will always return false.
  2. - *
  3. {@link #createTracker(Path, String)} will always return an instance - * of {@link PutTracker}.
  4. + *
  5. {@link #isUnderMagicPath(Path)} will always return false.
  6. + *
  7. {@link #createTracker(Path, String, PutTrackerStatistics)} will always + * return an instance of {@link PutTracker}.
  8. *
* *

Important

: must not directly or indirectly import a class which @@ -88,9 +90,11 @@ public String keyOfFinalDestination(List elements, String key) { * for the commit tracker. * @param path path of nominal write * @param key key of path of nominal write + * @param trackerStatistics tracker statistics * @return the tracker for this operation. */ - public PutTracker createTracker(Path path, String key) { + public PutTracker createTracker(Path path, String key, + PutTrackerStatistics trackerStatistics) { final List elements = splitPathToElements(path); PutTracker tracker; @@ -106,7 +110,8 @@ public PutTracker createTracker(Path path, String key) { key, destKey, pendingsetPath, - owner.getWriteOperationHelper()); + owner.getWriteOperationHelper(), + trackerStatistics); LOG.debug("Created {}", tracker); } else { LOG.warn("File being created has a \"magic\" path, but the filesystem" @@ -184,4 +189,13 @@ private boolean isCommitMetadataFile(List elements) { || last.endsWith(CommitConstants.PENDINGSET_SUFFIX); } + /** + * Is this path in/under a magic path...regardless of file type. + * This is used to optimize create() operations. + * @param path path to check + * @return true if the path is one a magic file write expects. + */ + public boolean isUnderMagicPath(Path path) { + return magicCommitEnabled && isMagicPath(splitPathToElements(path)); + } } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/Tasks.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/Tasks.java deleted file mode 100644 index c318e86605e0c..0000000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/Tasks.java +++ /dev/null @@ -1,423 +0,0 @@ -/* - * 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.s3a.commit; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Utility class for parallel execution, takes closures for the various - * actions. - * There is no retry logic: it is expected to be handled by the closures. - */ -public final class Tasks { - private static final Logger LOG = LoggerFactory.getLogger(Tasks.class); - - private Tasks() { - } - - /** - * Callback invoked to process an item. - * @param item type being processed - * @param exception class which may be raised - */ - @FunctionalInterface - public interface Task { - void run(I item) throws E; - } - - /** - * Callback invoked on a failure. - * @param item type being processed - * @param exception class which may be raised - */ - @FunctionalInterface - public interface FailureTask { - - /** - * process a failure. - * @param item item the task is processing - * @param exception the exception which was raised. - * @throws E Exception of type E - */ - void run(I item, Exception exception) throws E; - } - - /** - * Builder for task execution. - * @param item type - */ - public static class Builder { - private final Iterable items; - private Submitter service = null; - private FailureTask onFailure = null; - private boolean stopOnFailure = false; - private boolean suppressExceptions = false; - private Task revertTask = null; - private boolean stopRevertsOnFailure = false; - private Task abortTask = null; - private boolean stopAbortsOnFailure = false; - - /** - * Create the builder. - * @param items items to process - */ - Builder(Iterable items) { - this.items = items; - } - - /** - * Declare executor service: if null, the tasks are executed in a single - * thread. - * @param submitter service to schedule tasks with. - * @return this builder. - */ - public Builder executeWith(Submitter submitter) { - this.service = submitter; - return this; - } - - public Builder onFailure(FailureTask task) { - this.onFailure = task; - return this; - } - - public Builder stopOnFailure() { - this.stopOnFailure = true; - return this; - } - - public Builder suppressExceptions() { - return suppressExceptions(true); - } - - public Builder suppressExceptions(boolean suppress) { - this.suppressExceptions = suppress; - return this; - } - - public Builder revertWith(Task task) { - this.revertTask = task; - return this; - } - - public Builder stopRevertsOnFailure() { - this.stopRevertsOnFailure = true; - return this; - } - - public Builder abortWith(Task task) { - this.abortTask = task; - return this; - } - - public Builder stopAbortsOnFailure() { - this.stopAbortsOnFailure = true; - return this; - } - - public boolean run(Task task) throws E { - if (service != null) { - return runParallel(task); - } else { - return runSingleThreaded(task); - } - } - - private boolean runSingleThreaded(Task task) - throws E { - List succeeded = new ArrayList<>(); - List exceptions = new ArrayList<>(); - - Iterator iterator = items.iterator(); - boolean threw = true; - try { - while (iterator.hasNext()) { - I item = iterator.next(); - try { - task.run(item); - succeeded.add(item); - - } catch (Exception e) { - exceptions.add(e); - - if (onFailure != null) { - try { - onFailure.run(item, e); - } catch (Exception failException) { - LOG.error("Failed to clean up on failure", e); - // keep going - } - } - - if (stopOnFailure) { - break; - } - } - } - - threw = false; - - } finally { - // threw handles exceptions that were *not* caught by the catch block, - // and exceptions that were caught and possibly handled by onFailure - // are kept in exceptions. - if (threw || !exceptions.isEmpty()) { - if (revertTask != null) { - boolean failed = false; - for (I item : succeeded) { - try { - revertTask.run(item); - } catch (Exception e) { - LOG.error("Failed to revert task", e); - failed = true; - // keep going - } - if (stopRevertsOnFailure && failed) { - break; - } - } - } - - if (abortTask != null) { - boolean failed = false; - while (iterator.hasNext()) { - try { - abortTask.run(iterator.next()); - } catch (Exception e) { - failed = true; - LOG.error("Failed to abort task", e); - // keep going - } - if (stopAbortsOnFailure && failed) { - break; - } - } - } - } - } - - if (!suppressExceptions && !exceptions.isEmpty()) { - Tasks.throwOne(exceptions); - } - - return !threw && exceptions.isEmpty(); - } - - private boolean runParallel(final Task task) - throws E { - final Queue succeeded = new ConcurrentLinkedQueue<>(); - final Queue exceptions = new ConcurrentLinkedQueue<>(); - final AtomicBoolean taskFailed = new AtomicBoolean(false); - final AtomicBoolean abortFailed = new AtomicBoolean(false); - final AtomicBoolean revertFailed = new AtomicBoolean(false); - - List> futures = new ArrayList<>(); - - for (final I item : items) { - // submit a task for each item that will either run or abort the task - futures.add(service.submit(new Runnable() { - @Override - public void run() { - if (!(stopOnFailure && taskFailed.get())) { - // run the task - boolean threw = true; - try { - LOG.debug("Executing task"); - task.run(item); - succeeded.add(item); - LOG.debug("Task succeeded"); - - threw = false; - - } catch (Exception e) { - taskFailed.set(true); - exceptions.add(e); - LOG.info("Task failed", e); - - if (onFailure != null) { - try { - onFailure.run(item, e); - } catch (Exception failException) { - LOG.error("Failed to clean up on failure", e); - // swallow the exception - } - } - } finally { - if (threw) { - taskFailed.set(true); - } - } - - } else if (abortTask != null) { - // abort the task instead of running it - if (stopAbortsOnFailure && abortFailed.get()) { - return; - } - - boolean failed = true; - try { - LOG.info("Aborting task"); - abortTask.run(item); - failed = false; - } catch (Exception e) { - LOG.error("Failed to abort task", e); - // swallow the exception - } finally { - if (failed) { - abortFailed.set(true); - } - } - } - } - })); - } - - // let the above tasks complete (or abort) - waitFor(futures); - int futureCount = futures.size(); - futures.clear(); - - if (taskFailed.get() && revertTask != null) { - // at least one task failed, revert any that succeeded - LOG.info("Reverting all {} succeeded tasks from {} futures", - succeeded.size(), futureCount); - for (final I item : succeeded) { - futures.add(service.submit(() -> { - if (stopRevertsOnFailure && revertFailed.get()) { - return; - } - - boolean failed = true; - try { - revertTask.run(item); - failed = false; - } catch (Exception e) { - LOG.error("Failed to revert task", e); - // swallow the exception - } finally { - if (failed) { - revertFailed.set(true); - } - } - })); - } - - // let the revert tasks complete - waitFor(futures); - } - - if (!suppressExceptions && !exceptions.isEmpty()) { - Tasks.throwOne(exceptions); - } - - return !taskFailed.get(); - } - } - - /** - * Wait for all the futures to complete; there's a small sleep between - * each iteration; enough to yield the CPU. - * @param futures futures. - */ - private static void waitFor(Collection> futures) { - int size = futures.size(); - LOG.debug("Waiting for {} tasks to complete", size); - int oldNumFinished = 0; - while (true) { - int numFinished = (int) futures.stream().filter(Future::isDone).count(); - - if (oldNumFinished != numFinished) { - LOG.debug("Finished count -> {}/{}", numFinished, size); - oldNumFinished = numFinished; - } - - if (numFinished == size) { - // all of the futures are done, stop looping - break; - } else { - try { - Thread.sleep(10); - } catch (InterruptedException e) { - futures.forEach(future -> future.cancel(true)); - Thread.currentThread().interrupt(); - break; - } - } - } - } - - public static Builder foreach(Iterable items) { - return new Builder<>(items); - } - - public static Builder foreach(I[] items) { - return new Builder<>(Arrays.asList(items)); - } - - @SuppressWarnings("unchecked") - private static void throwOne( - Collection exceptions) - throws E { - Iterator iter = exceptions.iterator(); - Exception e = iter.next(); - Class exceptionClass = e.getClass(); - - while (iter.hasNext()) { - Exception other = iter.next(); - if (!exceptionClass.isInstance(other)) { - e.addSuppressed(other); - } - } - - Tasks.castAndThrow(e); - } - - @SuppressWarnings("unchecked") - private static void castAndThrow(Exception e) throws E { - if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } - throw (E) e; - } - - /** - * Interface to whatever lets us submit tasks. - */ - public interface Submitter { - - /** - * Submit work. - * @param task task to execute - * @return the future of the submitted task. - */ - Future submit(Runnable task); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/PendingSet.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/PendingSet.java index 318896e236137..108e6d45bc621 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/PendingSet.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/PendingSet.java @@ -38,6 +38,7 @@ import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.commit.ValidationFailure; +import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsSnapshot; import org.apache.hadoop.fs.statistics.IOStatisticsSource; import org.apache.hadoop.util.JsonSerialization; @@ -63,8 +64,7 @@ @SuppressWarnings("unused") @InterfaceAudience.Private @InterfaceStability.Unstable -public class PendingSet extends PersistentCommitData - implements IOStatisticsSource { +public class PendingSet extends PersistentCommitData { private static final Logger LOG = LoggerFactory.getLogger(PendingSet.class); /** @@ -112,38 +112,13 @@ public PendingSet(int size) { } /** - * Get a JSON serializer for this class. + * Get a shared JSON serializer for this class. * @return a serializer. */ public static JsonSerialization serializer() { - return new JsonSerialization<>(PendingSet.class, false, true); + return new JsonSerialization<>(PendingSet.class, false, false); } - /** - * Load an instance from a file, then validate it. - * @param fs filesystem - * @param path path - * @return the loaded instance - * @throws IOException IO failure - * @throws ValidationFailure if the data is invalid - */ - public static PendingSet load(FileSystem fs, Path path) - throws IOException { - return load(fs, path, null); - } - - /** - * Load an instance from a file, then validate it. - * @param fs filesystem - * @param status status of file to load - * @return the loaded instance - * @throws IOException IO failure - * @throws ValidationFailure if the data is invalid - */ - public static PendingSet load(FileSystem fs, FileStatus status) - throws IOException { - return load(fs, status.getPath(), status); - } /** * Load an instance from a file, then validate it. @@ -211,8 +186,8 @@ public void validate() throws ValidationFailure { } @Override - public byte[] toBytes() throws IOException { - return serializer().toBytes(this); + public byte[] toBytes(JsonSerialization serializer) throws IOException { + return serializer.toBytes(this); } /** @@ -224,9 +199,10 @@ public int size() { } @Override - public void save(FileSystem fs, Path path, boolean overwrite) - throws IOException { - serializer().save(fs, path, this, overwrite); + public IOStatistics save(final FileSystem fs, + final Path path, + final JsonSerialization serializer) throws IOException { + return saveFile(fs, path, this, serializer, true); } /** @return the version marker. */ diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/PersistentCommitData.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/PersistentCommitData.java index dba44b9a011d9..be2de6cf89cfd 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/PersistentCommitData.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/PersistentCommitData.java @@ -21,19 +21,33 @@ import java.io.IOException; import java.io.Serializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FSDataOutputStreamBuilder; +import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.commit.ValidationFailure; +import org.apache.hadoop.fs.statistics.IOStatistics; +import org.apache.hadoop.fs.statistics.IOStatisticsSource; +import org.apache.hadoop.util.JsonSerialization; + +import static org.apache.hadoop.fs.s3a.Constants.FS_S3A_CREATE_PERFORMANCE; /** * Class for single/multiple commit data structures. + * The mapreduce hierarchy {@code AbstractManifestData} is a fork + * of this; the Success data JSON format must stay compatible */ -@SuppressWarnings("serial") @InterfaceAudience.Private @InterfaceStability.Unstable -public abstract class PersistentCommitData implements Serializable { +public abstract class PersistentCommitData + implements Serializable, IOStatisticsSource { + private static final Logger LOG = LoggerFactory.getLogger(PersistentCommitData.class); /** * Supported version value: {@value}. @@ -52,18 +66,109 @@ public abstract class PersistentCommitData implements Serializable { * Serialize to JSON and then to a byte array, after performing a * preflight validation of the data to be saved. * @return the data in a persistable form. + * @param serializer serializer to use * @throws IOException serialization problem or validation failure. */ - public abstract byte[] toBytes() throws IOException; + public abstract byte[] toBytes(JsonSerialization serializer) throws IOException; /** * Save to a hadoop filesystem. + * The destination file is overwritten, and on s3a stores the + * performance flag is set to turn off all existence checks and + * parent dir cleanup. + * The assumption here is: the job knows what it is doing. + * * @param fs filesystem * @param path path - * @param overwrite should any existing file be overwritten + * @param serializer serializer to use + * @return IOStats from the output stream. + * * @throws IOException IO exception */ - public abstract void save(FileSystem fs, Path path, boolean overwrite) + public abstract IOStatistics save(FileSystem fs, Path path, JsonSerialization serializer) throws IOException; + /** + * Load an instance from a status, then validate it. + * This uses the openFile() API, which S3A supports for + * faster load and declaring sequential access, always + * @param type of persistent format + * @param fs filesystem + * @param status status of file to load + * @param serializer serializer to use + * @return the loaded instance + * @throws IOException IO failure + * @throws ValidationFailure if the data is invalid + */ + public static T load(FileSystem fs, + FileStatus status, + JsonSerialization serializer) + throws IOException { + Path path = status.getPath(); + LOG.debug("Reading commit data from file {}", path); + T result = serializer.load(fs, path, status); + result.validate(); + return result; + } + + /** + * Save to a file. + * This uses the createFile() API, which S3A supports for + * faster load and declaring sequential access, always + * + * @param type of persistent format + * @param fs filesystem + * @param path path to save to + * @param instance data to save + * @param serializer serializer to use + * @param performance skip all safety check on the write + * + * @return any IOStatistics from the output stream, or null + * + * @throws IOException IO failure + */ + public static IOStatistics saveFile( + final FileSystem fs, + final Path path, + final T instance, + final JsonSerialization serializer, + final boolean performance) + throws IOException { + + FSDataOutputStreamBuilder builder = fs.createFile(path) + .create() + .recursive() + .overwrite(true); + // switch to performance mode + builder.opt(FS_S3A_CREATE_PERFORMANCE, performance); + return saveToStream(path, instance, builder, serializer); + } + + /** + * Save to a file. + * This uses the createFile() API, which S3A supports for + * faster load and declaring sequential access, always + * @param type of persistent format + * @param path path to save to (used for logging) + * @param instance data to save + * @param builder builder already prepared for the write + * @param serializer serializer to use + * @return any IOStatistics from the output stream, or null + * @throws IOException IO failure + */ + public static IOStatistics saveToStream( + final Path path, + final T instance, + final FSDataOutputStreamBuilder builder, + final JsonSerialization serializer) throws IOException { + LOG.debug("saving commit data to file {}", path); + FSDataOutputStream dataOutputStream = builder.build(); + try { + dataOutputStream.write(serializer.toBytes(instance)); + } finally { + dataOutputStream.close(); + } + return dataOutputStream.getIOStatistics(); + } + } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SinglePendingCommit.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SinglePendingCommit.java index b53ef75d823df..77c3fed11fb24 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SinglePendingCommit.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SinglePendingCommit.java @@ -34,8 +34,6 @@ import com.amazonaws.services.s3.model.PartETag; import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.hadoop.util.Preconditions; - import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; @@ -43,9 +41,11 @@ import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.commit.ValidationFailure; +import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsSnapshot; import org.apache.hadoop.fs.statistics.IOStatisticsSource; import org.apache.hadoop.util.JsonSerialization; +import org.apache.hadoop.util.Preconditions; import static org.apache.hadoop.fs.s3a.commit.CommitUtils.validateCollectionClass; import static org.apache.hadoop.fs.s3a.commit.ValidationFailure.verify; @@ -69,8 +69,8 @@ @SuppressWarnings("unused") @InterfaceAudience.Private @InterfaceStability.Unstable -public class SinglePendingCommit extends PersistentCommitData - implements Iterable, IOStatisticsSource { +public class SinglePendingCommit extends PersistentCommitData + implements Iterable { /** * Serialization ID: {@value}. @@ -141,26 +141,32 @@ public SinglePendingCommit() { * @return a serializer. */ public static JsonSerialization serializer() { - return new JsonSerialization<>(SinglePendingCommit.class, false, true); + return new JsonSerialization<>(SinglePendingCommit.class, false, false); } /** * Load an instance from a file, then validate it. * @param fs filesystem * @param path path + * @param status nullable status of file to load + * @param serDeser serializer; if null use the shared static one. * @return the loaded instance * @throws IOException IO failure * @throws ValidationFailure if the data is invalid */ - public static SinglePendingCommit load(FileSystem fs, Path path) + public static SinglePendingCommit load(FileSystem fs, + Path path, + FileStatus status, + JsonSerialization serDeser) throws IOException { - return load(fs, path, null); + return load(fs, path, serDeser, null); } /** * Load an instance from a file, then validate it. * @param fs filesystem * @param path path + * @param serDeser deserializer * @param status status of file to load or null * @return the loaded instance * @throws IOException IO failure @@ -168,9 +174,12 @@ public static SinglePendingCommit load(FileSystem fs, Path path) */ public static SinglePendingCommit load(FileSystem fs, Path path, + JsonSerialization serDeser, @Nullable FileStatus status) throws IOException { - SinglePendingCommit instance = serializer().load(fs, path, status); + JsonSerialization jsonSerialization = + serDeser != null ? serDeser : serializer(); + SinglePendingCommit instance = jsonSerialization.load(fs, path, status); instance.filename = path.toString(); instance.validate(); return instance; @@ -264,15 +273,16 @@ public String toString() { } @Override - public byte[] toBytes() throws IOException { + public byte[] toBytes(JsonSerialization serializer) throws IOException { validate(); - return serializer().toBytes(this); + return serializer.toBytes(this); } @Override - public void save(FileSystem fs, Path path, boolean overwrite) - throws IOException { - serializer().save(fs, path, this, overwrite); + public IOStatistics save(final FileSystem fs, + final Path path, + final JsonSerialization serializer) throws IOException { + return saveFile(fs, path, this, serializer, true); } /** diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SuccessData.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SuccessData.java index 00f196a0b406e..0e0a03f587081 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SuccessData.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SuccessData.java @@ -30,13 +30,14 @@ import org.slf4j.LoggerFactory; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.commit.ValidationFailure; +import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsSnapshot; -import org.apache.hadoop.fs.statistics.IOStatisticsSource; import org.apache.hadoop.util.JsonSerialization; /** @@ -51,10 +52,24 @@ *
  • Not loadable? Something else.
  • * * - * This is an unstable structure intended for diagnostics and testing. - * Applications reading this data should use/check the {@link #name} field - * to differentiate from any other JSON-based manifest and to identify - * changes in the output format. + * This should be considered public, and MUST stay compatible + * at the JSON format level with that of + * {@code org.apache.hadoop.mapreduce.lib.output.committer.manifest.files.ManifestSuccessData} + *

    + * The JSON format SHOULD be considered public and evolving + * with compatibility across versions. + *

    + * All the Java serialization data is different and may change + * across versions with no stability guarantees other than + * "manifest summaries MAY be serialized between processes with + * the exact same version of this binary on their classpaths." + * That is sufficient for testing in Spark. + *

    + * To aid with Java serialization, the maps and lists are + * exclusively those which serialize well. + * IOStatisticsSnapshot has a lot of complexity in marshalling + * there; this class doesn't worry about concurrent access + * so is simpler. * * Note: to deal with scale issues, the S3A committers do not include any * more than the number of objects listed in @@ -65,8 +80,7 @@ @SuppressWarnings("unused") @InterfaceAudience.Private @InterfaceStability.Unstable -public class SuccessData extends PersistentCommitData - implements IOStatisticsSource { +public class SuccessData extends PersistentCommitData { private static final Logger LOG = LoggerFactory.getLogger(SuccessData.class); @@ -80,7 +94,7 @@ public class SuccessData extends PersistentCommitData /** * Serialization ID: {@value}. */ - private static final long serialVersionUID = 507133045258460083L + VERSION; + private static final long serialVersionUID = 507133045258460084L + VERSION; /** * Name to include in persisted data, so as to differentiate from @@ -92,7 +106,14 @@ public class SuccessData extends PersistentCommitData /** * Name of file; includes version marker. */ - private String name; + private String name = NAME; + + /** + * Did this succeed? + * It is implicitly true in a _SUCCESS file, but if the file + * is also saved to a log dir, then it depends on the outcome + */ + private boolean success = true; /** Timestamp of creation. */ private long timestamp; @@ -142,7 +163,17 @@ public class SuccessData extends PersistentCommitData * IOStatistics. */ @JsonProperty("iostatistics") - private IOStatisticsSnapshot iostats = new IOStatisticsSnapshot(); + private IOStatisticsSnapshot iostatistics = new IOStatisticsSnapshot(); + + /** + * State (committed, aborted). + */ + private String state; + + /** + * Stage: last stage executed. + */ + private String stage; @Override public void validate() throws ValidationFailure { @@ -153,16 +184,17 @@ public void validate() throws ValidationFailure { } @Override - public byte[] toBytes() throws IOException { - return serializer().toBytes(this); + public byte[] toBytes(JsonSerialization serializer) throws IOException { + return serializer.toBytes(this); } @Override - public void save(FileSystem fs, Path path, boolean overwrite) - throws IOException { + public IOStatistics save(final FileSystem fs, + final Path path, + final JsonSerialization serializer) throws IOException { // always set the name field before being saved. name = NAME; - serializer().save(fs, path, this, overwrite); + return saveFile(fs, path, this, serializer, true); } @Override @@ -250,8 +282,8 @@ public static SuccessData load(FileSystem fs, Path path) * Get a JSON serializer for this class. * @return a serializer. */ - private static JsonSerialization serializer() { - return new JsonSerialization<>(SuccessData.class, false, true); + public static JsonSerialization serializer() { + return new JsonSerialization<>(SuccessData.class, false, false); } public String getName() { @@ -371,10 +403,59 @@ public void setJobIdSource(final String jobIdSource) { @Override public IOStatisticsSnapshot getIOStatistics() { - return iostats; + return iostatistics; } public void setIOStatistics(final IOStatisticsSnapshot ioStatistics) { - this.iostats = ioStatistics; + this.iostatistics = ioStatistics; + } + + /** + * Set the success flag. + * @param success did the job succeed? + */ + public void setSuccess(boolean success) { + this.success = success; + } + + /** + * Get the success flag. + * @return did the job succeed? + */ + public boolean getSuccess() { + return success; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getStage() { + return stage; + } + + /** + * Add a diagnostics entry. + * @param key name + * @param value value + */ + public void putDiagnostic(String key, String value) { + diagnostics.put(key, value); + } + + /** + * Note a failure by setting success flag to false, + * then add the exception to the diagnostics. + * @param thrown throwable + */ + public void recordJobFailure(Throwable thrown) { + setSuccess(false); + String stacktrace = ExceptionUtils.getStackTrace(thrown); + diagnostics.put("exception", thrown.toString()); + diagnostics.put("stacktrace", stacktrace); } } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/package-info.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/package-info.java index 0d574948e91a3..36eedba83b854 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/package-info.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/package-info.java @@ -29,12 +29,11 @@ *

  • The summary information saved in the {@code _SUCCESS} file.
  • * * - * There are no guarantees of stability between versions; these are internal - * structures. * * The {@link org.apache.hadoop.fs.s3a.commit.files.SuccessData} file is - * the one visible to callers after a job completes; it is an unstable - * manifest intended for testing only. + * the one visible to callers after a job completes; it is compatible with + * the manifest committer format persisted in + * {@code org.apache.hadoop.mapreduce.lib.output.committer.manifest.files.ManifestSuccessData} * */ @InterfaceAudience.Private diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/AuditContextUpdater.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/AuditContextUpdater.java new file mode 100644 index 0000000000000..20024ba601d45 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/AuditContextUpdater.java @@ -0,0 +1,90 @@ +/* + * 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.s3a.commit.impl; + +import org.apache.hadoop.fs.audit.AuditConstants; +import org.apache.hadoop.fs.audit.CommonAuditContext; +import org.apache.hadoop.fs.s3a.commit.CommitConstants; +import org.apache.hadoop.mapreduce.JobContext; +import org.apache.hadoop.mapreduce.TaskAttemptContext; +import org.apache.hadoop.mapreduce.TaskAttemptID; + +import static org.apache.hadoop.fs.audit.CommonAuditContext.currentAuditContext; + +/** + * Class to track/update context information to set + * in threads. + */ +public final class AuditContextUpdater { + + /** + * Job ID. + */ + private final String jobId; + + /** + * Task attempt ID for auditing. + */ + private final String taskAttemptId; + + /** + * Construct. Stores job information + * to attach to thread contexts. + * @param jobContext job/task context. + */ + public AuditContextUpdater(final JobContext jobContext) { + this.jobId = jobContext.getJobID().toString(); + + if (jobContext instanceof TaskAttemptContext) { + // it's a task, extract info for auditing + final TaskAttemptID tid = ((TaskAttemptContext) jobContext).getTaskAttemptID(); + this.taskAttemptId = tid.toString(); + } else { + this.taskAttemptId = null; + } + } + + public AuditContextUpdater(String jobId) { + this.jobId = jobId; + this.taskAttemptId = null; + } + + /** + * Add job/task info to current audit context. + */ + public void updateCurrentAuditContext() { + final CommonAuditContext auditCtx = currentAuditContext(); + auditCtx.put(AuditConstants.PARAM_JOB_ID, jobId); + if (taskAttemptId != null) { + auditCtx.put(AuditConstants.PARAM_TASK_ATTEMPT_ID, taskAttemptId); + } else { + currentAuditContext().remove(CommitConstants.PARAM_TASK_ATTEMPT_ID); + } + + } + + /** + * Remove job/task info from the current audit context. + */ + public void resetCurrentAuditContext() { + currentAuditContext().remove(AuditConstants.PARAM_JOB_ID); + currentAuditContext().remove(CommitConstants.PARAM_TASK_ATTEMPT_ID); + } + +} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/CommitContext.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/CommitContext.java new file mode 100644 index 0000000000000..8bff165f2e8f5 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/CommitContext.java @@ -0,0 +1,401 @@ +/* + * 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.s3a.commit.impl; + +import java.io.Closeable; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.impl.WeakReferenceThreadMap; +import org.apache.hadoop.fs.s3a.commit.InternalCommitterConstants; +import org.apache.hadoop.fs.s3a.commit.files.PendingSet; +import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; +import org.apache.hadoop.io.IOUtils; +import org.apache.hadoop.mapreduce.JobContext; +import org.apache.hadoop.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.apache.hadoop.util.JsonSerialization; +import org.apache.hadoop.util.Preconditions; +import org.apache.hadoop.util.concurrent.HadoopExecutors; +import org.apache.hadoop.util.concurrent.HadoopThreadPoolExecutor; +import org.apache.hadoop.util.functional.TaskPool; + +import static org.apache.hadoop.fs.s3a.Constants.THREAD_POOL_SHUTDOWN_DELAY_SECONDS; +import static org.apache.hadoop.fs.s3a.commit.AbstractS3ACommitter.THREAD_PREFIX; +import static org.apache.hadoop.fs.s3a.commit.InternalCommitterConstants.THREAD_KEEP_ALIVE_TIME; + +/** + * Commit context. + * + * It is used to manage the final commit sequence where files become + * visible. + * + * Once the commit operation has completed, it must be closed. + * It MUST NOT be reused. + * + * Audit integration: job and task attributes are added to the thread local context + * on create, removed on close(). + * + * JSON Serializers are created on demand, on a per thread basis. + * A {@link WeakReferenceThreadMap} is used here; a GC may lose the + * references, but they will recreated as needed. + */ +public final class CommitContext implements Closeable { + + private static final Logger LOG = LoggerFactory.getLogger( + CommitContext.class); + + /** + * The actual commit operations. + */ + private final CommitOperations commitOperations; + + /** + * Job Context. + */ + private final JobContext jobContext; + + /** + * Serializer pool. + */ + + private final WeakReferenceThreadMap> + pendingSetSerializer = + new WeakReferenceThreadMap<>((k) -> PendingSet.serializer(), null); + + private final WeakReferenceThreadMap> + singleCommitSerializer = + new WeakReferenceThreadMap<>((k) -> SinglePendingCommit.serializer(), null); + + /** + * Submitter for per task operations, e.g loading manifests. + */ + private PoolSubmitter outerSubmitter; + + /** + * Submitter for operations within the tasks, + * such as POSTing the final commit operations. + */ + private PoolSubmitter innerSubmitter; + + /** + * Job Configuration. + */ + private final Configuration conf; + + /** + * Job ID. + */ + private final String jobId; + + /** + * Audit context; will be reset when this is closed. + */ + private final AuditContextUpdater auditContextUpdater; + + /** + * Number of committer threads. + */ + private final int committerThreads; + + /** + * Create. + * @param commitOperations commit callbacks + * @param jobContext job context + * @param committerThreads number of commit threads + */ + public CommitContext( + final CommitOperations commitOperations, + final JobContext jobContext, + final int committerThreads) { + this.commitOperations = commitOperations; + this.jobContext = jobContext; + this.conf = jobContext.getConfiguration(); + this.jobId = jobContext.getJobID().toString(); + this.auditContextUpdater = new AuditContextUpdater(jobContext); + this.auditContextUpdater.updateCurrentAuditContext(); + this.committerThreads = committerThreads; + + buildSubmitters(); + } + + /** + * Create for testing. + * This has no job context; instead the values + * are set explicitly. + * @param commitOperations commit callbacks + * @param conf job conf + * @param jobId ID + * @param committerThreads number of commit threads + */ + public CommitContext(final CommitOperations commitOperations, + final Configuration conf, + final String jobId, + final int committerThreads) { + this.commitOperations = commitOperations; + this.jobContext = null; + this.conf = conf; + this.jobId = jobId; + this.auditContextUpdater = new AuditContextUpdater(jobId); + this.auditContextUpdater.updateCurrentAuditContext(); + this.committerThreads = committerThreads; + buildSubmitters(); + } + + /** + * Build the submitters and thread pools if the number of committerThreads + * is greater than zero. + * This should only be called in constructors; it is synchronized to keep + * SpotBugs happy. + */ + private synchronized void buildSubmitters() { + if (committerThreads != 0) { + outerSubmitter = new PoolSubmitter(buildThreadPool(committerThreads)); + } + } + + /** + * Returns an {@link ExecutorService} for parallel tasks. The number of + * threads in the thread-pool is set by fs.s3a.committer.threads. + * If num-threads is 0, this will raise an exception. + * The threads have a lifespan set by + * {@link InternalCommitterConstants#THREAD_KEEP_ALIVE_TIME}. + * When the thread pool is full, the caller runs + * policy takes over. + * @param numThreads thread count, may be negative. + * @return an {@link ExecutorService} for the number of threads + */ + private ExecutorService buildThreadPool( + int numThreads) { + if (numThreads < 0) { + // a negative number means "multiple of available processors" + numThreads = numThreads * -Runtime.getRuntime().availableProcessors(); + } + Preconditions.checkArgument(numThreads > 0, + "Cannot create a thread pool with no threads"); + LOG.debug("creating thread pool of size {}", numThreads); + final ThreadFactory factory = new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat(THREAD_PREFIX + jobId + "-%d") + .build(); + return new HadoopThreadPoolExecutor(0, numThreads, + THREAD_KEEP_ALIVE_TIME, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + factory, + new ThreadPoolExecutor.CallerRunsPolicy()); + } + + /** + * Commit the operation, throwing an exception on any failure. + * See {@code CommitOperations#commitOrFail(SinglePendingCommit)}. + * @param commit commit to execute + * @throws IOException on a failure + */ + public void commitOrFail(SinglePendingCommit commit) throws IOException { + commitOperations.commitOrFail(commit); + } + + /** + * Commit a single pending commit; exceptions are caught + * and converted to an outcome. + * See {@link CommitOperations#commit(SinglePendingCommit, String)}. + * @param commit entry to commit + * @param origin origin path/string for outcome text + * @return the outcome + */ + public CommitOperations.MaybeIOE commit(SinglePendingCommit commit, + String origin) { + return commitOperations.commit(commit, origin); + } + + /** + * See {@link CommitOperations#abortSingleCommit(SinglePendingCommit)}. + * @param commit pending commit to abort + * @throws FileNotFoundException if the abort ID is unknown + * @throws IOException on any failure + */ + public void abortSingleCommit(final SinglePendingCommit commit) + throws IOException { + commitOperations.abortSingleCommit(commit); + } + + /** + * See {@link CommitOperations#revertCommit(SinglePendingCommit)}. + * @param commit pending commit + * @throws IOException failure + */ + public void revertCommit(final SinglePendingCommit commit) + throws IOException { + commitOperations.revertCommit(commit); + } + + /** + * See {@link CommitOperations#abortMultipartCommit(String, String)}.. + * @param destKey destination key + * @param uploadId upload to cancel + * @throws FileNotFoundException if the abort ID is unknown + * @throws IOException on any failure + */ + public void abortMultipartCommit( + final String destKey, + final String uploadId) + throws IOException { + commitOperations.abortMultipartCommit(destKey, uploadId); + } + + @Override + public synchronized void close() throws IOException { + + destroyThreadPools(); + auditContextUpdater.resetCurrentAuditContext(); + } + + @Override + public String toString() { + return "CommitContext{}"; + } + + /** + * Job Context. + * @return job context. + */ + public JobContext getJobContext() { + return jobContext; + } + + /** + * Return a submitter. + * If created with 0 threads, this returns null so + * TaskPool knows to run it in the current thread. + * @return a submitter or null + */ + public synchronized TaskPool.Submitter getOuterSubmitter() { + return outerSubmitter; + } + + /** + * Return a submitter. As this pool is used less often, + * create it on demand. + * If created with 0 threads, this returns null so + * TaskPool knows to run it in the current thread. + * @return a submitter or null + */ + public synchronized TaskPool.Submitter getInnerSubmitter() { + if (innerSubmitter == null && committerThreads > 0) { + innerSubmitter = new PoolSubmitter(buildThreadPool(committerThreads)); + } + return innerSubmitter; + } + + /** + * Get a serializer for .pending files. + * @return a serializer. + */ + public JsonSerialization getSinglePendingFileSerializer() { + return singleCommitSerializer.getForCurrentThread(); + } + + /** + * Get a serializer for .pendingset files. + * @return a serializer. + */ + public JsonSerialization getPendingSetSerializer() { + return pendingSetSerializer.getForCurrentThread(); + } + + /** + * Destroy any thread pools; wait for that to finish, + * but don't overreact if it doesn't finish in time. + */ + private synchronized void destroyThreadPools() { + try { + IOUtils.cleanupWithLogger(LOG, outerSubmitter, innerSubmitter); + } finally { + outerSubmitter = null; + innerSubmitter = null; + } + } + + /** + * Job configuration. + * @return configuration (never null) + */ + public Configuration getConf() { + return conf; + } + + /** + * Get the job ID. + * @return job ID. + */ + public String getJobId() { + return jobId; + } + + /** + * Submitter for a given thread pool. + */ + private final class PoolSubmitter implements TaskPool.Submitter, Closeable { + + private ExecutorService executor; + + private PoolSubmitter(ExecutorService executor) { + this.executor = executor; + } + + @Override + public synchronized void close() throws IOException { + if (executor != null) { + HadoopExecutors.shutdown(executor, LOG, + THREAD_POOL_SHUTDOWN_DELAY_SECONDS, TimeUnit.SECONDS); + } + executor = null; + } + + /** + * Forward to the submitter, wrapping in task + * context setting, so as to ensure that all operations + * have job/task attributes. + * @param task task to execute + * @return the future. + */ + @Override + public Future submit(Runnable task) { + return executor.submit(() -> { + auditContextUpdater.updateCurrentAuditContext(); + try { + task.run(); + } finally { + auditContextUpdater.resetCurrentAuditContext(); + } + }); + } + + } +} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitOperations.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/CommitOperations.java similarity index 82% rename from hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitOperations.java rename to hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/CommitOperations.java index 840cf8e0f23cc..0772e143f69d6 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitOperations.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/CommitOperations.java @@ -16,24 +16,26 @@ * limitations under the License. */ -package org.apache.hadoop.fs.s3a.commit; +package org.apache.hadoop.fs.s3a.commit.impl; -import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; +import javax.annotation.Nullable; + import com.amazonaws.services.s3.model.MultipartUpload; import com.amazonaws.services.s3.model.PartETag; import com.amazonaws.services.s3.model.UploadPartRequest; import com.amazonaws.services.s3.model.UploadPartResult; -import org.apache.hadoop.util.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,25 +49,33 @@ import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.S3AUtils; import org.apache.hadoop.fs.s3a.WriteOperations; +import org.apache.hadoop.fs.s3a.commit.CommitConstants; +import org.apache.hadoop.fs.s3a.commit.PathCommitException; import org.apache.hadoop.fs.s3a.commit.files.PendingSet; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; import org.apache.hadoop.fs.s3a.commit.files.SuccessData; import org.apache.hadoop.fs.s3a.impl.AbstractStoreOperation; import org.apache.hadoop.fs.s3a.impl.HeaderProcessing; import org.apache.hadoop.fs.s3a.impl.InternalConstants; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; import org.apache.hadoop.fs.s3a.statistics.CommitterStatistics; import org.apache.hadoop.fs.statistics.DurationTracker; import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsSource; +import org.apache.hadoop.mapreduce.JobContext; import org.apache.hadoop.util.DurationInfo; +import org.apache.hadoop.util.Preconditions; import org.apache.hadoop.util.Progressable; +import org.apache.hadoop.util.functional.TaskPool; import static java.util.Objects.requireNonNull; -import static org.apache.hadoop.fs.s3a.S3AUtils.*; +import static org.apache.hadoop.fs.s3a.S3AUtils.listAndFilter; import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_COMMIT_JOB; import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_MATERIALIZE_FILE; +import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_LOAD_SINGLE_PENDING_FILE; import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_STAGE_FILE_UPLOAD; -import static org.apache.hadoop.fs.s3a.commit.CommitConstants.*; +import static org.apache.hadoop.fs.s3a.commit.CommitConstants.XA_MAGIC_MARKER; +import static org.apache.hadoop.fs.s3a.commit.CommitConstants._SUCCESS; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.trackDuration; import static org.apache.hadoop.util.functional.RemoteIterators.cleanupRemoteIterator; @@ -115,7 +125,7 @@ public class CommitOperations extends AbstractStoreOperation * @throws IOException failure to bind. */ public CommitOperations(S3AFileSystem fs) throws IOException { - this(requireNonNull(fs), fs.newCommitterStatistics()); + this(requireNonNull(fs), fs.newCommitterStatistics(), "/"); } /** @@ -123,10 +133,12 @@ public CommitOperations(S3AFileSystem fs) throws IOException { * the commit operations. * @param fs FS to bind to * @param committerStatistics committer statistics + * @param outputPath destination of work. * @throws IOException failure to bind. */ public CommitOperations(S3AFileSystem fs, - CommitterStatistics committerStatistics) throws IOException { + CommitterStatistics committerStatistics, + String outputPath) throws IOException { super(requireNonNull(fs).createStoreContext()); this.fs = fs; statistics = requireNonNull(committerStatistics); @@ -134,7 +146,7 @@ public CommitOperations(S3AFileSystem fs, writeOperations = fs.createWriteOperationHelper( fs.getAuditSpanSource().createSpan( COMMITTER_COMMIT_JOB.getSymbol(), - "/", null)); + outputPath, null)); } /** @@ -168,7 +180,7 @@ public IOStatistics getIOStatistics() { * @param commit commit to execute * @throws IOException on a failure */ - private void commitOrFail( + public void commitOrFail( final SinglePendingCommit commit) throws IOException { commit(commit, commit.getFilename()).maybeRethrow(); } @@ -180,7 +192,7 @@ private void commitOrFail( * @param origin origin path/string for outcome text * @return the outcome */ - private MaybeIOE commit( + public MaybeIOE commit( final SinglePendingCommit commit, final String origin) { LOG.debug("Committing single commit {}", commit); @@ -225,10 +237,9 @@ private long innerCommit( // finalize the commit writeOperations.commitUpload( commit.getDestinationKey(), - commit.getUploadId(), - toPartEtags(commit.getEtags()), - commit.getLength() - ); + commit.getUploadId(), + toPartEtags(commit.getEtags()), + commit.getLength()); return commit.getLength(); } @@ -236,43 +247,69 @@ private long innerCommit( * Locate all files with the pending suffix under a directory. * @param pendingDir directory * @param recursive recursive listing? - * @return the list of all located entries + * @return iterator of all located entries * @throws IOException if there is a problem listing the path. */ - public List locateAllSinglePendingCommits( + public RemoteIterator locateAllSinglePendingCommits( Path pendingDir, boolean recursive) throws IOException { return listAndFilter(fs, pendingDir, recursive, PENDING_FILTER); } /** - * Load all single pending commits in the directory. + * Load all single pending commits in the directory, using the + * outer submitter. * All load failures are logged and then added to list of files which would * not load. + * * @param pendingDir directory containing commits * @param recursive do a recursive scan? + * @param commitContext commit context + * * @return tuple of loaded entries and those pending files which would * not load/validate. + * * @throws IOException on a failure to list the files. */ public Pair>> - loadSinglePendingCommits(Path pendingDir, boolean recursive) + loadSinglePendingCommits(Path pendingDir, + boolean recursive, + CommitContext commitContext) throws IOException { - List statusList = locateAllSinglePendingCommits( - pendingDir, recursive); - PendingSet commits = new PendingSet( - statusList.size()); - List> failures = new ArrayList<>(1); - for (LocatedFileStatus status : statusList) { - try { - commits.add(SinglePendingCommit.load(fs, status.getPath(), status)); - } catch (IOException e) { - LOG.warn("Failed to load commit file {}", status.getPath(), e); - failures.add(Pair.of(status, e)); - } - } + PendingSet commits = new PendingSet(); + List pendingFiles = Collections.synchronizedList( + new ArrayList<>(1)); + List> failures = Collections.synchronizedList( + new ArrayList<>(1)); + + TaskPool.foreach(locateAllSinglePendingCommits(pendingDir, recursive)) + //. stopOnFailure() + .suppressExceptions(false) + .executeWith(commitContext.getOuterSubmitter()) + .run(status -> { + Path path = status.getPath(); + try { + // load the file + SinglePendingCommit singleCommit = trackDuration(statistics, + COMMITTER_LOAD_SINGLE_PENDING_FILE.getSymbol(), () -> + SinglePendingCommit.load(fs, + path, + status, + commitContext.getSinglePendingFileSerializer())); + // aggregate stats + commits.getIOStatistics() + .aggregate(singleCommit.getIOStatistics()); + // then clear so they aren't marshalled again. + singleCommit.getIOStatistics().clear(); + pendingFiles.add(singleCommit); + } catch (IOException e) { + LOG.warn("Failed to load commit file {}", path, e); + failures.add(Pair.of(status, e)); + } + }); + commits.setCommits(pendingFiles); return Pair.of(commits, failures); } @@ -296,7 +333,7 @@ public IOException makeIOE(String key, Exception ex) { * @throws FileNotFoundException if the abort ID is unknown * @throws IOException on any failure */ - private void abortSingleCommit(SinglePendingCommit commit) + public void abortSingleCommit(SinglePendingCommit commit) throws IOException { String destKey = commit.getDestinationKey(); String origin = commit.getFilename() != null @@ -315,7 +352,7 @@ private void abortSingleCommit(SinglePendingCommit commit) * @throws FileNotFoundException if the abort ID is unknown * @throws IOException on any failure */ - private void abortMultipartCommit(String destKey, String uploadId) + public void abortMultipartCommit(String destKey, String uploadId) throws IOException { try (DurationInfo d = new DurationInfo(LOG, "Aborting commit ID %s to path %s", uploadId, destKey)) { @@ -328,11 +365,13 @@ private void abortMultipartCommit(String destKey, String uploadId) /** * Enumerate all pending files in a dir/tree, abort. * @param pendingDir directory of pending operations + * @param commitContext commit context * @param recursive recurse? * @return the outcome of all the abort operations * @throws IOException if there is a problem listing the path. */ public MaybeIOE abortAllSinglePendingCommits(Path pendingDir, + CommitContext commitContext, boolean recursive) throws IOException { Preconditions.checkArgument(pendingDir != null, "null pendingDir"); @@ -350,12 +389,14 @@ public MaybeIOE abortAllSinglePendingCommits(Path pendingDir, LOG.debug("No files to abort under {}", pendingDir); } while (pendingFiles.hasNext()) { - LocatedFileStatus status = pendingFiles.next(); + final LocatedFileStatus status = pendingFiles.next(); Path pendingFile = status.getPath(); if (pendingFile.getName().endsWith(CommitConstants.PENDING_SUFFIX)) { try { - abortSingleCommit(SinglePendingCommit.load(fs, pendingFile, - status)); + abortSingleCommit(SinglePendingCommit.load(fs, + pendingFile, + status, + commitContext.getSinglePendingFileSerializer())); } catch (FileNotFoundException e) { LOG.debug("listed file already deleted: {}", pendingFile); } catch (IOException | IllegalArgumentException e) { @@ -437,7 +478,7 @@ public void createSuccessMarker(Path outputPath, successData); try (DurationInfo ignored = new DurationInfo(LOG, "Writing success file %s", markerPath)) { - successData.save(fs, markerPath, true); + successData.save(fs, markerPath, SuccessData.serializer()); } } @@ -466,7 +507,7 @@ public void revertCommit(SinglePendingCommit commit) throws IOException { * @return a pending upload entry * @throws IOException failure */ - public SinglePendingCommit uploadFileToPendingCommit(File localFile, + public SinglePendingCommit uploadFileToPendingCommit(File localFile, Path destPath, String partition, long uploadPartSize, @@ -494,7 +535,8 @@ public SinglePendingCommit uploadFileToPendingCommit(File localFile, destPath)) { statistics.commitCreated(); - uploadId = writeOperations.initiateMultiPartUpload(destKey); + uploadId = writeOperations.initiateMultiPartUpload(destKey, + PutObjectOptions.keepingDirs()); long length = localFile.length(); SinglePendingCommit commitData = new SinglePendingCommit(); @@ -592,13 +634,41 @@ public void jobCompleted(boolean success) { } /** - * Begin the final commit. + * Create a commit context for a job or task. + * + * @param context job context + * @param path path for all work. + * @param committerThreads thread pool size + * @return the commit context to pass in. + * @throws IOException failure. + */ + public CommitContext createCommitContext( + JobContext context, + Path path, + int committerThreads) throws IOException { + return new CommitContext(this, context, + committerThreads); + } + + /** + * Create a stub commit context for tests. + * There's no job context. * @param path path for all work. + * @param jobId job ID; if null a random UUID is generated. + * @param committerThreads number of committer threads. * @return the commit context to pass in. * @throws IOException failure. */ - public CommitContext initiateCommitOperation(Path path) throws IOException { - return new CommitContext(); + public CommitContext createCommitContextForTesting( + Path path, @Nullable String jobId, int committerThreads) throws IOException { + final String id = jobId != null + ? jobId + : UUID.randomUUID().toString(); + + return new CommitContext(this, + getStoreContext().getConfiguration(), + id, + committerThreads); } /** @@ -624,98 +694,6 @@ public static Optional extractMagicFileLength(FileSystem fs, Path path) return HeaderProcessing.extractXAttrLongValue(bytes); } - /** - * Commit context. - * - * It is used to manage the final commit sequence where files become - * visible. - * - * This can only be created through {@link #initiateCommitOperation(Path)}. - * - * Once the commit operation has completed, it must be closed. - * It must not be reused. - */ - public final class CommitContext implements Closeable { - - - /** - * Create. - */ - private CommitContext() { - } - - /** - * Commit the operation, throwing an exception on any failure. - * See {@link CommitOperations#commitOrFail(SinglePendingCommit)}. - * @param commit commit to execute - * @throws IOException on a failure - */ - public void commitOrFail(SinglePendingCommit commit) throws IOException { - CommitOperations.this.commitOrFail(commit); - } - - /** - * Commit a single pending commit; exceptions are caught - * and converted to an outcome. - * See {@link CommitOperations#commit(SinglePendingCommit, String)}. - * @param commit entry to commit - * @param origin origin path/string for outcome text - * @return the outcome - */ - public MaybeIOE commit(SinglePendingCommit commit, - String origin) { - return CommitOperations.this.commit(commit, origin); - } - - /** - * See {@link CommitOperations#abortSingleCommit(SinglePendingCommit)}. - * @param commit pending commit to abort - * @throws FileNotFoundException if the abort ID is unknown - * @throws IOException on any failure - */ - public void abortSingleCommit(final SinglePendingCommit commit) - throws IOException { - CommitOperations.this.abortSingleCommit(commit); - } - - /** - * See {@link CommitOperations#revertCommit(SinglePendingCommit)}. - * @param commit pending commit - * @throws IOException failure - */ - public void revertCommit(final SinglePendingCommit commit) - throws IOException { - CommitOperations.this.revertCommit(commit); - } - - /** - * See {@link CommitOperations#abortMultipartCommit(String, String)}.. - * @param destKey destination key - * @param uploadId upload to cancel - * @throws FileNotFoundException if the abort ID is unknown - * @throws IOException on any failure - */ - public void abortMultipartCommit( - final String destKey, - final String uploadId) - throws IOException { - CommitOperations.this.abortMultipartCommit(destKey, uploadId); - } - - @Override - public void close() throws IOException { - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder( - "CommitContext{"); - sb.append('}'); - return sb.toString(); - } - - } - /** * A holder for a possible IOException; the call {@link #maybeRethrow()} * will throw any exception passed into the constructor, and be a no-op @@ -788,5 +766,4 @@ public static MaybeIOE of(IOException ex) { } } - } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitUtilsWithMR.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/CommitUtilsWithMR.java similarity index 74% rename from hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitUtilsWithMR.java rename to hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/CommitUtilsWithMR.java index 9e5ee860e85ff..c38ab2e9ba1af 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitUtilsWithMR.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/CommitUtilsWithMR.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.hadoop.fs.s3a.commit; +package org.apache.hadoop.fs.s3a.commit.impl; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; @@ -68,31 +68,67 @@ public static int getAppAttemptId(JobContext context) { /** * Compute the "magic" path for a job attempt. * @param jobUUID unique Job ID. + * @param appAttemptId the ID of the application attempt for this job. * @param dest the final output directory * @return the path to store job attempt data. */ - public static Path getMagicJobAttemptPath(String jobUUID, Path dest) { - return new Path(getMagicJobAttemptsPath(dest), - formatAppAttemptDir(jobUUID)); + public static Path getMagicJobAttemptPath(String jobUUID, + int appAttemptId, + Path dest) { + return new Path( + getMagicJobAttemptsPath(dest), + formatAppAttemptDir(jobUUID, appAttemptId)); + } + + /** + * Compute the "magic" path for a job. + * @param jobUUID unique Job ID. + * @param dest the final output directory + * @return the path to store job attempt data. + */ + public static Path getMagicJobPath(String jobUUID, + Path dest) { + return new Path( + getMagicJobAttemptsPath(dest), + formatJobDir(jobUUID)); } /** - * Format the application attempt directory. + * Build the name of the job directory, without + * app attempt. + * This is the path to use for cleanup. * @param jobUUID unique Job ID. - * @return the directory name for the application attempt + * @return the directory name for the job */ - public static String formatAppAttemptDir(String jobUUID) { + public static String formatJobDir( + String jobUUID) { return String.format("job-%s", jobUUID); } + /** + * Build the name of the job attempt directory. + * @param jobUUID unique Job ID. + * @param appAttemptId the ID of the application attempt for this job. + * @return the directory tree for the application attempt + */ + public static String formatAppAttemptDir( + String jobUUID, + int appAttemptId) { + return formatJobDir(jobUUID) + String.format("/%02d", appAttemptId); + } + /** * Compute the path where the output of magic task attempts are stored. * @param jobUUID unique Job ID. * @param dest destination of work + * @param appAttemptId the ID of the application attempt for this job. * @return the path where the output of magic task attempts are stored. */ - public static Path getMagicTaskAttemptsPath(String jobUUID, Path dest) { - return new Path(getMagicJobAttemptPath(jobUUID, dest), "tasks"); + public static Path getMagicTaskAttemptsPath( + String jobUUID, + Path dest, + int appAttemptId) { + return new Path(getMagicJobAttemptPath(jobUUID, appAttemptId, dest), "tasks"); } /** @@ -115,6 +151,8 @@ public static Path getMagicTaskAttemptPath(TaskAttemptContext context, /** * Get the base Magic attempt path, without any annotations to mark relative * references. + * If there is an app attempt property in the context configuration, that + * is included. * @param context task context. * @param jobUUID unique Job ID. * @param dest The output path to commit work into @@ -123,8 +161,9 @@ public static Path getMagicTaskAttemptPath(TaskAttemptContext context, public static Path getBaseMagicTaskAttemptPath(TaskAttemptContext context, String jobUUID, Path dest) { - return new Path(getMagicTaskAttemptsPath(jobUUID, dest), - String.valueOf(context.getTaskAttemptID())); + return new Path( + getMagicTaskAttemptsPath(jobUUID, dest, getAppAttemptId(context)), + String.valueOf(context.getTaskAttemptID())); } /** @@ -132,12 +171,13 @@ public static Path getBaseMagicTaskAttemptPath(TaskAttemptContext context, * This data is not magic * @param jobUUID unique Job ID. * @param out output directory of job + * @param appAttemptId the ID of the application attempt for this job. * @return the path to store temporary job attempt data. */ public static Path getTempJobAttemptPath(String jobUUID, - Path out) { + Path out, final int appAttemptId) { return new Path(new Path(out, TEMP_DATA), - formatAppAttemptDir(jobUUID)); + formatAppAttemptDir(jobUUID, appAttemptId)); } /** @@ -150,7 +190,7 @@ public static Path getTempJobAttemptPath(String jobUUID, public static Path getTempTaskAttemptPath(TaskAttemptContext context, final String jobUUID, Path out) { return new Path( - getTempJobAttemptPath(jobUUID, out), + getTempJobAttemptPath(jobUUID, out, getAppAttemptId(context)), String.valueOf(context.getTaskAttemptID())); } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/package-info.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/package-info.java new file mode 100644 index 0000000000000..b4977f34d57f4 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/impl/package-info.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +/** + * Internal classes which make use of mapreduce code. + * These MUST NOT be referred to in production code except + * in org.apache.hadoop.fs.s3a.commit classes which are only + * used within job/task committers. + */ + +@InterfaceAudience.Private +@InterfaceStability.Unstable +package org.apache.hadoop.fs.s3a.commit.impl; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/magic/MagicCommitTracker.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/magic/MagicCommitTracker.java index 7ea4e88308db4..c85571a1949a1 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/magic/MagicCommitTracker.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/magic/MagicCommitTracker.java @@ -26,20 +26,26 @@ import com.amazonaws.services.s3.model.PartETag; import com.amazonaws.services.s3.model.PutObjectRequest; -import org.apache.hadoop.util.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.s3a.Retries; import org.apache.hadoop.fs.s3a.WriteOperationHelper; import org.apache.hadoop.fs.s3a.commit.PutTracker; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; +import org.apache.hadoop.fs.s3a.statistics.PutTrackerStatistics; import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsSnapshot; +import org.apache.hadoop.util.Preconditions; +import static java.util.Objects.requireNonNull; +import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_MAGIC_MARKER_PUT; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.X_HEADER_MAGIC_MARKER; +import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.trackDurationOfInvocation; /** * Put tracker for Magic commits. @@ -57,6 +63,7 @@ public class MagicCommitTracker extends PutTracker { private final WriteOperationHelper writer; private final String bucket; private static final byte[] EMPTY = new byte[0]; + private final PutTrackerStatistics trackerStatistics; /** * Magic commit tracker. @@ -66,19 +73,22 @@ public class MagicCommitTracker extends PutTracker { * @param destKey key for the destination * @param pendingsetKey key of the pendingset file * @param writer writer instance to use for operations; includes audit span + * @param trackerStatistics tracker statistics */ public MagicCommitTracker(Path path, String bucket, String originalDestKey, String destKey, String pendingsetKey, - WriteOperationHelper writer) { + WriteOperationHelper writer, + PutTrackerStatistics trackerStatistics) { super(destKey); this.bucket = bucket; this.path = path; this.originalDestKey = originalDestKey; this.pendingPartKey = pendingsetKey; this.writer = writer; + this.trackerStatistics = requireNonNull(trackerStatistics); LOG.info("File {} is written as magic file to path {}", path, destKey); } @@ -126,6 +136,19 @@ public boolean aboutToComplete(String uploadId, Preconditions.checkArgument(!parts.isEmpty(), "No uploaded parts to save"); + // put a 0-byte file with the name of the original under-magic path + // Add the final file length as a header + // this is done before the task commit, so its duration can be + // included in the statistics + Map headers = new HashMap<>(); + headers.put(X_HEADER_MAGIC_MARKER, Long.toString(bytesWritten)); + PutObjectRequest originalDestPut = writer.createPutObjectRequest( + originalDestKey, + new ByteArrayInputStream(EMPTY), + 0, + new PutObjectOptions(true, null, headers)); + upload(originalDestPut); + // build the commit summary SinglePendingCommit commitData = new SinglePendingCommit(); commitData.touch(System.currentTimeMillis()); @@ -138,7 +161,8 @@ public boolean aboutToComplete(String uploadId, commitData.bindCommitData(parts); commitData.setIOStatistics( new IOStatisticsSnapshot(iostatistics)); - byte[] bytes = commitData.toBytes(); + + byte[] bytes = commitData.toBytes(SinglePendingCommit.serializer()); LOG.info("Uncommitted data pending to file {};" + " commit metadata for {} parts in {}. size: {} byte(s)", path.toUri(), parts.size(), pendingPartKey, bytesWritten); @@ -148,19 +172,20 @@ public boolean aboutToComplete(String uploadId, pendingPartKey, new ByteArrayInputStream(bytes), bytes.length, null); - writer.uploadObject(put); - - // Add the final file length as a header - Map headers = new HashMap<>(); - headers.put(X_HEADER_MAGIC_MARKER, Long.toString(bytesWritten)); - // now put a 0-byte file with the name of the original under-magic path - PutObjectRequest originalDestPut = writer.createPutObjectRequest( - originalDestKey, - new ByteArrayInputStream(EMPTY), - 0, - headers); - writer.uploadObject(originalDestPut); + upload(put); return false; + + } + /** + * PUT an object. + * @param request the request + * @throws IOException on problems + */ + @Retries.RetryTranslated + private void upload(PutObjectRequest request) throws IOException { + trackDurationOfInvocation(trackerStatistics, + COMMITTER_MAGIC_MARKER_PUT.getSymbol(), () -> + writer.uploadObject(request, PutObjectOptions.keepingDirs())); } @Override diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/magic/MagicS3GuardCommitter.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/magic/MagicS3GuardCommitter.java index c1ecd7d6b9b53..007e9b3709623 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/magic/MagicS3GuardCommitter.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/magic/MagicS3GuardCommitter.java @@ -32,11 +32,12 @@ import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.Invoker; import org.apache.hadoop.fs.s3a.commit.AbstractS3ACommitter; -import org.apache.hadoop.fs.s3a.commit.CommitOperations; +import org.apache.hadoop.fs.s3a.commit.impl.CommitContext; +import org.apache.hadoop.fs.s3a.commit.impl.CommitOperations; import org.apache.hadoop.fs.s3a.commit.CommitConstants; -import org.apache.hadoop.fs.s3a.commit.CommitUtilsWithMR; import org.apache.hadoop.fs.s3a.commit.files.PendingSet; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; +import org.apache.hadoop.fs.s3a.commit.impl.CommitUtilsWithMR; import org.apache.hadoop.fs.statistics.IOStatisticsLogging; import org.apache.hadoop.mapreduce.JobContext; import org.apache.hadoop.mapreduce.TaskAttemptContext; @@ -45,9 +46,10 @@ import static org.apache.hadoop.fs.s3a.S3AUtils.*; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.TASK_ATTEMPT_ID; +import static org.apache.hadoop.fs.s3a.commit.CommitConstants.TEMP_DATA; import static org.apache.hadoop.fs.s3a.commit.CommitUtils.*; import static org.apache.hadoop.fs.s3a.commit.MagicCommitPaths.*; -import static org.apache.hadoop.fs.s3a.commit.CommitUtilsWithMR.*; +import static org.apache.hadoop.fs.s3a.commit.impl.CommitUtilsWithMR.*; import static org.apache.hadoop.fs.statistics.IOStatisticsLogging.demandStringifyIOStatistics; /** @@ -100,25 +102,28 @@ public void setupJob(JobContext context) throws IOException { try (DurationInfo d = new DurationInfo(LOG, "Setup Job %s", jobIdString(context))) { super.setupJob(context); - Path jobAttemptPath = getJobAttemptPath(context); - getDestinationFS(jobAttemptPath, - context.getConfiguration()).mkdirs(jobAttemptPath); + Path jobPath = getJobPath(); + final FileSystem destFS = getDestinationFS(jobPath, + context.getConfiguration()); + destFS.delete(jobPath, true); + destFS.mkdirs(jobPath); } } /** * Get the list of pending uploads for this job attempt, by listing * all .pendingset files in the job attempt directory. - * @param context job context + * @param commitContext job context * @return a list of pending commits. * @throws IOException Any IO failure */ protected ActiveCommit listPendingUploadsToCommit( - JobContext context) + CommitContext commitContext) throws IOException { FileSystem fs = getDestFS(); - return ActiveCommit.fromStatusList(fs, - listAndFilter(fs, getJobAttemptPath(context), false, + return ActiveCommit.fromStatusIterator(fs, + listAndFilter(fs, getJobAttemptPath(commitContext.getJobContext()), + false, CommitOperations.PENDINGSET_FILTER)); } @@ -126,11 +131,16 @@ protected ActiveCommit listPendingUploadsToCommit( * Delete the magic directory. */ public void cleanupStagingDirs() { - Path path = magicSubdir(getOutputPath()); + final Path out = getOutputPath(); + Path path = magicSubdir(out); try(DurationInfo ignored = new DurationInfo(LOG, true, "Deleting magic directory %s", path)) { Invoker.ignoreIOExceptions(LOG, "cleanup magic directory", path.toString(), () -> deleteWithWarning(getDestFS(), path, true)); + // and the job temp directory with manifests + Invoker.ignoreIOExceptions(LOG, "cleanup job directory", path.toString(), + () -> deleteWithWarning(getDestFS(), + new Path(out, TEMP_DATA), true)); } } @@ -146,13 +156,8 @@ public void cleanupStagingDirs() { @Override public boolean needsTaskCommit(TaskAttemptContext context) throws IOException { - Path taskAttemptPath = getTaskAttemptPath(context); - try (DurationInfo d = new DurationInfo(LOG, - "needsTaskCommit task %s", context.getTaskAttemptID())) { - return taskAttemptPath.getFileSystem( - context.getConfiguration()) - .exists(taskAttemptPath); - } + // return true as a dir was created here in setup; + return true; } @Override @@ -167,9 +172,9 @@ public void commitTask(TaskAttemptContext context) throws IOException { throw e; } finally { // delete the task attempt so there's no possibility of a second attempt + // incurs a LIST, a bulk DELETE and maybe a parent dir creation, however + // as it happens during task commit, it should be off the critical path. deleteTaskAttemptPathQuietly(context); - destroyThreadPool(); - resetCommonContext(); } getCommitOperations().taskCompleted(true); LOG.debug("aggregate statistics\n{}", @@ -191,43 +196,48 @@ private PendingSet innerCommitTask( Path taskAttemptPath = getTaskAttemptPath(context); // load in all pending commits. CommitOperations actions = getCommitOperations(); - Pair>> - loaded = actions.loadSinglePendingCommits( - taskAttemptPath, true); - PendingSet pendingSet = loaded.getKey(); - List> failures = loaded.getValue(); - if (!failures.isEmpty()) { - // At least one file failed to load - // revert all which did; report failure with first exception - LOG.error("At least one commit file could not be read: failing"); - abortPendingUploads(context, pendingSet.getCommits(), true); - throw failures.get(0).getValue(); - } - // patch in IDs - String jobId = getUUID(); - String taskId = String.valueOf(context.getTaskAttemptID()); - for (SinglePendingCommit commit : pendingSet.getCommits()) { - commit.setJobId(jobId); - commit.setTaskId(taskId); - } - pendingSet.putExtraData(TASK_ATTEMPT_ID, taskId); - pendingSet.setJobId(jobId); - Path jobAttemptPath = getJobAttemptPath(context); - TaskAttemptID taskAttemptID = context.getTaskAttemptID(); - Path taskOutcomePath = new Path(jobAttemptPath, - taskAttemptID.getTaskID().toString() + - CommitConstants.PENDINGSET_SUFFIX); - LOG.info("Saving work of {} to {}", taskAttemptID, taskOutcomePath); - LOG.debug("task statistics\n{}", - IOStatisticsLogging.demandStringifyIOStatisticsSource(pendingSet)); - try { - // We will overwrite if there exists a pendingSet file already - pendingSet.save(getDestFS(), taskOutcomePath, true); - } catch (IOException e) { - LOG.warn("Failed to save task commit data to {} ", - taskOutcomePath, e); - abortPendingUploads(context, pendingSet.getCommits(), true); - throw e; + PendingSet pendingSet; + try (CommitContext commitContext = initiateTaskOperation(context)) { + Pair>> + loaded = actions.loadSinglePendingCommits( + taskAttemptPath, true, commitContext); + pendingSet = loaded.getKey(); + List> failures = loaded.getValue(); + if (!failures.isEmpty()) { + // At least one file failed to load + // revert all which did; report failure with first exception + LOG.error("At least one commit file could not be read: failing"); + abortPendingUploads(commitContext, pendingSet.getCommits(), true); + throw failures.get(0).getValue(); + } + // patch in IDs + String jobId = getUUID(); + String taskId = String.valueOf(context.getTaskAttemptID()); + for (SinglePendingCommit commit : pendingSet.getCommits()) { + commit.setJobId(jobId); + commit.setTaskId(taskId); + } + pendingSet.putExtraData(TASK_ATTEMPT_ID, taskId); + pendingSet.setJobId(jobId); + Path jobAttemptPath = getJobAttemptPath(context); + TaskAttemptID taskAttemptID = context.getTaskAttemptID(); + Path taskOutcomePath = new Path(jobAttemptPath, + taskAttemptID.getTaskID().toString() + + CommitConstants.PENDINGSET_SUFFIX); + LOG.info("Saving work of {} to {}", taskAttemptID, taskOutcomePath); + LOG.debug("task statistics\n{}", + IOStatisticsLogging.demandStringifyIOStatisticsSource(pendingSet)); + try { + // We will overwrite if there exists a pendingSet file already + pendingSet.save(getDestFS(), + taskOutcomePath, + commitContext.getPendingSetSerializer()); + } catch (IOException e) { + LOG.warn("Failed to save task commit data to {} ", + taskOutcomePath, e); + abortPendingUploads(commitContext, pendingSet.getCommits(), true); + throw e; + } } return pendingSet; } @@ -246,25 +256,35 @@ private PendingSet innerCommitTask( public void abortTask(TaskAttemptContext context) throws IOException { Path attemptPath = getTaskAttemptPath(context); try (DurationInfo d = new DurationInfo(LOG, - "Abort task %s", context.getTaskAttemptID())) { - getCommitOperations().abortAllSinglePendingCommits(attemptPath, true); + "Abort task %s", context.getTaskAttemptID()); + CommitContext commitContext = initiateTaskOperation(context)) { + getCommitOperations().abortAllSinglePendingCommits(attemptPath, + commitContext, + true); } finally { deleteQuietly( attemptPath.getFileSystem(context.getConfiguration()), attemptPath, true); - destroyThreadPool(); - resetCommonContext(); } } + /** + * Compute the path under which all job attempts will be placed. + * @return the path to store job attempt data. + */ + @Override + protected Path getJobPath() { + return getMagicJobPath(getUUID(), getOutputPath()); + } + /** * Compute the path where the output of a given job attempt will be placed. * For the magic committer, the path includes the job UUID. * @param appAttemptId the ID of the application attempt for this job. * @return the path to store job attempt data. */ - protected Path getJobAttemptPath(int appAttemptId) { - return getMagicJobAttemptPath(getUUID(), getOutputPath()); + protected final Path getJobAttemptPath(int appAttemptId) { + return getMagicJobAttemptPath(getUUID(), appAttemptId, getOutputPath()); } /** @@ -274,12 +294,12 @@ protected Path getJobAttemptPath(int appAttemptId) { * @param context the context of the task attempt. * @return the path where a task attempt should be stored. */ - public Path getTaskAttemptPath(TaskAttemptContext context) { + public final Path getTaskAttemptPath(TaskAttemptContext context) { return getMagicTaskAttemptPath(context, getUUID(), getOutputPath()); } @Override - protected Path getBaseTaskAttemptPath(TaskAttemptContext context) { + protected final Path getBaseTaskAttemptPath(TaskAttemptContext context) { return getBaseMagicTaskAttemptPath(context, getUUID(), getOutputPath()); } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/DirectoryStagingCommitter.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/DirectoryStagingCommitter.java index 1a5a63c940f47..99683db984983 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/DirectoryStagingCommitter.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/DirectoryStagingCommitter.java @@ -30,6 +30,7 @@ import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.PathExistsException; import org.apache.hadoop.fs.s3a.commit.InternalCommitterConstants; +import org.apache.hadoop.fs.s3a.commit.impl.CommitContext; import org.apache.hadoop.mapreduce.JobContext; import org.apache.hadoop.mapreduce.TaskAttemptContext; @@ -98,17 +99,18 @@ public void setupJob(JobContext context) throws IOException { * Pre-commit actions for a job. * Here: look at the conflict resolution mode and choose * an action based on the current policy. - * @param context job context + * @param commitContext commit context * @param pending pending commits * @throws IOException any failure */ @Override public void preCommitJob( - final JobContext context, + final CommitContext commitContext, final ActiveCommit pending) throws IOException { + final JobContext context = commitContext.getJobContext(); // see if the files can be loaded. - super.preCommitJob(context, pending); + super.preCommitJob(commitContext, pending); Path outputPath = getOutputPath(); FileSystem fs = getDestFS(); Configuration fsConf = fs.getConf(); diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/PartitionedStagingCommitter.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/PartitionedStagingCommitter.java index 214c7abdc732a..5d1a20e4240c2 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/PartitionedStagingCommitter.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/PartitionedStagingCommitter.java @@ -32,12 +32,13 @@ import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.commit.PathCommitException; -import org.apache.hadoop.fs.s3a.commit.Tasks; import org.apache.hadoop.fs.s3a.commit.files.PendingSet; +import org.apache.hadoop.fs.s3a.commit.files.PersistentCommitData; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; -import org.apache.hadoop.mapreduce.JobContext; +import org.apache.hadoop.fs.s3a.commit.impl.CommitContext; import org.apache.hadoop.mapreduce.TaskAttemptContext; import org.apache.hadoop.util.DurationInfo; +import org.apache.hadoop.util.functional.TaskPool; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.COMMITTER_NAME_PARTITIONED; @@ -89,7 +90,8 @@ public String toString() { @Override protected int commitTaskInternal(TaskAttemptContext context, - List taskOutput) throws IOException { + List taskOutput, + CommitContext commitContext) throws IOException { Path attemptPath = getTaskAttemptPath(context); Set partitions = Paths.getPartitions(attemptPath, taskOutput); @@ -109,7 +111,7 @@ protected int commitTaskInternal(TaskAttemptContext context, } } } - return super.commitTaskInternal(context, taskOutput); + return super.commitTaskInternal(context, taskOutput, commitContext); } /** @@ -121,13 +123,13 @@ protected int commitTaskInternal(TaskAttemptContext context, *
  • APPEND: allowed.; no need to check.
  • *
  • REPLACE deletes all existing partitions.
  • * - * @param context job context + * @param commitContext commit context * @param pending the pending operations * @throws IOException any failure */ @Override public void preCommitJob( - final JobContext context, + final CommitContext commitContext, final ActiveCommit pending) throws IOException { FileSystem fs = getDestFS(); @@ -135,7 +137,7 @@ public void preCommitJob( // enforce conflict resolution Configuration fsConf = fs.getConf(); boolean shouldPrecheckPendingFiles = true; - switch (getConflictResolutionMode(context, fsConf)) { + switch (getConflictResolutionMode(commitContext.getJobContext(), fsConf)) { case FAIL: // FAIL checking is done on the task side, so this does nothing break; @@ -144,17 +146,17 @@ public void preCommitJob( break; case REPLACE: // identify and replace the destination partitions - replacePartitions(context, pending); + replacePartitions(commitContext, pending); // and so there is no need to do another check. shouldPrecheckPendingFiles = false; break; default: throw new PathCommitException("", getRole() + ": unknown conflict resolution mode: " - + getConflictResolutionMode(context, fsConf)); + + getConflictResolutionMode(commitContext.getJobContext(), fsConf)); } if (shouldPrecheckPendingFiles) { - precommitCheckPendingFiles(context, pending); + precommitCheckPendingFiles(commitContext, pending); } } @@ -176,17 +178,16 @@ public void preCommitJob( * } * * - * @param context job context + * @param commitContext commit context * @param pending the pending operations * @throws IOException any failure */ private void replacePartitions( - final JobContext context, + final CommitContext commitContext, final ActiveCommit pending) throws IOException { Map partitions = new ConcurrentHashMap<>(); FileSystem sourceFS = pending.getSourceFS(); - Tasks.Submitter submitter = buildSubmitter(context); try (DurationInfo ignored = new DurationInfo(LOG, "Replacing partitions")) { @@ -194,13 +195,15 @@ private void replacePartitions( // for a marginal optimisation, the previous parent is tracked, so // if a task writes many files to the same dir, the synchronized map // is updated only once. - Tasks.foreach(pending.getSourceFiles()) + TaskPool.foreach(pending.getSourceFiles()) .stopOnFailure() .suppressExceptions(false) - .executeWith(submitter) + .executeWith(commitContext.getOuterSubmitter()) .run(status -> { - PendingSet pendingSet = PendingSet.load(sourceFS, - status); + PendingSet pendingSet = PersistentCommitData.load( + sourceFS, + status, + commitContext.getPendingSetSerializer()); Path lastParent = null; for (SinglePendingCommit commit : pendingSet.getCommits()) { Path parent = commit.destinationPath().getParent(); @@ -213,10 +216,10 @@ private void replacePartitions( } // now do the deletes FileSystem fs = getDestFS(); - Tasks.foreach(partitions.keySet()) + TaskPool.foreach(partitions.keySet()) .stopOnFailure() .suppressExceptions(false) - .executeWith(submitter) + .executeWith(commitContext.getOuterSubmitter()) .run(partitionPath -> { LOG.debug("{}: removing partition path to be replaced: " + getRole(), partitionPath); diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/StagingCommitter.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/StagingCommitter.java index 121ea7f851c02..36eae012dea7d 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/StagingCommitter.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/staging/StagingCommitter.java @@ -39,24 +39,25 @@ import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.commit.AbstractS3ACommitter; import org.apache.hadoop.fs.s3a.commit.CommitConstants; -import org.apache.hadoop.fs.s3a.commit.CommitOperations; import org.apache.hadoop.fs.s3a.commit.InternalCommitterConstants; -import org.apache.hadoop.fs.s3a.commit.Tasks; import org.apache.hadoop.fs.s3a.commit.files.PendingSet; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; +import org.apache.hadoop.fs.s3a.commit.impl.CommitContext; import org.apache.hadoop.mapreduce.JobContext; import org.apache.hadoop.mapreduce.TaskAttemptContext; import org.apache.hadoop.mapreduce.lib.output.FileOutputCommitter; import org.apache.hadoop.util.DurationInfo; +import org.apache.hadoop.util.functional.TaskPool; -import static org.apache.hadoop.util.Preconditions.*; +import static java.util.Objects.requireNonNull; import static org.apache.hadoop.fs.s3a.Constants.*; import static org.apache.hadoop.fs.s3a.S3AUtils.*; import static org.apache.hadoop.fs.s3a.Invoker.*; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.*; import static org.apache.hadoop.fs.s3a.commit.CommitUtils.*; -import static org.apache.hadoop.fs.s3a.commit.CommitUtilsWithMR.*; +import static org.apache.hadoop.fs.s3a.commit.impl.CommitUtilsWithMR.*; import static org.apache.hadoop.util.functional.RemoteIterators.cleanupRemoteIterator; +import static org.apache.hadoop.util.functional.RemoteIterators.toList; /** * Committer based on the contributed work of the @@ -111,7 +112,7 @@ public class StagingCommitter extends AbstractS3ACommitter { public StagingCommitter(Path outputPath, TaskAttemptContext context) throws IOException { super(outputPath, context); - this.constructorOutputPath = checkNotNull(getOutputPath(), "output path"); + this.constructorOutputPath = requireNonNull(getOutputPath(), "output path"); Configuration conf = getConf(); this.uploadPartSize = conf.getLongBytes( MULTIPART_SIZE, DEFAULT_MULTIPART_SIZE); @@ -121,8 +122,8 @@ public StagingCommitter(Path outputPath, setWorkPath(buildWorkPath(context, getUUID())); this.wrappedCommitter = createWrappedCommitter(context, conf); setOutputPath(constructorOutputPath); - Path finalOutputPath = getOutputPath(); - checkNotNull(finalOutputPath, "Output path cannot be null"); + Path finalOutputPath = requireNonNull(getOutputPath(), + "Output path cannot be null"); S3AFileSystem fs = getS3AFileSystem(finalOutputPath, context.getConfiguration(), false); s3KeyPrefix = fs.pathToKey(finalOutputPath); @@ -243,10 +244,18 @@ private static Path getJobAttemptPath(int appAttemptId, Path out) { @Override protected Path getJobAttemptPath(int appAttemptId) { - return new Path(getPendingJobAttemptsPath(commitsDirectory), + return new Path(getJobPath(), String.valueOf(appAttemptId)); } + /** + * Compute the path under which all job attempts will be placed. + * @return the path to store job attempt data. + */ + protected Path getJobPath() { + return getPendingJobAttemptsPath(commitsDirectory); + } + /** * Compute the path where the output of pending task attempts are stored. * @param context the context of the job with pending tasks. @@ -275,7 +284,7 @@ public static Path getTaskAttemptPath(TaskAttemptContext context, Path out) { * @return the location of pending job attempts. */ private static Path getPendingJobAttemptsPath(Path out) { - checkNotNull(out, "Null 'out' path"); + requireNonNull(out, "Null 'out' path"); return new Path(out, TEMPORARY); } @@ -296,12 +305,12 @@ public Path getCommittedTaskPath(TaskAttemptContext context) { * @param context task context */ private static void validateContext(TaskAttemptContext context) { - checkNotNull(context, "null context"); - checkNotNull(context.getTaskAttemptID(), + requireNonNull(context, "null context"); + requireNonNull(context.getTaskAttemptID(), "null task attempt ID"); - checkNotNull(context.getTaskAttemptID().getTaskID(), + requireNonNull(context.getTaskAttemptID().getTaskID(), "null task ID"); - checkNotNull(context.getTaskAttemptID().getJobID(), + requireNonNull(context.getTaskAttemptID().getJobID(), "null job ID"); } @@ -342,14 +351,13 @@ protected List getTaskOutput(TaskAttemptContext context) throws IOException { // get files on the local FS in the attempt path - Path attemptPath = getTaskAttemptPath(context); - checkNotNull(attemptPath, - "No attemptPath path in {}", this); + Path attemptPath = requireNonNull(getTaskAttemptPath(context), + "No attemptPath path"); LOG.debug("Scanning {} for files to commit", attemptPath); - return listAndFilter(getTaskAttemptFilesystem(context), - attemptPath, true, HIDDEN_FILE_FILTER); + return toList(listAndFilter(getTaskAttemptFilesystem(context), + attemptPath, true, HIDDEN_FILE_FILTER)); } /** @@ -425,46 +433,46 @@ public void setupJob(JobContext context) throws IOException { /** * Get the list of pending uploads for this job attempt. - * @param context job context + * @param commitContext job context * @return a list of pending uploads. * @throws IOException Any IO failure */ @Override protected ActiveCommit listPendingUploadsToCommit( - JobContext context) + CommitContext commitContext) throws IOException { - return listPendingUploads(context, false); + return listPendingUploads(commitContext, false); } /** * Get the list of pending uploads for this job attempt, swallowing * exceptions. - * @param context job context + * @param commitContext commit context * @return a list of pending uploads. If an exception was swallowed, * then this may not match the actual set of pending operations * @throws IOException shouldn't be raised, but retained for the compiler */ protected ActiveCommit listPendingUploadsToAbort( - JobContext context) throws IOException { - return listPendingUploads(context, true); + CommitContext commitContext) throws IOException { + return listPendingUploads(commitContext, true); } /** * Get the list of pending uploads for this job attempt. - * @param context job context + * @param commitContext commit context * @param suppressExceptions should exceptions be swallowed? * @return a list of pending uploads. If exceptions are being swallowed, * then this may not match the actual set of pending operations * @throws IOException Any IO failure which wasn't swallowed. */ protected ActiveCommit listPendingUploads( - JobContext context, boolean suppressExceptions) throws IOException { + CommitContext commitContext, boolean suppressExceptions) throws IOException { try (DurationInfo ignored = new DurationInfo(LOG, "Listing pending uploads")) { - Path wrappedJobAttemptPath = getJobAttemptPath(context); + Path wrappedJobAttemptPath = getJobAttemptPath(commitContext.getJobContext()); final FileSystem attemptFS = wrappedJobAttemptPath.getFileSystem( - context.getConfiguration()); - return ActiveCommit.fromStatusList(attemptFS, + commitContext.getConf()); + return ActiveCommit.fromStatusIterator(attemptFS, listAndFilter(attemptFS, wrappedJobAttemptPath, false, HIDDEN_FILE_FILTER)); @@ -491,27 +499,39 @@ public void cleanupStagingDirs() { } } + /** + * Staging committer cleanup includes calling wrapped committer's + * cleanup method, and removing all destination paths in the final + * filesystem. + * @param commitContext commit context + * @param suppressExceptions should exceptions be suppressed? + * @throws IOException IO failures if exceptions are not suppressed. + */ @Override @SuppressWarnings("deprecation") - protected void cleanup(JobContext context, + protected void cleanup(CommitContext commitContext, boolean suppressExceptions) throws IOException { maybeIgnore(suppressExceptions, "Cleanup wrapped committer", - () -> wrappedCommitter.cleanupJob(context)); + () -> wrappedCommitter.cleanupJob( + commitContext.getJobContext())); maybeIgnore(suppressExceptions, "Delete destination paths", - () -> deleteDestinationPaths(context)); - super.cleanup(context, suppressExceptions); + () -> deleteDestinationPaths( + commitContext.getJobContext())); + super.cleanup(commitContext, suppressExceptions); } @Override - protected void abortJobInternal(JobContext context, + protected void abortJobInternal(CommitContext commitContext, boolean suppressExceptions) throws IOException { String r = getRole(); + JobContext context = commitContext.getJobContext(); boolean failed = false; try (DurationInfo d = new DurationInfo(LOG, "%s: aborting job in state %s ", r, jobIdString(context))) { - ActiveCommit pending = listPendingUploadsToAbort(context); - abortPendingUploads(context, pending, suppressExceptions, true); + ActiveCommit pending = listPendingUploadsToAbort(commitContext); + abortPendingUploads(commitContext, + pending, suppressExceptions, true); } catch (FileNotFoundException e) { // nothing to list LOG.debug("No job directory to read uploads from"); @@ -519,7 +539,7 @@ protected void abortJobInternal(JobContext context, failed = true; maybeIgnore(suppressExceptions, "aborting job", e); } finally { - super.abortJobInternal(context, failed || suppressExceptions); + super.abortJobInternal(commitContext, failed || suppressExceptions); } } @@ -590,17 +610,16 @@ public boolean needsTaskCommit(TaskAttemptContext context) @Override public void commitTask(TaskAttemptContext context) throws IOException { try (DurationInfo d = new DurationInfo(LOG, - "%s: commit task %s", getRole(), context.getTaskAttemptID())) { - int count = commitTaskInternal(context, getTaskOutput(context)); + "%s: commit task %s", getRole(), context.getTaskAttemptID()); + CommitContext commitContext + = initiateTaskOperation(context)) { + int count = commitTaskInternal(context, getTaskOutput(context), commitContext); LOG.info("{}: upload file count: {}", getRole(), count); } catch (IOException e) { LOG.error("{}: commit of task {} failed", getRole(), context.getTaskAttemptID(), e); getCommitOperations().taskCompleted(false); throw e; - } finally { - destroyThreadPool(); - resetCommonContext(); } getCommitOperations().taskCompleted(true); } @@ -610,11 +629,13 @@ public void commitTask(TaskAttemptContext context) throws IOException { * writing a pending entry for them. * @param context task context * @param taskOutput list of files from the output + * @param commitContext commit context * @return number of uploads committed. * @throws IOException IO Failures. */ protected int commitTaskInternal(final TaskAttemptContext context, - List taskOutput) + List taskOutput, + CommitContext commitContext) throws IOException { LOG.debug("{}: commitTaskInternal", getRole()); Configuration conf = context.getConfiguration(); @@ -649,10 +670,10 @@ protected int commitTaskInternal(final TaskAttemptContext context, pendingCommits.putExtraData(TASK_ATTEMPT_ID, context.getTaskAttemptID().toString()); try { - Tasks.foreach(taskOutput) + TaskPool.foreach(taskOutput) .stopOnFailure() .suppressExceptions(false) - .executeWith(buildSubmitter(context)) + .executeWith(commitContext.getOuterSubmitter()) .run(stat -> { Path path = stat.getPath(); File localFile = new File(path.toUri().getPath()); @@ -676,13 +697,14 @@ protected int commitTaskInternal(final TaskAttemptContext context, } // save the data - // although overwrite=false, there's still a risk of > 1 entry being - // committed if the FS doesn't have create-no-overwrite consistency. + // overwrite any existing file, so whichever task attempt + // committed last wins. LOG.debug("Saving {} pending commit(s)) to file {}", pendingCommits.size(), commitsAttemptPath); - pendingCommits.save(commitsFS, commitsAttemptPath, false); + pendingCommits.save(commitsFS, commitsAttemptPath, + commitContext.getPendingSetSerializer()); threw = false; } finally { @@ -690,12 +712,11 @@ protected int commitTaskInternal(final TaskAttemptContext context, LOG.error( "{}: Exception during commit process, aborting {} commit(s)", getRole(), commits.size()); - try(CommitOperations.CommitContext commitContext - = initiateCommitOperation(); - DurationInfo ignored = new DurationInfo(LOG, + try(DurationInfo ignored = new DurationInfo(LOG, "Aborting %s uploads", commits.size())) { - Tasks.foreach(commits) + TaskPool.foreach(commits) .suppressExceptions() + .executeWith(commitContext.getOuterSubmitter()) .run(commitContext::abortSingleCommit); } deleteTaskAttemptPathQuietly(context); @@ -738,9 +759,6 @@ public void abortTask(TaskAttemptContext context) throws IOException { LOG.error("{}: exception when aborting task {}", getRole(), context.getTaskAttemptID(), e); throw e; - } finally { - destroyThreadPool(); - resetCommonContext(); } } @@ -859,16 +877,16 @@ public static String getConfictModeOption(JobContext context, * Pre-commit actions for a job. * Loads all the pending files to verify they can be loaded * and parsed. - * @param context job context + * @param commitContext commit context * @param pending pending commits * @throws IOException any failure */ @Override public void preCommitJob( - final JobContext context, + CommitContext commitContext, final ActiveCommit pending) throws IOException { // see if the files can be loaded. - precommitCheckPendingFiles(context, pending); + precommitCheckPendingFiles(commitContext, pending); } } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/CreateFileBuilder.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/CreateFileBuilder.java new file mode 100644 index 0000000000000..0392afac59d91 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/CreateFileBuilder.java @@ -0,0 +1,263 @@ +/* + * 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.s3a.impl; + +import java.io.IOException; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CreateFlag; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FSDataOutputStreamBuilder; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.PathIOException; +import org.apache.hadoop.fs.s3a.Constants; +import org.apache.hadoop.util.Progressable; + +import static org.apache.hadoop.fs.s3a.Constants.FS_S3A_CREATE_HEADER; +import static org.apache.hadoop.fs.s3a.impl.InternalConstants.CREATE_FILE_KEYS; + +/** + * Builder used in create file; takes a callback to the operation + * to create the file. + * Is non-recursive unless explicitly changed. + */ +public class CreateFileBuilder extends + FSDataOutputStreamBuilder { + + /** + * Flag set to create with overwrite. + */ + public static final EnumSet CREATE_OVERWRITE_FLAGS = + EnumSet.of(CreateFlag.CREATE, CreateFlag.OVERWRITE); + + /** + * Flag set to create without overwrite. + */ + public static final EnumSet CREATE_NO_OVERWRITE_FLAGS = + EnumSet.of(CreateFlag.CREATE); + + /** + * Classic create file option set: overwriting. + */ + public static final CreateFileOptions OPTIONS_CREATE_FILE_OVERWRITE = + new CreateFileOptions(CREATE_OVERWRITE_FLAGS, true, false, null); + + /** + * Classic create file option set: no overwrite. + */ + public static final CreateFileOptions OPTIONS_CREATE_FILE_NO_OVERWRITE = + new CreateFileOptions(CREATE_NO_OVERWRITE_FLAGS, true, false, null); + + /** + * Callback interface. + */ + private final CreateFileBuilderCallbacks callbacks; + + /** + * Constructor. + * @param fileSystem fs; used by superclass. + * @param path qualified path to create + * @param callbacks callbacks. + */ + public CreateFileBuilder( + @Nonnull final FileSystem fileSystem, + @Nonnull final Path path, + @Nonnull final CreateFileBuilderCallbacks callbacks) { + + super(fileSystem, path); + this.callbacks = callbacks; + } + + @Override + public CreateFileBuilder getThisBuilder() { + return this; + } + + @Override + public FSDataOutputStream build() throws IOException { + Path path = getPath(); + + final Configuration options = getOptions(); + final Map headers = new HashMap<>(); + final Set mandatoryKeys = getMandatoryKeys(); + final Set keysToValidate = new HashSet<>(); + + // pick up all headers from the mandatory list and strip them before + // validating the keys + String headerPrefix = FS_S3A_CREATE_HEADER + "."; + final int prefixLen = headerPrefix.length(); + mandatoryKeys.stream().forEach(key -> { + if (key.startsWith(headerPrefix) && key.length() > prefixLen) { + headers.put(key.substring(prefixLen), options.get(key)); + } else { + keysToValidate.add(key); + } + }); + + rejectUnknownMandatoryKeys(keysToValidate, CREATE_FILE_KEYS, "for " + path); + + // and add any optional headers + getOptionalKeys().stream() + .filter(key -> key.startsWith(headerPrefix) && key.length() > prefixLen) + .forEach(key -> headers.put(key.substring(prefixLen), options.get(key))); + + + EnumSet flags = getFlags(); + if (flags.contains(CreateFlag.APPEND)) { + throw new UnsupportedOperationException("Append is not supported"); + } + if (!flags.contains(CreateFlag.CREATE) && + !flags.contains(CreateFlag.OVERWRITE)) { + throw new PathIOException(path.toString(), + "Must specify either create or overwrite"); + } + + final boolean performance = + options.getBoolean(Constants.FS_S3A_CREATE_PERFORMANCE, false); + return callbacks.createFileFromBuilder( + path, + getProgress(), + new CreateFileOptions(flags, isRecursive(), performance, headers)); + + } + + /** + * Pass flags down. + * @param flags input flags. + * @return this builder. + */ + public CreateFileBuilder withFlags(EnumSet flags) { + if (flags.contains(CreateFlag.CREATE)) { + create(); + } + if (flags.contains(CreateFlag.APPEND)) { + append(); + } + overwrite(flags.contains(CreateFlag.OVERWRITE)); + return this; + } + + /** + * make the flag getter public. + * @return creation flags. + */ + public EnumSet getFlags() { + return super.getFlags(); + } + + /** + * Callbacks for creating the file. + */ + public interface CreateFileBuilderCallbacks { + + /** + * Create a file from the builder. + * @param path path to file + * @param progress progress callback + * @param options options for the file + * @return the stream + * @throws IOException any IO problem + */ + FSDataOutputStream createFileFromBuilder( + Path path, + Progressable progress, + CreateFileOptions options) throws IOException; + } + + /** + * Create file options as built from the builder set or the classic + * entry point. + */ + public static final class CreateFileOptions { + + /** + * creation flags. + * create parent dirs? + * progress callback. + * performance flag. + */ + private final EnumSet flags; + + /** + * create parent dirs? + */ + private final boolean recursive; + + /** + * performance flag. + */ + private final boolean performance; + + /** + * Headers; may be null. + */ + private final Map headers; + + /** + * @param flags creation flags + * @param recursive create parent dirs? + * @param performance performance flag + * @param headers nullable header map. + */ + public CreateFileOptions( + final EnumSet flags, + final boolean recursive, + final boolean performance, + final Map headers) { + this.flags = flags; + this.recursive = recursive; + this.performance = performance; + this.headers = headers; + } + + @Override + public String toString() { + return "CreateFileOptions{" + + "flags=" + flags + + ", recursive=" + recursive + + ", performance=" + performance + + ", headers=" + headers + + '}'; + } + + public EnumSet getFlags() { + return flags; + } + + public boolean isRecursive() { + return recursive; + } + + public boolean isPerformance() { + return performance; + } + + public Map getHeaders() { + return headers; + } + } + +} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/GetContentSummaryOperation.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/GetContentSummaryOperation.java index 248bffb9401fb..257cef8192b2c 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/GetContentSummaryOperation.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/GetContentSummaryOperation.java @@ -220,8 +220,7 @@ public interface GetContentSummaryCallbacks { /*** * List all entries under a path. - * - * @param path + * @param path path. * @param recursive if the subdirectories need to be traversed recursively * @return an iterator over the listing. * @throws IOException failure diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/InternalConstants.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/InternalConstants.java index 49b9feeb6f1f2..6e4946dfb53ac 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/InternalConstants.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/InternalConstants.java @@ -18,7 +18,9 @@ package org.apache.hadoop.fs.s3a.impl; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -160,4 +162,12 @@ private InternalConstants() { * will go through the AccessPoint. */ public static final String ARN_BUCKET_OPTION = "fs.s3a.bucket.%s.accesspoint.arn"; + + /** + * The known keys used in a createFile call. + */ + public static final Set CREATE_FILE_KEYS = + Collections.unmodifiableSet( + new HashSet<>(Arrays.asList(Constants.FS_S3A_CREATE_PERFORMANCE))); + } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MkdirOperation.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MkdirOperation.java index 2b9a0e89b1da4..98a91b1881ba1 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MkdirOperation.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MkdirOperation.java @@ -41,6 +41,18 @@ * It performs the directory listing probe ahead of the simple object HEAD * call for this reason -the object is the failure mode which SHOULD NOT * be encountered on normal execution. + * + * Magic paths are handled specially + *
      + *
    • The only path check is for a directory already existing there.
    • + *
    • No ancestors are checked
    • + *
    • Parent markers are never deleted, irrespective of FS settings
    • + *
    + * As a result, irrespective of depth, the operations performed are only + *
      + *
    1. One LIST
    2. + *
    3. If needed, one PUT
    4. + *
    */ public class MkdirOperation extends ExecutingStoreOperation { @@ -51,13 +63,21 @@ public class MkdirOperation extends ExecutingStoreOperation { private final MkdirCallbacks callbacks; + /** + * Should checks for ancestors existing be skipped? + * This flag is set when working with magic directories. + */ + private final boolean isMagicPath; + public MkdirOperation( final StoreContext storeContext, final Path dir, - final MkdirCallbacks callbacks) { + final MkdirCallbacks callbacks, + final boolean isMagicPath) { super(storeContext); this.dir = dir; this.callbacks = callbacks; + this.isMagicPath = isMagicPath; } /** @@ -77,6 +97,14 @@ public Boolean execute() throws IOException { return true; } + // get the file status of the path. + // this is done even for a magic path, to avoid always issuing PUT + // requests. Doing that without a check wouild seem to be an + // optimization, but it is not because + // 1. PUT is slower than HEAD + // 2. Write capacity is less than read capacity on a shard + // 3. It adds needless entries in versioned buckets, slowing + // down subsequent operations. FileStatus fileStatus = getPathStatusExpectingDir(dir); if (fileStatus != null) { if (fileStatus.isDirectory()) { @@ -85,7 +113,17 @@ public Boolean execute() throws IOException { throw new FileAlreadyExistsException("Path is a file: " + dir); } } - // dir, walk up tree + // file status was null + + // is the path magic? + // If so, we declare success without looking any further + if (isMagicPath) { + // Create the marker file immediately, + // and don't delete markers + callbacks.createFakeDirectory(dir, true); + return true; + } + // Walk path to root, ensuring closest ancestor is a directory, not file Path fPart = dir.getParent(); try { @@ -110,14 +148,15 @@ public Boolean execute() throws IOException { LOG.info("mkdirs({}}: Access denied when looking" + " for parent directory {}; skipping checks", dir, fPart); - LOG.debug("{}", e.toString(), e); + LOG.debug("{}", e, e); } // if we get here there is no directory at the destination. // so create one. - String key = getStoreContext().pathToKey(dir); - // Create the marker file, maybe delete the parent entries - callbacks.createFakeDirectory(key); + + // Create the marker file, delete the parent entries + // if the filesystem isn't configured to retain them + callbacks.createFakeDirectory(dir, false); return true; } @@ -140,15 +179,21 @@ private S3AFileStatus probePathStatusOrNull(final Path path, /** * Get the status of a path -optimized for paths * where there is a directory marker or child entries. + * + * Under a magic path, there's no check for a file, + * just the listing. + * * @param path path to probe. + * * @return the status + * * @throws IOException failure */ private S3AFileStatus getPathStatusExpectingDir(final Path path) throws IOException { S3AFileStatus status = probePathStatusOrNull(path, StatusProbeEnum.DIRECTORIES); - if (status == null) { + if (status == null && !isMagicPath) { status = probePathStatusOrNull(path, StatusProbeEnum.FILE); } @@ -174,10 +219,15 @@ S3AFileStatus probePathStatus(Path path, /** * Create a fake directory, always ending in "/". * Retry policy: retrying; translated. - * @param key name of directory object. + * the keepMarkers flag controls whether or not markers + * are automatically kept (this is set when creating + * directories under a magic path, always) + * @param dir dir to create + * @param keepMarkers always keep markers + * * @throws IOException IO failure */ @Retries.RetryTranslated - void createFakeDirectory(String key) throws IOException; + void createFakeDirectory(Path dir, boolean keepMarkers) throws IOException; } } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/PutObjectOptions.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/PutObjectOptions.java new file mode 100644 index 0000000000000..e14285a1ca8b1 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/PutObjectOptions.java @@ -0,0 +1,104 @@ +/* + * 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.s3a.impl; + +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Extensible structure for options when putting/writing objects. + */ +public final class PutObjectOptions { + + /** + * Can the PUT operation skip marker deletion? + */ + private final boolean keepMarkers; + + /** + * Storage class, if not null. + */ + private final String storageClass; + + /** + * Headers; may be null. + */ + private final Map headers; + + /** + * Constructor. + * @param keepMarkers Can the PUT operation skip marker deletion? + * @param storageClass Storage class, if not null. + * @param headers Headers; may be null. + */ + public PutObjectOptions( + final boolean keepMarkers, + @Nullable final String storageClass, + @Nullable final Map headers) { + this.keepMarkers = keepMarkers; + this.storageClass = storageClass; + this.headers = headers; + } + + /** + * Get the marker retention flag. + * @return true if markers are to be retained. + */ + public boolean isKeepMarkers() { + return keepMarkers; + } + + /** + * Headers for the put/post request. + * @return headers or null. + */ + public Map getHeaders() { + return headers; + } + + @Override + public String toString() { + return "PutObjectOptions{" + + "keepMarkers=" + keepMarkers + + ", storageClass='" + storageClass + '\'' + + '}'; + } + + private static final PutObjectOptions KEEP_DIRS = new PutObjectOptions(true, + null, null); + private static final PutObjectOptions DELETE_DIRS = new PutObjectOptions(false, + null, null); + + /** + * Get the options to keep directories. + * @return an instance which keeps dirs + */ + public static PutObjectOptions keepingDirs() { + return KEEP_DIRS; + } + + /** + * Get the options to delete directory markers. + * @return an instance which deletes dirs + */ + public static PutObjectOptions deletingDirs() { + return DELETE_DIRS; + } + +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/RequestFactoryImpl.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/RequestFactoryImpl.java index 5a693c6c3d43d..1e6629f9c7343 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/RequestFactoryImpl.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/RequestFactoryImpl.java @@ -23,7 +23,9 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; +import javax.annotation.Nullable; import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.services.s3.model.AbortMultipartUploadRequest; @@ -362,15 +364,19 @@ protected void copyEncryptionParameters( * Adds the ACL, storage class and metadata * @param key key of object * @param metadata metadata header + * @param options options for the request, including headers * @param srcfile source file * @return the request */ @Override public PutObjectRequest newPutObjectRequest(String key, - ObjectMetadata metadata, File srcfile) { + ObjectMetadata metadata, + final PutObjectOptions options, + File srcfile) { Preconditions.checkNotNull(srcfile); PutObjectRequest putObjectRequest = new PutObjectRequest(getBucket(), key, srcfile); + maybeSetMetadata(options, metadata); setOptionalPutRequestParameters(putObjectRequest); putObjectRequest.setCannedAcl(cannedACL); if (storageClass != null) { @@ -386,15 +392,18 @@ public PutObjectRequest newPutObjectRequest(String key, * operation. * @param key key of object * @param metadata metadata header + * @param options options for the request * @param inputStream source data. * @return the request */ @Override public PutObjectRequest newPutObjectRequest(String key, ObjectMetadata metadata, + @Nullable final PutObjectOptions options, InputStream inputStream) { Preconditions.checkNotNull(inputStream); Preconditions.checkArgument(isNotEmpty(key), "Null/empty key"); + maybeSetMetadata(options, metadata); PutObjectRequest putObjectRequest = new PutObjectRequest(getBucket(), key, inputStream, metadata); setOptionalPutRequestParameters(putObjectRequest); @@ -418,7 +427,7 @@ public int read() throws IOException { final ObjectMetadata md = createObjectMetadata(0L, true); md.setContentType(HeaderProcessing.CONTENT_TYPE_X_DIRECTORY); PutObjectRequest putObjectRequest = - newPutObjectRequest(key, md, im); + newPutObjectRequest(key, md, null, im); return putObjectRequest; } @@ -444,11 +453,14 @@ public AbortMultipartUploadRequest newAbortMultipartUploadRequest( @Override public InitiateMultipartUploadRequest newMultipartUploadRequest( - String destKey) { + final String destKey, + @Nullable final PutObjectOptions options) { + final ObjectMetadata objectMetadata = newObjectMetadata(-1); + maybeSetMetadata(options, objectMetadata); final InitiateMultipartUploadRequest initiateMPURequest = new InitiateMultipartUploadRequest(getBucket(), destKey, - newObjectMetadata(-1)); + objectMetadata); initiateMPURequest.setCannedACL(getCannedACL()); if (getStorageClass() != null) { initiateMPURequest.withStorageClass(getStorageClass()); @@ -601,6 +613,23 @@ public void setEncryptionSecrets(final EncryptionSecrets secrets) { encryptionSecrets = secrets; } + /** + * Set the metadata from the options if the options are not + * null and the metadata contains headers. + * @param options options for the request + * @param objectMetadata metadata to patch + */ + private void maybeSetMetadata( + @Nullable PutObjectOptions options, + final ObjectMetadata objectMetadata) { + if (options != null) { + Map headers = options.getHeaders(); + if (headers != null) { + objectMetadata.setUserMetadata(headers); + } + } + } + /** * Create a builder. * @return new builder. diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/S3AMultipartUploader.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/S3AMultipartUploader.java index db6beaff5b222..3a6a04e5e711c 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/S3AMultipartUploader.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/S3AMultipartUploader.java @@ -123,7 +123,8 @@ public CompletableFuture startUpload( String key = context.pathToKey(dest); return context.submit(new CompletableFuture<>(), () -> { - String uploadId = writeOperations.initiateMultiPartUpload(key); + String uploadId = writeOperations.initiateMultiPartUpload(key, + PutObjectOptions.keepingDirs()); statistics.uploadStarted(); return BBUploadHandle.from(ByteBuffer.wrap( uploadId.getBytes(Charsets.UTF_8))); diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/BlockOutputStreamStatistics.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/BlockOutputStreamStatistics.java index 772b965d4f4a3..bd1466b2a432f 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/BlockOutputStreamStatistics.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/BlockOutputStreamStatistics.java @@ -25,7 +25,8 @@ * Block output stream statistics. */ public interface BlockOutputStreamStatistics extends Closeable, - S3AStatisticInterface { + S3AStatisticInterface, + PutTrackerStatistics { /** * Block is queued for upload. diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/CommitterStatistics.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/CommitterStatistics.java index fd232a058d0b8..53c25b0c65aea 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/CommitterStatistics.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/CommitterStatistics.java @@ -18,6 +18,8 @@ package org.apache.hadoop.fs.s3a.statistics; +import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; + /** * Statistics for S3A committers. */ @@ -63,4 +65,10 @@ public interface CommitterStatistics * @param success success flag */ void jobCompleted(boolean success); + + /** + * Return the writeable IOStatisticsStore. + * @return the statistics + */ + IOStatisticsStore getIOStatistics(); } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/PutTrackerStatistics.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/PutTrackerStatistics.java new file mode 100644 index 0000000000000..b422e0c9d53aa --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/PutTrackerStatistics.java @@ -0,0 +1,29 @@ +/* + * 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.s3a.statistics; + +/** + * Interface for PUT tracking. + * It is subclassed by {@link BlockOutputStreamStatistics}, + * so that operations performed by the PutTracker update + * the stream statistics. + * Having a separate interface helps isolate operations. + */ +public interface PutTrackerStatistics extends S3AStatisticInterface { +} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/EmptyS3AStatisticsContext.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/EmptyS3AStatisticsContext.java index f618270798e08..5c0995e41b3dd 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/EmptyS3AStatisticsContext.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/EmptyS3AStatisticsContext.java @@ -33,6 +33,7 @@ import org.apache.hadoop.fs.s3a.statistics.StatisticsFromAwsSdk; import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.DurationTracker; +import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.emptyStatistics; import static org.apache.hadoop.fs.statistics.IOStatisticsSupport.stubDurationTracker; @@ -137,6 +138,7 @@ private static class EmptyS3AStatisticImpl implements public DurationTracker trackDuration(String key, long count) { return stubDurationTracker(); } + } /** @@ -381,6 +383,11 @@ public void taskCompleted(final boolean success) { @Override public void jobCompleted(final boolean success) { } + + @Override + public IOStatisticsStore getIOStatistics() { + return null; + } } private static final class EmptyBlockOutputStreamStatistics diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/auditing.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/auditing.md index 7c004627357b4..8ccc36cf83bb1 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/auditing.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/auditing.md @@ -226,18 +226,25 @@ If any of the field values were `null`, the field is omitted. | `cm` | Command | `S3GuardTool$BucketInfo` | | `fs` | FileSystem ID | `af5943a9-b6f6-4eec-9c58-008982fc492a` | | `id` | Span ID | `3c0d9b7e-2a63-43d9-a220-3c574d768ef3-3` | -| `ji` | Job ID | `(Generated by query engine)` | +| `ji` | Job ID (S3A committer)| `(Generated by query engine)` | | `op` | Filesystem API call | `op_rename` | | `p1` | Path 1 of operation | `s3a://alice-london/path1` | | `p2` | Path 2 of operation | `s3a://alice-london/path2` | | `pr` | Principal | `alice` | | `ps` | Unique process UUID | `235865a0-d399-4696-9978-64568db1b51c` | +| `ta` | Task Attempt ID (S3A committer) | | | `t0` | Thread 0: thread span was created in | `100` | | `t1` | Thread 1: thread this operation was executed in | `200` | | `ts` | Timestamp (UTC epoch millis) | `1617116985923` | +_Notes_ -Thread IDs are from the current thread in the JVM. +* Thread IDs are from the current thread in the JVM, so can be compared to those in````````` + Log4J logs. They are never unique. +* Task Attempt/Job IDs are only ever set during operations involving the S3A committers, specifically + all operations excecuted by the committer. + Operations executed in the same thread as the committer's instantiation _may_ also report the + IDs, even if they are unrelated to the actual task. Consider them "best effort". ```java Long.toString(Thread.currentThread().getId()) diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committers.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committers.md index b19f30f1a3412..cfeff28d54e87 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committers.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committers.md @@ -549,7 +549,7 @@ Conflict management is left to the execution engine itself. | `fs.s3a.buffer.dir` | Local filesystem directory for data being written and/or staged. | `${env.LOCAL_DIRS:-${hadoop.tmp.dir}}/s3a` | | `fs.s3a.committer.magic.enabled` | Enable "magic committer" support in the filesystem. | `true` | | `fs.s3a.committer.abort.pending.uploads` | list and abort all pending uploads under the destination path when the job is committed or aborted. | `true` | -| `fs.s3a.committer.threads` | Number of threads in committers for parallel operations on files. | 8 | +| `fs.s3a.committer.threads` | Number of threads in committers for parallel operations on files.| -4 | | `fs.s3a.committer.generate.uuid` | Generate a Job UUID if none is passed down from Spark | `false` | | `fs.s3a.committer.require.uuid` |Require the Job UUID to be passed down from Spark | `false` | @@ -587,10 +587,15 @@ Conflict management is left to the execution engine itself. fs.s3a.committer.threads - 8 + -4 Number of threads in committers for parallel operations on files - (upload, commit, abort, delete...) + (upload, commit, abort, delete...). + Two thread pools this size are created, one for the outer + task-level parallelism, and one for parallel execution + within tasks (POSTs to commit individual uploads) + If the value is negative, it is inverted and then multiplied + by the number of cores in the CPU. diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/performance.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/performance.md index f398c4cbcbe37..06eb137cd9bd9 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/performance.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/performance.md @@ -55,6 +55,36 @@ it isn't, and some attempts to preserve the metaphor are "aggressively suboptima To make most efficient use of S3, care is needed. +## Improving read performance using Vectored IO +The S3A FileSystem supports implementation of vectored read api using which +a client can provide a list of file ranges to read returning a future read +object associated with each range. For full api specification please see +[FSDataInputStream](../../hadoop-common-project/hadoop-common/filesystem/fsdatainputstream.html). + +The following properties can be configured to optimise vectored reads based +on the client requirements. + +```xml + + fs.s3a.vectored.read.min.seek.size + 4K + + What is the smallest reasonable seek in bytes such + that we group ranges together during vectored + read operation. + + + +fs.s3a.vectored.read.max.merged.size +1M + + What is the largest merged read size in bytes such + that we group ranges together during vectored read. + Setting this value to 0 will disable merging of ranges. + + +``` + ## Improving data input performance through fadvise The S3A Filesystem client supports the notion of input policies, similar diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractVectoredRead.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractVectoredRead.java new file mode 100644 index 0000000000000..18a727dcdceed --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractVectoredRead.java @@ -0,0 +1,159 @@ +/* + * 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.contract.s3a; + +import java.io.EOFException; +import java.io.InterruptedIOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FileRange; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.contract.AbstractContractVectoredReadTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; +import org.apache.hadoop.fs.s3a.Constants; +import org.apache.hadoop.fs.s3a.S3AFileSystem; +import org.apache.hadoop.fs.s3a.S3ATestUtils; +import org.apache.hadoop.test.LambdaTestUtils; + +import static org.apache.hadoop.fs.contract.ContractTestUtils.validateVectoredReadResult; +import static org.apache.hadoop.test.MoreAsserts.assertEqual; + +public class ITestS3AContractVectoredRead extends AbstractContractVectoredReadTest { + + public ITestS3AContractVectoredRead(String bufferType) { + super(bufferType); + } + + @Override + protected AbstractFSContract createContract(Configuration conf) { + return new S3AContract(conf); + } + + /** + * Overriding in S3 vectored read api fails fast in case of EOF + * requested range. + */ + @Override + public void testEOFRanges() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = new ArrayList<>(); + fileRanges.add(FileRange.createFileRange(DATASET_LEN, 100)); + verifyExceptionalVectoredRead(fs, fileRanges, EOFException.class); + } + + @Test + public void testMinSeekAndMaxSizeConfigsPropagation() throws Exception { + Configuration conf = getFileSystem().getConf(); + S3ATestUtils.removeBaseAndBucketOverrides(conf, + Constants.AWS_S3_VECTOR_READS_MAX_MERGED_READ_SIZE, + Constants.AWS_S3_VECTOR_READS_MIN_SEEK_SIZE); + S3ATestUtils.disableFilesystemCaching(conf); + final int configuredMinSeek = 2 * 1024; + final int configuredMaxSize = 10 * 1024 * 1024; + conf.set(Constants.AWS_S3_VECTOR_READS_MIN_SEEK_SIZE, "2K"); + conf.set(Constants.AWS_S3_VECTOR_READS_MAX_MERGED_READ_SIZE, "10M"); + try (S3AFileSystem fs = S3ATestUtils.createTestFileSystem(conf)) { + try (FSDataInputStream fis = fs.open(path(VECTORED_READ_FILE_NAME))) { + int newMinSeek = fis.minSeekForVectorReads(); + int newMaxSize = fis.maxReadSizeForVectorReads(); + assertEqual(newMinSeek, configuredMinSeek, + "configured s3a min seek for vectored reads"); + assertEqual(newMaxSize, configuredMaxSize, + "configured s3a max size for vectored reads"); + } + } + } + + @Test + public void testMinSeekAndMaxSizeDefaultValues() throws Exception { + Configuration conf = getFileSystem().getConf(); + S3ATestUtils.removeBaseAndBucketOverrides(conf, + Constants.AWS_S3_VECTOR_READS_MIN_SEEK_SIZE, + Constants.AWS_S3_VECTOR_READS_MAX_MERGED_READ_SIZE); + try (S3AFileSystem fs = S3ATestUtils.createTestFileSystem(conf)) { + try (FSDataInputStream fis = fs.open(path(VECTORED_READ_FILE_NAME))) { + int minSeek = fis.minSeekForVectorReads(); + int maxSize = fis.maxReadSizeForVectorReads(); + assertEqual(minSeek, Constants.DEFAULT_AWS_S3_VECTOR_READS_MIN_SEEK_SIZE, + "default s3a min seek for vectored reads"); + assertEqual(maxSize, Constants.DEFAULT_AWS_S3_VECTOR_READS_MAX_MERGED_READ_SIZE, + "default s3a max read size for vectored reads"); + } + } + } + + @Test + public void testStopVectoredIoOperationsCloseStream() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = createSampleNonOverlappingRanges(); + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))){ + in.readVectored(fileRanges, getAllocate()); + in.close(); + LambdaTestUtils.intercept(InterruptedIOException.class, + () -> validateVectoredReadResult(fileRanges, DATASET)); + } + // reopening the stream should succeed. + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))){ + in.readVectored(fileRanges, getAllocate()); + validateVectoredReadResult(fileRanges, DATASET); + } + } + + @Test + public void testStopVectoredIoOperationsUnbuffer() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = createSampleNonOverlappingRanges(); + try (FSDataInputStream in = fs.open(path(VECTORED_READ_FILE_NAME))){ + in.readVectored(fileRanges, getAllocate()); + in.unbuffer(); + LambdaTestUtils.intercept(InterruptedIOException.class, + () -> validateVectoredReadResult(fileRanges, DATASET)); + // re-initiating the vectored reads after unbuffer should succeed. + in.readVectored(fileRanges, getAllocate()); + validateVectoredReadResult(fileRanges, DATASET); + } + + } + + /** + * S3 vectored IO doesn't support overlapping ranges. + */ + @Override + public void testOverlappingRanges() throws Exception { + FileSystem fs = getFileSystem(); + List fileRanges = getSampleOverlappingRanges(); + verifyExceptionalVectoredRead(fs, fileRanges, UnsupportedOperationException.class); + } + + /** + * S3 vectored IO doesn't support overlapping ranges. + */ + @Override + public void testSameRanges() throws Exception { + // Same ranges are special case of overlapping only. + FileSystem fs = getFileSystem(); + List fileRanges = getSampleSameRanges(); + verifyExceptionalVectoredRead(fs, fileRanges, UnsupportedOperationException.class); + } +} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFileOperationCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFileOperationCost.java index 27c70b2b2148d..dae6312d48098 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFileOperationCost.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFileOperationCost.java @@ -18,8 +18,6 @@ package org.apache.hadoop.fs.s3a; - -import org.apache.hadoop.fs.FileAlreadyExistsException; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.impl.StatusProbeEnum; @@ -369,67 +367,6 @@ public void testNeedEmptyDirectoryProbeRequiresList() throws Throwable { fs.s3GetFileStatus(new Path("/something"), "/something", StatusProbeEnum.HEAD_ONLY, true)); } - @Test - public void testCreateCost() throws Throwable { - describe("Test file creation cost"); - Path testFile = methodPath(); - // when overwrite is false, the path is checked for existence. - create(testFile, false, - CREATE_FILE_NO_OVERWRITE); - // but when true: only the directory checks take place. - create(testFile, true, CREATE_FILE_OVERWRITE); - } - - @Test - public void testCreateCostFileExists() throws Throwable { - describe("Test cost of create file failing with existing file"); - Path testFile = file(methodPath()); - - // now there is a file there, an attempt with overwrite == false will - // fail on the first HEAD. - interceptOperation(FileAlreadyExistsException.class, "", - FILE_STATUS_FILE_PROBE, - () -> file(testFile, false)); - } - - @Test - public void testCreateCostDirExists() throws Throwable { - describe("Test cost of create file failing with existing dir"); - Path testFile = dir(methodPath()); - - // now there is a file there, an attempt with overwrite == false will - // fail on the first HEAD. - interceptOperation(FileAlreadyExistsException.class, "", - GET_FILE_STATUS_ON_DIR_MARKER, - () -> file(testFile, false)); - } - - /** - * Use the builder API. - * This always looks for a parent unless the caller says otherwise. - */ - @Test - public void testCreateBuilder() throws Throwable { - describe("Test builder file creation cost"); - Path testFile = methodPath(); - dir(testFile.getParent()); - - // builder defaults to looking for parent existence (non-recursive) - buildFile(testFile, false, false, - GET_FILE_STATUS_FNFE // destination file - .plus(FILE_STATUS_DIR_PROBE)); // parent dir - // recursive = false and overwrite=true: - // only make sure the dest path isn't a directory. - buildFile(testFile, true, true, - FILE_STATUS_DIR_PROBE); - - // now there is a file there, an attempt with overwrite == false will - // fail on the first HEAD. - interceptOperation(FileAlreadyExistsException.class, "", - GET_FILE_STATUS_ON_FILE, - () -> buildFile(testFile, false, true, - GET_FILE_STATUS_ON_FILE)); - } @Test public void testCostOfGlobStatus() throws Throwable { diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AMiscOperations.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AMiscOperations.java index b4d2527a46af8..2d29282ad0195 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AMiscOperations.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AMiscOperations.java @@ -36,8 +36,8 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonPathCapabilities; import org.apache.hadoop.fs.FSDataOutputStream; -import org.apache.hadoop.fs.FileAlreadyExistsException; import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; import org.apache.hadoop.fs.store.audit.AuditSpan; import org.apache.hadoop.fs.store.EtagChecksum; import org.apache.hadoop.test.LambdaTestUtils; @@ -102,18 +102,6 @@ public void testCreateNonRecursiveSuccess() throws IOException { assertIsFile(shouldWork); } - @Test(expected = FileNotFoundException.class) - public void testCreateNonRecursiveNoParent() throws IOException { - createNonRecursive(path("/recursive/node")); - } - - @Test(expected = FileAlreadyExistsException.class) - public void testCreateNonRecursiveParentIsFile() throws IOException { - Path parent = path("/file.txt"); - touch(getFileSystem(), parent); - createNonRecursive(new Path(parent, "fail")); - } - @Test public void testPutObjectDirect() throws Throwable { final S3AFileSystem fs = getFileSystem(); @@ -126,7 +114,7 @@ public void testPutObjectDirect() throws Throwable { new ByteArrayInputStream("PUT".getBytes()), metadata); LambdaTestUtils.intercept(IllegalStateException.class, - () -> fs.putObjectDirect(put)); + () -> fs.putObjectDirect(put, PutObjectOptions.keepingDirs())); assertPathDoesNotExist("put object was created", path); } } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MockS3AFileSystem.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MockS3AFileSystem.java index b597460bfd75a..2eb35daab4c2b 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MockS3AFileSystem.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MockS3AFileSystem.java @@ -40,7 +40,11 @@ import org.apache.hadoop.fs.s3a.audit.AuditTestSupport; import org.apache.hadoop.fs.s3a.auth.delegation.EncryptionSecrets; import org.apache.hadoop.fs.s3a.commit.staging.StagingTestBase; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; import org.apache.hadoop.fs.s3a.impl.RequestFactoryImpl; +import org.apache.hadoop.fs.s3a.impl.StoreContext; +import org.apache.hadoop.fs.s3a.impl.StoreContextBuilder; +import org.apache.hadoop.fs.s3a.impl.StubContextAccessor; import org.apache.hadoop.fs.s3a.statistics.CommitterStatistics; import org.apache.hadoop.fs.s3a.statistics.impl.EmptyS3AStatisticsContext; import org.apache.hadoop.fs.statistics.DurationTrackerFactory; @@ -211,7 +215,11 @@ public boolean exists(Path f) throws IOException { } @Override - void finishedWrite(String key, long length, String eTag, String versionId) { + void finishedWrite(String key, + long length, + String eTag, + String versionId, + final PutObjectOptions putOptions) { } @@ -377,11 +385,29 @@ public CommitterStatistics newCommitterStatistics() { @Override public void operationRetried(Exception ex) { - /** no-op */ + /* no-op */ } @Override protected DurationTrackerFactory getDurationTrackerFactory() { return stubDurationTrackerFactory(); } + + /** + * Build an immutable store context. + * If called while the FS is being initialized, + * some of the context will be incomplete. + * new store context instances should be created as appropriate. + * @return the store context of this FS. + */ + public StoreContext createStoreContext() { + return new StoreContextBuilder().setFsURI(getUri()) + .setBucket(getBucket()) + .setConfiguration(getConf()) + .setUsername(getUsername()) + .setAuditor(getAuditor()) + .setContextAccessors(new StubContextAccessor(getBucket())) + .build(); + } + } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MultipartTestUtils.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MultipartTestUtils.java index 04c2b2a09bda2..9ea33cf69c92b 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MultipartTestUtils.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MultipartTestUtils.java @@ -22,6 +22,7 @@ import com.amazonaws.services.s3.model.PartETag; import com.amazonaws.services.s3.model.UploadPartRequest; import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; import org.apache.hadoop.fs.store.audit.AuditSpan; import org.apache.hadoop.io.IOUtils; @@ -78,7 +79,7 @@ public static IdKey createPartUpload(S3AFileSystem fs, String key, int len, WriteOperationHelper writeHelper = fs.getWriteOperationHelper(); byte[] data = dataset(len, 'a', 'z'); InputStream in = new ByteArrayInputStream(data); - String uploadId = writeHelper.initiateMultiPartUpload(key); + String uploadId = writeHelper.initiateMultiPartUpload(key, PutObjectOptions.keepingDirs()); UploadPartRequest req = writeHelper.newUploadPartRequest(key, uploadId, partNo, len, in, null, 0L); PartETag partEtag = writeHelper.uploadPart(req).getPartETag(); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestConstants.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestConstants.java index 47ff2f326ef74..a6269c437665a 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestConstants.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestConstants.java @@ -245,4 +245,10 @@ public interface S3ATestConstants { * used. */ int KMS_KEY_GENERATION_REQUEST_PARAMS_BYTES_WRITTEN = 94; + + /** + * Build directory property. + * Value: {@value}. + */ + String PROJECT_BUILD_DIRECTORY_PROPERTY = "project.build.directory"; } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestUtils.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestUtils.java index 21ad7f87d6e34..48cb52c5ac29c 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestUtils.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestUtils.java @@ -35,8 +35,6 @@ import org.apache.hadoop.fs.s3a.auth.MarshalledCredentialBinding; import org.apache.hadoop.fs.s3a.auth.MarshalledCredentials; import org.apache.hadoop.fs.s3a.auth.delegation.EncryptionSecrets; -import org.apache.hadoop.fs.s3a.commit.CommitConstants; - import org.apache.hadoop.fs.s3a.impl.ChangeDetectionPolicy; import org.apache.hadoop.fs.s3a.impl.ContextAccessors; import org.apache.hadoop.fs.s3a.impl.StatusProbeEnum; @@ -577,6 +575,19 @@ public static Configuration prepareTestConfiguration(final Configuration conf) { return conf; } + /** + * build dir. + * @return the directory for the project's build, as set by maven, + * falling back to pwd + "target" if running from an IDE; + */ + public static File getProjectBuildDir() { + String propval = System.getProperty(PROJECT_BUILD_DIRECTORY_PROPERTY); + if (StringUtils.isEmpty(propval)) { + propval = "target"; + } + return new File(propval).getAbsoluteFile(); + } + /** * Clear any Hadoop credential provider path. * This is needed if people's test setups switch to credential providers, @@ -1301,18 +1312,6 @@ public static long lsR(FileSystem fileSystem, Path path, boolean recursive) public static final DateFormat LISTING_FORMAT = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss"); - /** - * Skip a test if the FS isn't marked as supporting magic commits. - * @param fs filesystem - */ - public static void assumeMagicCommitEnabled(S3AFileSystem fs) - throws IOException { - assume("Magic commit option disabled on " + fs, - fs.hasPathCapability( - fs.getWorkingDirectory(), - CommitConstants.STORE_CAPABILITY_MAGIC_COMMITTER)); - } - /** * Probe for the configuration containing a specific credential provider. * If the list is empty, there will be no match, even if the named provider diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestS3ABlockOutputStream.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestS3ABlockOutputStream.java index 21f268dfb2ed5..08f4f8bc9df5c 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestS3ABlockOutputStream.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestS3ABlockOutputStream.java @@ -22,6 +22,7 @@ import org.apache.hadoop.fs.PathIOException; import org.apache.hadoop.fs.s3a.audit.AuditTestSupport; import org.apache.hadoop.fs.s3a.commit.PutTracker; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; import org.apache.hadoop.fs.s3a.statistics.impl.EmptyS3AStatisticsContext; import org.apache.hadoop.util.Progressable; import org.junit.Before; @@ -65,7 +66,8 @@ private S3ABlockOutputStream.BlockOutputStreamBuilder mockS3ABuilder() { .withKey("") .withProgress(progressable) .withPutTracker(putTracker) - .withWriteOperations(oHelper); + .withWriteOperations(oHelper) + .withPutOptions(PutObjectOptions.keepingDirs()); return builder; } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestS3AInputStreamRetry.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestS3AInputStreamRetry.java index 62f5bff35c4fa..c62bf5daca3a4 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestS3AInputStreamRetry.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestS3AInputStreamRetry.java @@ -111,7 +111,8 @@ private S3AInputStream getMockedS3AInputStream() { s3AReadOpContext, s3ObjectAttributes, getMockedInputStreamCallback(), - s3AReadOpContext.getS3AStatisticsContext().newInputStreamStatistics()); + s3AReadOpContext.getS3AStatisticsContext().newInputStreamStatistics(), + null); } /** diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/audit/ITestAuditAccessChecks.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/audit/ITestAuditAccessChecks.java index a5a1b454d03ad..4b0b65fd783ae 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/audit/ITestAuditAccessChecks.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/audit/ITestAuditAccessChecks.java @@ -75,7 +75,9 @@ public Configuration createConfiguration() { @Override public void setup() throws Exception { super.setup(); - auditor = (AccessCheckingAuditor) getFileSystem().getAuditor(); + final S3AFileSystem fs = getFileSystem(); + auditor = (AccessCheckingAuditor) fs.getAuditor(); + setSpanSource(fs); } @Test diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumeRole.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumeRole.java index 86bfa2bb07d7d..51eac7e8cc349 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumeRole.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumeRole.java @@ -47,9 +47,10 @@ import org.apache.hadoop.fs.s3a.S3ATestConstants; import org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider; import org.apache.hadoop.fs.s3a.commit.CommitConstants; -import org.apache.hadoop.fs.s3a.commit.CommitOperations; import org.apache.hadoop.fs.s3a.commit.files.PendingSet; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; +import org.apache.hadoop.fs.s3a.commit.impl.CommitContext; +import org.apache.hadoop.fs.s3a.commit.impl.CommitOperations; import org.apache.hadoop.fs.s3a.s3guard.S3GuardTool; import org.apache.hadoop.fs.s3a.statistics.CommitterStatistics; @@ -72,7 +73,7 @@ * Tests use of assumed roles. * Only run if an assumed role is provided. */ -@SuppressWarnings({"IOResourceOpenedButNotSafelyClosed", "ThrowableNotThrown"}) +@SuppressWarnings("ThrowableNotThrown") public class ITestAssumeRole extends AbstractS3ATestBase { private static final Logger LOG = @@ -563,9 +564,9 @@ public void testRestrictedCommitActions() throws Throwable { roleFS = (S3AFileSystem) writeableDir.getFileSystem(conf); CommitterStatistics committerStatistics = fs.newCommitterStatistics(); CommitOperations fullOperations = new CommitOperations(fs, - committerStatistics); + committerStatistics, "/"); CommitOperations operations = new CommitOperations(roleFS, - committerStatistics); + committerStatistics, "/"); File localSrc = File.createTempFile("source", ""); writeCSVData(localSrc); @@ -595,37 +596,37 @@ public void testRestrictedCommitActions() throws Throwable { SinglePendingCommit pending = fullOperations.uploadFileToPendingCommit(src, dest, "", uploadPartSize, progress); - pending.save(fs, new Path(readOnlyDir, - name + CommitConstants.PENDING_SUFFIX), true); + pending.save(fs, + new Path(readOnlyDir, name + CommitConstants.PENDING_SUFFIX), + SinglePendingCommit.serializer()); assertTrue(src.delete()); })); progress.assertCount("progress counter is not expected", range); - try { + try(CommitContext commitContext = + operations.createCommitContextForTesting(uploadDest, + null, 0)) { // we expect to be able to list all the files here Pair>> pendingCommits = operations.loadSinglePendingCommits(readOnlyDir, - true); + true, commitContext); // all those commits must fail List commits = pendingCommits.getLeft().getCommits(); assertEquals(range, commits.size()); - try(CommitOperations.CommitContext commitContext - = operations.initiateCommitOperation(uploadDest)) { - commits.parallelStream().forEach( - (c) -> { - CommitOperations.MaybeIOE maybeIOE = - commitContext.commit(c, "origin"); - Path path = c.destinationPath(); - assertCommitAccessDenied(path, maybeIOE); - }); - } + commits.parallelStream().forEach( + (c) -> { + CommitOperations.MaybeIOE maybeIOE = + commitContext.commit(c, "origin"); + Path path = c.destinationPath(); + assertCommitAccessDenied(path, maybeIOE); + }); // fail of all list and abort of .pending files. LOG.info("abortAllSinglePendingCommits({})", readOnlyDir); assertCommitAccessDenied(readOnlyDir, - operations.abortAllSinglePendingCommits(readOnlyDir, true)); + operations.abortAllSinglePendingCommits(readOnlyDir, commitContext, true)); // try writing a magic file Path magicDestPath = new Path(readOnlyDir, diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractCommitITest.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractCommitITest.java index 17ce7e4ddcacd..9d9000cafb936 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractCommitITest.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractCommitITest.java @@ -18,9 +18,12 @@ package org.apache.hadoop.fs.s3a.commit; +import java.io.File; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.util.List; -import java.util.stream.Collectors; import org.assertj.core.api.Assertions; import org.slf4j.Logger; @@ -34,17 +37,20 @@ import org.apache.hadoop.fs.contract.ContractTestUtils; import org.apache.hadoop.fs.s3a.AbstractS3ATestBase; import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.WriteOperationHelper; -import org.apache.hadoop.fs.store.audit.AuditSpan; import org.apache.hadoop.fs.s3a.commit.files.SuccessData; import org.apache.hadoop.mapreduce.JobContext; import org.apache.hadoop.mapreduce.RecordWriter; import org.apache.hadoop.mapreduce.TaskAttemptContext; import org.apache.hadoop.mapreduce.TypeConverter; +import org.apache.hadoop.mapreduce.lib.output.committer.manifest.files.ManifestPrinter; +import org.apache.hadoop.mapreduce.lib.output.committer.manifest.files.ManifestSuccessData; import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl; import org.apache.hadoop.mapreduce.v2.api.records.JobId; import org.apache.hadoop.mapreduce.v2.util.MRBuilderUtils; +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.YEAR; import static org.apache.hadoop.fs.s3a.Constants.*; import static org.apache.hadoop.fs.s3a.MultipartTestUtils.listMultipartUploads; import static org.apache.hadoop.fs.s3a.S3ATestUtils.*; @@ -60,6 +66,17 @@ public abstract class AbstractCommitITest extends AbstractS3ATestBase { private static final Logger LOG = LoggerFactory.getLogger(AbstractCommitITest.class); + /** + * Helper class for commit operations and assertions. + */ + private CommitterTestHelper testHelper; + + /** + * Directory for job summary reports. + * This should be set up in test suites testing against real object stores. + */ + private File reportDir; + /** * Creates a configuration for commit operations: commit is enabled in the FS * and output is multipart to on-heap arrays. @@ -81,6 +98,8 @@ protected Configuration createConfiguration() { conf.setLong(MIN_MULTIPART_THRESHOLD, MULTIPART_MIN_SIZE); conf.setInt(MULTIPART_SIZE, MULTIPART_MIN_SIZE); conf.set(FAST_UPLOAD_BUFFER, FAST_UPLOAD_BUFFER_ARRAY); + // and bind the report dir + conf.set(OPT_SUMMARY_REPORT_DIR, reportDir.toURI().toString()); return conf; } @@ -92,6 +111,36 @@ public Logger log() { return LOG; } + /** + * Get directory for reports; valid after + * setup. + * @return where success/failure reports go. + */ + protected File getReportDir() { + return reportDir; + } + + @Override + public void setup() throws Exception { + // set the manifest committer to a localfs path for reports across + // all threads. + // do this before superclass setup so reportDir is non-null there + // and can be used in creating the configuration. + reportDir = new File(getProjectBuildDir(), "reports"); + reportDir.mkdirs(); + + super.setup(); + testHelper = new CommitterTestHelper(getFileSystem()); + } + + /** + * Get helper class. + * @return helper; only valid after setup. + */ + public CommitterTestHelper getTestHelper() { + return testHelper; + } + /*** * Bind to the named committer. * @@ -117,12 +166,14 @@ public void rmdir(Path dir, Configuration conf) throws IOException { if (dir != null) { describe("deleting %s", dir); FileSystem fs = dir.getFileSystem(conf); + fs.delete(dir, true); + } } /** - * Create a random Job ID using the fork ID as part of the number. + * Create a random Job ID using the fork ID and the current time. * @return fork ID string in a format parseable by Jobs * @throws Exception failure */ @@ -132,7 +183,14 @@ public static String randomJobId() throws Exception { String trailingDigits = testUniqueForkId.substring(l - 4, l); try { int digitValue = Integer.valueOf(trailingDigits); - return String.format("20070712%04d_%04d", + DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendValue(YEAR, 4) + .appendValue(MONTH_OF_YEAR, 2) + .appendValue(DAY_OF_MONTH, 2) + .toFormatter(); + return String.format("%s%04d_%04d", + LocalDateTime.now().format(formatter), (long)(Math.random() * 1000), digitValue); } catch (NumberFormatException e) { @@ -146,22 +204,9 @@ public static String randomJobId() throws Exception { * @return a count of aborts * @throws IOException trouble. */ - protected int abortMultipartUploadsUnderPath(Path path) throws IOException { - S3AFileSystem fs = getFileSystem(); - if (fs != null && path != null) { - String key = fs.pathToKey(path); - int count = 0; - try (AuditSpan span = span()) { - WriteOperationHelper writeOps = fs.getWriteOperationHelper(); - count = writeOps.abortMultipartUploadsUnderPath(key); - if (count > 0) { - log().info("Multipart uploads deleted: {}", count); - } - } - return count; - } else { - return 0; - } + protected void abortMultipartUploadsUnderPath(Path path) throws IOException { + getTestHelper() + .abortMultipartUploadsUnderPath(path); } /** @@ -183,10 +228,9 @@ protected void assertMultipartUploadsPending(Path path) throws IOException { protected void assertNoMultipartUploadsPending(Path path) throws IOException { List uploads = listMultipartUploads(getFileSystem(), pathToPrefix(path)); - if (!uploads.isEmpty()) { - String result = uploads.stream().collect(Collectors.joining("\n")); - fail("Multipart uploads in progress under " + path + " \n" + result); - } + Assertions.assertThat(uploads) + .describedAs("Multipart uploads in progress under " + path) + .isEmpty(); } /** @@ -341,6 +385,13 @@ public static SuccessData validateSuccessFile(final Path outputPath, .describedAs("JobID in " + commitDetails) .isEqualTo(jobId); } + // also load as a manifest success data file + // to verify consistency and that the CLI tool works. + Path success = new Path(outputPath, _SUCCESS); + final ManifestPrinter showManifest = new ManifestPrinter(); + ManifestSuccessData manifestSuccessData = + showManifest.loadAndPrintManifest(fs, success); + return successData; } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractITCommitProtocol.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractITCommitProtocol.java index bf99d49576993..b193cca03db00 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractITCommitProtocol.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractITCommitProtocol.java @@ -126,7 +126,7 @@ public abstract class AbstractITCommitProtocol extends AbstractCommitITest { private static final Text VAL_2 = new Text("val2"); /** A job to abort in test case teardown. */ - private List abortInTeardown = new ArrayList<>(1); + private final List abortInTeardown = new ArrayList<>(1); private final StandardCommitterFactory standardCommitterFactory = new StandardCommitterFactory(); @@ -562,7 +562,7 @@ protected void commit(AbstractS3ACommitter committer, describe("\ncommitting job"); committer.commitJob(jContext); describe("commit complete\n"); - verifyCommitterHasNoThreads(committer); + } } @@ -617,6 +617,9 @@ public void testRecoveryAndCleanup() throws Exception { assertNotNull("null outputPath in committer " + committer, committer.getOutputPath()); + // note the task attempt path. + Path job1TaskAttempt0Path = committer.getTaskAttemptPath(tContext); + // Commit the task. This will promote data and metadata to where // job commits will pick it up on commit or abort. commitTask(committer, tContext); @@ -636,6 +639,15 @@ public void testRecoveryAndCleanup() throws Exception { intercept(PathCommitException.class, "recover", () -> committer2.recoverTask(tContext2)); + // the new task attempt path is different from the first, because the + // job attempt counter is used in the path + final Path job2TaskAttempt0Path = committer2.getTaskAttemptPath(tContext2); + LOG.info("Job attempt 1 task attempt path {}; attempt 2 path {}", + job1TaskAttempt0Path, job2TaskAttempt0Path); + assertNotEquals("Task attempt paths must differ", + job1TaskAttempt0Path, + job2TaskAttempt0Path); + // at this point, task attempt 0 has failed to recover // it should be abortable though. This will be a no-op as it already // committed @@ -645,7 +657,7 @@ public void testRecoveryAndCleanup() throws Exception { committer2.abortJob(jContext2, JobStatus.State.KILLED); // now, state of system may still have pending data assertNoMultipartUploadsPending(outDir); - verifyCommitterHasNoThreads(committer2); + } protected void assertTaskAttemptPathDoesNotExist( @@ -747,7 +759,9 @@ private void validateMapFileOutputContent( assertPathExists("Map output", expectedMapDir); assertIsDirectory(expectedMapDir); FileStatus[] files = fs.listStatus(expectedMapDir); - assertTrue("No files found in " + expectedMapDir, files.length > 0); + Assertions.assertThat(files) + .describedAs("Files found in " + expectedMapDir) + .hasSizeGreaterThan(0); assertPathExists("index file in " + expectedMapDir, new Path(expectedMapDir, MapFile.INDEX_FILE_NAME)); assertPathExists("data file in " + expectedMapDir, @@ -795,9 +809,9 @@ public void testCommitLifecycle() throws Exception { try { applyLocatedFiles(getFileSystem().listFiles(outDir, false), - (status) -> - assertFalse("task committed file to dest :" + status, - status.getPath().toString().contains("part"))); + (status) -> Assertions.assertThat(status.getPath().toString()) + .describedAs("task committed file to dest :" + status) + .doesNotContain("part")); } catch (FileNotFoundException ignored) { log().info("Outdir {} is not created by task commit phase ", outDir); @@ -1071,27 +1085,34 @@ public void testMapFileOutputCommitter() throws Exception { // hidden filenames (_ or . prefixes) describe("listing"); FileStatus[] filtered = fs.listStatus(outDir, HIDDEN_FILE_FILTER); - assertEquals("listed children under " + ls, - 1, filtered.length); + Assertions.assertThat(filtered) + .describedAs("listed children under " + ls) + .hasSize(1); FileStatus fileStatus = filtered[0]; - assertTrue("Not the part file: " + fileStatus, - fileStatus.getPath().getName().startsWith(PART_00000)); + Assertions.assertThat(fileStatus.getPath().getName()) + .describedAs("Not a part file: " + fileStatus) + .startsWith(PART_00000); describe("getReaders()"); - assertEquals("Number of MapFile.Reader entries with shared FS " - + outDir + " : " + ls, - 1, getReaders(fs, outDir, conf).length); + Assertions.assertThat(getReaders(fs, outDir, conf)) + .describedAs("Number of MapFile.Reader entries with shared FS %s: %s", + outDir, ls) + .hasSize(1); describe("getReaders(new FS)"); FileSystem fs2 = FileSystem.get(outDir.toUri(), conf); - assertEquals("Number of MapFile.Reader entries with shared FS2 " - + outDir + " : " + ls, - 1, getReaders(fs2, outDir, conf).length); + Assertions.assertThat(getReaders(fs2, outDir, conf)) + .describedAs("Number of MapFile.Reader entries with shared FS2 %s: %s", + outDir, ls) + .hasSize(1); describe("MapFileOutputFormat.getReaders"); - assertEquals("Number of MapFile.Reader entries with new FS in " - + outDir + " : " + ls, - 1, MapFileOutputFormat.getReaders(outDir, conf).length); + + Assertions.assertThat(MapFileOutputFormat.getReaders(outDir, conf)) + .describedAs("Number of MapFile.Reader entries with new FS in %s: %s", + outDir, ls) + .hasSize(1); + } /** Open the output generated by this format. */ @@ -1165,7 +1186,7 @@ public void testAbortTaskThenJob() throws Exception { committer.abortJob(jobData.jContext, JobStatus.State.FAILED); assertJobAbortCleanedUp(jobData); - verifyCommitterHasNoThreads(committer); + } /** @@ -1219,7 +1240,7 @@ public void testFailAbort() throws Exception { // try again; expect abort to be idempotent. committer.abortJob(jContext, JobStatus.State.FAILED); assertNoMultipartUploadsPending(outDir); - verifyCommitterHasNoThreads(committer); + } public void assertPart0000DoesNotExist(Path dir) throws Exception { @@ -1433,7 +1454,7 @@ public void testAMWorkflow() throws Throwable { AbstractS3ACommitter committer2 = (AbstractS3ACommitter) outputFormat.getOutputCommitter(newAttempt); committer2.abortTask(tContext); - verifyCommitterHasNoThreads(committer2); + assertNoMultipartUploadsPending(getOutDir()); } @@ -1777,11 +1798,10 @@ public void testS3ACommitterFactoryBinding() throws Throwable { conf.setInt(MRJobConfig.APPLICATION_ATTEMPT_ID, 1); TaskAttemptContext tContext = new TaskAttemptContextImpl(conf, taskAttempt0); - String name = getCommitterName(); S3ACommitterFactory factory = new S3ACommitterFactory(); - assertEquals("Wrong committer from factory", - createCommitter(outDir, tContext).getClass(), - factory.createOutputCommitter(outDir, tContext).getClass()); + Assertions.assertThat(factory.createOutputCommitter(outDir, tContext).getClass()) + .describedAs("Committer from factory with name %s", getCommitterName()) + .isEqualTo(createCommitter(outDir, tContext).getClass()); } /** @@ -1830,7 +1850,7 @@ protected void validateTaskAttemptWorkingDirectory( protected void commitTask(final AbstractS3ACommitter committer, final TaskAttemptContext tContext) throws IOException { committer.commitTask(tContext); - verifyCommitterHasNoThreads(committer); + } /** @@ -1842,15 +1862,7 @@ protected void commitTask(final AbstractS3ACommitter committer, protected void commitJob(final AbstractS3ACommitter committer, final JobContext jContext) throws IOException { committer.commitJob(jContext); - verifyCommitterHasNoThreads(committer); - } - /** - * Verify that the committer does not have a thread pool. - * @param committer committer to validate. - */ - protected void verifyCommitterHasNoThreads(AbstractS3ACommitter committer) { - assertFalse("Committer has an active thread pool", - committer.hasThreadPool()); } + } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/CommitterTestHelper.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/CommitterTestHelper.java new file mode 100644 index 0000000000000..cd703f96da8dd --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/CommitterTestHelper.java @@ -0,0 +1,148 @@ +/* + * 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.s3a.commit; + +import java.io.IOException; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.s3a.MultipartTestUtils; +import org.apache.hadoop.fs.s3a.S3AFileSystem; + +import static java.util.Objects.requireNonNull; +import static org.apache.hadoop.fs.contract.ContractTestUtils.verifyPathExists; +import static org.apache.hadoop.fs.s3a.commit.CommitConstants.BASE; +import static org.apache.hadoop.fs.s3a.commit.CommitConstants.MAGIC; +import static org.apache.hadoop.fs.s3a.commit.CommitConstants.STREAM_CAPABILITY_MAGIC_OUTPUT; +import static org.apache.hadoop.fs.s3a.commit.CommitConstants.XA_MAGIC_MARKER; +import static org.apache.hadoop.fs.s3a.commit.impl.CommitOperations.extractMagicFileLength; + +/** + * Helper for committer tests: extra assertions and the like. + */ +public class CommitterTestHelper { + + private static final Logger LOG = + LoggerFactory.getLogger(CommitterTestHelper.class); + + /** + * Filesystem under test. + */ + private final S3AFileSystem fileSystem; + + /** + * Constructor. + * @param fileSystem filesystem to work with. + */ + public CommitterTestHelper(S3AFileSystem fileSystem) { + this.fileSystem = requireNonNull(fileSystem); + } + + /** + * Get the filesystem. + * @return the filesystem. + */ + public S3AFileSystem getFileSystem() { + return fileSystem; + } + + /** + * Assert a path refers to a marker file of an expected length; + * the length is extracted from the custom header. + * @param path magic file. + * @param dataSize expected data size + * @throws IOException IO failure + */ + public void assertIsMarkerFile(Path path, long dataSize) throws IOException { + final S3AFileSystem fs = getFileSystem(); + FileStatus status = verifyPathExists(fs, + "uploaded file commit", path); + Assertions.assertThat(status.getLen()) + .describedAs("Marker File file %s: %s", path, status) + .isEqualTo(0); + Assertions.assertThat(extractMagicFileLength(fs, path)) + .describedAs("XAttribute " + XA_MAGIC_MARKER + " of " + path) + .isNotEmpty() + .hasValue(dataSize); + } + + /** + * Assert a file does not have the magic marker header. + * @param path magic file. + * @throws IOException IO failure + */ + public void assertFileLacksMarkerHeader(Path path) throws IOException { + Assertions.assertThat(extractMagicFileLength(getFileSystem(), + path)) + .describedAs("XAttribute " + XA_MAGIC_MARKER + " of " + path) + .isEmpty(); + } + + /** + * Create a new path which has the same filename as the dest file, but + * is in a magic directory under the destination dir. + * @param destFile final destination file + * @return magic path + */ + public static Path makeMagic(Path destFile) { + return new Path(destFile.getParent(), + MAGIC + '/' + BASE + "/" + destFile.getName()); + } + + /** + * Assert that an output stream is magic. + * @param stream stream to probe. + */ + public static void assertIsMagicStream(final FSDataOutputStream stream) { + Assertions.assertThat(stream.hasCapability(STREAM_CAPABILITY_MAGIC_OUTPUT)) + .describedAs("Stream capability %s in stream %s", + STREAM_CAPABILITY_MAGIC_OUTPUT, stream) + .isTrue(); + } + + /** + * Abort all multipart uploads under a path. + * @param path path for uploads to abort; may be null + * @return a count of aborts + * @throws IOException trouble. + */ + public void abortMultipartUploadsUnderPath(Path path) { + + MultipartTestUtils.clearAnyUploads(getFileSystem(), path); + } + + /** + * Get a list of all pending uploads under a prefix, one which can be printed. + * @param prefix prefix to look under + * @return possibly empty list + * @throws IOException IO failure. + */ + public List listMultipartUploads( + String prefix) throws IOException { + + return MultipartTestUtils.listMultipartUploads(getFileSystem(), prefix); + } + +} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestCommitOperationCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestCommitOperationCost.java new file mode 100644 index 0000000000000..d0c86b738ca10 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestCommitOperationCost.java @@ -0,0 +1,325 @@ +/* + * 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.s3a.commit; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.LocatedFileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.permission.FsPermission; +import org.apache.hadoop.fs.s3a.S3AFileSystem; +import org.apache.hadoop.fs.s3a.commit.files.PersistentCommitData; +import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; +import org.apache.hadoop.fs.s3a.commit.impl.CommitOperations; +import org.apache.hadoop.fs.s3a.performance.AbstractS3ACostTest; +import org.apache.hadoop.fs.statistics.IOStatisticsLogging; + +import static org.apache.hadoop.fs.s3a.Statistic.ACTION_HTTP_GET_REQUEST; +import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_MAGIC_FILES_CREATED; +import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_MAGIC_MARKER_PUT; +import static org.apache.hadoop.fs.s3a.Statistic.DIRECTORIES_CREATED; +import static org.apache.hadoop.fs.s3a.Statistic.FAKE_DIRECTORIES_DELETED; +import static org.apache.hadoop.fs.s3a.Statistic.OBJECT_BULK_DELETE_REQUEST; +import static org.apache.hadoop.fs.s3a.Statistic.OBJECT_DELETE_REQUEST; +import static org.apache.hadoop.fs.s3a.Statistic.OBJECT_LIST_REQUEST; +import static org.apache.hadoop.fs.s3a.Statistic.OBJECT_METADATA_REQUESTS; +import static org.apache.hadoop.fs.s3a.Statistic.OBJECT_MULTIPART_UPLOAD_INITIATED; +import static org.apache.hadoop.fs.s3a.Statistic.OBJECT_PUT_REQUESTS; +import static org.apache.hadoop.fs.s3a.commit.CommitConstants.MAGIC; +import static org.apache.hadoop.fs.s3a.commit.CommitterTestHelper.assertIsMagicStream; +import static org.apache.hadoop.fs.s3a.commit.CommitterTestHelper.makeMagic; +import static org.apache.hadoop.fs.s3a.performance.OperationCost.LIST_FILES_LIST_OP; +import static org.apache.hadoop.fs.s3a.performance.OperationCost.LIST_OPERATION; +import static org.apache.hadoop.fs.s3a.performance.OperationCost.NO_HEAD_OR_LIST; +import static org.apache.hadoop.fs.statistics.IOStatisticsLogging.ioStatisticsToPrettyString; +import static org.apache.hadoop.util.functional.RemoteIterators.toList; + +/** + * Assert cost of commit operations; + *
      + *
    1. Even on marker deleting filesystems, + * operations under magic dirs do not trigger marker deletion.
    2. + *
    3. Loading pending files from FileStatus entries skips HEAD checks.
    4. + *
    5. Mkdir under magic dirs doesn't check ancestor or dest type
    6. + *
    + */ +public class ITestCommitOperationCost extends AbstractS3ACostTest { + + private static final Logger LOG = + LoggerFactory.getLogger(ITestCommitOperationCost.class); + + /** + * Helper for the tests. + */ + private CommitterTestHelper testHelper; + + /** + * Create with markers kept, always. + */ + public ITestCommitOperationCost() { + super(false); + } + + @Override + public void setup() throws Exception { + super.setup(); + testHelper = new CommitterTestHelper(getFileSystem()); + } + + @Override + public void teardown() throws Exception { + try { + if (testHelper != null) { + testHelper.abortMultipartUploadsUnderPath(methodPath()); + } + } finally { + super.teardown(); + } + } + + /** + * Get a method-relative path. + * @param filename filename + * @return new path + * @throws IOException failure to create/parse the path. + */ + private Path methodSubPath(String filename) throws IOException { + return new Path(methodPath(), filename); + } + + /** + * Return the FS IOStats, prettified. + * @return string for assertions. + */ + protected String fileSystemIOStats() { + return ioStatisticsToPrettyString(getFileSystem().getIOStatistics()); + } + + @Test + public void testMagicMkdir() throws Throwable { + describe("Mkdirs __magic always skips dir marker deletion"); + S3AFileSystem fs = getFileSystem(); + Path baseDir = methodPath(); + // create dest dir marker, always + fs.mkdirs(baseDir); + Path magicDir = new Path(baseDir, MAGIC); + verifyMetrics(() -> { + fs.mkdirs(magicDir); + return fileSystemIOStats(); + }, + with(OBJECT_BULK_DELETE_REQUEST, 0), + with(OBJECT_DELETE_REQUEST, 0), + with(DIRECTORIES_CREATED, 1)); + verifyMetrics(() -> { + fs.delete(magicDir, true); + return fileSystemIOStats(); + }, + with(OBJECT_BULK_DELETE_REQUEST, 0), + with(OBJECT_DELETE_REQUEST, 1), + with(DIRECTORIES_CREATED, 0)); + assertPathExists("parent", baseDir); + } + + /** + * When a magic subdir is deleted, parent dirs are not recreated. + */ + @Test + public void testMagicMkdirs() throws Throwable { + describe("Mkdirs __magic/subdir always skips dir marker deletion"); + S3AFileSystem fs = getFileSystem(); + Path baseDir = methodPath(); + Path magicDir = new Path(baseDir, MAGIC); + fs.delete(baseDir, true); + + Path magicSubdir = new Path(magicDir, "subdir"); + verifyMetrics(() -> { + fs.mkdirs(magicSubdir, FsPermission.getDirDefault()); + return "after mkdirs " + fileSystemIOStats(); + }, + always(LIST_OPERATION), + with(OBJECT_BULK_DELETE_REQUEST, 0), + with(OBJECT_DELETE_REQUEST, 0), + with(DIRECTORIES_CREATED, 1)); + assertPathExists("magicSubdir", magicSubdir); + + verifyMetrics(() -> { + fs.delete(magicSubdir, true); + return "after delete " + fileSystemIOStats(); + }, + with(OBJECT_BULK_DELETE_REQUEST, 0), + with(OBJECT_DELETE_REQUEST, 1), + with(OBJECT_LIST_REQUEST, 1), + with(OBJECT_METADATA_REQUESTS, 1), + with(DIRECTORIES_CREATED, 0)); + // no marker dir creation + assertPathDoesNotExist("magicDir", magicDir); + assertPathDoesNotExist("baseDir", baseDir); + } + + /** + * Active stream; a field is used so closures can write to + * it. + */ + private FSDataOutputStream stream; + + /** + * Abort any active stream. + * @throws IOException failure + */ + private void abortActiveStream() throws IOException { + if (stream != null) { + stream.abort(); + stream.close(); + } + } + + @Test + public void testCostOfCreatingMagicFile() throws Throwable { + describe("Files created under magic paths skip existence checks and marker deletes"); + S3AFileSystem fs = getFileSystem(); + Path destFile = methodSubPath("file.txt"); + fs.delete(destFile.getParent(), true); + Path magicDest = makeMagic(destFile); + + // when the file is created, there is no check for overwrites + // or the dest being a directory, even if overwrite=false + try { + verifyMetrics(() -> { + stream = fs.create(magicDest, false); + return stream.toString(); + }, + always(NO_HEAD_OR_LIST), + with(COMMITTER_MAGIC_FILES_CREATED, 1), + with(COMMITTER_MAGIC_MARKER_PUT, 0), + with(OBJECT_MULTIPART_UPLOAD_INITIATED, 1)); + assertIsMagicStream(stream); + + stream.write("hello".getBytes(StandardCharsets.UTF_8)); + + // when closing, there will be no directories deleted + // we do expect two PUT requests, because the marker and manifests + // are both written + LOG.info("closing magic stream to {}", magicDest); + verifyMetrics(() -> { + stream.close(); + return stream.toString(); + }, + always(NO_HEAD_OR_LIST), + with(OBJECT_PUT_REQUESTS, 2), + with(COMMITTER_MAGIC_MARKER_PUT, 2), + with(OBJECT_BULK_DELETE_REQUEST, 0), + with(OBJECT_DELETE_REQUEST, 0)); + + } catch (Exception e) { + abortActiveStream(); + throw e; + } + // list the manifests + final CommitOperations commitOperations = new CommitOperations(fs); + List pending = verifyMetrics(() -> + toList(commitOperations. + locateAllSinglePendingCommits(magicDest.getParent(), false)), + always(LIST_FILES_LIST_OP)); + Assertions.assertThat(pending) + .describedAs("pending commits") + .hasSize(1); + + // load the only pending commit + SinglePendingCommit singleCommit = verifyMetrics(() -> + PersistentCommitData.load(fs, + pending.get(0), + SinglePendingCommit.serializer()), + always(NO_HEAD_OR_LIST), + with(ACTION_HTTP_GET_REQUEST, 1)); + + // commit it through the commit operations. + verifyMetrics(() -> { + commitOperations.commitOrFail(singleCommit); + return ioStatisticsToPrettyString( + commitOperations.getIOStatistics()); + }, + always(NO_HEAD_OR_LIST), // no probes for the dest path + with(FAKE_DIRECTORIES_DELETED, 0), // no fake dirs + with(OBJECT_DELETE_REQUEST, 0)); // no deletes + + LOG.info("Final Statistics {}", + IOStatisticsLogging.ioStatisticsToPrettyString(stream.getIOStatistics())); + } + + /** + * saving pending files MUST NOT trigger HEAD/LIST calls + * when created under a magic path; when opening + * with an S3AFileStatus the HEAD will be skipped too. + */ + @Test + public void testCostOfSavingLoadingPendingFile() throws Throwable { + describe("Verify costs of saving .pending file under a magic path"); + + S3AFileSystem fs = getFileSystem(); + Path partDir = methodSubPath("file.pending"); + Path destFile = new Path(partDir, "file.pending"); + Path magicDest = makeMagic(destFile); + // create a pending file with minimal values needed + // for validation to work + final SinglePendingCommit commit = new SinglePendingCommit(); + commit.touch(System.currentTimeMillis()); + commit.setUri(destFile.toUri().toString()); + commit.setBucket(fs.getBucket()); + commit.setLength(0); + commit.setDestinationKey(fs.pathToKey(destFile)); + commit.setUploadId("uploadId"); + commit.setEtags(new ArrayList<>()); + // fail fast if the commit data is incomplete + commit.validate(); + + // save the file: no checks will be made + verifyMetrics(() -> { + commit.save(fs, magicDest, + SinglePendingCommit.serializer()); + return commit.toString(); + }, + with(COMMITTER_MAGIC_FILES_CREATED, 0), + always(NO_HEAD_OR_LIST), + with(OBJECT_BULK_DELETE_REQUEST, 0), + with(OBJECT_DELETE_REQUEST, 0) + ); + + LOG.info("File written; Validating"); + testHelper.assertFileLacksMarkerHeader(magicDest); + FileStatus status = fs.getFileStatus(magicDest); + + LOG.info("Reading file {}", status); + // opening a file with a status passed in will skip the HEAD + verifyMetrics(() -> + PersistentCommitData.load(fs, status, SinglePendingCommit.serializer()), + always(NO_HEAD_OR_LIST), + with(ACTION_HTTP_GET_REQUEST, 1)); + } + +} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestCommitOperations.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestCommitOperations.java index 3f0e2e7a1348b..a76a65be8bb3e 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestCommitOperations.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestCommitOperations.java @@ -23,8 +23,8 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.UUID; -import com.amazonaws.services.s3.model.PartETag; import org.assertj.core.api.Assertions; import org.junit.Test; import org.slf4j.Logger; @@ -33,12 +33,15 @@ import org.apache.commons.io.FileUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FSDataOutputStreamBuilder; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.auth.ProgressCounter; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; +import org.apache.hadoop.fs.s3a.commit.impl.CommitContext; +import org.apache.hadoop.fs.s3a.commit.impl.CommitOperations; import org.apache.hadoop.fs.s3a.commit.magic.MagicCommitTracker; import org.apache.hadoop.fs.s3a.commit.magic.MagicS3GuardCommitter; import org.apache.hadoop.fs.s3a.commit.magic.MagicS3GuardCommitterFactory; @@ -52,11 +55,13 @@ import static org.apache.hadoop.fs.contract.ContractTestUtils.*; import static org.apache.hadoop.fs.s3a.S3ATestUtils.*; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.*; -import static org.apache.hadoop.fs.s3a.commit.CommitOperations.extractMagicFileLength; import static org.apache.hadoop.fs.s3a.commit.CommitUtils.*; +import static org.apache.hadoop.fs.s3a.commit.CommitterTestHelper.assertIsMagicStream; import static org.apache.hadoop.fs.s3a.commit.MagicCommitPaths.*; import static org.apache.hadoop.fs.s3a.Constants.*; +import static org.apache.hadoop.fs.s3a.statistics.impl.EmptyS3AStatisticsContext.EMPTY_BLOCK_OUTPUT_STREAM_STATISTICS; import static org.apache.hadoop.mapreduce.lib.output.PathOutputCommitterFactory.*; +import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** * Test the low-level binding of the S3A FS to the magic commit mechanism, @@ -69,6 +74,8 @@ public class ITestCommitOperations extends AbstractCommitITest { private static final byte[] DATASET = dataset(1000, 'a', 32); private static final String S3A_FACTORY_KEY = String.format( COMMITTER_FACTORY_SCHEME_PATTERN, "s3a"); + private static final String JOB_ID = UUID.randomUUID().toString(); + private ProgressCounter progress; @Override @@ -94,11 +101,13 @@ public void testCreateTrackerNormalPath() throws Throwable { MagicCommitIntegration integration = new MagicCommitIntegration(fs, true); String filename = "notdelayed.txt"; - Path destFile = methodPath(filename); + Path destFile = methodSubPath(filename); String origKey = fs.pathToKey(destFile); - PutTracker tracker = integration.createTracker(destFile, origKey); - assertFalse("wrong type: " + tracker + " for " + destFile, - tracker instanceof MagicCommitTracker); + PutTracker tracker = integration.createTracker(destFile, origKey, + EMPTY_BLOCK_OUTPUT_STREAM_STATISTICS); + Assertions.assertThat(tracker) + .describedAs("Tracker for %s", destFile) + .isNotInstanceOf(MagicCommitTracker.class); } /** @@ -111,36 +120,45 @@ public void testCreateTrackerMagicPath() throws Throwable { MagicCommitIntegration integration = new MagicCommitIntegration(fs, true); String filename = "delayed.txt"; - Path destFile = methodPath(filename); + Path destFile = methodSubPath(filename); String origKey = fs.pathToKey(destFile); Path pendingPath = makeMagic(destFile); verifyIsMagicCommitPath(fs, pendingPath); String pendingPathKey = fs.pathToKey(pendingPath); - assertTrue("wrong path of " + pendingPathKey, - pendingPathKey.endsWith(filename)); + Assertions.assertThat(pendingPathKey) + .describedAs("pending path") + .endsWith(filename); final List elements = splitPathToElements(pendingPath); - assertEquals("splitPathToElements()", filename, lastElement(elements)); + Assertions.assertThat(lastElement(elements)) + .describedAs("splitPathToElements(%s)", pendingPath) + .isEqualTo(filename); List finalDestination = finalDestination(elements); - assertEquals("finalDestination()", - filename, - lastElement(finalDestination)); - final String destKey = elementsToKey(finalDestination); - assertEquals("destination key", origKey, destKey); + Assertions.assertThat(lastElement(finalDestination)) + .describedAs("finalDestination(%s)", pendingPath) + .isEqualTo(filename); + Assertions.assertThat(elementsToKey(finalDestination)) + .describedAs("destination key") + .isEqualTo(origKey); PutTracker tracker = integration.createTracker(pendingPath, - pendingPathKey); - assertTrue("wrong type: " + tracker + " for " + pendingPathKey, - tracker instanceof MagicCommitTracker); - assertEquals("tracker destination key", origKey, tracker.getDestKey()); - - Path pendingSuffixedPath = new Path(pendingPath, - "part-0000" + PENDING_SUFFIX); - assertFalse("still a delayed complete path " + pendingSuffixedPath, - fs.isMagicCommitPath(pendingSuffixedPath)); - Path pendingSet = new Path(pendingPath, - "part-0000" + PENDINGSET_SUFFIX); - assertFalse("still a delayed complete path " + pendingSet, - fs.isMagicCommitPath(pendingSet)); + pendingPathKey, EMPTY_BLOCK_OUTPUT_STREAM_STATISTICS); + Assertions.assertThat(tracker) + .describedAs("Tracker for %s", pendingPathKey) + .isInstanceOf(MagicCommitTracker.class); + Assertions.assertThat(tracker.getDestKey()) + .describedAs("tracker destination key") + .isEqualTo(origKey); + + assertNotDelayedWrite(new Path(pendingPath, + "part-0000" + PENDING_SUFFIX)); + assertNotDelayedWrite(new Path(pendingPath, + "part-0000" + PENDINGSET_SUFFIX)); + } + + private void assertNotDelayedWrite(Path pendingSuffixedPath) { + Assertions.assertThat(getFileSystem().isMagicCommitPath(pendingSuffixedPath)) + .describedAs("Expected %s to not be magic/delayed write", pendingSuffixedPath) + .isFalse(); } @Test @@ -148,7 +166,7 @@ public void testCreateAbortEmptyFile() throws Throwable { describe("create then abort an empty file; throttled"); S3AFileSystem fs = getFileSystem(); String filename = "empty-abort.txt"; - Path destFile = methodPath(filename); + Path destFile = methodSubPath(filename); Path pendingFilePath = makeMagic(destFile); touch(fs, pendingFilePath); @@ -160,13 +178,22 @@ public void testCreateAbortEmptyFile() throws Throwable { // abort,; rethrow on failure LOG.info("Abort call"); - actions.abortAllSinglePendingCommits(pendingDataPath.getParent(), true) - .maybeRethrow(); + Path parent = pendingDataPath.getParent(); + try (CommitContext commitContext = + actions.createCommitContextForTesting(parent, JOB_ID, 0)) { + actions.abortAllSinglePendingCommits(parent, commitContext, true) + .maybeRethrow(); + } assertPathDoesNotExist("pending file not deleted", pendingDataPath); assertPathDoesNotExist("dest file was created", destFile); } + /** + * Create a new commit operations instance for the test FS. + * @return commit operations. + * @throws IOException IO failure. + */ private CommitOperations newCommitOperations() throws IOException { return new CommitOperations(getFileSystem()); @@ -198,10 +225,16 @@ public void testCommitSmallFile() throws Throwable { @Test public void testAbortNonexistentDir() throws Throwable { describe("Attempt to abort a directory that does not exist"); - Path destFile = methodPath("testAbortNonexistentPath"); - newCommitOperations() - .abortAllSinglePendingCommits(destFile, true) - .maybeRethrow(); + Path destFile = methodSubPath("testAbortNonexistentPath"); + final CommitOperations operations = newCommitOperations(); + try (CommitContext commitContext + = operations.createCommitContextForTesting(destFile, JOB_ID, 0)) { + final CommitOperations.MaybeIOE outcome = operations + .abortAllSinglePendingCommits(destFile, commitContext, true); + outcome.maybeRethrow(); + Assertions.assertThat(outcome) + .isEqualTo(CommitOperations.MaybeIOE.NONE); + } } @Test @@ -244,7 +277,7 @@ public void testBaseRelativePath() throws Throwable { describe("Test creating file with a __base marker and verify that it ends" + " up in where expected"); S3AFileSystem fs = getFileSystem(); - Path destDir = methodPath("testBaseRelativePath"); + Path destDir = methodSubPath("testBaseRelativePath"); fs.delete(destDir, true); Path pendingBaseDir = new Path(destDir, MAGIC + "/child/" + BASE); String child = "subdir/child.txt"; @@ -270,14 +303,17 @@ public void testMarkerFileRename() fs.delete(destDir, true); Path magicDest = makeMagic(destFile); Path magicDir = magicDest.getParent(); - fs.mkdirs(magicDir); + fs.mkdirs(magicDest); // use the builder API to verify it works exactly the // same. - try (FSDataOutputStream stream = fs.createFile(magicDest) - .overwrite(true) - .recursive() - .build()) { + FSDataOutputStreamBuilder builder = fs.createFile(magicDest) + .overwrite(true); + builder.recursive(); + // this has a broken return type; not sure why + builder.must(FS_S3A_CREATE_PERFORMANCE, true); + + try (FSDataOutputStream stream = builder.build()) { assertIsMagicStream(stream); stream.write(DATASET); } @@ -286,9 +322,7 @@ public void testMarkerFileRename() fs.rename(magicDest, magic2); // the renamed file has no header - Assertions.assertThat(extractMagicFileLength(fs, magic2)) - .describedAs("XAttribute " + XA_MAGIC_MARKER + " of " + magic2) - .isEmpty(); + getTestHelper().assertFileLacksMarkerHeader(magic2); // abort the upload, which is driven by the .pending files // there must be 1 deleted file; during test debugging with aborted // runs there may be more. @@ -298,17 +332,6 @@ public void testMarkerFileRename() .isGreaterThanOrEqualTo(1); } - /** - * Assert that an output stream is magic. - * @param stream stream to probe. - */ - protected void assertIsMagicStream(final FSDataOutputStream stream) { - Assertions.assertThat(stream.hasCapability(STREAM_CAPABILITY_MAGIC_OUTPUT)) - .describedAs("Stream capability %s in stream %s", - STREAM_CAPABILITY_MAGIC_OUTPUT, stream) - .isTrue(); - } - /** * Create a file through the magic commit mechanism. * @param filename file to create (with __magic path.) @@ -318,39 +341,27 @@ protected void assertIsMagicStream(final FSDataOutputStream stream) { private void createCommitAndVerify(String filename, byte[] data) throws Exception { S3AFileSystem fs = getFileSystem(); - Path destFile = methodPath(filename); + Path destFile = methodSubPath(filename); fs.delete(destFile.getParent(), true); Path magicDest = makeMagic(destFile); assertPathDoesNotExist("Magic file should not exist", magicDest); long dataSize = data != null ? data.length : 0; - try(FSDataOutputStream stream = fs.create(magicDest, true)) { + try (FSDataOutputStream stream = fs.create(magicDest, true)) { assertIsMagicStream(stream); if (dataSize > 0) { stream.write(data); } - stream.close(); } - FileStatus status = fs.getFileStatus(magicDest); - assertEquals("Magic marker file is not zero bytes: " + status, - 0, 0); - Assertions.assertThat(extractMagicFileLength(fs, - magicDest)) - .describedAs("XAttribute " + XA_MAGIC_MARKER + " of " + magicDest) - .isNotEmpty() - .hasValue(dataSize); + getTestHelper().assertIsMarkerFile(magicDest, dataSize); commit(filename, destFile); verifyFileContents(fs, destFile, data); // the destination file doesn't have the attribute - Assertions.assertThat(extractMagicFileLength(fs, - destFile)) - .describedAs("XAttribute " + XA_MAGIC_MARKER + " of " + destFile) - .isEmpty(); + getTestHelper().assertFileLacksMarkerHeader(destFile); } /** * Commit the file, with before and after checks on the dest and magic * values. - * Failures can be set; they'll be reset after the commit. * @param filename filename of file * @param destFile destination path of file * @throws Exception any failure of the operation @@ -371,20 +382,21 @@ private void commit(String filename, Path destFile) throws IOException { + final CommitOperations actions = newCommitOperations(); validateIntermediateAndFinalPaths(magicFile, destFile); SinglePendingCommit commit = SinglePendingCommit.load(getFileSystem(), - validatePendingCommitData(filename, magicFile)); - - commitOrFail(destFile, commit, newCommitOperations()); + validatePendingCommitData(filename, magicFile), + null, + SinglePendingCommit.serializer()); - verifyCommitExists(commit); + commitOrFail(destFile, commit, actions); } private void commitOrFail(final Path destFile, final SinglePendingCommit commit, final CommitOperations actions) throws IOException { - try (CommitOperations.CommitContext commitContext - = actions.initiateCommitOperation(destFile)) { + try (CommitContext commitContext + = actions.createCommitContextForTesting(destFile, JOB_ID, 0)) { commitContext.commitOrFail(commit); } } @@ -401,26 +413,6 @@ private void validateIntermediateAndFinalPaths(Path magicFilePath, assertPathDoesNotExist("dest file was created", destFile); } - /** - * Verify that the path at the end of a commit exists. - * This does not validate the size. - * @param commit commit to verify - * @throws FileNotFoundException dest doesn't exist - * @throws ValidationFailure commit arg is invalid - * @throws IOException invalid commit, IO failure - */ - private void verifyCommitExists(SinglePendingCommit commit) - throws FileNotFoundException, ValidationFailure, IOException { - commit.validate(); - // this will force an existence check - Path path = getFileSystem().keyToQualifiedPath(commit.getDestinationKey()); - FileStatus status = getFileSystem().getFileStatus(path); - LOG.debug("Destination entry: {}", status); - if (!status.isFile()) { - throw new PathCommitException(path, "Not a file: " + status); - } - } - /** * Validate that a pending commit data file exists, load it and validate * its contents. @@ -443,14 +435,19 @@ private Path validatePendingCommitData(String filename, SinglePendingCommit persisted = SinglePendingCommit.serializer() .load(fs, pendingDataPath); persisted.validate(); - assertTrue("created timestamp wrong in " + persisted, - persisted.getCreated() > 0); - assertTrue("saved timestamp wrong in " + persisted, - persisted.getSaved() > 0); + Assertions.assertThat(persisted.getCreated()) + .describedAs("Created timestamp in %s", persisted) + .isGreaterThan(0); + Assertions.assertThat(persisted.getSaved()) + .describedAs("saved timestamp in %s", persisted) + .isGreaterThan(0); List etags = persisted.getEtags(); - assertEquals("etag list " + persisted, 1, etags.size()); - List partList = CommitOperations.toPartEtags(etags); - assertEquals("part list " + persisted, 1, partList.size()); + Assertions.assertThat(etags) + .describedAs("Etag list") + .hasSize(1); + Assertions.assertThat(CommitOperations.toPartEtags(etags)) + .describedAs("Etags to parts") + .hasSize(1); return pendingDataPath; } @@ -460,24 +457,16 @@ private Path validatePendingCommitData(String filename, * @return new path * @throws IOException failure to create/parse the path. */ - private Path methodPath(String filename) throws IOException { + private Path methodSubPath(String filename) throws IOException { return new Path(methodPath(), filename); } - /** - * Get a unique path for a method. - * @return a path - * @throws IOException - */ - protected Path methodPath() throws IOException { - return path(getMethodName()); - } - @Test public void testUploadEmptyFile() throws Throwable { + describe("Upload a zero byte file to a magic path"); File tempFile = File.createTempFile("commit", ".txt"); CommitOperations actions = newCommitOperations(); - Path dest = methodPath("testUploadEmptyFile"); + Path dest = methodSubPath("testUploadEmptyFile"); S3AFileSystem fs = getFileSystem(); fs.delete(dest, false); @@ -492,11 +481,14 @@ public void testUploadEmptyFile() throws Throwable { commitOrFail(dest, pendingCommit, actions); - FileStatus status = verifyPathExists(fs, - "uploaded file commit", dest); progress.assertCount("Progress counter should be 1.", 1); - assertEquals("File length in " + status, 0, status.getLen()); + FileStatus status = verifyPathExists(fs, + "uploaded file commit", dest); + Assertions.assertThat(status.getLen()) + .describedAs("Committed File file %s: %s", dest, status) + .isEqualTo(0); + getTestHelper().assertFileLacksMarkerHeader(dest); } @Test @@ -505,7 +497,7 @@ public void testUploadSmallFile() throws Throwable { String text = "hello, world"; FileUtils.write(tempFile, text, "UTF-8"); CommitOperations actions = newCommitOperations(); - Path dest = methodPath("testUploadSmallFile"); + Path dest = methodSubPath("testUploadSmallFile"); S3AFileSystem fs = getFileSystem(); fs.delete(dest, true); @@ -523,51 +515,57 @@ public void testUploadSmallFile() throws Throwable { commitOrFail(dest, pendingCommit, actions); String s = readUTF8(fs, dest, -1); - assertEquals(text, s); + Assertions.assertThat(s) + .describedAs("contents of committed file %s", dest) + .isEqualTo(text); progress.assertCount("Progress counter should be 1.", 1); } - @Test(expected = FileNotFoundException.class) + @Test public void testUploadMissingFile() throws Throwable { File tempFile = File.createTempFile("commit", ".txt"); tempFile.delete(); CommitOperations actions = newCommitOperations(); - Path dest = methodPath("testUploadMissingile"); - - actions.uploadFileToPendingCommit(tempFile, dest, null, - DEFAULT_MULTIPART_SIZE, progress); - progress.assertCount("Progress counter should be 1.", - 1); + Path dest = methodSubPath("testUploadMissingFile"); + intercept(FileNotFoundException.class, () -> + actions.uploadFileToPendingCommit(tempFile, dest, null, + DEFAULT_MULTIPART_SIZE, progress)); + progress.assertCount("Progress counter should be 0.", + 0); } @Test public void testRevertCommit() throws Throwable { - Path destFile = methodPath("part-0000"); + describe("Revert a commit; the destination file will be deleted"); + Path destFile = methodSubPath("part-0000"); S3AFileSystem fs = getFileSystem(); touch(fs, destFile); - CommitOperations actions = newCommitOperations(); SinglePendingCommit commit = new SinglePendingCommit(); + CommitOperations actions = newCommitOperations(); commit.setDestinationKey(fs.pathToKey(destFile)); - - actions.revertCommit(commit); - - assertPathExists("parent of reverted commit", destFile.getParent()); + newCommitOperations().revertCommit(commit); + assertPathDoesNotExist("should have been reverted", destFile); } @Test public void testRevertMissingCommit() throws Throwable { - Path destFile = methodPath("part-0000"); + Path destFile = methodSubPath("part-0000"); S3AFileSystem fs = getFileSystem(); fs.delete(destFile, false); - CommitOperations actions = newCommitOperations(); SinglePendingCommit commit = new SinglePendingCommit(); commit.setDestinationKey(fs.pathToKey(destFile)); + newCommitOperations().revertCommit(commit); + assertPathDoesNotExist("should have been reverted", destFile); + } - actions.revertCommit(commit); - - assertPathExists("parent of reverted (nonexistent) commit", - destFile.getParent()); + @Test + public void testFailuresInAbortListing() throws Throwable { + Path path = path("testFailuresInAbort"); + getFileSystem().mkdirs(path); + LOG.info("Aborting"); + newCommitOperations().abortPendingUploadsUnderPath(path); + LOG.info("Abort completed"); } /** @@ -578,16 +576,16 @@ public void testRevertMissingCommit() throws Throwable { @Test public void testWriteNormalStream() throws Throwable { S3AFileSystem fs = getFileSystem(); - assumeMagicCommitEnabled(fs); Path destFile = path("normal"); try (FSDataOutputStream out = fs.create(destFile, true)) { out.writeChars("data"); assertFalse("stream has magic output: " + out, out.hasCapability(STREAM_CAPABILITY_MAGIC_OUTPUT)); - out.close(); } FileStatus status = fs.getFileStatus(destFile); - assertTrue("Empty marker file: " + status, status.getLen() > 0); + Assertions.assertThat(status.getLen()) + .describedAs("Normal file %s: %s", destFile, status) + .isGreaterThan(0); } /** @@ -598,7 +596,7 @@ public void testBulkCommitFiles() throws Throwable { describe("verify bulk commit"); File localFile = File.createTempFile("commit", ".txt"); CommitOperations actions = newCommitOperations(); - Path destDir = methodPath("out"); + Path destDir = methodSubPath("out"); S3AFileSystem fs = getFileSystem(); fs.delete(destDir, false); @@ -612,7 +610,7 @@ public void testBulkCommitFiles() throws Throwable { destFile3); List commits = new ArrayList<>(3); - for (Path destination : destinations) { + for (Path destination: destinations) { SinglePendingCommit commit1 = actions.uploadFileToPendingCommit(localFile, destination, null, @@ -624,8 +622,8 @@ public void testBulkCommitFiles() throws Throwable { assertPathDoesNotExist("destination dir", destDir); assertPathDoesNotExist("subdirectory", subdir); LOG.info("Initiating commit operations"); - try (CommitOperations.CommitContext commitContext - = actions.initiateCommitOperation(destDir)) { + try (CommitContext commitContext + = actions.createCommitContextForTesting(destDir, JOB_ID, 0)) { LOG.info("Commit #1"); commitContext.commitOrFail(commits.get(0)); final String firstCommitContextString = commitContext.toString(); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestS3ACommitterFactory.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestS3ACommitterFactory.java index a8547d672894a..2ad2568d5cc20 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestS3ACommitterFactory.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestS3ACommitterFactory.java @@ -157,7 +157,7 @@ public void testBindingsInFSConfig() throws Throwable { } /** - * Create an invalid committer via the FS binding, + * Create an invalid committer via the FS binding. */ public void testInvalidFileBinding() throws Throwable { taskConfRef.unset(FS_S3A_COMMITTER_NAME); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/TestTasks.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/TestTasks.java deleted file mode 100644 index 7ff5c3d280938..0000000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/TestTasks.java +++ /dev/null @@ -1,569 +0,0 @@ -/* - * 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.s3a.commit; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import org.apache.hadoop.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.test.HadoopTestBase; - -import static org.apache.hadoop.test.LambdaTestUtils.intercept; - -/** - * Test Tasks class. - */ -@RunWith(Parameterized.class) -public class TestTasks extends HadoopTestBase { - private static final Logger LOG = LoggerFactory.getLogger(TestTasks.class); - public static final int ITEM_COUNT = 16; - private static final int FAILPOINT = 8; - - private final int numThreads; - /** - * Thread pool for task execution. - */ - private ExecutorService threadPool; - - /** - * Task submitter bonded to the thread pool, or - * null for the 0-thread case. - */ - Tasks.Submitter submitter; - private final CounterTask failingTask - = new CounterTask("failing committer", FAILPOINT, Item::commit); - - private final FailureCounter failures - = new FailureCounter("failures", 0, null); - private final CounterTask reverter - = new CounterTask("reverter", 0, Item::revert); - private final CounterTask aborter - = new CounterTask("aborter", 0, Item::abort); - - /** - * Test array for parameterized test runs: how many threads and - * to use. Threading makes some of the assertions brittle; there are - * more checks on single thread than parallel ops. - * @return a list of parameter tuples. - */ - @Parameterized.Parameters(name = "threads={0}") - public static Collection params() { - return Arrays.asList(new Object[][]{ - {0}, - {1}, - {3}, - {8}, - {16}, - }); - } - - - private List items; - - - /** - * Construct the parameterized test. - * @param numThreads number of threads - */ - public TestTasks(int numThreads) { - this.numThreads = numThreads; - } - - /** - * In a parallel test run there is more than one thread doing the execution. - * @return true if the threadpool size is >1 - */ - public boolean isParallel() { - return numThreads > 1; - } - - @Before - public void setup() { - items = IntStream.rangeClosed(1, ITEM_COUNT) - .mapToObj(i -> new Item(i, - String.format("With %d threads", numThreads))) - .collect(Collectors.toList()); - - if (numThreads > 0) { - threadPool = Executors.newFixedThreadPool(numThreads, - new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat(getMethodName() + "-pool-%d") - .build()); - submitter = new PoolSubmitter(); - } else { - submitter = null; - } - - } - - @After - public void teardown() { - if (threadPool != null) { - threadPool.shutdown(); - threadPool = null; - } - } - - private class PoolSubmitter implements Tasks.Submitter { - - @Override - public Future submit(final Runnable task) { - return threadPool.submit(task); - } - - } - - /** - * create the builder. - * @return pre-inited builder - */ - private Tasks.Builder builder() { - return Tasks.foreach(items).executeWith(submitter); - } - - private void assertRun(Tasks.Builder builder, - CounterTask task) throws IOException { - boolean b = builder.run(task); - assertTrue("Run of " + task + " failed", b); - } - - private void assertFailed(Tasks.Builder builder, - CounterTask task) throws IOException { - boolean b = builder.run(task); - assertFalse("Run of " + task + " unexpectedly succeeded", b); - } - - private String itemsToString() { - return "[" + items.stream().map(Item::toString) - .collect(Collectors.joining("\n")) +"]"; - } - - @Test - public void testSimpleInvocation() throws Throwable { - CounterTask t = new CounterTask("simple", 0, Item::commit); - assertRun(builder(), t); - t.assertInvoked("", ITEM_COUNT); - } - - @Test - public void testFailNoStoppingSuppressed() throws Throwable { - assertFailed(builder().suppressExceptions(), failingTask); - failingTask.assertInvoked("Continued through operations", ITEM_COUNT); - items.forEach(Item::assertCommittedOrFailed); - } - - @Test - public void testFailFastSuppressed() throws Throwable { - assertFailed(builder() - .suppressExceptions() - .stopOnFailure(), - failingTask); - if (isParallel()) { - failingTask.assertInvokedAtLeast("stop fast", FAILPOINT); - } else { - failingTask.assertInvoked("stop fast", FAILPOINT); - } - } - - @Test - public void testFailedCallAbortSuppressed() throws Throwable { - assertFailed(builder() - .stopOnFailure() - .suppressExceptions() - .abortWith(aborter), - failingTask); - failingTask.assertInvokedAtLeast("success", FAILPOINT); - if (!isParallel()) { - aborter.assertInvokedAtLeast("abort", 1); - // all uncommitted items were aborted - items.stream().filter(i -> !i.committed) - .map(Item::assertAborted); - items.stream().filter(i -> i.committed) - .forEach(i -> assertFalse(i.toString(), i.aborted)); - } - } - - @Test - public void testFailedCalledWhenNotStoppingSuppressed() throws Throwable { - assertFailed(builder() - .suppressExceptions() - .onFailure(failures), - failingTask); - failingTask.assertInvokedAtLeast("success", FAILPOINT); - // only one failure was triggered - failures.assertInvoked("failure event", 1); - } - - @Test - public void testFailFastCallRevertSuppressed() throws Throwable { - assertFailed(builder() - .stopOnFailure() - .revertWith(reverter) - .abortWith(aborter) - .suppressExceptions() - .onFailure(failures), - failingTask); - failingTask.assertInvokedAtLeast("success", FAILPOINT); - if (!isParallel()) { - aborter.assertInvokedAtLeast("abort", 1); - // all uncommitted items were aborted - items.stream().filter(i -> !i.committed) - .filter(i -> !i.failed) - .forEach(Item::assertAborted); - } - // all committed were reverted - items.stream().filter(i -> i.committed && !i.failed) - .forEach(Item::assertReverted); - // all reverted items are committed - items.stream().filter(i -> i.reverted) - .forEach(Item::assertCommitted); - - // only one failure was triggered - failures.assertInvoked("failure event", 1); - } - - @Test - public void testFailSlowCallRevertSuppressed() throws Throwable { - assertFailed(builder() - .suppressExceptions() - .revertWith(reverter) - .onFailure(failures), - failingTask); - failingTask.assertInvokedAtLeast("success", FAILPOINT); - // all committed were reverted - // identify which task failed from the set - int failing = failures.getItem().id; - items.stream() - .filter(i -> i.id != failing) - .filter(i -> i.committed) - .forEach(Item::assertReverted); - // all reverted items are committed - items.stream().filter(i -> i.reverted) - .forEach(Item::assertCommitted); - - // only one failure was triggered - failures.assertInvoked("failure event", 1); - } - - @Test - public void testFailFastExceptions() throws Throwable { - intercept(IOException.class, - () -> builder() - .stopOnFailure() - .run(failingTask)); - if (isParallel()) { - failingTask.assertInvokedAtLeast("stop fast", FAILPOINT); - } else { - failingTask.assertInvoked("stop fast", FAILPOINT); - } - } - - @Test - public void testFailSlowExceptions() throws Throwable { - intercept(IOException.class, - () -> builder() - .run(failingTask)); - failingTask.assertInvoked("continued through operations", ITEM_COUNT); - items.forEach(Item::assertCommittedOrFailed); - } - - @Test - public void testFailFastExceptionsWithAbortFailure() throws Throwable { - CounterTask failFirst = new CounterTask("task", 1, Item::commit); - CounterTask a = new CounterTask("aborter", 1, Item::abort); - intercept(IOException.class, - () -> builder() - .stopOnFailure() - .abortWith(a) - .run(failFirst)); - if (!isParallel()) { - // expect the other tasks to be aborted - a.assertInvokedAtLeast("abort", ITEM_COUNT - 1); - } - } - - @Test - public void testFailFastExceptionsWithAbortFailureStopped() throws Throwable { - CounterTask failFirst = new CounterTask("task", 1, Item::commit); - CounterTask a = new CounterTask("aborter", 1, Item::abort); - intercept(IOException.class, - () -> builder() - .stopOnFailure() - .stopAbortsOnFailure() - .abortWith(a) - .run(failFirst)); - if (!isParallel()) { - // expect the other tasks to be aborted - a.assertInvoked("abort", 1); - } - } - - /** - * Fail the last one committed, all the rest will be reverted. - * The actual ID of the last task has to be picke dup from the - * failure callback, as in the pool it may be one of any. - */ - @Test - public void testRevertAllSuppressed() throws Throwable { - CounterTask failLast = new CounterTask("task", ITEM_COUNT, Item::commit); - - assertFailed(builder() - .suppressExceptions() - .stopOnFailure() - .revertWith(reverter) - .abortWith(aborter) - .onFailure(failures), - failLast); - failLast.assertInvoked("success", ITEM_COUNT); - int abCount = aborter.getCount(); - int revCount = reverter.getCount(); - assertEquals(ITEM_COUNT, 1 + abCount + revCount); - // identify which task failed from the set - int failing = failures.getItem().id; - // all committed were reverted - items.stream() - .filter(i -> i.id != failing) - .filter(i -> i.committed) - .forEach(Item::assertReverted); - items.stream() - .filter(i -> i.id != failing) - .filter(i -> !i.committed) - .forEach(Item::assertAborted); - // all reverted items are committed - items.stream().filter(i -> i.reverted) - .forEach(Item::assertCommitted); - - // only one failure was triggered - failures.assertInvoked("failure event", 1); - } - - - /** - * The Item which tasks process. - */ - private final class Item { - private final int id; - private final String text; - - private volatile boolean committed, aborted, reverted, failed; - - private Item(int item, String text) { - this.id = item; - this.text = text; - } - - boolean commit() { - committed = true; - return true; - } - - boolean abort() { - aborted = true; - return true; - } - - boolean revert() { - reverted = true; - return true; - } - - boolean fail() { - failed = true; - return true; - } - - public Item assertCommitted() { - assertTrue(toString() + " was not committed in\n" - + itemsToString(), - committed); - return this; - } - - public Item assertCommittedOrFailed() { - assertTrue(toString() + " was not committed nor failed in\n" - + itemsToString(), - committed || failed); - return this; - } - - public Item assertAborted() { - assertTrue(toString() + " was not aborted in\n" - + itemsToString(), - aborted); - return this; - } - - public Item assertReverted() { - assertTrue(toString() + " was not reverted in\n" - + itemsToString(), - reverted); - return this; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("Item{"); - sb.append(String.format("[%02d]", id)); - sb.append(", committed=").append(committed); - sb.append(", aborted=").append(aborted); - sb.append(", reverted=").append(reverted); - sb.append(", failed=").append(failed); - sb.append(", text=").append(text); - sb.append('}'); - return sb.toString(); - } - } - - /** - * Class which can count invocations and, if limit > 0, will raise - * an exception on the specific invocation of {@link #note(Object)} - * whose count == limit. - */ - private class BaseCounter { - private final AtomicInteger counter = new AtomicInteger(0); - private final int limit; - private final String name; - private Item item; - private final Optional> action; - - /** - * Base counter, tracks items. - * @param name name for string/exception/logs. - * @param limit limit at which an exception is raised, 0 == never - * @param action optional action to invoke after the increment, - * before limit check - */ - BaseCounter(String name, - int limit, - Function action) { - this.name = name; - this.limit = limit; - this.action = Optional.ofNullable(action); - } - - /** - * Apply the action to an item; log at info afterwards with both the - * before and after string values of the item. - * @param i item to process. - * @throws IOException failure in the action - */ - void process(Item i) throws IOException { - this.item = i; - int count = counter.incrementAndGet(); - if (limit == count) { - i.fail(); - LOG.info("{}: Failed {}", this, i); - throw new IOException(String.format("%s: Limit %d reached for %s", - this, limit, i)); - } - String before = i.toString(); - action.map(a -> a.apply(i)); - LOG.info("{}: {} -> {}", this, before, i); - } - - int getCount() { - return counter.get(); - } - - Item getItem() { - return item; - } - - void assertInvoked(String text, int expected) { - assertEquals(toString() + ": " + text, expected, getCount()); - } - - void assertInvokedAtLeast(String text, int expected) { - int actual = getCount(); - assertTrue(toString() + ": " + text - + "-expected " + expected - + " invocations, but got " + actual - + " in " + itemsToString(), - expected <= actual); - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder( - "BaseCounter{"); - sb.append("name='").append(name).append('\''); - sb.append(", count=").append(counter.get()); - sb.append(", limit=").append(limit); - sb.append(", item=").append(item); - sb.append('}'); - return sb.toString(); - } - } - - private final class CounterTask - extends BaseCounter implements Tasks.Task { - - private CounterTask(String name, int limit, - Function action) { - super(name, limit, action); - } - - @Override - public void run(Item item) throws IOException { - process(item); - } - - } - - private final class FailureCounter - extends BaseCounter implements Tasks.FailureTask { - private Exception exception; - - private FailureCounter(String name, int limit, - Function action) { - super(name, limit, action); - } - - @Override - public void run(Item item, Exception ex) throws IOException { - process(item); - this.exception = ex; - } - - private Exception getException() { - return exception; - } - - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestMagicCommitProtocol.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestMagicCommitProtocol.java index 5265163d83fc6..32cab03770c2d 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestMagicCommitProtocol.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestMagicCommitProtocol.java @@ -25,8 +25,6 @@ import org.assertj.core.api.Assertions; import org.junit.Test; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.LocatedFileStatus; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.contract.ContractTestUtils; @@ -34,7 +32,6 @@ import org.apache.hadoop.fs.s3a.commit.AbstractITCommitProtocol; import org.apache.hadoop.fs.s3a.commit.AbstractS3ACommitter; import org.apache.hadoop.fs.s3a.commit.CommitConstants; -import org.apache.hadoop.fs.s3a.commit.CommitOperations; import org.apache.hadoop.fs.s3a.commit.CommitUtils; import org.apache.hadoop.fs.s3a.commit.CommitterFaultInjection; import org.apache.hadoop.fs.s3a.commit.CommitterFaultInjectionImpl; @@ -45,7 +42,7 @@ import static org.apache.hadoop.fs.s3a.S3AUtils.listAndFilter; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.*; -import static org.hamcrest.CoreMatchers.containsString; +import static org.apache.hadoop.util.functional.RemoteIterators.toList; /** * Test the magic committer's commit protocol. @@ -99,8 +96,9 @@ public MagicS3GuardCommitter createFailingCommitter( protected void validateTaskAttemptPathDuringWrite(Path p, final long expectedLength) throws IOException { String pathStr = p.toString(); - assertTrue("not magic " + pathStr, - pathStr.contains(MAGIC)); + Assertions.assertThat(pathStr) + .describedAs("Magic path") + .contains(MAGIC); assertPathDoesNotExist("task attempt visible", p); } @@ -116,9 +114,9 @@ protected void validateTaskAttemptPathAfterWrite(Path marker, // if you list the parent dir and find the marker, it // is really 0 bytes long String name = marker.getName(); - List filtered = listAndFilter(fs, + List filtered = toList(listAndFilter(fs, marker.getParent(), false, - (path) -> path.getName().equals(name)); + (path) -> path.getName().equals(name))); Assertions.assertThat(filtered) .hasSize(1); Assertions.assertThat(filtered.get(0)) @@ -126,14 +124,7 @@ protected void validateTaskAttemptPathAfterWrite(Path marker, "Listing should return 0 byte length"); // marker file is empty - FileStatus st = fs.getFileStatus(marker); - assertEquals("file length in " + st, 0, st.getLen()); - // xattr header - Assertions.assertThat(CommitOperations.extractMagicFileLength(fs, - marker)) - .describedAs("XAttribute " + XA_MAGIC_MARKER) - .isNotEmpty() - .hasValue(expectedLength); + getTestHelper().assertIsMarkerFile(marker, expectedLength); } /** @@ -151,8 +142,8 @@ protected void validateTaskAttemptWorkingDirectory( assertEquals("Wrong schema for working dir " + wd + " with committer " + committer, "s3a", wd.getScheme()); - assertThat(wd.getPath(), - containsString('/' + CommitConstants.MAGIC + '/')); + Assertions.assertThat(wd.getPath()) + .contains('/' + CommitConstants.MAGIC + '/'); } /** diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestS3AHugeMagicCommits.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestS3AHugeMagicCommits.java index 3c15454e7edfb..aeea195aacf89 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestS3AHugeMagicCommits.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestS3AHugeMagicCommits.java @@ -20,8 +20,8 @@ import java.io.IOException; import java.util.List; -import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,10 +33,11 @@ import org.apache.hadoop.fs.s3a.Constants; import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.commit.CommitConstants; -import org.apache.hadoop.fs.s3a.commit.CommitOperations; import org.apache.hadoop.fs.s3a.commit.CommitUtils; import org.apache.hadoop.fs.s3a.commit.files.PendingSet; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; +import org.apache.hadoop.fs.s3a.commit.impl.CommitContext; +import org.apache.hadoop.fs.s3a.commit.impl.CommitOperations; import org.apache.hadoop.fs.s3a.scale.AbstractSTestS3AHugeFiles; import static org.apache.hadoop.fs.s3a.MultipartTestUtils.listMultipartUploads; @@ -54,6 +55,7 @@ public class ITestS3AHugeMagicCommits extends AbstractSTestS3AHugeFiles { private static final Logger LOG = LoggerFactory.getLogger( ITestS3AHugeMagicCommits.class); + private static final int COMMITTER_THREADS = 64; private Path magicDir; private Path jobDir; @@ -123,16 +125,16 @@ public void test_030_postCreationAssertions() throws Throwable { Path destDir = getHugefile().getParent(); assertPathExists("Magic dir", new Path(destDir, CommitConstants.MAGIC)); String destDirKey = fs.pathToKey(destDir); - List uploads = listMultipartUploads(fs, destDirKey); - assertEquals("Pending uploads: " - + uploads.stream() - .collect(Collectors.joining("\n")), 1, uploads.size()); + Assertions.assertThat(listMultipartUploads(fs, destDirKey)) + .describedAs("Pending uploads") + .hasSize(1); assertNotNull("jobDir", jobDir); - Pair>> - results = operations.loadSinglePendingCommits(jobDir, false); - try(CommitOperations.CommitContext commitContext - = operations.initiateCommitOperation(jobDir)) { + try(CommitContext commitContext + = operations.createCommitContextForTesting(jobDir, null, COMMITTER_THREADS)) { + Pair>> + results = operations.loadSinglePendingCommits(jobDir, false, commitContext + ); for (SinglePendingCommit singlePendingCommit : results.getKey().getCommits()) { commitContext.commitOrFail(singlePendingCommit); @@ -140,10 +142,9 @@ public void test_030_postCreationAssertions() throws Throwable { } timer.end("time to commit %s", pendingDataFile); // upload is no longer pending - uploads = listMultipartUploads(fs, destDirKey); - assertEquals("Pending uploads" - + uploads.stream().collect(Collectors.joining("\n")), - 0, operations.listPendingUploadsUnderPath(destDir).size()); + Assertions.assertThat(operations.listPendingUploadsUnderPath(destDir)) + .describedAs("Pending uploads undedr path") + .isEmpty(); // at this point, the huge file exists, so the normal assertions // on that file must be valid. Verify. super.test_030_postCreationAssertions(); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/MockedStagingCommitter.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/MockedStagingCommitter.java index d3da8185c8d65..beccf4f328c05 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/MockedStagingCommitter.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/MockedStagingCommitter.java @@ -26,6 +26,7 @@ import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.MockS3AFileSystem; +import org.apache.hadoop.fs.s3a.commit.files.SuccessData; import org.apache.hadoop.fs.s3a.commit.staging.StagingTestBase.ClientErrors; import org.apache.hadoop.fs.s3a.commit.staging.StagingTestBase.ClientResults; import org.apache.hadoop.fs.statistics.IOStatisticsSnapshot; @@ -76,11 +77,12 @@ public void commitJob(JobContext context) throws IOException { } @Override - protected void maybeCreateSuccessMarker(JobContext context, + protected SuccessData maybeCreateSuccessMarker(JobContext context, List filenames, final IOStatisticsSnapshot ioStatistics) throws IOException { //skipped + return null; } public ClientResults getResults() throws IOException { diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/StagingTestBase.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/StagingTestBase.java index 6e13fd0227a3b..6f2953762439a 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/StagingTestBase.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/StagingTestBase.java @@ -105,10 +105,15 @@ public class StagingTestBase { /** The raw bucket URI Path before any canonicalization. */ public static final URI RAW_BUCKET_URI = RAW_BUCKET_PATH.toUri(); - public static Path outputPath = + + @SuppressWarnings("StaticNonFinalField") + private static Path outputPath = new Path("s3a://" + BUCKET + "/" + OUTPUT_PREFIX); - public static URI outputPathUri = outputPath.toUri(); - public static Path root; + + @SuppressWarnings("StaticNonFinalField") + private static URI outputPathUri = getOutputPath().toUri(); + @SuppressWarnings("StaticNonFinalField") + private static Path root; protected StagingTestBase() { } @@ -131,8 +136,8 @@ protected static S3AFileSystem createAndBindMockFSInstance(Configuration conf, URI uri = RAW_BUCKET_URI; wrapperFS.initialize(uri, conf); root = wrapperFS.makeQualified(new Path("/")); - outputPath = new Path(root, OUTPUT_PREFIX); - outputPathUri = outputPath.toUri(); + outputPath = new Path(getRoot(), OUTPUT_PREFIX); + outputPathUri = getOutputPath().toUri(); FileSystemTestHelper.addFileSystemForTesting(uri, conf, wrapperFS); return mockFs; } @@ -154,7 +159,7 @@ private static S3AFileSystem mockS3AFileSystemRobustly() { */ public static MockS3AFileSystem lookupWrapperFS(Configuration conf) throws IOException { - return (MockS3AFileSystem) FileSystem.get(outputPathUri, conf); + return (MockS3AFileSystem) FileSystem.get(getOutputPathUri(), conf); } public static void verifyCompletion(FileSystem mockS3) throws IOException { @@ -169,13 +174,13 @@ public static void verifyDeleted(FileSystem mockS3, Path path) public static void verifyDeleted(FileSystem mockS3, String child) throws IOException { - verifyDeleted(mockS3, new Path(outputPath, child)); + verifyDeleted(mockS3, new Path(getOutputPath(), child)); } public static void verifyCleanupTempFiles(FileSystem mockS3) throws IOException { verifyDeleted(mockS3, - new Path(outputPath, CommitConstants.TEMPORARY)); + new Path(getOutputPath(), CommitConstants.TEMPORARY)); } protected static void assertConflictResolution( @@ -189,7 +194,7 @@ protected static void assertConflictResolution( public static void pathsExist(FileSystem mockS3, String... children) throws IOException { for (String child : children) { - pathExists(mockS3, new Path(outputPath, child)); + pathExists(mockS3, new Path(getOutputPath(), child)); } } @@ -231,7 +236,7 @@ public static void mkdirsHasOutcome(FileSystem mockS3, public static void canDelete(FileSystem mockS3, String... children) throws IOException { for (String child : children) { - canDelete(mockS3, new Path(outputPath, child)); + canDelete(mockS3, new Path(getOutputPath(), child)); } } @@ -243,7 +248,7 @@ public static void canDelete(FileSystem mockS3, Path f) throws IOException { public static void verifyExistenceChecked(FileSystem mockS3, String child) throws IOException { - verifyExistenceChecked(mockS3, new Path(outputPath, child)); + verifyExistenceChecked(mockS3, new Path(getOutputPath(), child)); } public static void verifyExistenceChecked(FileSystem mockS3, Path path) @@ -262,6 +267,18 @@ public static void verifyMkdirsInvoked(FileSystem mockS3, Path path) verify(mockS3).mkdirs(path); } + protected static URI getOutputPathUri() { + return outputPathUri; + } + + static Path getRoot() { + return root; + } + + static Path getOutputPath() { + return outputPath; + } + /** * Provides setup/teardown of a MiniDFSCluster for tests that need one. */ diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestDirectoryCommitterScale.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestDirectoryCommitterScale.java index cb7202b10d133..4d24c07dacfe2 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestDirectoryCommitterScale.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestDirectoryCommitterScale.java @@ -28,6 +28,7 @@ import java.util.stream.IntStream; import com.amazonaws.services.s3.model.PartETag; + import org.apache.hadoop.thirdparty.com.google.common.collect.Maps; import org.assertj.core.api.Assertions; import org.junit.AfterClass; @@ -48,18 +49,21 @@ import org.apache.hadoop.fs.s3a.commit.CommitConstants; import org.apache.hadoop.fs.s3a.commit.files.PendingSet; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; +import org.apache.hadoop.fs.s3a.commit.impl.CommitContext; +import org.apache.hadoop.fs.s3a.commit.impl.CommitOperations; import org.apache.hadoop.mapred.JobConf; import org.apache.hadoop.mapreduce.JobContext; import org.apache.hadoop.mapreduce.JobStatus; import org.apache.hadoop.mapreduce.TaskAttemptContext; import org.apache.hadoop.util.DurationInfo; +import org.apache.hadoop.util.JsonSerialization; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.CONFLICT_MODE_APPEND; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.FS_S3A_COMMITTER_STAGING_CONFLICT_MODE; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.PENDINGSET_SUFFIX; import static org.apache.hadoop.fs.s3a.commit.staging.StagingTestBase.BUCKET; -import static org.apache.hadoop.fs.s3a.commit.staging.StagingTestBase.outputPath; -import static org.apache.hadoop.fs.s3a.commit.staging.StagingTestBase.outputPathUri; +import static org.apache.hadoop.fs.s3a.commit.staging.StagingTestBase.getOutputPath; +import static org.apache.hadoop.fs.s3a.commit.staging.StagingTestBase.getOutputPathUri; import static org.apache.hadoop.fs.s3a.commit.staging.StagingTestBase.pathIsDirectory; /** @@ -83,6 +87,7 @@ public class TestDirectoryCommitterScale public static final int TOTAL_COMMIT_COUNT = FILES_PER_TASK * TASKS; public static final int BLOCKS_PER_TASK = 1000; + private static final int COMMITTER_THREAD_COUNT = 100; private static File stagingDir; @@ -95,13 +100,13 @@ public class TestDirectoryCommitterScale @Override DirectoryCommitterForTesting newJobCommitter() throws Exception { - return new DirectoryCommitterForTesting(outputPath, + return new DirectoryCommitterForTesting(getOutputPath(), createTaskAttemptForJob()); } @BeforeClass public static void setupStaging() throws Exception { - stagingDir = File.createTempFile("staging", ""); + stagingDir = File.createTempFile("staging", null); stagingDir.delete(); stagingDir.mkdir(); stagingPath = new Path(stagingDir.toURI()); @@ -125,7 +130,7 @@ protected JobConf createJobConf() { JobConf conf = super.createJobConf(); conf.setInt( CommitConstants.FS_S3A_COMMITTER_THREADS, - 100); + COMMITTER_THREAD_COUNT); return conf; } @@ -149,7 +154,8 @@ public void test_010_createTaskFiles() throws Exception { */ private void createTasks() throws IOException { // create a stub multipart commit containing multiple files. - + JsonSerialization serializer = + SinglePendingCommit.serializer(); // step1: a list of tags. // this is the md5sum of hadoop 3.2.1.tar String tag = "9062dcf18ffaee254821303bbd11c72b"; @@ -164,12 +170,14 @@ private void createTasks() throws IOException { // these get overwritten base.setDestinationKey("/base"); base.setUploadId("uploadId"); - base.setUri(outputPathUri.toString()); + base.setUri(getOutputPathUri().toString()); + byte[] bytes = base.toBytes(serializer); SinglePendingCommit[] singles = new SinglePendingCommit[FILES_PER_TASK]; - byte[] bytes = base.toBytes(); + for (int i = 0; i < FILES_PER_TASK; i++) { - singles[i] = SinglePendingCommit.serializer().fromBytes(bytes); + + singles[i] = serializer.fromBytes(bytes); } // now create the files, using this as the template @@ -182,7 +190,7 @@ private void createTasks() throws IOException { String uploadId = String.format("%05d-task-%04d-file-%02d", uploadCount, task, i); // longer paths to take up more space. - Path p = new Path(outputPath, + Path p = new Path(getOutputPath(), "datasets/examples/testdirectoryscale/" + "year=2019/month=09/day=26/hour=20/second=53" + uploadId); @@ -199,7 +207,7 @@ private void createTasks() throws IOException { } Path path = new Path(stagingPath, String.format("task-%04d." + PENDINGSET_SUFFIX, task)); - pending.save(localFS, path, true); + pending.save(localFS, path, PendingSet.serializer()); } } @@ -211,12 +219,14 @@ public void test_020_loadFilesToAttempt() throws Exception { Configuration jobConf = getJobConf(); jobConf.set( FS_S3A_COMMITTER_STAGING_CONFLICT_MODE, CONFLICT_MODE_APPEND); - FileSystem mockS3 = getMockS3A(); - pathIsDirectory(mockS3, outputPath); - try (DurationInfo ignored = - new DurationInfo(LOG, "listing pending uploads")) { + S3AFileSystem mockS3 = getMockS3A(); + pathIsDirectory(mockS3, getOutputPath()); + final CommitOperations operations = new CommitOperations(getWrapperFS()); + try (CommitContext commitContext + = operations.createCommitContextForTesting(getOutputPath(), + null, COMMITTER_THREAD_COUNT)) { AbstractS3ACommitter.ActiveCommit activeCommit - = committer.listPendingUploadsToCommit(getJob()); + = committer.listPendingUploadsToCommit(commitContext); Assertions.assertThat(activeCommit.getSourceFiles()) .describedAs("Source files of %s", activeCommit) .hasSize(TASKS); @@ -232,7 +242,7 @@ public void test_030_commitFiles() throws Exception { jobConf.set( FS_S3A_COMMITTER_STAGING_CONFLICT_MODE, CONFLICT_MODE_APPEND); S3AFileSystem mockS3 = getMockS3A(); - pathIsDirectory(mockS3, outputPath); + pathIsDirectory(mockS3, getOutputPath()); try (DurationInfo ignored = new DurationInfo(LOG, "Committing Job")) { @@ -261,7 +271,7 @@ public void test_040_abortFiles() throws Exception { jobConf.set( FS_S3A_COMMITTER_STAGING_CONFLICT_MODE, CONFLICT_MODE_APPEND); FileSystem mockS3 = getMockS3A(); - pathIsDirectory(mockS3, outputPath); + pathIsDirectory(mockS3, getOutputPath()); committer.abortJob(getJob(), JobStatus.State.FAILED); } @@ -304,11 +314,11 @@ public Path getJobAttemptPath(JobContext context) { } @Override - protected void commitJobInternal(final JobContext context, + protected void commitJobInternal(final CommitContext commitContext, final ActiveCommit pending) throws IOException { activeCommit = pending; - super.commitJobInternal(context, pending); + super.commitJobInternal(commitContext, pending); } } } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingCommitter.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingCommitter.java index 94c0b29a3cdcc..11edf0d216376 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingCommitter.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingCommitter.java @@ -37,7 +37,6 @@ import org.apache.hadoop.util.Sets; import org.assertj.core.api.Assertions; -import org.hamcrest.core.StringStartsWith; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -58,6 +57,7 @@ import org.apache.hadoop.fs.s3a.commit.AbstractS3ACommitter; import org.apache.hadoop.fs.s3a.commit.PathCommitException; import org.apache.hadoop.fs.s3a.commit.files.PendingSet; +import org.apache.hadoop.fs.s3a.commit.files.PersistentCommitData; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; import org.apache.hadoop.mapred.JobConf; import org.apache.hadoop.mapreduce.JobContext; @@ -168,7 +168,7 @@ public void setupCommitter() throws Exception { this.tac = new TaskAttemptContextImpl( new Configuration(job.getConfiguration()), AID); - this.jobCommitter = new MockedStagingCommitter(outputPath, tac); + this.jobCommitter = new MockedStagingCommitter(getOutputPath(), tac); jobCommitter.setupJob(job); // get the task's configuration copy so modifications take effect @@ -183,7 +183,7 @@ public void setupCommitter() throws Exception { this.conf.set(BUFFER_DIR, String.format("%s/local-0/, %s/local-1 ", tmp, tmp)); - this.committer = new MockedStagingCommitter(outputPath, tac); + this.committer = new MockedStagingCommitter(getOutputPath(), tac); Paths.resetTempFolderCache(); } @@ -335,10 +335,11 @@ public void testAttemptPathConstructionWithSchema() throws Exception { config.set(BUFFER_DIR, "file:/tmp/mr-local-0,file:/tmp/mr-local-1"); - assertThat("Path should be the same with file scheme", + Assertions.assertThat( getLocalTaskAttemptTempDir(config, - jobUUID, tac.getTaskAttemptID()).toString(), - StringStartsWith.startsWith(commonPath)); + jobUUID, tac.getTaskAttemptID()).toString()) + .describedAs("Path should be the same with file scheme") + .startsWith(commonPath); } @Test @@ -379,7 +380,7 @@ public void testSingleTaskCommit() throws Exception { assertEquals("Should name the commits file with the task ID: " + results, "task_job_0001_r_000002", stats[0].getPath().getName()); - PendingSet pending = PendingSet.load(dfs, stats[0]); + PendingSet pending = PersistentCommitData.load(dfs, stats[0], PendingSet.serializer()); assertEquals("Should have one pending commit", 1, pending.size()); SinglePendingCommit commit = pending.getCommits().get(0); assertEquals("Should write to the correct bucket:" + results, @@ -419,7 +420,7 @@ public void testSingleTaskEmptyFileCommit() throws Exception { assertEquals("Should name the commits file with the task ID", "task_job_0001_r_000002", stats[0].getPath().getName()); - PendingSet pending = PendingSet.load(dfs, stats[0]); + PendingSet pending = PersistentCommitData.load(dfs, stats[0], PendingSet.serializer()); assertEquals("Should have one pending commit", 1, pending.size()); } @@ -442,7 +443,7 @@ public void testSingleTaskMultiFileCommit() throws Exception { "task_job_0001_r_000002", stats[0].getPath().getName()); List pending = - PendingSet.load(dfs, stats[0]).getCommits(); + PersistentCommitData.load(dfs, stats[0], PendingSet.serializer()).getCommits(); assertEquals("Should have correct number of pending commits", files.size(), pending.size()); @@ -717,7 +718,7 @@ private Set runTasks(JobContext jobContext, TaskAttemptContext attempt = new TaskAttemptContextImpl( new Configuration(jobContext.getConfiguration()), attemptID); MockedStagingCommitter taskCommitter = new MockedStagingCommitter( - outputPath, attempt); + getOutputPath(), attempt); commitTask(taskCommitter, attempt, numFiles); } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingDirectoryOutputCommitter.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingDirectoryOutputCommitter.java index 98075b827a7c2..b92605ca25357 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingDirectoryOutputCommitter.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingDirectoryOutputCommitter.java @@ -29,8 +29,11 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.PathExistsException; +import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.commit.AbstractS3ACommitter; import org.apache.hadoop.fs.s3a.commit.InternalCommitterConstants; +import org.apache.hadoop.fs.s3a.commit.impl.CommitContext; +import org.apache.hadoop.fs.s3a.commit.impl.CommitOperations; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.*; import static org.apache.hadoop.fs.s3a.commit.staging.StagingTestBase.*; @@ -46,7 +49,7 @@ public class TestStagingDirectoryOutputCommitter @Override DirectoryStagingCommitter newJobCommitter() throws Exception { - return new DirectoryStagingCommitter(outputPath, + return new DirectoryStagingCommitter(getOutputPath(), createTaskAttemptForJob()); } @@ -63,7 +66,7 @@ public void testBadConflictMode() throws Throwable { public void testDefaultConflictResolution() throws Exception { getJob().getConfiguration().unset( FS_S3A_COMMITTER_STAGING_CONFLICT_MODE); - pathIsDirectory(getMockS3A(), outputPath); + pathIsDirectory(getMockS3A(), getOutputPath()); verifyJobSetupAndCommit(); } @@ -75,7 +78,8 @@ public void testFailConflictResolution() throws Exception { } protected void verifyFailureConflictOutcome() throws Exception { - pathIsDirectory(getMockS3A(), outputPath); + final S3AFileSystem mockFS = getMockS3A(); + pathIsDirectory(mockFS, getOutputPath()); final DirectoryStagingCommitter committer = newJobCommitter(); // this should fail @@ -86,20 +90,23 @@ protected void verifyFailureConflictOutcome() throws Exception { // but there are no checks in job commit (HADOOP-15469) // this is done by calling the preCommit method directly, - committer.preCommitJob(getJob(), AbstractS3ACommitter.ActiveCommit.empty()); - reset(getMockS3A()); - pathDoesNotExist(getMockS3A(), outputPath); + final CommitContext commitContext = new CommitOperations(getWrapperFS()). + createCommitContext(getJob(), getOutputPath(), 0); + committer.preCommitJob(commitContext, AbstractS3ACommitter.ActiveCommit.empty()); + + reset(mockFS); + pathDoesNotExist(mockFS, getOutputPath()); committer.setupJob(getJob()); - verifyExistenceChecked(getMockS3A(), outputPath); - verifyMkdirsInvoked(getMockS3A(), outputPath); - verifyNoMoreInteractions(getMockS3A()); + verifyExistenceChecked(mockFS, getOutputPath()); + verifyMkdirsInvoked(mockFS, getOutputPath()); + verifyNoMoreInteractions(mockFS); - reset(getMockS3A()); - pathDoesNotExist(getMockS3A(), outputPath); + reset(mockFS); + pathDoesNotExist(mockFS, getOutputPath()); committer.commitJob(getJob()); - verifyCompletion(getMockS3A()); + verifyCompletion(mockFS); } @Test @@ -108,7 +115,7 @@ public void testAppendConflictResolution() throws Exception { getJob().getConfiguration().set( FS_S3A_COMMITTER_STAGING_CONFLICT_MODE, CONFLICT_MODE_APPEND); FileSystem mockS3 = getMockS3A(); - pathIsDirectory(mockS3, outputPath); + pathIsDirectory(mockS3, getOutputPath()); verifyJobSetupAndCommit(); } @@ -120,7 +127,7 @@ protected void verifyJobSetupAndCommit() FileSystem mockS3 = getMockS3A(); Mockito.reset(mockS3); - pathExists(mockS3, outputPath); + pathExists(mockS3, getOutputPath()); committer.commitJob(getJob()); verifyCompletion(mockS3); @@ -130,7 +137,7 @@ protected void verifyJobSetupAndCommit() public void testReplaceConflictResolution() throws Exception { FileSystem mockS3 = getMockS3A(); - pathIsDirectory(mockS3, outputPath); + pathIsDirectory(mockS3, getOutputPath()); getJob().getConfiguration().set( FS_S3A_COMMITTER_STAGING_CONFLICT_MODE, CONFLICT_MODE_REPLACE); @@ -140,17 +147,17 @@ public void testReplaceConflictResolution() throws Exception { committer.setupJob(getJob()); Mockito.reset(mockS3); - pathExists(mockS3, outputPath); - canDelete(mockS3, outputPath); + pathExists(mockS3, getOutputPath()); + canDelete(mockS3, getOutputPath()); committer.commitJob(getJob()); - verifyDeleted(mockS3, outputPath); + verifyDeleted(mockS3, getOutputPath()); verifyCompletion(mockS3); } @Test public void testReplaceConflictFailsIfDestIsFile() throws Exception { - pathIsFile(getMockS3A(), outputPath); + pathIsFile(getMockS3A(), getOutputPath()); getJob().getConfiguration().set( FS_S3A_COMMITTER_STAGING_CONFLICT_MODE, CONFLICT_MODE_REPLACE); @@ -166,7 +173,7 @@ public void testReplaceConflictFailsIfDestIsFile() throws Exception { @Test public void testAppendConflictFailsIfDestIsFile() throws Exception { - pathIsFile(getMockS3A(), outputPath); + pathIsFile(getMockS3A(), getOutputPath()); getJob().getConfiguration().set( FS_S3A_COMMITTER_STAGING_CONFLICT_MODE, CONFLICT_MODE_APPEND); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedFileListing.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedFileListing.java index 76a0de225371e..64a9be0888ffa 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedFileListing.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedFileListing.java @@ -51,13 +51,13 @@ public class TestStagingPartitionedFileListing @Override PartitionedStagingCommitter newJobCommitter() throws IOException { - return new PartitionedStagingCommitter(outputPath, + return new PartitionedStagingCommitter(getOutputPath(), createTaskAttemptForJob()); } @Override PartitionedStagingCommitter newTaskCommitter() throws IOException { - return new PartitionedStagingCommitter(outputPath, getTAC()); + return new PartitionedStagingCommitter(getOutputPath(), getTAC()); } private FileSystem attemptFS; diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedJobCommit.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedJobCommit.java index 86b677c70a305..28161979f0b79 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedJobCommit.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedJobCommit.java @@ -34,7 +34,7 @@ import org.apache.hadoop.fs.s3a.commit.PathCommitException; import org.apache.hadoop.fs.s3a.commit.files.PendingSet; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; -import org.apache.hadoop.mapreduce.JobContext; +import org.apache.hadoop.fs.s3a.commit.impl.CommitContext; import org.apache.hadoop.mapreduce.TaskAttemptContext; import static org.apache.hadoop.test.LambdaTestUtils.intercept; @@ -67,7 +67,7 @@ private final class PartitionedStagingCommitterForTesting private PartitionedStagingCommitterForTesting(TaskAttemptContext context) throws IOException { - super(StagingTestBase.outputPath, context); + super(StagingTestBase.getOutputPath(), context); } /** @@ -75,14 +75,15 @@ private PartitionedStagingCommitterForTesting(TaskAttemptContext context) * This is quite complex as the mock pending uploads need to be saved * to a filesystem for the next stage of the commit process. * To simulate multiple commit, more than one .pendingset file is created, - * @param context job context + * @param commitContext job context * @return an active commit containing a list of paths to valid pending set * file. * @throws IOException IO failure */ + @SuppressWarnings("CollectionDeclaredAsConcreteClass") @Override protected ActiveCommit listPendingUploadsToCommit( - JobContext context) throws IOException { + CommitContext commitContext) throws IOException { LocalFileSystem localFS = FileSystem.getLocal(getConf()); ActiveCommit activeCommit = new ActiveCommit(localFS, @@ -109,17 +110,17 @@ protected ActiveCommit listPendingUploadsToCommit( File file = File.createTempFile("staging", ".pendingset"); file.deleteOnExit(); Path path = new Path(file.toURI()); - pendingSet.save(localFS, path, true); + pendingSet.save(localFS, path, PendingSet.serializer()); activeCommit.add(localFS.getFileStatus(path)); } return activeCommit; } @Override - protected void abortJobInternal(JobContext context, + protected void abortJobInternal(CommitContext commitContext, boolean suppressExceptions) throws IOException { this.aborted = true; - super.abortJobInternal(context, suppressExceptions); + super.abortJobInternal(commitContext, suppressExceptions); } } @@ -242,7 +243,7 @@ public void testReplaceWithDeleteFailure() throws Exception { pathsExist(mockS3, "dateint=20161116/hour=14"); when(mockS3 .delete( - new Path(outputPath, "dateint=20161116/hour=14"), + new Path(getOutputPath(), "dateint=20161116/hour=14"), true)) .thenThrow(new PathCommitException("fake", "Fake IOException for delete")); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedTaskCommit.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedTaskCommit.java index fb252102491d6..4e82b94314d34 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedTaskCommit.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/TestStagingPartitionedTaskCommit.java @@ -47,13 +47,13 @@ public class TestStagingPartitionedTaskCommit @Override PartitionedStagingCommitter newJobCommitter() throws IOException { - return new PartitionedStagingCommitter(outputPath, + return new PartitionedStagingCommitter(getOutputPath(), createTaskAttemptForJob()); } @Override PartitionedStagingCommitter newTaskCommitter() throws Exception { - return new PartitionedStagingCommitter(outputPath, getTAC()); + return new PartitionedStagingCommitter(getOutputPath(), getTAC()); } // The set of files used by this test @@ -104,7 +104,7 @@ public void testFail() throws Exception { // test failure when one partition already exists reset(mockS3); - Path existsPath = new Path(outputPath, relativeFiles.get(1)).getParent(); + Path existsPath = new Path(getOutputPath(), relativeFiles.get(1)).getParent(); pathExists(mockS3, existsPath); intercept(PathExistsException.class, "", @@ -133,7 +133,7 @@ public void testAppend() throws Exception { // test success when one partition already exists reset(mockS3); - pathExists(mockS3, new Path(outputPath, relativeFiles.get(2)).getParent()); + pathExists(mockS3, new Path(getOutputPath(), relativeFiles.get(2)).getParent()); committer.commitTask(getTAC()); verifyFilesCreated(committer); @@ -178,7 +178,7 @@ public void testReplace() throws Exception { // test success when one partition already exists reset(mockS3); - pathExists(mockS3, new Path(outputPath, relativeFiles.get(3)).getParent()); + pathExists(mockS3, new Path(getOutputPath(), relativeFiles.get(3)).getParent()); committer.commitTask(getTAC()); verifyFilesCreated(committer); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/integration/ITestStagingCommitProtocol.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/integration/ITestStagingCommitProtocol.java index 3a820bcc11e21..bb3031b32c1a8 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/integration/ITestStagingCommitProtocol.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/integration/ITestStagingCommitProtocol.java @@ -52,7 +52,7 @@ protected String suitename() { @Override protected Configuration createConfiguration() { Configuration conf = super.createConfiguration(); - conf.setInt(FS_S3A_COMMITTER_THREADS, 1); + conf.setInt(FS_S3A_COMMITTER_THREADS, 4); // disable unique filenames so that the protocol tests of FileOutputFormat // and this test generate consistent names. diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/terasort/ITestTerasortOnS3A.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/terasort/ITestTerasortOnS3A.java index 991969b0f05ce..d28ee5172b632 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/terasort/ITestTerasortOnS3A.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/terasort/ITestTerasortOnS3A.java @@ -330,7 +330,7 @@ public void test_140_teracomplete() throws Throwable { stage.accept("teravalidate"); stage.accept("overall"); String text = results.toString(); - File resultsFile = File.createTempFile("results", ".csv"); + File resultsFile = new File(getReportDir(), committerName + ".csv"); FileUtils.write(resultsFile, text, StandardCharsets.UTF_8); LOG.info("Results are in {}\n{}", resultsFile, text); } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/StubContextAccessor.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/StubContextAccessor.java new file mode 100644 index 0000000000000..41180667e1000 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/StubContextAccessor.java @@ -0,0 +1,82 @@ +/* + * 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.s3a.impl; + +import java.io.File; +import java.io.IOException; + +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.s3a.MockS3AFileSystem; +import org.apache.hadoop.fs.s3a.api.RequestFactory; +import org.apache.hadoop.fs.s3a.audit.AuditTestSupport; +import org.apache.hadoop.fs.store.audit.AuditSpan; + +/** + * A Stub context acccessor for test. + */ +public final class StubContextAccessor + implements ContextAccessors { + + private final String bucket; + + /** + * Construct. + * @param bucket bucket to use when qualifying keys.]= + */ + public StubContextAccessor(String bucket) { + this.bucket = bucket; + } + + @Override + public Path keyToPath(final String key) { + return new Path("s3a://" + bucket + "/" + key); + } + + @Override + public String pathToKey(final Path path) { + return null; + } + + @Override + public File createTempFile(final String prefix, final long size) + throws IOException { + throw new UnsupportedOperationException("unsppported"); + } + + @Override + public String getBucketLocation() throws IOException { + return null; + } + + @Override + public Path makeQualified(final Path path) { + return path; + } + + @Override + public AuditSpan getActiveAuditSpan() { + return AuditTestSupport.NOOP_SPAN; + } + + @Override + public RequestFactory getRequestFactory() { + return MockS3AFileSystem.REQUEST_FACTORY; + } + +} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestCreateFileBuilder.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestCreateFileBuilder.java new file mode 100644 index 0000000000000..65d7aa6192dd8 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestCreateFileBuilder.java @@ -0,0 +1,173 @@ +/* + * 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.s3a.impl; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CreateFlag; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FSDataOutputStreamBuilder; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.test.HadoopTestBase; +import org.apache.hadoop.util.Progressable; + +import static org.apache.hadoop.fs.s3a.Constants.FS_S3A_CREATE_HEADER; +import static org.apache.hadoop.fs.s3a.Constants.FS_S3A_CREATE_PERFORMANCE; +import static org.apache.hadoop.test.LambdaTestUtils.intercept; + +/** + * Unit test of {@link CreateFileBuilder}. + */ +public class TestCreateFileBuilder extends HadoopTestBase { + + private static final BuilderCallbacks CALLBACKS = new BuilderCallbacks(); + + private CreateFileBuilder mkBuilder() throws IOException { + return new CreateFileBuilder( + FileSystem.getLocal(new Configuration()), + new Path("/"), + CALLBACKS); + } + + private BuilderOutputStream unwrap(FSDataOutputStream out) { + OutputStream s = out.getWrappedStream(); + Assertions.assertThat(s) + .isInstanceOf(BuilderOutputStream.class); + return (BuilderOutputStream) s; + } + + private BuilderOutputStream build(FSDataOutputStreamBuilder builder) + throws IOException { + return unwrap(builder.build()); + } + + @Test + public void testSimpleBuild() throws Throwable { + Assertions.assertThat(build(mkBuilder().create())) + .matches(p -> !p.isOverwrite()) + .matches(p -> !p.isPerformance()); + } + + @Test + public void testAppendForbidden() throws Throwable { + intercept(UnsupportedOperationException.class, () -> + build(mkBuilder().append())); + } + + @Test + public void testPerformanceSupport() throws Throwable { + CreateFileBuilder builder = mkBuilder().create(); + builder.must(FS_S3A_CREATE_PERFORMANCE, true); + Assertions.assertThat(build(builder)) + .matches(p -> p.isPerformance()); + } + + @Test + public void testHeaderOptions() throws Throwable { + final CreateFileBuilder builder = mkBuilder().create() + .must(FS_S3A_CREATE_HEADER + ".retention", "permanent") + .opt(FS_S3A_CREATE_HEADER + ".owner", "engineering"); + final Map headers = build(builder).getHeaders(); + Assertions.assertThat(headers) + .containsEntry("retention", "permanent") + .containsEntry("owner", "engineering"); + } + + @Test + public void testIncompleteHeader() throws Throwable { + final CreateFileBuilder builder = mkBuilder().create() + .must(FS_S3A_CREATE_HEADER, "permanent"); + intercept(IllegalArgumentException.class, () -> + build(builder)); + } + + private static final class BuilderCallbacks implements + CreateFileBuilder.CreateFileBuilderCallbacks { + + @Override + public FSDataOutputStream createFileFromBuilder(final Path path, + final Progressable progress, + final CreateFileBuilder.CreateFileOptions options) throws IOException { + return new FSDataOutputStream( + new BuilderOutputStream( + progress, + options), + null); + } + } + + /** + * Stream which will be wrapped and which returns the flags used + * creating the object. + */ + private static final class BuilderOutputStream extends OutputStream { + + private final Progressable progress; + + + private final CreateFileBuilder.CreateFileOptions options; + + private BuilderOutputStream(final Progressable progress, + final CreateFileBuilder.CreateFileOptions options) { + this.progress = progress; + this.options = options; + } + + private boolean isOverwrite() { + return options.getFlags().contains(CreateFlag.OVERWRITE); + } + + private Progressable getProgress() { + return progress; + } + + private boolean isPerformance() { + return options.isPerformance(); + } + + private CreateFileBuilder.CreateFileOptions getOptions() { + return options; + } + + private Map getHeaders() { + return options.getHeaders(); + } + + @Override + public void write(final int b) throws IOException { + + } + + @Override + public String toString() { + return "BuilderOutputStream{" + + "progress=" + progress + + ", options=" + options + + "} " + super.toString(); + } + } + +} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestRequestFactory.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestRequestFactory.java index 9bc3aef83aacb..5c243bb820f02 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestRequestFactory.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestRequestFactory.java @@ -90,7 +90,7 @@ public void testRequestFactoryWithCannedACL() throws Throwable { ObjectMetadata md = factory.newObjectMetadata(128); Assertions.assertThat( factory.newPutObjectRequest(path, md, - new ByteArrayInputStream(new byte[0])) + null, new ByteArrayInputStream(new byte[0])) .getCannedAcl()) .describedAs("ACL of PUT") .isEqualTo(acl); @@ -98,7 +98,8 @@ public void testRequestFactoryWithCannedACL() throws Throwable { .getCannedAccessControlList()) .describedAs("ACL of COPY") .isEqualTo(acl); - Assertions.assertThat(factory.newMultipartUploadRequest(path) + Assertions.assertThat(factory.newMultipartUploadRequest(path, + null) .getCannedACL()) .describedAs("ACL of MPU") .isEqualTo(acl); @@ -172,12 +173,12 @@ private void createFactoryObjects(RequestFactory factory) { a(factory.newListObjectsV1Request(path, "/", 1)); a(factory.newListNextBatchOfObjectsRequest(new ObjectListing())); a(factory.newListObjectsV2Request(path, "/", 1)); - a(factory.newMultipartUploadRequest(path)); + a(factory.newMultipartUploadRequest(path, null)); File srcfile = new File("/tmp/a"); a(factory.newPutObjectRequest(path, - factory.newObjectMetadata(-1), srcfile)); + factory.newObjectMetadata(-1), null, srcfile)); ByteArrayInputStream stream = new ByteArrayInputStream(new byte[0]); - a(factory.newPutObjectRequest(path, md, stream)); + a(factory.newPutObjectRequest(path, md, null, stream)); a(factory.newSelectRequest(path)); } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/AbstractS3ACostTest.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/AbstractS3ACostTest.java index 3511020aa6cef..8bbf52b578e1a 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/AbstractS3ACostTest.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/AbstractS3ACostTest.java @@ -132,16 +132,7 @@ public void setup() throws Exception { .isEqualTo(isKeepingMarkers() ? DirectoryPolicy.MarkerPolicy.Keep : DirectoryPolicy.MarkerPolicy.Delete); - // All counter statistics of the filesystem are added as metrics. - // Durations too, as they have counters of success and failure. - OperationCostValidator.Builder builder = OperationCostValidator.builder( - getFileSystem()); - EnumSet.allOf(Statistic.class).stream() - .filter(s -> - s.getType() == StatisticTypeEnum.TYPE_COUNTER - || s.getType() == StatisticTypeEnum.TYPE_DURATION) - .forEach(s -> builder.withMetric(s)); - costValidator = builder.build(); + setupCostValidator(); // determine bulk delete settings final Configuration fsConf = getFileSystem().getConf(); @@ -154,6 +145,19 @@ public void setup() throws Exception { setSpanSource(fs); } + protected void setupCostValidator() { + // All counter statistics of the filesystem are added as metrics. + // Durations too, as they have counters of success and failure. + OperationCostValidator.Builder builder = OperationCostValidator.builder( + getFileSystem()); + EnumSet.allOf(Statistic.class).stream() + .filter(s -> + s.getType() == StatisticTypeEnum.TYPE_COUNTER + || s.getType() == StatisticTypeEnum.TYPE_DURATION) + .forEach(s -> builder.withMetric(s)); + costValidator = builder.build(); + } + public boolean isDeleting() { return isDeleting; } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestCreateFileCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestCreateFileCost.java new file mode 100644 index 0000000000000..39530d97bf794 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestCreateFileCost.java @@ -0,0 +1,248 @@ +/* + * 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.s3a.performance; + +import java.io.IOException; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FSDataOutputStreamBuilder; +import org.apache.hadoop.fs.FileAlreadyExistsException; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.contract.ContractTestUtils; +import org.apache.hadoop.fs.s3a.S3AFileSystem; + + +import static java.util.Objects.requireNonNull; +import static org.apache.hadoop.fs.contract.ContractTestUtils.toChar; +import static org.apache.hadoop.fs.s3a.Constants.FS_S3A_CREATE_HEADER; +import static org.apache.hadoop.fs.s3a.Constants.FS_S3A_CREATE_PERFORMANCE; +import static org.apache.hadoop.fs.s3a.Constants.XA_HEADER_PREFIX; +import static org.apache.hadoop.fs.s3a.Statistic.OBJECT_BULK_DELETE_REQUEST; +import static org.apache.hadoop.fs.s3a.Statistic.OBJECT_DELETE_REQUEST; +import static org.apache.hadoop.fs.s3a.performance.OperationCost.CREATE_FILE_NO_OVERWRITE; +import static org.apache.hadoop.fs.s3a.performance.OperationCost.CREATE_FILE_OVERWRITE; +import static org.apache.hadoop.fs.s3a.performance.OperationCost.FILE_STATUS_DIR_PROBE; +import static org.apache.hadoop.fs.s3a.performance.OperationCost.FILE_STATUS_FILE_PROBE; +import static org.apache.hadoop.fs.s3a.performance.OperationCost.GET_FILE_STATUS_ON_DIR_MARKER; +import static org.apache.hadoop.fs.s3a.performance.OperationCost.GET_FILE_STATUS_ON_FILE; +import static org.apache.hadoop.fs.s3a.performance.OperationCost.HEAD_OPERATION; +import static org.apache.hadoop.fs.s3a.performance.OperationCost.NO_HEAD_OR_LIST; + +/** + * Assert cost of createFile operations, especially + * with the FS_S3A_CREATE_PERFORMANCE option. + */ +@SuppressWarnings("resource") +public class ITestCreateFileCost extends AbstractS3ACostTest { + + /** + * Create with markers kept, always. + */ + public ITestCreateFileCost() { + super(false); + } + + @Test + public void testCreateNoOverwrite() throws Throwable { + describe("Test file creation without overwrite"); + Path testFile = methodPath(); + // when overwrite is false, the path is checked for existence. + create(testFile, false, + CREATE_FILE_NO_OVERWRITE); + } + + @Test + public void testCreateOverwrite() throws Throwable { + describe("Test file creation with overwrite"); + Path testFile = methodPath(); + // when overwrite is true: only the directory checks take place. + create(testFile, true, CREATE_FILE_OVERWRITE); + } + + @Test + public void testCreateNoOverwriteFileExists() throws Throwable { + describe("Test cost of create file failing with existing file"); + Path testFile = file(methodPath()); + + // now there is a file there, an attempt with overwrite == false will + // fail on the first HEAD. + interceptOperation(FileAlreadyExistsException.class, "", + FILE_STATUS_FILE_PROBE, + () -> file(testFile, false)); + } + + @Test + public void testCreateFileOverDir() throws Throwable { + describe("Test cost of create file failing with existing dir"); + Path testFile = dir(methodPath()); + + // now there is a file there, an attempt with overwrite == false will + // fail on the first HEAD. + interceptOperation(FileAlreadyExistsException.class, "", + GET_FILE_STATUS_ON_DIR_MARKER, + () -> file(testFile, false)); + } + + /** + * Use the builder API. + * on s3a this skips parent checks, always. + */ + @Test + public void testCreateBuilderSequence() throws Throwable { + describe("Test builder file creation cost"); + Path testFile = methodPath(); + dir(testFile.getParent()); + + // s3a fs skips the recursive checks to avoid race + // conditions with other processes/threads deleting + // files and so briefly the path not being present + // only make sure the dest path isn't a directory. + buildFile(testFile, true, false, + FILE_STATUS_DIR_PROBE); + + // now there is a file there, an attempt with overwrite == false will + // fail on the first HEAD. + interceptOperation(FileAlreadyExistsException.class, "", + GET_FILE_STATUS_ON_FILE, + () -> buildFile(testFile, false, true, + GET_FILE_STATUS_ON_FILE)); + } + + @Test + public void testCreateFilePerformanceFlag() throws Throwable { + describe("createFile with performance flag skips safety checks"); + S3AFileSystem fs = getFileSystem(); + + Path path = methodPath(); + FSDataOutputStreamBuilder builder = fs.createFile(path) + .overwrite(false) + .recursive(); + + // this has a broken return type; something to do with the return value of + // the createFile() call. only fixable via risky changes to the FileSystem class + builder.must(FS_S3A_CREATE_PERFORMANCE, true); + + verifyMetrics(() -> build(builder), + always(NO_HEAD_OR_LIST), + with(OBJECT_BULK_DELETE_REQUEST, 0), + with(OBJECT_DELETE_REQUEST, 0)); + } + + @Test + public void testCreateFileRecursive() throws Throwable { + describe("createFile without performance flag performs overwrite safety checks"); + S3AFileSystem fs = getFileSystem(); + + final Path path = methodPath(); + FSDataOutputStreamBuilder builder = fs.createFile(path) + .recursive() + .overwrite(false); + + // include a custom header to probe for after + final String custom = "custom"; + builder.must(FS_S3A_CREATE_HEADER + ".h1", custom); + + verifyMetrics(() -> build(builder), + always(CREATE_FILE_NO_OVERWRITE)); + + // the header is there and the probe should be a single HEAD call. + String header = verifyMetrics(() -> + toChar(requireNonNull( + fs.getXAttr(path, XA_HEADER_PREFIX + "h1"), + "no header")), + always(HEAD_OPERATION)); + Assertions.assertThat(header) + .isEqualTo(custom); + } + + @Test + public void testCreateFileNonRecursive() throws Throwable { + describe("nonrecursive createFile does not check parents"); + S3AFileSystem fs = getFileSystem(); + + verifyMetrics(() -> + build(fs.createFile(methodPath()).overwrite(true)), + always(CREATE_FILE_OVERWRITE)); + } + + + @Test + public void testCreateNonRecursive() throws Throwable { + describe("nonrecursive createFile does not check parents"); + S3AFileSystem fs = getFileSystem(); + + verifyMetrics(() -> { + fs.createNonRecursive(methodPath(), + true, 1000, (short)1, 0L, null) + .close(); + return ""; + }, + always(CREATE_FILE_OVERWRITE)); + } + + private FSDataOutputStream build(final FSDataOutputStreamBuilder builder) + throws IOException { + FSDataOutputStream out = builder.build(); + out.close(); + return out; + } + + /** + * Shows how the performance option allows the FS to become ill-formed. + */ + @Test + public void testPerformanceFlagPermitsInvalidStores() throws Throwable { + describe("createFile with performance flag over a directory"); + S3AFileSystem fs = getFileSystem(); + + Path path = methodPath(); + Path child = new Path(path, "child"); + ContractTestUtils.touch(fs, child); + try { + FSDataOutputStreamBuilder builder = fs.createFile(path) + .overwrite(false); + // this has a broken return type; a java typesystem quirk. + builder.must(FS_S3A_CREATE_PERFORMANCE, true); + + verifyMetrics(() -> build(builder), + always(NO_HEAD_OR_LIST), + with(OBJECT_BULK_DELETE_REQUEST, 0), + with(OBJECT_DELETE_REQUEST, 0)); + // the file is there + assertIsFile(path); + // the child is there + assertIsFile(child); + + // delete the path + fs.delete(path, true); + // the child is still there + assertIsFile(child); + // and the directory exists again + assertIsDirectory(path); + } finally { + // always delete the child, so if the test suite fails, the + // store is at least well-formed. + fs.delete(child, true); + } + } + +} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ADeleteCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ADeleteCost.java index 01cadc7c86e3e..be4de7942f7b9 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ADeleteCost.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ADeleteCost.java @@ -266,9 +266,10 @@ public void testDirMarkersFileCreation() throws Throwable { final int directories = directoriesInPath(srcDir); verifyMetrics(() -> { - file(new Path(srcDir, "source.txt")); + final Path srcPath = new Path(srcDir, "source.txt"); + file(srcPath); LOG.info("Metrics: {}\n{}", getMetricSummary(), getFileSystem()); - return "after touch(fs, srcFilePath) " + getMetricSummary(); + return "after touch(fs, " + srcPath + ")" + getMetricSummary(); }, with(DIRECTORIES_CREATED, 0), with(DIRECTORIES_DELETED, 0), @@ -276,10 +277,10 @@ public void testDirMarkersFileCreation() throws Throwable { withWhenKeeping(getDeleteMarkerStatistic(), 0), withWhenKeeping(FAKE_DIRECTORIES_DELETED, 0), // delete all possible fake dirs above the file - withWhenDeleting(getDeleteMarkerStatistic(), - isBulkDelete() ? 1: directories), withWhenDeleting(FAKE_DIRECTORIES_DELETED, - directories)); + directories), + withWhenDeleting(getDeleteMarkerStatistic(), + isBulkDelete() ? 1: directories)); } } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCost.java index 03bc10f86cd25..08a19600d5830 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCost.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCost.java @@ -57,7 +57,8 @@ public final class OperationCost { public static final int DELETE_MARKER_REQUEST = DELETE_OBJECT_REQUEST; /** - * No IO takes place. + * No Head or List IO takes place; other operations + * may still take place. */ public static final OperationCost NO_IO = new OperationCost(0, 0); @@ -87,7 +88,7 @@ public final class OperationCost { /** * Cost of getFileStatus on root directory. */ - public static final OperationCost ROOT_FILE_STATUS_PROBE = NO_IO; + public static final OperationCost ROOT_FILE_STATUS_PROBE = NO_HEAD_OR_LIST; /** * Cost of {@link org.apache.hadoop.fs.s3a.impl.StatusProbeEnum#ALL}. diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCostValidator.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCostValidator.java index 72e51ee3b9958..3a8a1f6ad7b6f 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCostValidator.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCostValidator.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -34,6 +35,7 @@ import org.apache.hadoop.fs.s3a.S3AInstrumentation; import org.apache.hadoop.fs.s3a.S3ATestUtils; import org.apache.hadoop.fs.s3a.Statistic; +import org.apache.hadoop.fs.s3a.statistics.StatisticTypeEnum; import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; import org.apache.hadoop.metrics2.lib.MutableCounter; import org.apache.hadoop.metrics2.lib.MutableMetric; @@ -274,7 +276,7 @@ public Builder withMetric(Statistic statistic) { /** * Add a varargs list of metrics. - * @param stat statistics to monitor. + * @param stats statistics to monitor. * @return this. */ public Builder withMetrics(Statistic...stats) { @@ -282,6 +284,20 @@ public Builder withMetrics(Statistic...stats) { return this; } + /** + * Add all counters and duration types to the + * metrics which can be asserted over. + * @return this. + */ + public Builder withAllCounters() { + EnumSet.allOf(Statistic.class).stream() + .filter(s -> + s.getType() == StatisticTypeEnum.TYPE_COUNTER + || s.getType() == StatisticTypeEnum.TYPE_DURATION) + .forEach(metrics::add); + return this; + } + /** * Instantiate. * @return the validator. diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/AbstractSTestS3AHugeFiles.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/AbstractSTestS3AHugeFiles.java index 15700ce953589..f8d47011de3f0 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/AbstractSTestS3AHugeFiles.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/AbstractSTestS3AHugeFiles.java @@ -19,8 +19,13 @@ package org.apache.hadoop.fs.s3a.scale; import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.IntFunction; import com.amazonaws.event.ProgressEvent; import com.amazonaws.event.ProgressEventType; @@ -35,7 +40,9 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileRange; import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.contract.ContractTestUtils; import org.apache.hadoop.fs.s3a.Constants; @@ -47,6 +54,7 @@ import org.apache.hadoop.util.Progressable; import static org.apache.hadoop.fs.contract.ContractTestUtils.*; +import static org.apache.hadoop.fs.contract.ContractTestUtils.validateVectoredReadResult; import static org.apache.hadoop.fs.s3a.Constants.*; import static org.apache.hadoop.fs.s3a.S3ATestUtils.*; import static org.apache.hadoop.fs.s3a.Statistic.STREAM_WRITE_BLOCK_UPLOADS_BYTES_PENDING; @@ -446,6 +454,30 @@ public void test_040_PositionedReadHugeFile() throws Throwable { toHuman(timer.nanosPerOperation(ops))); } + @Test + public void test_045_vectoredIOHugeFile() throws Throwable { + assumeHugeFileExists(); + List rangeList = new ArrayList<>(); + rangeList.add(FileRange.createFileRange(5856368, 116770)); + rangeList.add(FileRange.createFileRange(3520861, 116770)); + rangeList.add(FileRange.createFileRange(8191913, 116770)); + rangeList.add(FileRange.createFileRange(1520861, 116770)); + rangeList.add(FileRange.createFileRange(2520861, 116770)); + rangeList.add(FileRange.createFileRange(9191913, 116770)); + rangeList.add(FileRange.createFileRange(2820861, 156770)); + IntFunction allocate = ByteBuffer::allocate; + FileSystem fs = getFileSystem(); + CompletableFuture builder = + fs.openFile(hugefile).build(); + try (FSDataInputStream in = builder.get()) { + in.readVectored(rangeList, allocate); + byte[] readFullRes = new byte[(int)filesize]; + in.readFully(0, readFullRes); + // Comparing vectored read results with read fully. + validateVectoredReadResult(rangeList, readFullRes); + } + } + /** * Read in the entire file using read() calls. * @throws Throwable failure diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADirectoryPerformance.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADirectoryPerformance.java index 946e59e9e3c01..91ea0c8e62fb3 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADirectoryPerformance.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADirectoryPerformance.java @@ -31,6 +31,7 @@ import org.apache.hadoop.fs.s3a.Statistic; import org.apache.hadoop.fs.s3a.WriteOperationHelper; import org.apache.hadoop.fs.s3a.api.RequestFactory; +import org.apache.hadoop.fs.s3a.impl.PutObjectOptions; import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.store.audit.AuditSpan; import org.apache.hadoop.util.functional.RemoteIterators; @@ -257,9 +258,9 @@ public void testMultiPagesListingPerformanceAndCorrectness() ObjectMetadata om = fs.newObjectMetadata(0L); PutObjectRequest put = requestFactory .newPutObjectRequest(fs.pathToKey(file), om, - new FailingInputStream()); + null, new FailingInputStream()); futures.add(submit(executorService, () -> - writeOperationHelper.putObject(put))); + writeOperationHelper.putObject(put, PutObjectOptions.keepingDirs()))); } LOG.info("Waiting for PUTs to complete"); waitForCompletion(futures); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3AMultipartUploadSizeLimits.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3AMultipartUploadSizeLimits.java index 231cfd884e0c8..b83d12b4c1a66 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3AMultipartUploadSizeLimits.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3AMultipartUploadSizeLimits.java @@ -35,7 +35,7 @@ import org.apache.hadoop.fs.s3a.S3AInstrumentation; import org.apache.hadoop.fs.s3a.Statistic; import org.apache.hadoop.fs.s3a.auth.ProgressCounter; -import org.apache.hadoop.fs.s3a.commit.CommitOperations; +import org.apache.hadoop.fs.s3a.commit.impl.CommitOperations; import static org.apache.hadoop.fs.StreamCapabilities.ABORTABLE_STREAM; import static org.apache.hadoop.fs.contract.ContractTestUtils.createFile; diff --git a/hadoop-tools/hadoop-aws/src/test/resources/log4j.properties b/hadoop-tools/hadoop-aws/src/test/resources/log4j.properties index fc287e9845c76..c831999008fec 100644 --- a/hadoop-tools/hadoop-aws/src/test/resources/log4j.properties +++ b/hadoop-tools/hadoop-aws/src/test/resources/log4j.properties @@ -52,7 +52,7 @@ log4j.logger.org.apache.hadoop.ipc.Server=WARN # for debugging low level S3a operations, uncomment these lines # Log all S3A classes -#log4j.logger.org.apache.hadoop.fs.s3a=DEBUG +log4j.logger.org.apache.hadoop.fs.s3a=DEBUG #log4j.logger.org.apache.hadoop.fs.s3a.S3AUtils=INFO #log4j.logger.org.apache.hadoop.fs.s3a.Listing=INFO diff --git a/hadoop-tools/hadoop-benchmark/pom.xml b/hadoop-tools/hadoop-benchmark/pom.xml new file mode 100644 index 0000000000000..3d742fab5c877 --- /dev/null +++ b/hadoop-tools/hadoop-benchmark/pom.xml @@ -0,0 +1,94 @@ + + + + 4.0.0 + + org.apache.hadoop + hadoop-project + 3.4.0-SNAPSHOT + ../../hadoop-project/pom.xml + + hadoop-benchmark + 3.4.0-SNAPSHOT + jar + + Apache Hadoop Common Benchmark + Apache Hadoop Common Benchmark + + + + org.apache.hadoop + hadoop-common + + + org.openjdk.jmh + jmh-core + + + org.openjdk.jmh + jmh-generator-annprocess + + + + + + + maven-assembly-plugin + + + + org.apache.hadoop.benchmark.VectoredReadBenchmark + + + + src/main/assembly/uber.xml + + + + + make-assembly + package + + single + + + + + + org.codehaus.mojo + findbugs-maven-plugin + + ${basedir}/src/main/findbugs/exclude.xml + + + + com.github.spotbugs + spotbugs-maven-plugin + + ${basedir}/src/main/findbugs/exclude.xml + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + diff --git a/hadoop-tools/hadoop-benchmark/src/main/assembly/uber.xml b/hadoop-tools/hadoop-benchmark/src/main/assembly/uber.xml new file mode 100644 index 0000000000000..014eab951b3cf --- /dev/null +++ b/hadoop-tools/hadoop-benchmark/src/main/assembly/uber.xml @@ -0,0 +1,33 @@ + + + uber + + jar + + false + + + / + true + true + runtime + + + + + metaInf-services + + + diff --git a/hadoop-tools/hadoop-benchmark/src/main/findbugs/exclude.xml b/hadoop-tools/hadoop-benchmark/src/main/findbugs/exclude.xml new file mode 100644 index 0000000000000..05f2a067cf01e --- /dev/null +++ b/hadoop-tools/hadoop-benchmark/src/main/findbugs/exclude.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/hadoop-tools/hadoop-benchmark/src/main/java/org/apache/hadoop/benchmark/VectoredReadBenchmark.java b/hadoop-tools/hadoop-benchmark/src/main/java/org/apache/hadoop/benchmark/VectoredReadBenchmark.java new file mode 100644 index 0000000000000..631842f78e20d --- /dev/null +++ b/hadoop-tools/hadoop-benchmark/src/main/java/org/apache/hadoop/benchmark/VectoredReadBenchmark.java @@ -0,0 +1,245 @@ +/* + * 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.benchmark; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousFileChannel; +import java.nio.channels.CompletionHandler; +import java.nio.file.FileSystems; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.IntFunction; + +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.LocalFileSystem; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileRange; +import org.apache.hadoop.fs.impl.FileRangeImpl; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class VectoredReadBenchmark { + + static final Path DATA_PATH = getTestDataPath(); + static final String DATA_PATH_PROPERTY = "bench.data"; + static final int READ_SIZE = 64 * 1024; + static final long SEEK_SIZE = 1024L * 1024; + + + static Path getTestDataPath() { + String value = System.getProperty(DATA_PATH_PROPERTY); + return new Path(value == null ? "/tmp/taxi.orc" : value); + } + + @State(Scope.Thread) + public static class FileSystemChoice { + + @Param({"local", "raw"}) + private String fileSystemKind; + + private Configuration conf; + private FileSystem fs; + + @Setup(Level.Trial) + public void setup() { + conf = new Configuration(); + try { + LocalFileSystem local = FileSystem.getLocal(conf); + fs = "raw".equals(fileSystemKind) ? local.getRaw() : local; + } catch (IOException e) { + throw new IllegalArgumentException("Can't get filesystem", e); + } + } + } + + @State(Scope.Thread) + public static class BufferChoice { + @Param({"direct", "array"}) + private String bufferKind; + + private IntFunction allocate; + @Setup(Level.Trial) + public void setup() { + allocate = "array".equals(bufferKind) + ? ByteBuffer::allocate : ByteBuffer::allocateDirect; + } + } + + @Benchmark + public void asyncRead(FileSystemChoice fsChoice, + BufferChoice bufferChoice, + Blackhole blackhole) throws Exception { + FSDataInputStream stream = fsChoice.fs.open(DATA_PATH); + List ranges = new ArrayList<>(); + for(int m=0; m < 100; ++m) { + FileRange range = FileRange.createFileRange(m * SEEK_SIZE, READ_SIZE); + ranges.add(range); + } + stream.readVectored(ranges, bufferChoice.allocate); + for(FileRange range: ranges) { + blackhole.consume(range.getData().get()); + } + stream.close(); + } + + static class Joiner implements CompletionHandler { + private int remaining; + private final ByteBuffer[] result; + private Throwable exception = null; + + Joiner(int total) { + remaining = total; + result = new ByteBuffer[total]; + } + + synchronized void finish() { + remaining -= 1; + if (remaining == 0) { + notify(); + } + } + + synchronized ByteBuffer[] join() throws InterruptedException, IOException { + while (remaining > 0 && exception == null) { + wait(); + } + if (exception != null) { + throw new IOException("problem reading", exception); + } + return result; + } + + + @Override + public synchronized void completed(ByteBuffer buffer, FileRange attachment) { + result[--remaining] = buffer; + if (remaining == 0) { + notify(); + } + } + + @Override + public synchronized void failed(Throwable exc, FileRange attachment) { + this.exception = exc; + notify(); + } + } + + static class FileRangeCallback extends FileRangeImpl implements + CompletionHandler { + private final AsynchronousFileChannel channel; + private final ByteBuffer buffer; + private int completed = 0; + private final Joiner joiner; + + FileRangeCallback(AsynchronousFileChannel channel, long offset, + int length, Joiner joiner, ByteBuffer buffer) { + super(offset, length); + this.channel = channel; + this.joiner = joiner; + this.buffer = buffer; + } + + @Override + public void completed(Integer result, FileRangeCallback attachment) { + final int bytes = result; + if (bytes == -1) { + failed(new EOFException("Read past end of file"), this); + } + completed += bytes; + if (completed < this.getLength()) { + channel.read(buffer, this.getOffset() + completed, this, this); + } else { + buffer.flip(); + joiner.finish(); + } + } + + @Override + public void failed(Throwable exc, FileRangeCallback attachment) { + joiner.failed(exc, this); + } + } + + @Benchmark + public void asyncFileChanArray(BufferChoice bufferChoice, + Blackhole blackhole) throws Exception { + java.nio.file.Path path = FileSystems.getDefault().getPath(DATA_PATH.toString()); + AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); + List ranges = new ArrayList<>(); + Joiner joiner = new Joiner(100); + for(int m=0; m < 100; ++m) { + ByteBuffer buffer = bufferChoice.allocate.apply(READ_SIZE); + FileRangeCallback range = new FileRangeCallback(channel, m * SEEK_SIZE, + READ_SIZE, joiner, buffer); + ranges.add(range); + channel.read(buffer, range.getOffset(), range, range); + } + joiner.join(); + channel.close(); + blackhole.consume(ranges); + } + + @Benchmark + public void syncRead(FileSystemChoice fsChoice, + Blackhole blackhole) throws Exception { + FSDataInputStream stream = fsChoice.fs.open(DATA_PATH); + List result = new ArrayList<>(); + for(int m=0; m < 100; ++m) { + byte[] buffer = new byte[READ_SIZE]; + stream.readFully(m * SEEK_SIZE, buffer); + result.add(buffer); + } + blackhole.consume(result); + stream.close(); + } + + /** + * Run the benchmarks. + * @param args the pathname of a 100MB data file + * @throws Exception any ex. + */ + public static void main(String[] args) throws Exception { + OptionsBuilder opts = new OptionsBuilder(); + opts.include("VectoredReadBenchmark"); + opts.jvmArgs("-server", "-Xms256m", "-Xmx2g", + "-D" + DATA_PATH_PROPERTY + "=" + args[0]); + opts.forks(1); + new Runner(opts.build()).run(); + } +} diff --git a/hadoop-tools/hadoop-benchmark/src/main/java/org/apache/hadoop/benchmark/package-info.java b/hadoop-tools/hadoop-benchmark/src/main/java/org/apache/hadoop/benchmark/package-info.java new file mode 100644 index 0000000000000..95d6977e3aba7 --- /dev/null +++ b/hadoop-tools/hadoop-benchmark/src/main/java/org/apache/hadoop/benchmark/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** + * Benchmark for Vectored Read IO operations. + */ +package org.apache.hadoop.benchmark; diff --git a/hadoop-tools/pom.xml b/hadoop-tools/pom.xml index f026bc261e00b..4e934cd101f85 100644 --- a/hadoop-tools/pom.xml +++ b/hadoop-tools/pom.xml @@ -51,6 +51,7 @@ hadoop-azure-datalake hadoop-aliyun hadoop-fs2img + hadoop-benchmark diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-core/src/main/java/org/apache/hadoop/yarn/service/monitor/probe/HttpProbe.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-core/src/main/java/org/apache/hadoop/yarn/service/monitor/probe/HttpProbe.java index 492a11b2c67a3..40a87937629d1 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-core/src/main/java/org/apache/hadoop/yarn/service/monitor/probe/HttpProbe.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-core/src/main/java/org/apache/hadoop/yarn/service/monitor/probe/HttpProbe.java @@ -88,8 +88,9 @@ public ProbeStatus ping(ComponentInstance instance) { } String ip = instance.getContainerStatus().getIPs().get(0); HttpURLConnection connection = null; + String hostString = urlString.replace(HOST_TOKEN, ip); try { - URL url = new URL(urlString.replace(HOST_TOKEN, ip)); + URL url = new URL(hostString); connection = getConnection(url, this.timeout); int rc = connection.getResponseCode(); if (rc < min || rc > max) { @@ -101,7 +102,8 @@ public ProbeStatus ping(ComponentInstance instance) { status.succeed(this); } } catch (Throwable e) { - String error = "Probe " + urlString + " failed for IP " + ip + ": " + e; + String error = + "Probe " + hostString + " failed for IP " + ip + ": " + e; log.info(error, e); status.fail(this, new IOException(error, e)); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/logaggregation/AggregatedLogDeletionService.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/logaggregation/AggregatedLogDeletionService.java index 9427068cfc505..4f10b2fd4ce23 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/logaggregation/AggregatedLogDeletionService.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/logaggregation/AggregatedLogDeletionService.java @@ -19,6 +19,8 @@ package org.apache.hadoop.yarn.logaggregation; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Timer; import java.util.TimerTask; @@ -57,23 +59,21 @@ public class AggregatedLogDeletionService extends AbstractService { private Timer timer = null; private long checkIntervalMsecs; - private LogDeletionTask task; + private List tasks; - static class LogDeletionTask extends TimerTask { + public static class LogDeletionTask extends TimerTask { private Configuration conf; private long retentionMillis; private String suffix = null; private Path remoteRootLogDir = null; private ApplicationClientProtocol rmClient = null; - public LogDeletionTask(Configuration conf, long retentionSecs, ApplicationClientProtocol rmClient) { + public LogDeletionTask(Configuration conf, long retentionSecs, + ApplicationClientProtocol rmClient, + LogAggregationFileController fileController) { this.conf = conf; this.retentionMillis = retentionSecs * 1000; this.suffix = LogAggregationUtils.getBucketSuffix(); - LogAggregationFileControllerFactory factory = - new LogAggregationFileControllerFactory(conf); - LogAggregationFileController fileController = - factory.getFileControllerForWrite(); this.remoteRootLogDir = fileController.getRemoteRootLogDir(); this.rmClient = rmClient; } @@ -101,7 +101,7 @@ public void run() { } } } catch (Throwable t) { - logException("Error reading root log dir this deletion " + + logException("Error reading root log dir, this deletion " + "attempt is being aborted", t); } LOG.info("aggregated log deletion finished."); @@ -220,7 +220,7 @@ public AggregatedLogDeletionService() { @Override protected void serviceStart() throws Exception { - scheduleLogDeletionTask(); + scheduleLogDeletionTasks(); super.serviceStart(); } @@ -249,13 +249,13 @@ public void refreshLogRetentionSettings() throws IOException { setConfig(conf); stopRMClient(); stopTimer(); - scheduleLogDeletionTask(); + scheduleLogDeletionTasks(); } else { LOG.warn("Failed to execute refreshLogRetentionSettings : Aggregated Log Deletion Service is not started"); } } - private void scheduleLogDeletionTask() throws IOException { + private void scheduleLogDeletionTasks() throws IOException { Configuration conf = getConfig(); if (!conf.getBoolean(YarnConfiguration.LOG_AGGREGATION_ENABLED, YarnConfiguration.DEFAULT_LOG_AGGREGATION_ENABLED)) { @@ -271,9 +271,28 @@ private void scheduleLogDeletionTask() throws IOException { return; } setLogAggCheckIntervalMsecs(retentionSecs); - task = new LogDeletionTask(conf, retentionSecs, createRMClient()); - timer = new Timer(); - timer.scheduleAtFixedRate(task, 0, checkIntervalMsecs); + + tasks = createLogDeletionTasks(conf, retentionSecs, createRMClient()); + for (LogDeletionTask task : tasks) { + timer = new Timer(); + timer.scheduleAtFixedRate(task, 0, checkIntervalMsecs); + } + } + + @VisibleForTesting + public List createLogDeletionTasks(Configuration conf, long retentionSecs, + ApplicationClientProtocol rmClient) + throws IOException { + List tasks = new ArrayList<>(); + LogAggregationFileControllerFactory factory = new LogAggregationFileControllerFactory(conf); + List fileControllers = + factory.getConfiguredLogAggregationFileControllerList(); + for (LogAggregationFileController fileController : fileControllers) { + LogDeletionTask task = new LogDeletionTask(conf, retentionSecs, rmClient, + fileController); + tasks.add(task); + } + return tasks; } private void stopTimer() { @@ -295,14 +314,18 @@ protected Configuration createConf() { // as @Idempotent, it will automatically take care of RM restart/failover. @VisibleForTesting protected ApplicationClientProtocol createRMClient() throws IOException { - return ClientRMProxy.createRMProxy(getConfig(), - ApplicationClientProtocol.class); + return ClientRMProxy.createRMProxy(getConfig(), ApplicationClientProtocol.class); } @VisibleForTesting protected void stopRMClient() { - if (task != null && task.getRMClient() != null) { - RPC.stopProxy(task.getRMClient()); + for (LogDeletionTask task : tasks) { + if (task != null && task.getRMClient() != null) { + RPC.stopProxy(task.getRMClient()); + //The RMClient instance is the same for all deletion tasks. + //It is enough to close the RM client once + break; + } } } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/logaggregation/filecontroller/ifile/LogAggregationIndexedFileController.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/logaggregation/filecontroller/ifile/LogAggregationIndexedFileController.java index 6cd1f53d4ac8e..d4431d56b39a4 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/logaggregation/filecontroller/ifile/LogAggregationIndexedFileController.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/logaggregation/filecontroller/ifile/LogAggregationIndexedFileController.java @@ -282,7 +282,8 @@ private Pair initializeWriterInRolling( checksumFileInputStream = fc.open(remoteLogCheckSumFile); int nameLength = checksumFileInputStream.readInt(); byte[] b = new byte[nameLength]; - int actualLength = checksumFileInputStream.read(b); + checksumFileInputStream.readFully(b); + int actualLength = b.length; if (actualLength == nameLength) { String recoveredLogFile = new String( b, Charset.forName("UTF-8")); @@ -765,7 +766,8 @@ public Map parseCheckSumFiles( checksumFileInputStream = fc.open(file.getPath()); int nameLength = checksumFileInputStream.readInt(); byte[] b = new byte[nameLength]; - int actualLength = checksumFileInputStream.read(b); + checksumFileInputStream.readFully(b); + int actualLength = b.length; if (actualLength == nameLength) { nodeName = new String(b, Charset.forName("UTF-8")); index = checksumFileInputStream.readLong(); @@ -799,7 +801,8 @@ private Long parseChecksum(FileStatus file) { checksumFileInputStream = fileContext.open(file.getPath()); int nameLength = checksumFileInputStream.readInt(); byte[] b = new byte[nameLength]; - int actualLength = checksumFileInputStream.read(b); + checksumFileInputStream.readFully(b); + int actualLength = b.length; if (actualLength == nameLength) { nodeName = new String(b, StandardCharsets.UTF_8); index = checksumFileInputStream.readLong(); @@ -938,7 +941,8 @@ public IndexedLogsMeta loadIndexedLogsMeta(Path remoteLogPath, long end, // Load UUID and make sure the UUID is correct. byte[] uuidRead = new byte[UUID_LENGTH]; - int uuidReadLen = fsDataIStream.read(uuidRead); + fsDataIStream.readFully(uuidRead); + int uuidReadLen = uuidRead.length; if (this.uuid == null) { this.uuid = createUUID(appId); } @@ -1322,7 +1326,8 @@ private byte[] loadUUIDFromLogFile(final FileContext fc, .endsWith(CHECK_SUM_FILE_SUFFIX)) { fsDataInputStream = fc.open(checkPath); byte[] b = new byte[uuid.length]; - int actual = fsDataInputStream.read(b); + fsDataInputStream.readFully(b); + int actual = b.length; if (actual != uuid.length || Arrays.equals(b, uuid)) { deleteFileWithRetries(fc, checkPath); } else if (id == null){ diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/GenericExceptionHandler.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/GenericExceptionHandler.java index 2bad02c9f088e..b8fc9e00541d8 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/GenericExceptionHandler.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/GenericExceptionHandler.java @@ -93,8 +93,8 @@ public Response toResponse(Exception e) { && e.getCause() instanceof UnmarshalException) { s = Response.Status.BAD_REQUEST; } else { - LOG.warn("INTERNAL_SERVER_ERROR", e); - s = Response.Status.INTERNAL_SERVER_ERROR; + LOG.warn("SERVICE_UNAVAILABLE", e); + s = Response.Status.SERVICE_UNAVAILABLE; } // let jaxb handle marshalling data out in the same format requested diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/LogAggregationTestUtils.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/LogAggregationTestUtils.java new file mode 100644 index 0000000000000..3cd563a648932 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/LogAggregationTestUtils.java @@ -0,0 +1,68 @@ +/** + * 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.yarn.logaggregation; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.yarn.conf.YarnConfiguration; +import org.apache.hadoop.yarn.logaggregation.filecontroller.LogAggregationFileController; + +import java.util.List; + +import static org.apache.hadoop.yarn.conf.YarnConfiguration.LOG_AGGREGATION_FILE_CONTROLLER_FMT; +import static org.apache.hadoop.yarn.conf.YarnConfiguration.LOG_AGGREGATION_REMOTE_APP_LOG_DIR_FMT; +import static org.apache.hadoop.yarn.conf.YarnConfiguration.LOG_AGGREGATION_REMOTE_APP_LOG_DIR_SUFFIX_FMT; + + +public class LogAggregationTestUtils { + public static final String REMOTE_LOG_ROOT = "target/app-logs/"; + + public static void enableFileControllers(Configuration conf, + List> fileControllers, + List fileControllerNames) { + enableFcs(conf, REMOTE_LOG_ROOT, fileControllers, fileControllerNames); + } + + public static void enableFileControllers(Configuration conf, + String remoteLogRoot, + List> fileControllers, + List fileControllerNames) { + enableFcs(conf, remoteLogRoot, fileControllers, fileControllerNames); + } + + + private static void enableFcs(Configuration conf, + String remoteLogRoot, + List> fileControllers, + List fileControllerNames) { + conf.set(YarnConfiguration.LOG_AGGREGATION_FILE_FORMATS, + StringUtils.join(fileControllerNames, ",")); + for (int i = 0; i < fileControllers.size(); i++) { + Class fileController = fileControllers.get(i); + String controllerName = fileControllerNames.get(i); + + conf.setClass(String.format(LOG_AGGREGATION_FILE_CONTROLLER_FMT, controllerName), + fileController, LogAggregationFileController.class); + conf.set(String.format(LOG_AGGREGATION_REMOTE_APP_LOG_DIR_FMT, controllerName), + remoteLogRoot + controllerName + "/"); + conf.set(String.format(LOG_AGGREGATION_REMOTE_APP_LOG_DIR_SUFFIX_FMT, controllerName), + controllerName); + } + } +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/TestAggregatedLogDeletionService.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/TestAggregatedLogDeletionService.java index f855f9181cdfe..285ac43322a57 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/TestAggregatedLogDeletionService.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/TestAggregatedLogDeletionService.java @@ -18,96 +18,55 @@ package org.apache.hadoop.yarn.logaggregation; -import static org.apache.hadoop.yarn.conf.YarnConfiguration.LOG_AGGREGATION_FILE_CONTROLLER_FMT; - -import java.io.IOException; -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - +import org.apache.commons.lang3.tuple.Pair; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FilterFileSystem; import org.apache.hadoop.fs.Path; -import org.apache.hadoop.security.AccessControlException; -import org.apache.hadoop.yarn.api.ApplicationClientProtocol; -import org.apache.hadoop.yarn.api.protocolrecords.GetApplicationReportRequest; -import org.apache.hadoop.yarn.api.protocolrecords.GetApplicationReportResponse; -import org.apache.hadoop.yarn.api.records.ApplicationId; -import org.apache.hadoop.yarn.api.records.ApplicationReport; -import org.apache.hadoop.yarn.api.records.YarnApplicationState; +import org.apache.hadoop.util.Lists; import org.apache.hadoop.yarn.conf.YarnConfiguration; +import org.apache.hadoop.yarn.logaggregation.filecontroller.LogAggregationFileController; +import org.apache.hadoop.yarn.logaggregation.filecontroller.ifile.LogAggregationIndexedFileController; import org.apache.hadoop.yarn.logaggregation.filecontroller.tfile.LogAggregationTFileController; +import org.apache.hadoop.yarn.logaggregation.testutils.LogAggregationTestcase; +import org.apache.hadoop.yarn.logaggregation.testutils.LogAggregationTestcaseBuilder; +import org.apache.hadoop.yarn.logaggregation.testutils.LogAggregationTestcaseBuilder.AppDescriptor; +import org.apache.log4j.Level; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; -import org.junit.Assert; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.*; +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.List; -public class TestAggregatedLogDeletionService { +import static org.apache.hadoop.yarn.conf.YarnConfiguration.LOG_AGGREGATION_FILE_CONTROLLER_FMT; +import static org.apache.hadoop.yarn.logaggregation.LogAggregationTestUtils.enableFileControllers; +import static org.apache.hadoop.yarn.logaggregation.testutils.LogAggregationTestcaseBuilder.NO_TIMEOUT; +import static org.mockito.Mockito.mock; +public class TestAggregatedLogDeletionService { private static final String T_FILE = "TFile"; + private static final String I_FILE = "IFile"; private static final String USER_ME = "me"; private static final String DIR_HOST1 = "host1"; private static final String DIR_HOST2 = "host2"; private static final String ROOT = "mockfs://foo/"; - private static final String REMOTE_ROOT_LOG_DIR = ROOT + "tmp/logs"; + private static final String REMOTE_ROOT_LOG_DIR = ROOT + "tmp/logs/"; private static final String SUFFIX = "logs"; - private static final String NEW_SUFFIX = LogAggregationUtils.getBucketSuffix() + SUFFIX; private static final int TEN_DAYS_IN_SECONDS = 10 * 24 * 3600; - private static PathWithFileStatus createPathWithFileStatusForAppId(Path remoteRootLogDir, - ApplicationId appId, - String user, String suffix, - long modificationTime) { - Path path = LogAggregationUtils.getRemoteAppLogDir( - remoteRootLogDir, appId, user, suffix); - FileStatus fileStatus = createEmptyFileStatus(modificationTime, path); - return new PathWithFileStatus(path, fileStatus); - } - - private static FileStatus createEmptyFileStatus(long modificationTime, Path path) { - return new FileStatus(0, true, 0, 0, modificationTime, path); - } - - private static PathWithFileStatus createFileLogPathWithFileStatus(Path baseDir, String childDir, - long modificationTime) { - Path logPath = new Path(baseDir, childDir); - FileStatus fStatus = createFileStatusWithLengthForFile(10, modificationTime, logPath); - return new PathWithFileStatus(logPath, fStatus); - } - - private static PathWithFileStatus createDirLogPathWithFileStatus(Path baseDir, String childDir, - long modificationTime) { - Path logPath = new Path(baseDir, childDir); - FileStatus fStatus = createFileStatusWithLengthForDir(10, modificationTime, logPath); - return new PathWithFileStatus(logPath, fStatus); - } - - private static PathWithFileStatus createDirBucketDirLogPathWithFileStatus(Path remoteRootLogPath, - String user, - String suffix, - ApplicationId appId, - long modificationTime) { - Path bucketDir = LogAggregationUtils.getRemoteBucketDir(remoteRootLogPath, user, suffix, appId); - FileStatus fStatus = new FileStatus(0, true, 0, 0, modificationTime, bucketDir); - return new PathWithFileStatus(bucketDir, fStatus); - } - - private static FileStatus createFileStatusWithLengthForFile(long length, - long modificationTime, - Path logPath) { - return new FileStatus(length, false, 1, 1, modificationTime, logPath); - } + private static final List> + ALL_FILE_CONTROLLERS = Arrays.asList( + LogAggregationIndexedFileController.class, + LogAggregationTFileController.class); + public static final List ALL_FILE_CONTROLLER_NAMES = Arrays.asList(I_FILE, T_FILE); - private static FileStatus createFileStatusWithLengthForDir(long length, - long modificationTime, - Path logPath) { - return new FileStatus(length, true, 1, 1, modificationTime, logPath); + @BeforeClass + public static void beforeClass() { + org.apache.log4j.Logger.getRootLogger().setLevel(Level.DEBUG); } @Before @@ -138,80 +97,34 @@ public void testDeletion() throws Exception { long toKeepTime = now - (1500 * 1000); Configuration conf = setupConfiguration(1800, -1); - - Path rootPath = new Path(ROOT); - FileSystem rootFs = rootPath.getFileSystem(conf); - FileSystem mockFs = ((FilterFileSystem)rootFs).getRawFileSystem(); - - Path remoteRootLogPath = new Path(REMOTE_ROOT_LOG_DIR); - PathWithFileStatus userDir = createDirLogPathWithFileStatus(remoteRootLogPath, USER_ME, - toKeepTime); - - when(mockFs.listStatus(remoteRootLogPath)).thenReturn(new FileStatus[]{userDir.fileStatus}); - - ApplicationId appId1 = ApplicationId.newInstance(now, 1); - ApplicationId appId2 = ApplicationId.newInstance(now, 2); - ApplicationId appId3 = ApplicationId.newInstance(now, 3); - ApplicationId appId4 = ApplicationId.newInstance(now, 4); - - PathWithFileStatus suffixDir = createDirLogPathWithFileStatus(userDir.path, NEW_SUFFIX, - toDeleteTime); - PathWithFileStatus bucketDir = createDirLogPathWithFileStatus(remoteRootLogPath, SUFFIX, - toDeleteTime); - - PathWithFileStatus app1 = createPathWithFileStatusForAppId(remoteRootLogPath, appId1, - USER_ME, SUFFIX, toDeleteTime); - PathWithFileStatus app2 = createPathWithFileStatusForAppId(remoteRootLogPath, appId2, - USER_ME, SUFFIX, toDeleteTime); - PathWithFileStatus app3 = createPathWithFileStatusForAppId(remoteRootLogPath, appId3, - USER_ME, SUFFIX, toDeleteTime); - PathWithFileStatus app4 = createPathWithFileStatusForAppId(remoteRootLogPath, appId4, - USER_ME, SUFFIX, toDeleteTime); - - when(mockFs.listStatus(userDir.path)).thenReturn(new FileStatus[] {suffixDir.fileStatus}); - when(mockFs.listStatus(suffixDir.path)).thenReturn(new FileStatus[] {bucketDir.fileStatus}); - when(mockFs.listStatus(bucketDir.path)).thenReturn(new FileStatus[] { - app1.fileStatus, app2.fileStatus, app3.fileStatus, app4.fileStatus}); - - PathWithFileStatus app2Log1 = createFileLogPathWithFileStatus(app2.path, DIR_HOST1, - toDeleteTime); - PathWithFileStatus app2Log2 = createFileLogPathWithFileStatus(app2.path, DIR_HOST2, toKeepTime); - PathWithFileStatus app3Log1 = createFileLogPathWithFileStatus(app3.path, DIR_HOST1, - toDeleteTime); - PathWithFileStatus app3Log2 = createFileLogPathWithFileStatus(app3.path, DIR_HOST2, - toDeleteTime); - PathWithFileStatus app4Log1 = createFileLogPathWithFileStatus(app4.path, DIR_HOST1, - toDeleteTime); - PathWithFileStatus app4Log2 = createFileLogPathWithFileStatus(app4.path, DIR_HOST2, toKeepTime); - - when(mockFs.listStatus(app1.path)).thenReturn(new FileStatus[]{}); - when(mockFs.listStatus(app2.path)).thenReturn(new FileStatus[]{app2Log1.fileStatus, - app2Log2.fileStatus}); - when(mockFs.listStatus(app3.path)).thenReturn(new FileStatus[]{app3Log1.fileStatus, - app3Log2.fileStatus}); - when(mockFs.listStatus(app4.path)).thenReturn(new FileStatus[]{app4Log1.fileStatus, - app4Log2.fileStatus}); - when(mockFs.delete(app3.path, true)).thenThrow( - new AccessControlException("Injected Error\nStack Trace :(")); - - final List finishedApplications = Collections.unmodifiableList( - Arrays.asList(appId1, appId2, appId3)); - final List runningApplications = Collections.singletonList(appId4); - - AggregatedLogDeletionService deletionService = - new AggregatedLogDeletionServiceForTest(runningApplications, finishedApplications); - deletionService.init(conf); - deletionService.start(); - - int timeout = 2000; - verify(mockFs, timeout(timeout)).delete(app1.path, true); - verify(mockFs, timeout(timeout).times(0)).delete(app2.path, true); - verify(mockFs, timeout(timeout)).delete(app3.path, true); - verify(mockFs, timeout(timeout).times(0)).delete(app4.path, true); - verify(mockFs, timeout(timeout)).delete(app4Log1.path, true); - verify(mockFs, timeout(timeout).times(0)).delete(app4Log2.path, true); - - deletionService.stop(); + long timeout = 2000L; + LogAggregationTestcaseBuilder.create(conf) + .withRootPath(ROOT) + .withRemoteRootLogPath(REMOTE_ROOT_LOG_DIR) + .withUserDir(USER_ME, toKeepTime) + .withSuffixDir(SUFFIX, toDeleteTime) + .withBucketDir(toDeleteTime) + .withApps(Lists.newArrayList( + new AppDescriptor(toDeleteTime, Lists.newArrayList()), + new AppDescriptor(toDeleteTime, Lists.newArrayList( + Pair.of(DIR_HOST1, toDeleteTime), + Pair.of(DIR_HOST2, toKeepTime))), + new AppDescriptor(toDeleteTime, Lists.newArrayList( + Pair.of(DIR_HOST1, toDeleteTime), + Pair.of(DIR_HOST2, toDeleteTime))), + new AppDescriptor(toDeleteTime, Lists.newArrayList( + Pair.of(DIR_HOST1, toDeleteTime), + Pair.of(DIR_HOST2, toKeepTime))))) + .withFinishedApps(1, 2, 3) + .withRunningApps(4) + .injectExceptionForAppDirDeletion(3) + .build() + .startDeletionService() + .verifyAppDirsDeleted(timeout, 1, 3) + .verifyAppDirsNotDeleted(timeout, 2, 4) + .verifyAppFileDeleted(4, 1, timeout) + .verifyAppFileNotDeleted(4, 2, timeout) + .teardown(1); } @Test @@ -224,74 +137,48 @@ public void testRefreshLogRetentionSettings() throws Exception { Configuration conf = setupConfiguration(1800, 1); - Path rootPath = new Path(ROOT); - FileSystem rootFs = rootPath.getFileSystem(conf); - FileSystem mockFs = ((FilterFileSystem) rootFs).getRawFileSystem(); - - ApplicationId appId1 = ApplicationId.newInstance(System.currentTimeMillis(), 1); - ApplicationId appId2 = ApplicationId.newInstance(System.currentTimeMillis(), 2); - - Path remoteRootLogPath = new Path(REMOTE_ROOT_LOG_DIR); - - PathWithFileStatus userDir = createDirLogPathWithFileStatus(remoteRootLogPath, USER_ME, - before50Secs); - PathWithFileStatus suffixDir = createDirLogPathWithFileStatus(userDir.path, NEW_SUFFIX, - before50Secs); - PathWithFileStatus bucketDir = createDirBucketDirLogPathWithFileStatus(remoteRootLogPath, - USER_ME, SUFFIX, appId1, before50Secs); - - when(mockFs.listStatus(remoteRootLogPath)).thenReturn(new FileStatus[] {userDir.fileStatus}); - - //Set time last modified of app1Dir directory and its files to before2000Secs - PathWithFileStatus app1 = createPathWithFileStatusForAppId(remoteRootLogPath, appId1, - USER_ME, SUFFIX, before2000Secs); - - //Set time last modified of app1Dir directory and its files to before50Secs - PathWithFileStatus app2 = createPathWithFileStatusForAppId(remoteRootLogPath, appId2, - USER_ME, SUFFIX, before50Secs); - - when(mockFs.listStatus(userDir.path)).thenReturn(new FileStatus[]{suffixDir.fileStatus}); - when(mockFs.listStatus(suffixDir.path)).thenReturn(new FileStatus[]{bucketDir.fileStatus}); - when(mockFs.listStatus(bucketDir.path)).thenReturn(new FileStatus[]{app1.fileStatus, - app2.fileStatus}); - - PathWithFileStatus app1Log1 = createFileLogPathWithFileStatus(app1.path, DIR_HOST1, - before2000Secs); - PathWithFileStatus app2Log1 = createFileLogPathWithFileStatus(app2.path, DIR_HOST1, - before50Secs); - - when(mockFs.listStatus(app1.path)).thenReturn(new FileStatus[] {app1Log1.fileStatus}); - when(mockFs.listStatus(app2.path)).thenReturn(new FileStatus[] {app2Log1.fileStatus}); - - final List finishedApplications = - Collections.unmodifiableList(Arrays.asList(appId1, appId2)); - - AggregatedLogDeletionService deletionSvc = new AggregatedLogDeletionServiceForTest(null, - finishedApplications, conf); - - deletionSvc.init(conf); - deletionSvc.start(); + LogAggregationTestcase testcase = LogAggregationTestcaseBuilder.create(conf) + .withRootPath(ROOT) + .withRemoteRootLogPath(REMOTE_ROOT_LOG_DIR) + .withUserDir(USER_ME, before50Secs) + .withSuffixDir(SUFFIX, before50Secs) + .withBucketDir(before50Secs) + .withApps(Lists.newArrayList( + //Set time last modified of app1Dir directory and its files to before2000Secs + new AppDescriptor(before2000Secs, Lists.newArrayList( + Pair.of(DIR_HOST1, before2000Secs))), + //Set time last modified of app1Dir directory and its files to before50Secs + new AppDescriptor(before50Secs, Lists.newArrayList( + Pair.of(DIR_HOST1, before50Secs)))) + ) + .withFinishedApps(1, 2) + .withRunningApps() + .build(); - //app1Dir would be deleted since its done above log retention period - verify(mockFs, timeout(10000)).delete(app1.path, true); - //app2Dir is not expected to be deleted since it is below the threshold - verify(mockFs, timeout(3000).times(0)).delete(app2.path, true); - - //Now, let's change the confs + testcase + .startDeletionService() + //app1Dir would be deleted since it is done above log retention period + .verifyAppDirDeleted(1, 10000L) + //app2Dir is not expected to be deleted since it is below the threshold + .verifyAppDirNotDeleted(2, 3000L); + + //Now, let's change the log aggregation retention configs conf.setInt(YarnConfiguration.LOG_AGGREGATION_RETAIN_SECONDS, 50); conf.setInt(YarnConfiguration.LOG_AGGREGATION_RETAIN_CHECK_INTERVAL_SECONDS, checkIntervalSeconds); - //We have not called refreshLogSettings,hence don't expect to see the changed conf values - assertTrue(checkIntervalMilliSeconds != deletionSvc.getCheckIntervalMsecs()); - - //refresh the log settings - deletionSvc.refreshLogRetentionSettings(); - //Check interval time should reflect the new value - Assert.assertEquals(checkIntervalMilliSeconds, deletionSvc.getCheckIntervalMsecs()); - //app2Dir should be deleted since it falls above the threshold - verify(mockFs, timeout(10000)).delete(app2.path, true); - deletionSvc.stop(); + testcase + //We have not called refreshLogSettings, hence don't expect to see + // the changed conf values + .verifyCheckIntervalMilliSecondsNotEqualTo(checkIntervalMilliSeconds) + //refresh the log settings + .refreshLogRetentionSettings() + //Check interval time should reflect the new value + .verifyCheckIntervalMilliSecondsEqualTo(checkIntervalMilliSeconds) + //app2Dir should be deleted since it falls above the threshold + .verifyAppDirDeleted(2, 10000L) + //Close expected 2 times: once for refresh and once for stopping + .teardown(2); } @Test @@ -303,52 +190,30 @@ public void testCheckInterval() throws Exception { // prevent us from picking up the same mockfs instance from another test FileSystem.closeAll(); - Path rootPath = new Path(ROOT); - FileSystem rootFs = rootPath.getFileSystem(conf); - FileSystem mockFs = ((FilterFileSystem)rootFs).getRawFileSystem(); - - Path remoteRootLogPath = new Path(REMOTE_ROOT_LOG_DIR); - - PathWithFileStatus userDir = createDirLogPathWithFileStatus(remoteRootLogPath, USER_ME, now); - PathWithFileStatus suffixDir = createDirLogPathWithFileStatus(userDir.path, NEW_SUFFIX, now); - - when(mockFs.listStatus(remoteRootLogPath)).thenReturn(new FileStatus[]{userDir.fileStatus}); - - ApplicationId appId1 = ApplicationId.newInstance(System.currentTimeMillis(), 1); - PathWithFileStatus bucketDir = createDirBucketDirLogPathWithFileStatus(remoteRootLogPath, - USER_ME, SUFFIX, appId1, now); - - PathWithFileStatus app1 = createPathWithFileStatusForAppId(remoteRootLogPath, appId1, - USER_ME, SUFFIX, now); - PathWithFileStatus app1Log1 = createFileLogPathWithFileStatus(app1.path, DIR_HOST1, now); - when(mockFs.listStatus(userDir.path)).thenReturn(new FileStatus[] {suffixDir.fileStatus}); - when(mockFs.listStatus(suffixDir.path)).thenReturn(new FileStatus[] {bucketDir.fileStatus}); - when(mockFs.listStatus(bucketDir.path)).thenReturn(new FileStatus[] {app1.fileStatus}); - when(mockFs.listStatus(app1.path)).thenReturn(new FileStatus[]{app1Log1.fileStatus}); - - final List finishedApplications = Collections.singletonList(appId1); - - AggregatedLogDeletionService deletionSvc = new AggregatedLogDeletionServiceForTest(null, - finishedApplications); - deletionSvc.init(conf); - deletionSvc.start(); - - verify(mockFs, timeout(10000).atLeast(4)).listStatus(any(Path.class)); - verify(mockFs, never()).delete(app1.path, true); - - // modify the timestamp of the logs and verify if it's picked up quickly - app1.changeModificationTime(toDeleteTime); - app1Log1.changeModificationTime(toDeleteTime); - bucketDir.changeModificationTime(toDeleteTime); - when(mockFs.listStatus(userDir.path)).thenReturn(new FileStatus[] {suffixDir.fileStatus}); - when(mockFs.listStatus(suffixDir.path)).thenReturn(new FileStatus[] {bucketDir.fileStatus }); - when(mockFs.listStatus(bucketDir.path)).thenReturn(new FileStatus[] {app1.fileStatus }); - when(mockFs.listStatus(app1.path)).thenReturn(new FileStatus[]{app1Log1.fileStatus}); - - verify(mockFs, timeout(10000)).delete(app1.path, true); - - deletionSvc.stop(); + LogAggregationTestcaseBuilder.create(conf) + .withRootPath(ROOT) + .withRemoteRootLogPath(REMOTE_ROOT_LOG_DIR) + .withUserDir(USER_ME, now) + .withSuffixDir(SUFFIX, now) + .withBucketDir(now) + .withApps(Lists.newArrayList( + new AppDescriptor(now, + Lists.newArrayList(Pair.of(DIR_HOST1, now))), + new AppDescriptor(now))) + .withFinishedApps(1) + .withRunningApps() + .build() + .startDeletionService() + .verifyAnyPathListedAtLeast(4, 10000L) + .verifyAppDirNotDeleted(1, NO_TIMEOUT) + // modify the timestamp of the logs and verify if it is picked up quickly + .changeModTimeOfApp(1, toDeleteTime) + .changeModTimeOfAppLogDir(1, 1, toDeleteTime) + .changeModTimeOfBucketDir(toDeleteTime) + .reinitAllPaths() + .verifyAppDirDeleted(1, 10000L) + .teardown(1); } @Test @@ -357,44 +222,78 @@ public void testRobustLogDeletion() throws Exception { // prevent us from picking up the same mockfs instance from another test FileSystem.closeAll(); - Path rootPath = new Path(ROOT); - FileSystem rootFs = rootPath.getFileSystem(conf); - FileSystem mockFs = ((FilterFileSystem)rootFs).getRawFileSystem(); - - Path remoteRootLogPath = new Path(REMOTE_ROOT_LOG_DIR); - - PathWithFileStatus userDir = createDirLogPathWithFileStatus(remoteRootLogPath, USER_ME, 0); - PathWithFileStatus suffixDir = createDirLogPathWithFileStatus(userDir.path, NEW_SUFFIX, 0); - PathWithFileStatus bucketDir = createDirLogPathWithFileStatus(suffixDir.path, "0", 0); - - when(mockFs.listStatus(remoteRootLogPath)).thenReturn(new FileStatus[]{userDir.fileStatus}); - when(mockFs.listStatus(userDir.path)).thenReturn(new FileStatus[]{suffixDir.fileStatus}); - when(mockFs.listStatus(suffixDir.path)).thenReturn(new FileStatus[]{bucketDir.fileStatus}); - - ApplicationId appId1 = ApplicationId.newInstance(System.currentTimeMillis(), 1); - ApplicationId appId2 = ApplicationId.newInstance(System.currentTimeMillis(), 2); - ApplicationId appId3 = ApplicationId.newInstance(System.currentTimeMillis(), 3); - - PathWithFileStatus app1 = createDirLogPathWithFileStatus(bucketDir.path, appId1.toString(), 0); - PathWithFileStatus app2 = createDirLogPathWithFileStatus(bucketDir.path, "application_a", 0); - PathWithFileStatus app3 = createDirLogPathWithFileStatus(bucketDir.path, appId3.toString(), 0); - PathWithFileStatus app3Log3 = createDirLogPathWithFileStatus(app3.path, DIR_HOST1, 0); + long modTime = 0L; + + LogAggregationTestcaseBuilder.create(conf) + .withRootPath(ROOT) + .withRemoteRootLogPath(REMOTE_ROOT_LOG_DIR) + .withUserDir(USER_ME, modTime) + .withSuffixDir(SUFFIX, modTime) + .withBucketDir(modTime, "0") + .withApps(Lists.newArrayList( + new AppDescriptor(modTime), + new AppDescriptor(modTime), + new AppDescriptor(modTime, Lists.newArrayList(Pair.of(DIR_HOST1, modTime))))) + .withAdditionalAppDirs(Lists.newArrayList(Pair.of("application_a", modTime))) + .withFinishedApps(1, 3) + .withRunningApps() + .injectExceptionForAppDirDeletion(1) + .build() + .runDeletionTask(TEN_DAYS_IN_SECONDS) + .verifyAppDirDeleted(3, NO_TIMEOUT); + } - when(mockFs.listStatus(bucketDir.path)).thenReturn(new FileStatus[]{ - app1.fileStatus,app2.fileStatus, app3.fileStatus}); - when(mockFs.listStatus(app1.path)).thenThrow( - new RuntimeException("Should be caught and logged")); - when(mockFs.listStatus(app2.path)).thenReturn(new FileStatus[]{}); - when(mockFs.listStatus(app3.path)).thenReturn(new FileStatus[]{app3Log3.fileStatus}); + @Test + public void testDeletionTwoControllers() throws IOException { + long now = System.currentTimeMillis(); + long toDeleteTime = now - (2000 * 1000); + long toKeepTime = now - (1500 * 1000); - final List finishedApplications = Collections.unmodifiableList( - Arrays.asList(appId1, appId3)); - ApplicationClientProtocol rmClient = createMockRMClient(finishedApplications, null); - AggregatedLogDeletionService.LogDeletionTask deletionTask = - new AggregatedLogDeletionService.LogDeletionTask(conf, TEN_DAYS_IN_SECONDS, rmClient); - deletionTask.run(); - verify(mockFs).delete(app3.path, true); + Configuration conf = setupConfiguration(1800, -1); + enableFileControllers(conf, REMOTE_ROOT_LOG_DIR, ALL_FILE_CONTROLLERS, + ALL_FILE_CONTROLLER_NAMES); + long timeout = 2000L; + LogAggregationTestcaseBuilder.create(conf) + .withRootPath(ROOT) + .withRemoteRootLogPath(REMOTE_ROOT_LOG_DIR) + .withBothFileControllers() + .withUserDir(USER_ME, toKeepTime) + .withSuffixDir(SUFFIX, toDeleteTime) + .withBucketDir(toDeleteTime) + .withApps(//Apps for TFile + Lists.newArrayList( + new AppDescriptor(T_FILE, toDeleteTime, Lists.newArrayList()), + new AppDescriptor(T_FILE, toDeleteTime, Lists.newArrayList( + Pair.of(DIR_HOST1, toDeleteTime), + Pair.of(DIR_HOST2, toKeepTime))), + new AppDescriptor(T_FILE, toDeleteTime, Lists.newArrayList( + Pair.of(DIR_HOST1, toDeleteTime), + Pair.of(DIR_HOST2, toDeleteTime))), + new AppDescriptor(T_FILE, toDeleteTime, Lists.newArrayList( + Pair.of(DIR_HOST1, toDeleteTime), + Pair.of(DIR_HOST2, toKeepTime))), + //Apps for IFile + new AppDescriptor(I_FILE, toDeleteTime, Lists.newArrayList()), + new AppDescriptor(I_FILE, toDeleteTime, Lists.newArrayList( + Pair.of(DIR_HOST1, toDeleteTime), + Pair.of(DIR_HOST2, toKeepTime))), + new AppDescriptor(I_FILE, toDeleteTime, Lists.newArrayList( + Pair.of(DIR_HOST1, toDeleteTime), + Pair.of(DIR_HOST2, toDeleteTime))), + new AppDescriptor(I_FILE, toDeleteTime, Lists.newArrayList( + Pair.of(DIR_HOST1, toDeleteTime), + Pair.of(DIR_HOST2, toKeepTime))))) + .withFinishedApps(1, 2, 3, 5, 6, 7) + .withRunningApps(4, 8) + .injectExceptionForAppDirDeletion(3, 6) + .build() + .startDeletionService() + .verifyAppDirsDeleted(timeout, 1, 3, 5, 7) + .verifyAppDirsNotDeleted(timeout, 2, 4, 6, 8) + .verifyAppFilesDeleted(timeout, Lists.newArrayList(Pair.of(4, 1), Pair.of(8, 1))) + .verifyAppFilesNotDeleted(timeout, Lists.newArrayList(Pair.of(4, 2), Pair.of(8, 2))) + .teardown(1); } static class MockFileSystem extends FilterFileSystem { @@ -403,98 +302,10 @@ static class MockFileSystem extends FilterFileSystem { } public void initialize(URI name, Configuration conf) throws IOException {} - } - - private static ApplicationClientProtocol createMockRMClient( - List finishedApplications, - List runningApplications) throws Exception { - final ApplicationClientProtocol mockProtocol = mock(ApplicationClientProtocol.class); - if (finishedApplications != null && !finishedApplications.isEmpty()) { - for (ApplicationId appId : finishedApplications) { - GetApplicationReportRequest request = GetApplicationReportRequest.newInstance(appId); - GetApplicationReportResponse response = createApplicationReportWithFinishedApplication(); - when(mockProtocol.getApplicationReport(request)).thenReturn(response); - } - } - if (runningApplications != null && !runningApplications.isEmpty()) { - for (ApplicationId appId : runningApplications) { - GetApplicationReportRequest request = GetApplicationReportRequest.newInstance(appId); - GetApplicationReportResponse response = createApplicationReportWithRunningApplication(); - when(mockProtocol.getApplicationReport(request)).thenReturn(response); - } - } - return mockProtocol; - } - - private static GetApplicationReportResponse createApplicationReportWithRunningApplication() { - ApplicationReport report = mock(ApplicationReport.class); - when(report.getYarnApplicationState()).thenReturn( - YarnApplicationState.RUNNING); - GetApplicationReportResponse response = - mock(GetApplicationReportResponse.class); - when(response.getApplicationReport()).thenReturn(report); - return response; - } - - private static GetApplicationReportResponse createApplicationReportWithFinishedApplication() { - ApplicationReport report = mock(ApplicationReport.class); - when(report.getYarnApplicationState()).thenReturn(YarnApplicationState.FINISHED); - GetApplicationReportResponse response = mock(GetApplicationReportResponse.class); - when(response.getApplicationReport()).thenReturn(report); - return response; - } - - private static class PathWithFileStatus { - private final Path path; - private FileStatus fileStatus; - - PathWithFileStatus(Path path, FileStatus fileStatus) { - this.path = path; - this.fileStatus = fileStatus; - } - - public void changeModificationTime(long modTime) { - fileStatus = new FileStatus(fileStatus.getLen(), fileStatus.isDirectory(), - fileStatus.getReplication(), - fileStatus.getBlockSize(), modTime, fileStatus.getPath()); - } - } - - private static class AggregatedLogDeletionServiceForTest extends AggregatedLogDeletionService { - private final List finishedApplications; - private final List runningApplications; - private final Configuration conf; - - AggregatedLogDeletionServiceForTest(List runningApplications, - List finishedApplications) { - this(runningApplications, finishedApplications, null); - } - - AggregatedLogDeletionServiceForTest(List runningApplications, - List finishedApplications, - Configuration conf) { - this.runningApplications = runningApplications; - this.finishedApplications = finishedApplications; - this.conf = conf; - } - - @Override - protected ApplicationClientProtocol createRMClient() throws IOException { - try { - return createMockRMClient(finishedApplications, runningApplications); - } catch (Exception e) { - throw new IOException(e); - } - } - - @Override - protected Configuration createConf() { - return conf; - } @Override - protected void stopRMClient() { - // DO NOTHING + public boolean hasPathCapability(Path path, String capability) { + return true; } } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/filecontroller/TestLogAggregationFileControllerFactory.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/filecontroller/TestLogAggregationFileControllerFactory.java index 2d2fb49c0efc9..c1b991b9bc1b5 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/filecontroller/TestLogAggregationFileControllerFactory.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/filecontroller/TestLogAggregationFileControllerFactory.java @@ -18,24 +18,6 @@ package org.apache.hadoop.yarn.logaggregation.filecontroller; -import static org.apache.hadoop.yarn.conf.YarnConfiguration.LOG_AGGREGATION_FILE_CONTROLLER_FMT; -import static org.apache.hadoop.yarn.conf.YarnConfiguration.LOG_AGGREGATION_REMOTE_APP_LOG_DIR_FMT; -import static org.apache.hadoop.yarn.conf.YarnConfiguration.LOG_AGGREGATION_REMOTE_APP_LOG_DIR_SUFFIX_FMT; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.Writer; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configured; import org.apache.hadoop.fs.FileSystem; @@ -56,6 +38,21 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.apache.hadoop.yarn.conf.YarnConfiguration.LOG_AGGREGATION_FILE_FORMATS; +import static org.apache.hadoop.yarn.logaggregation.LogAggregationTestUtils.REMOTE_LOG_ROOT; +import static org.apache.hadoop.yarn.logaggregation.LogAggregationTestUtils.enableFileControllers; +import static org.junit.Assert.*; + /** * Test LogAggregationFileControllerFactory. */ @@ -63,7 +60,6 @@ public class TestLogAggregationFileControllerFactory extends Configured { private static final Logger LOG = LoggerFactory.getLogger( TestLogAggregationFileControllerFactory.class); - private static final String REMOTE_LOG_ROOT = "target/app-logs/"; private static final String REMOTE_DEFAULT_DIR = "default/"; private static final String APP_OWNER = "test"; @@ -87,8 +83,7 @@ public class TestLogAggregationFileControllerFactory extends Configured { public void setup() throws IOException { Configuration conf = new YarnConfiguration(); conf.setBoolean(YarnConfiguration.LOG_AGGREGATION_ENABLED, true); - conf.set(YarnConfiguration.NM_REMOTE_APP_LOG_DIR, REMOTE_LOG_ROOT + - REMOTE_DEFAULT_DIR); + conf.set(YarnConfiguration.NM_REMOTE_APP_LOG_DIR, REMOTE_LOG_ROOT + REMOTE_DEFAULT_DIR); conf.set(YarnConfiguration.NM_REMOTE_APP_LOG_DIR_SUFFIX, "log"); setConf(conf); } @@ -143,36 +138,15 @@ public void testDefaultLogAggregationFileControllerFactory() @Test(expected = Exception.class) public void testLogAggregationFileControllerFactoryClassNotSet() { Configuration conf = getConf(); - conf.set(YarnConfiguration.LOG_AGGREGATION_FILE_FORMATS, - "TestLogAggregationFileController"); + conf.set(LOG_AGGREGATION_FILE_FORMATS, "TestLogAggregationFileController"); new LogAggregationFileControllerFactory(conf); fail("TestLogAggregationFileController's class was not set, " + "but the factory creation did not fail."); } - private void enableFileControllers( - List> fileControllers, - List fileControllerNames) { - Configuration conf = getConf(); - conf.set(YarnConfiguration.LOG_AGGREGATION_FILE_FORMATS, - StringUtils.join(fileControllerNames, ",")); - for (int i = 0; i < fileControllers.size(); i++) { - Class fileController = - fileControllers.get(i); - String controllerName = fileControllerNames.get(i); - - conf.setClass(String.format(LOG_AGGREGATION_FILE_CONTROLLER_FMT, - controllerName), fileController, LogAggregationFileController.class); - conf.set(String.format(LOG_AGGREGATION_REMOTE_APP_LOG_DIR_FMT, - controllerName), REMOTE_LOG_ROOT + controllerName + "/"); - conf.set(String.format(LOG_AGGREGATION_REMOTE_APP_LOG_DIR_SUFFIX_FMT, - controllerName), controllerName); - } - } - @Test public void testLogAggregationFileControllerFactory() throws Exception { - enableFileControllers(ALL_FILE_CONTROLLERS, ALL_FILE_CONTROLLER_NAMES); + enableFileControllers(getConf(), ALL_FILE_CONTROLLERS, ALL_FILE_CONTROLLER_NAMES); LogAggregationFileControllerFactory factory = new LogAggregationFileControllerFactory(getConf()); List list = @@ -199,8 +173,7 @@ public void testLogAggregationFileControllerFactory() throws Exception { @Test public void testClassConfUsed() { - enableFileControllers(Collections.singletonList( - LogAggregationTFileController.class), + enableFileControllers(getConf(), Collections.singletonList(LogAggregationTFileController.class), Collections.singletonList("TFile")); LogAggregationFileControllerFactory factory = new LogAggregationFileControllerFactory(getConf()); @@ -215,7 +188,7 @@ public void testClassConfUsed() { @Test public void testNodemanagerConfigurationIsUsed() { Configuration conf = getConf(); - conf.set(YarnConfiguration.LOG_AGGREGATION_FILE_FORMATS, "TFile"); + conf.set(LOG_AGGREGATION_FILE_FORMATS, "TFile"); LogAggregationFileControllerFactory factory = new LogAggregationFileControllerFactory(conf); LogAggregationFileController fc = factory.getFileControllerForWrite(); @@ -231,7 +204,7 @@ public void testDefaultConfUsed() { Configuration conf = getConf(); conf.unset(YarnConfiguration.NM_REMOTE_APP_LOG_DIR); conf.unset(YarnConfiguration.NM_REMOTE_APP_LOG_DIR_SUFFIX); - conf.set(YarnConfiguration.LOG_AGGREGATION_FILE_FORMATS, "TFile"); + conf.set(LOG_AGGREGATION_FILE_FORMATS, "TFile"); LogAggregationFileControllerFactory factory = new LogAggregationFileControllerFactory(getConf()); @@ -268,20 +241,19 @@ public void postWrite(LogAggregationFileControllerContext record) } @Override - public void initializeWriter(LogAggregationFileControllerContext context) - throws IOException { + public void initializeWriter(LogAggregationFileControllerContext context) { // Do Nothing } @Override public boolean readAggregatedLogs(ContainerLogsRequest logRequest, - OutputStream os) throws IOException { + OutputStream os) { return false; } @Override public List readAggregatedLogsMeta( - ContainerLogsRequest logRequest) throws IOException { + ContainerLogsRequest logRequest) { return null; } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/AggregatedLogDeletionServiceForTest.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/AggregatedLogDeletionServiceForTest.java new file mode 100644 index 0000000000000..76ec8aab537a5 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/AggregatedLogDeletionServiceForTest.java @@ -0,0 +1,72 @@ +/** + * 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.yarn.logaggregation.testutils; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.yarn.api.ApplicationClientProtocol; +import org.apache.hadoop.yarn.api.records.ApplicationId; +import org.apache.hadoop.yarn.logaggregation.AggregatedLogDeletionService; + +import java.io.IOException; +import java.util.List; + +import static org.apache.hadoop.yarn.logaggregation.testutils.MockRMClientUtils.createMockRMClient; + +public class AggregatedLogDeletionServiceForTest extends AggregatedLogDeletionService { + private final List finishedApplications; + private final List runningApplications; + private final Configuration conf; + private ApplicationClientProtocol mockRMClient; + + public AggregatedLogDeletionServiceForTest(List runningApplications, + List finishedApplications) { + this(runningApplications, finishedApplications, null); + } + + public AggregatedLogDeletionServiceForTest(List runningApplications, + List finishedApplications, + Configuration conf) { + this.runningApplications = runningApplications; + this.finishedApplications = finishedApplications; + this.conf = conf; + } + + @Override + protected ApplicationClientProtocol createRMClient() throws IOException { + if (mockRMClient != null) { + return mockRMClient; + } + try { + mockRMClient = + createMockRMClient(finishedApplications, runningApplications); + } catch (Exception e) { + throw new IOException(e); + } + return mockRMClient; + } + + @Override + protected Configuration createConf() { + return conf; + } + + public ApplicationClientProtocol getMockRMClient() { + return mockRMClient; + } +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/FileStatusUtils.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/FileStatusUtils.java new file mode 100644 index 0000000000000..c4af8618614a1 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/FileStatusUtils.java @@ -0,0 +1,76 @@ +/** + * 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.yarn.logaggregation.testutils; + +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.yarn.api.records.ApplicationId; +import org.apache.hadoop.yarn.logaggregation.LogAggregationUtils; + +public class FileStatusUtils { + public static PathWithFileStatus createPathWithFileStatusForAppId(Path remoteRootLogDir, + ApplicationId appId, + String user, String suffix, + long modificationTime) { + Path path = LogAggregationUtils.getRemoteAppLogDir( + remoteRootLogDir, appId, user, suffix); + FileStatus fileStatus = createEmptyFileStatus(modificationTime, path); + return new PathWithFileStatus(path, fileStatus); + } + + public static FileStatus createEmptyFileStatus(long modificationTime, Path path) { + return new FileStatus(0, true, 0, 0, modificationTime, path); + } + + public static PathWithFileStatus createFileLogPathWithFileStatus(Path baseDir, String childDir, + long modificationTime) { + Path logPath = new Path(baseDir, childDir); + FileStatus fStatus = createFileStatusWithLengthForFile(10, modificationTime, logPath); + return new PathWithFileStatus(logPath, fStatus); + } + + public static PathWithFileStatus createDirLogPathWithFileStatus(Path baseDir, String childDir, + long modificationTime) { + Path logPath = new Path(baseDir, childDir); + FileStatus fStatus = createFileStatusWithLengthForDir(10, modificationTime, logPath); + return new PathWithFileStatus(logPath, fStatus); + } + + public static PathWithFileStatus createDirBucketDirLogPathWithFileStatus(Path remoteRootLogPath, + String user, + String suffix, + ApplicationId appId, + long modificationTime) { + Path bucketDir = LogAggregationUtils.getRemoteBucketDir(remoteRootLogPath, user, suffix, appId); + FileStatus fStatus = new FileStatus(0, true, 0, 0, modificationTime, bucketDir); + return new PathWithFileStatus(bucketDir, fStatus); + } + + public static FileStatus createFileStatusWithLengthForFile(long length, + long modificationTime, + Path logPath) { + return new FileStatus(length, false, 1, 1, modificationTime, logPath); + } + + public static FileStatus createFileStatusWithLengthForDir(long length, + long modificationTime, + Path logPath) { + return new FileStatus(length, true, 1, 1, modificationTime, logPath); + } +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/LogAggregationTestcase.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/LogAggregationTestcase.java new file mode 100644 index 0000000000000..f2074f8c8e688 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/LogAggregationTestcase.java @@ -0,0 +1,444 @@ +/** + * 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.yarn.logaggregation.testutils; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.FilterFileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.util.Sets; +import org.apache.hadoop.yarn.api.ApplicationClientProtocol; +import org.apache.hadoop.yarn.api.records.ApplicationId; +import org.apache.hadoop.yarn.logaggregation.AggregatedLogDeletionService; +import org.apache.hadoop.yarn.logaggregation.AggregatedLogDeletionService.LogDeletionTask; +import org.apache.hadoop.yarn.logaggregation.testutils.LogAggregationTestcaseBuilder.AppDescriptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.apache.hadoop.yarn.logaggregation.testutils.FileStatusUtils.*; +import static org.apache.hadoop.yarn.logaggregation.testutils.LogAggregationTestcaseBuilder.NO_TIMEOUT; +import static org.apache.hadoop.yarn.logaggregation.testutils.MockRMClientUtils.createMockRMClient; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class LogAggregationTestcase { + private static final Logger LOG = LoggerFactory.getLogger(LogAggregationTestcase.class); + + private final Configuration conf; + private final long now; + private PathWithFileStatus bucketDir; + private final long bucketDirModTime; + private PathWithFileStatus userDir; + private final String userDirName; + private final long userDirModTime; + private PathWithFileStatus suffixDir; + private final String suffix; + private final String suffixDirName; + private final long suffixDirModTime; + private final String bucketId; + private final Path remoteRootLogPath; + private final Map injectedAppDirDeletionExceptions; + private final List fileControllers; + private final List> additionalAppDirs; + + private final List applicationIds = new ArrayList<>(); + private final int[] runningAppIds; + private final int[] finishedAppIds; + private final List> appFiles = new ArrayList<>(); + private final FileSystem mockFs; + private List appDirs; + private final List appDescriptors; + private AggregatedLogDeletionServiceForTest deletionService; + private ApplicationClientProtocol rmClient; + + public LogAggregationTestcase(LogAggregationTestcaseBuilder builder) throws IOException { + conf = builder.conf; + now = builder.now; + bucketDir = builder.bucketDir; + bucketDirModTime = builder.bucketDirModTime; + userDir = builder.userDir; + userDirName = builder.userDirName; + userDirModTime = builder.userDirModTime; + suffix = builder.suffix; + suffixDir = builder.suffixDir; + suffixDirName = builder.suffixDirName; + suffixDirModTime = builder.suffixDirModTime; + bucketId = builder.bucketId; + appDescriptors = builder.apps; + runningAppIds = builder.runningAppIds; + finishedAppIds = builder.finishedAppIds; + remoteRootLogPath = builder.remoteRootLogPath; + injectedAppDirDeletionExceptions = builder.injectedAppDirDeletionExceptions; + fileControllers = builder.fileControllers; + additionalAppDirs = builder.additionalAppDirs; + + mockFs = ((FilterFileSystem) builder.rootFs).getRawFileSystem(); + validateAppControllers(); + setupMocks(); + + setupDeletionService(); + } + + private void validateAppControllers() { + Set controllers = appDescriptors.stream() + .map(a -> a.fileController) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Set availableControllers = fileControllers != null ? + new HashSet<>(this.fileControllers) : Sets.newHashSet(); + Set difference = Sets.difference(controllers, availableControllers); + if (!difference.isEmpty()) { + throw new IllegalStateException(String.format("Invalid controller defined!" + + " Available: %s, Actual: %s", availableControllers, controllers)); + } + } + + private void setupMocks() throws IOException { + createApplicationsByDescriptors(); + + List rootPaths = determineRootPaths(); + for (Path rootPath : rootPaths) { + String controllerName = rootPath.getName(); + ApplicationId arbitraryAppIdForBucketDir = this.applicationIds.get(0); + userDir = createDirLogPathWithFileStatus(rootPath, userDirName, userDirModTime); + suffixDir = createDirLogPathWithFileStatus(userDir.path, suffixDirName, suffixDirModTime); + if (bucketId != null) { + bucketDir = createDirLogPathWithFileStatus(suffixDir.path, bucketId, bucketDirModTime); + } else { + bucketDir = createDirBucketDirLogPathWithFileStatus(rootPath, userDirName, suffix, + arbitraryAppIdForBucketDir, bucketDirModTime); + } + setupListStatusForPath(rootPath, userDir); + initFileSystemListings(controllerName); + } + } + + private List determineRootPaths() { + List rootPaths = new ArrayList<>(); + if (fileControllers != null && !fileControllers.isEmpty()) { + for (String fileController : fileControllers) { + //Generic path: //bucket-// + // / + + //remoteRootLogPath: / + //example: mockfs://foo/tmp/logs/ + + //userDir: // + //example: mockfs://foo/tmp/logs/me/ + + //suffixDir: //bucket-/ + //example: mockfs://foo/tmp/logs/me/bucket-logs/ + + //bucketDir: //bucket-// + //example: mockfs://foo/tmp/logs/me/bucket-logs/0001/ + + //remoteRootLogPath with controller: / + //example: mockfs://foo/tmp/logs/IFile + rootPaths.add(new Path(remoteRootLogPath, fileController)); + } + } else { + rootPaths.add(remoteRootLogPath); + } + return rootPaths; + } + + private void initFileSystemListings(String controllerName) throws IOException { + setupListStatusForPath(userDir, suffixDir); + setupListStatusForPath(suffixDir, bucketDir); + setupListStatusForPath(bucketDir, appDirs.stream() + .filter(app -> app.path.toString().contains(controllerName)) + .map(app -> app.fileStatus) + .toArray(FileStatus[]::new)); + + for (Pair appDirPair : additionalAppDirs) { + PathWithFileStatus appDir = createDirLogPathWithFileStatus(bucketDir.path, + appDirPair.getLeft(), appDirPair.getRight()); + setupListStatusForPath(appDir, new FileStatus[] {}); + } + } + + private void createApplicationsByDescriptors() throws IOException { + int len = appDescriptors.size(); + appDirs = new ArrayList<>(len); + + for (int i = 0; i < len; i++) { + AppDescriptor appDesc = appDescriptors.get(i); + ApplicationId applicationId = appDesc.createApplicationId(now, i + 1); + applicationIds.add(applicationId); + Path basePath = this.remoteRootLogPath; + if (appDesc.fileController != null) { + basePath = new Path(basePath, appDesc.fileController); + } + + PathWithFileStatus appDir = createPathWithFileStatusForAppId( + basePath, applicationId, userDirName, suffix, appDesc.modTimeOfAppDir); + LOG.debug("Created application with ID '{}' to path '{}'", applicationId, appDir.path); + appDirs.add(appDir); + addAppChildrenFiles(appDesc, appDir); + } + + setupFsMocksForAppsAndChildrenFiles(); + + for (Map.Entry e : injectedAppDirDeletionExceptions.entrySet()) { + when(mockFs.delete(this.appDirs.get(e.getKey()).path, true)).thenThrow(e.getValue()); + } + } + + private void setupFsMocksForAppsAndChildrenFiles() throws IOException { + for (int i = 0; i < appDirs.size(); i++) { + List appChildren = appFiles.get(i); + Path appPath = appDirs.get(i).path; + setupListStatusForPath(appPath, + appChildren.stream() + .map(child -> child.fileStatus) + .toArray(FileStatus[]::new)); + } + } + + private void setupListStatusForPath(Path dir, PathWithFileStatus pathWithFileStatus) + throws IOException { + setupListStatusForPath(dir, new FileStatus[]{pathWithFileStatus.fileStatus}); + } + + private void setupListStatusForPath(PathWithFileStatus dir, PathWithFileStatus pathWithFileStatus) + throws IOException { + setupListStatusForPath(dir, new FileStatus[]{pathWithFileStatus.fileStatus}); + } + + private void setupListStatusForPath(Path dir, FileStatus[] fileStatuses) throws IOException { + LOG.debug("Setting up listStatus. Parent: {}, files: {}", dir, fileStatuses); + when(mockFs.listStatus(dir)).thenReturn(fileStatuses); + } + + private void setupListStatusForPath(PathWithFileStatus dir, FileStatus[] fileStatuses) + throws IOException { + LOG.debug("Setting up listStatus. Parent: {}, files: {}", dir.path, fileStatuses); + when(mockFs.listStatus(dir.path)).thenReturn(fileStatuses); + } + + private void setupDeletionService() { + List finishedApps = createFinishedAppsList(); + List runningApps = createRunningAppsList(); + deletionService = new AggregatedLogDeletionServiceForTest(runningApps, finishedApps, conf); + } + + public LogAggregationTestcase startDeletionService() { + deletionService.init(conf); + deletionService.start(); + return this; + } + + private List createRunningAppsList() { + List runningApps = new ArrayList<>(); + for (int i : runningAppIds) { + ApplicationId appId = this.applicationIds.get(i - 1); + runningApps.add(appId); + } + return runningApps; + } + + private List createFinishedAppsList() { + List finishedApps = new ArrayList<>(); + for (int i : finishedAppIds) { + ApplicationId appId = this.applicationIds.get(i - 1); + finishedApps.add(appId); + } + return finishedApps; + } + + public LogAggregationTestcase runDeletionTask(long retentionSeconds) throws Exception { + List finishedApps = createFinishedAppsList(); + List runningApps = createRunningAppsList(); + rmClient = createMockRMClient(finishedApps, runningApps); + List tasks = deletionService.createLogDeletionTasks(conf, retentionSeconds, + rmClient); + for (LogDeletionTask deletionTask : tasks) { + deletionTask.run(); + } + + return this; + } + + private void addAppChildrenFiles(AppDescriptor appDesc, PathWithFileStatus appDir) { + List appChildren = new ArrayList<>(); + for (Pair fileWithModDate : appDesc.filesWithModDate) { + PathWithFileStatus appChildFile = createFileLogPathWithFileStatus(appDir.path, + fileWithModDate.getLeft(), + fileWithModDate.getRight()); + appChildren.add(appChildFile); + } + this.appFiles.add(appChildren); + } + + public LogAggregationTestcase verifyAppDirsDeleted(long timeout, int... ids) throws IOException { + for (int id : ids) { + verifyAppDirDeleted(id, timeout); + } + return this; + } + + public LogAggregationTestcase verifyAppDirsNotDeleted(long timeout, int... ids) + throws IOException { + for (int id : ids) { + verifyAppDirNotDeleted(id, timeout); + } + return this; + } + + public LogAggregationTestcase verifyAppDirDeleted(int id, long timeout) throws IOException { + verifyAppDirDeletion(id, 1, timeout); + return this; + } + + public LogAggregationTestcase verifyAppDirNotDeleted(int id, long timeout) throws IOException { + verifyAppDirDeletion(id, 0, timeout); + return this; + } + + public LogAggregationTestcase verifyAppFilesDeleted(long timeout, + List> pairs) + throws IOException { + for (Pair pair : pairs) { + verifyAppFileDeleted(pair.getLeft(), pair.getRight(), timeout); + } + return this; + } + + public LogAggregationTestcase verifyAppFilesNotDeleted(long timeout, + List> pairs) + throws IOException { + for (Pair pair : pairs) { + verifyAppFileNotDeleted(pair.getLeft(), pair.getRight(), timeout); + } + return this; + } + + public LogAggregationTestcase verifyAppFileDeleted(int id, int fileNo, long timeout) + throws IOException { + verifyAppFileDeletion(id, fileNo, 1, timeout); + return this; + } + + public LogAggregationTestcase verifyAppFileNotDeleted(int id, int fileNo, long timeout) + throws IOException { + verifyAppFileDeletion(id, fileNo, 0, timeout); + return this; + } + + private void verifyAppDirDeletion(int id, int times, long timeout) throws IOException { + if (timeout == NO_TIMEOUT) { + verify(mockFs, times(times)).delete(this.appDirs.get(id - 1).path, true); + } else { + verify(mockFs, timeout(timeout).times(times)).delete(this.appDirs.get(id - 1).path, true); + } + } + + private void verifyAppFileDeletion(int appId, int fileNo, int times, long timeout) + throws IOException { + List childrenFiles = this.appFiles.get(appId - 1); + PathWithFileStatus file = childrenFiles.get(fileNo - 1); + verify(mockFs, timeout(timeout).times(times)).delete(file.path, true); + } + + private void verifyMockRmClientWasClosedNTimes(int expectedRmClientCloses) + throws IOException { + ApplicationClientProtocol mockRMClient; + if (deletionService != null) { + mockRMClient = deletionService.getMockRMClient(); + } else { + mockRMClient = rmClient; + } + verify((Closeable)mockRMClient, times(expectedRmClientCloses)).close(); + } + + public void teardown(int expectedRmClientCloses) throws IOException { + deletionService.stop(); + verifyMockRmClientWasClosedNTimes(expectedRmClientCloses); + } + + public LogAggregationTestcase refreshLogRetentionSettings() throws IOException { + deletionService.refreshLogRetentionSettings(); + return this; + } + + public AggregatedLogDeletionService getDeletionService() { + return deletionService; + } + + public LogAggregationTestcase verifyCheckIntervalMilliSecondsEqualTo( + int checkIntervalMilliSeconds) { + assertEquals(checkIntervalMilliSeconds, deletionService.getCheckIntervalMsecs()); + return this; + } + + public LogAggregationTestcase verifyCheckIntervalMilliSecondsNotEqualTo( + int checkIntervalMilliSeconds) { + assertTrue(checkIntervalMilliSeconds != deletionService.getCheckIntervalMsecs()); + return this; + } + + public LogAggregationTestcase verifyAnyPathListedAtLeast(int atLeast, long timeout) + throws IOException { + verify(mockFs, timeout(timeout).atLeast(atLeast)).listStatus(any(Path.class)); + return this; + } + + public LogAggregationTestcase changeModTimeOfApp(int appId, long modTime) { + PathWithFileStatus appDir = appDirs.get(appId - 1); + appDir.changeModificationTime(modTime); + return this; + } + + public LogAggregationTestcase changeModTimeOfAppLogDir(int appId, int fileNo, long modTime) { + List childrenFiles = this.appFiles.get(appId - 1); + PathWithFileStatus file = childrenFiles.get(fileNo - 1); + file.changeModificationTime(modTime); + return this; + } + + public LogAggregationTestcase changeModTimeOfBucketDir(long modTime) { + bucketDir.changeModificationTime(modTime); + return this; + } + + public LogAggregationTestcase reinitAllPaths() throws IOException { + List rootPaths = determineRootPaths(); + for (Path rootPath : rootPaths) { + String controllerName = rootPath.getName(); + initFileSystemListings(controllerName); + } + setupFsMocksForAppsAndChildrenFiles(); + return this; + } + +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/LogAggregationTestcaseBuilder.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/LogAggregationTestcaseBuilder.java new file mode 100644 index 0000000000000..f532dddce0fd9 --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/LogAggregationTestcaseBuilder.java @@ -0,0 +1,172 @@ +/** + * 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.yarn.logaggregation.testutils; + +import org.apache.commons.compress.utils.Lists; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.security.AccessControlException; +import org.apache.hadoop.yarn.api.records.ApplicationId; +import org.apache.hadoop.yarn.logaggregation.LogAggregationUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.apache.hadoop.yarn.logaggregation.TestAggregatedLogDeletionService.ALL_FILE_CONTROLLER_NAMES; + +public class LogAggregationTestcaseBuilder { + public static final long NO_TIMEOUT = -1; + final long now; + final Configuration conf; + Path remoteRootLogPath; + String suffix; + String userDirName; + long userDirModTime; + final Map injectedAppDirDeletionExceptions = new HashMap<>(); + List fileControllers; + long suffixDirModTime; + long bucketDirModTime; + String suffixDirName; + List apps = Lists.newArrayList(); + int[] finishedAppIds; + int[] runningAppIds; + PathWithFileStatus userDir; + PathWithFileStatus suffixDir; + PathWithFileStatus bucketDir; + String bucketId; + List> additionalAppDirs = new ArrayList<>(); + FileSystem rootFs; + + public LogAggregationTestcaseBuilder(Configuration conf) { + this.conf = conf; + this.now = System.currentTimeMillis(); + } + + public static LogAggregationTestcaseBuilder create(Configuration conf) { + return new LogAggregationTestcaseBuilder(conf); + } + + public LogAggregationTestcaseBuilder withRootPath(String root) throws IOException { + Path rootPath = new Path(root); + rootFs = rootPath.getFileSystem(conf); + return this; + } + + public LogAggregationTestcaseBuilder withRemoteRootLogPath(String remoteRootLogDir) { + remoteRootLogPath = new Path(remoteRootLogDir); + return this; + } + + public LogAggregationTestcaseBuilder withUserDir(String userDirName, long modTime) { + this.userDirName = userDirName; + this.userDirModTime = modTime; + return this; + } + + public LogAggregationTestcaseBuilder withSuffixDir(String suffix, long modTime) { + this.suffix = suffix; + this.suffixDirName = LogAggregationUtils.getBucketSuffix() + suffix; + this.suffixDirModTime = modTime; + return this; + } + + /** + * Bucket dir paths will be generated later. + * @param modTime The modification time + * @return The builder + */ + public LogAggregationTestcaseBuilder withBucketDir(long modTime) { + this.bucketDirModTime = modTime; + return this; + } + + public LogAggregationTestcaseBuilder withBucketDir(long modTime, String bucketId) { + this.bucketDirModTime = modTime; + this.bucketId = bucketId; + return this; + } + + public final LogAggregationTestcaseBuilder withApps(List apps) { + this.apps = apps; + return this; + } + + public LogAggregationTestcaseBuilder withFinishedApps(int... apps) { + this.finishedAppIds = apps; + return this; + } + + public LogAggregationTestcaseBuilder withRunningApps(int... apps) { + this.runningAppIds = apps; + return this; + } + + public LogAggregationTestcaseBuilder withBothFileControllers() { + this.fileControllers = ALL_FILE_CONTROLLER_NAMES; + return this; + } + + public LogAggregationTestcaseBuilder withAdditionalAppDirs(List> appDirs) { + this.additionalAppDirs = appDirs; + return this; + } + + public LogAggregationTestcaseBuilder injectExceptionForAppDirDeletion(int... indices) { + for (int i : indices) { + AccessControlException e = new AccessControlException("Injected Error\nStack Trace :("); + this.injectedAppDirDeletionExceptions.put(i, e); + } + return this; + } + + public LogAggregationTestcase build() throws IOException { + return new LogAggregationTestcase(this); + } + + public static final class AppDescriptor { + final long modTimeOfAppDir; + List> filesWithModDate = new ArrayList<>(); + String fileController; + + public AppDescriptor(long modTimeOfAppDir) { + this.modTimeOfAppDir = modTimeOfAppDir; + } + + public AppDescriptor(long modTimeOfAppDir, List> filesWithModDate) { + this.modTimeOfAppDir = modTimeOfAppDir; + this.filesWithModDate = filesWithModDate; + } + + public AppDescriptor(String fileController, long modTimeOfAppDir, + List> filesWithModDate) { + this(modTimeOfAppDir, filesWithModDate); + this.fileController = fileController; + } + + + public ApplicationId createApplicationId(long now, int id) { + return ApplicationId.newInstance(now, id); + } + } +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/MockRMClientUtils.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/MockRMClientUtils.java new file mode 100644 index 0000000000000..6eb1eb1ecbe0b --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/MockRMClientUtils.java @@ -0,0 +1,74 @@ +/** + * 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.yarn.logaggregation.testutils; + +import org.apache.hadoop.test.MockitoUtil; +import org.apache.hadoop.yarn.api.ApplicationClientProtocol; +import org.apache.hadoop.yarn.api.protocolrecords.GetApplicationReportRequest; +import org.apache.hadoop.yarn.api.protocolrecords.GetApplicationReportResponse; +import org.apache.hadoop.yarn.api.records.ApplicationId; +import org.apache.hadoop.yarn.api.records.ApplicationReport; +import org.apache.hadoop.yarn.api.records.YarnApplicationState; + +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MockRMClientUtils { + public static ApplicationClientProtocol createMockRMClient( + List finishedApplications, + List runningApplications) throws Exception { + final ApplicationClientProtocol mockProtocol = + MockitoUtil.mockProtocol(ApplicationClientProtocol.class); + if (finishedApplications != null && !finishedApplications.isEmpty()) { + for (ApplicationId appId : finishedApplications) { + GetApplicationReportRequest request = GetApplicationReportRequest.newInstance(appId); + GetApplicationReportResponse response = createApplicationReportWithFinishedApplication(); + when(mockProtocol.getApplicationReport(request)).thenReturn(response); + } + } + if (runningApplications != null && !runningApplications.isEmpty()) { + for (ApplicationId appId : runningApplications) { + GetApplicationReportRequest request = GetApplicationReportRequest.newInstance(appId); + GetApplicationReportResponse response = createApplicationReportWithRunningApplication(); + when(mockProtocol.getApplicationReport(request)).thenReturn(response); + } + } + return mockProtocol; + } + + public static GetApplicationReportResponse createApplicationReportWithRunningApplication() { + ApplicationReport report = mock(ApplicationReport.class); + when(report.getYarnApplicationState()).thenReturn( + YarnApplicationState.RUNNING); + GetApplicationReportResponse response = + mock(GetApplicationReportResponse.class); + when(response.getApplicationReport()).thenReturn(report); + return response; + } + + public static GetApplicationReportResponse createApplicationReportWithFinishedApplication() { + ApplicationReport report = mock(ApplicationReport.class); + when(report.getYarnApplicationState()).thenReturn(YarnApplicationState.FINISHED); + GetApplicationReportResponse response = mock(GetApplicationReportResponse.class); + when(response.getApplicationReport()).thenReturn(report); + return response; + } +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/PathWithFileStatus.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/PathWithFileStatus.java new file mode 100644 index 0000000000000..5e743f1138e4e --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/test/java/org/apache/hadoop/yarn/logaggregation/testutils/PathWithFileStatus.java @@ -0,0 +1,45 @@ +/** + * 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.yarn.logaggregation.testutils; + +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; + +public class PathWithFileStatus { + public final Path path; + public FileStatus fileStatus; + + public PathWithFileStatus(Path path, FileStatus fileStatus) { + this.path = path; + this.fileStatus = fileStatus; + } + + public void changeModificationTime(long modTime) { + fileStatus = new FileStatus(fileStatus.getLen(), fileStatus.isDirectory(), + fileStatus.getReplication(), + fileStatus.getBlockSize(), modTime, fileStatus.getPath()); + } + + @Override + public String toString() { + return "PathWithFileStatus{" + + "path=" + path + + '}'; + } +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/AbstractLeafQueue.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/AbstractLeafQueue.java index ac5c8a15167f1..08fedb578cab9 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/AbstractLeafQueue.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/AbstractLeafQueue.java @@ -578,6 +578,8 @@ public void submitApplicationAttempt(FiCaSchedulerApp application, public void submitApplicationAttempt(FiCaSchedulerApp application, String userName, boolean isMoveApp) { // Careful! Locking order is important! + boolean isAppAlreadySubmitted = applicationAttemptMap.containsKey( + application.getApplicationAttemptId()); writeLock.lock(); try { // TODO, should use getUser, use this method just to avoid UT failure @@ -591,7 +593,7 @@ public void submitApplicationAttempt(FiCaSchedulerApp application, } // We don't want to update metrics for move app - if (!isMoveApp) { + if (!isMoveApp && !isAppAlreadySubmitted) { boolean unmanagedAM = application.getAppSchedulingInfo() != null && application.getAppSchedulingInfo().isUnmanagedAM(); usageTracker.getMetrics().submitAppAttempt(userName, unmanagedAM); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/conf/LeveldbConfigurationStore.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/conf/LeveldbConfigurationStore.java index a351799872fe6..6aa37f399e41a 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/conf/LeveldbConfigurationStore.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/conf/LeveldbConfigurationStore.java @@ -204,8 +204,8 @@ public void logMutation(LogMutation logMutation) throws IOException { @Override public void confirmMutation(LogMutation pendingMutation, boolean isValid) { - WriteBatch updateBatch = db.createWriteBatch(); if (isValid) { + WriteBatch updateBatch = db.createWriteBatch(); for (Map.Entry changes : pendingMutation.getUpdates().entrySet()) { if (changes.getValue() == null || changes.getValue().isEmpty()) { @@ -215,8 +215,8 @@ public void confirmMutation(LogMutation pendingMutation, } } increaseConfigVersion(); + db.write(updateBatch); } - db.write(updateBatch); } private byte[] serLogMutations(LinkedList mutations) throws diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/CapacitySchedulerPage.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/CapacitySchedulerPage.java index 9ce37ac028e63..1018dc818dbf8 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/CapacitySchedulerPage.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/CapacitySchedulerPage.java @@ -103,7 +103,7 @@ private void renderLeafQueueInfoWithPartition(Block html) { ? NodeLabel.DEFAULT_NODE_LABEL_PARTITION : nodeLabel; // first display the queue's label specific details : ResponseInfo ri = - info("\'" + lqinfo.getQueuePath().substring(5) + info("\'" + lqinfo.getQueuePath() + "\' Queue Status for Partition \'" + nodeLabelDisplay + "\'"); renderQueueCapacityInfo(ri, nodeLabel); html.__(InfoBlock.class); @@ -113,7 +113,7 @@ private void renderLeafQueueInfoWithPartition(Block html) { // second display the queue specific details : ri = - info("\'" + lqinfo.getQueuePath().substring(5) + "\' Queue Status") + info("\'" + lqinfo.getQueuePath() + "\' Queue Status") .__("Queue State:", lqinfo.getQueueState()); renderCommonLeafQueueInfo(ri); @@ -125,7 +125,7 @@ private void renderLeafQueueInfoWithPartition(Block html) { private void renderLeafQueueInfoWithoutParition(Block html) { ResponseInfo ri = - info("\'" + lqinfo.getQueuePath().substring(5) + "\' Queue Status") + info("\'" + lqinfo.getQueuePath() + "\' Queue Status") .__("Queue State:", lqinfo.getQueueState()); renderQueueCapacityInfo(ri, ""); renderCommonLeafQueueInfo(ri); @@ -348,7 +348,7 @@ public void render(Block html) { span().$style(join(width(usedCapPercent), ";font-size:1px;left:0%;", absUsedCap > absCap ? Q_OVER : Q_UNDER)). __('.').__(). - span(".q", "Queue: "+info.getQueuePath().substring(5)).__(). + span(".q", info.getQueuePath()).__(). span().$class("qstats").$style(left(Q_STATS_POS)). __(join(percent(used), " used")).__(); @@ -492,7 +492,7 @@ public void render(Block html) { a(_Q).$style(width(Q_MAX_WIDTH)). span().$style(join(width(used), ";left:0%;", used > 1 ? Q_OVER : Q_UNDER)).__(".").__(). - span(".q", "Queue: root").__(). + span(".q", "root").__(). span().$class("qstats").$style(left(Q_STATS_POS)). __(join(percent(used), " used")).__(). __(QueueBlock.class).__(); @@ -522,7 +522,7 @@ public void render(Block html) { a(_Q).$style(width(Q_MAX_WIDTH)). span().$style(join(width(used), ";left:0%;", used > 1 ? Q_OVER : Q_UNDER)).__(".").__(). - span(".q", "Queue: root").__(). + span(".q", "root").__(). span().$class("qstats").$style(left(Q_STATS_POS)). __(join(percent(used), " used")).__(). __(QueueBlock.class).__().__(); @@ -656,12 +656,9 @@ public void render(HtmlBlock.Block html) { " }", " });", " $('#cs').bind('select_node.jstree', function(e, data) {", - " var q = $('.q', data.rslt.obj).first().text();", - " if (q == 'Queue: root') q = '';", - " else {", - " q = q.substr(q.lastIndexOf(':') + 2);", - " q = '^' + q.substr(q.lastIndexOf('.') + 1) + '$';", - " }", + " var queues = $('.q', data.rslt.obj);", + " var q = '^' + queues.first().text();", + " q += queues.length == 1 ? '$' : '\\\\.';", // Update this filter column index for queue if new columns are added // Current index for queue column is 5 " $('#apps').dataTable().fnFilter(q, 5, true);", diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/TestClientRMTokens.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/TestClientRMTokens.java index f3c0c2a85a860..556fd5bdf00d8 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/TestClientRMTokens.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/TestClientRMTokens.java @@ -18,7 +18,6 @@ package org.apache.hadoop.yarn.server.resourcemanager; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -36,6 +35,7 @@ import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; +import org.apache.hadoop.test.LambdaTestUtils; import org.apache.hadoop.thirdparty.protobuf.InvalidProtocolBufferException; import org.apache.hadoop.io.DataOutputBuffer; import org.apache.hadoop.net.NetUtils; @@ -111,7 +111,7 @@ public void resetSecretManager() { } @Test - public void testDelegationToken() throws IOException, InterruptedException { + public void testDelegationToken() throws Exception { final YarnConfiguration conf = new YarnConfiguration(); conf.set(YarnConfiguration.RM_PRINCIPAL, "testuser/localhost@apache.org"); @@ -198,14 +198,11 @@ public void testDelegationToken() throws IOException, InterruptedException { } Thread.sleep(50l); LOG.info("At time: " + System.currentTimeMillis() + ", token should be invalid"); - // Token should have expired. - try { - clientRMWithDT.getNewApplication(request); - fail("Should not have succeeded with an expired token"); - } catch (Exception e) { - assertEquals(InvalidToken.class.getName(), e.getClass().getName()); - assertTrue(e.getMessage().contains("is expired")); - } + // Token should have expired. + final ApplicationClientProtocol finalClientRMWithDT = clientRMWithDT; + final GetNewApplicationRequest finalRequest = request; + LambdaTestUtils.intercept(InvalidToken.class, "Token has expired", + () -> finalClientRMWithDT.getNewApplication(finalRequest)); // Test cancellation // Stop the existing proxy, start another. diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/TestCapacitySchedulerApps.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/TestCapacitySchedulerApps.java index ea22c24b35527..d192e7dcc6933 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/TestCapacitySchedulerApps.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/TestCapacitySchedulerApps.java @@ -18,6 +18,7 @@ package org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity; +import java.util.ArrayList; import java.util.List; import org.apache.hadoop.conf.Configuration; @@ -116,6 +117,8 @@ public class TestCapacitySchedulerApps { + public static final int MAX_PARALLEL_APPS = 5; + public static final String USER_0 = "user_0"; private ResourceManager resourceManager = null; private RMContext mockContext; @@ -237,18 +240,7 @@ public void testKillAllAppsInvalidSource() throws Exception { YarnScheduler scheduler = rm.getResourceScheduler(); // submit an app - MockRMAppSubmissionData data = - MockRMAppSubmissionData.Builder.createWithMemory(GB, rm) - .withAppName("test-move-1") - .withUser("user_0") - .withAcls(null) - .withQueue("a1") - .withUnmanagedAM(false) - .build(); - RMApp app = MockRMAppSubmitter.submit(rm, data); - ApplicationAttemptId appAttemptId = - rm.getApplicationReport(app.getApplicationId()) - .getCurrentApplicationAttemptId(); + ApplicationAttemptId appAttemptId = submitApp(rm); // check preconditions List appsInA1 = scheduler.getAppsInQueue("a1"); @@ -1020,18 +1012,7 @@ public void testMoveAllApps() throws Exception { (AbstractYarnScheduler) rm.getResourceScheduler(); // submit an app - MockRMAppSubmissionData data = - MockRMAppSubmissionData.Builder.createWithMemory(GB, rm) - .withAppName("test-move-1") - .withUser("user_0") - .withAcls(null) - .withQueue("a1") - .withUnmanagedAM(false) - .build(); - RMApp app = MockRMAppSubmitter.submit(rm, data); - ApplicationAttemptId appAttemptId = - rm.getApplicationReport(app.getApplicationId()) - .getCurrentApplicationAttemptId(); + ApplicationAttemptId appAttemptId = submitApp(rm); // check preconditions assertOneAppInQueue(scheduler, "a1"); @@ -1057,23 +1038,61 @@ public void testMoveAllApps() throws Exception { } @Test - public void testMoveAllAppsInvalidDestination() throws Exception { + public void testMaxParallelAppsPendingQueueMetrics() throws Exception { MockRM rm = setUpMove(); ResourceScheduler scheduler = rm.getResourceScheduler(); + CapacityScheduler cs = (CapacityScheduler) scheduler; + cs.getQueueContext().getConfiguration().setInt(CapacitySchedulerConfiguration.getQueuePrefix(A1) + + CapacitySchedulerConfiguration.MAX_PARALLEL_APPLICATIONS, MAX_PARALLEL_APPS); + cs.reinitialize(cs.getQueueContext().getConfiguration(), mockContext); + List attemptIds = new ArrayList<>(); + + for (int i = 0; i < 2 * MAX_PARALLEL_APPS; i++) { + attemptIds.add(submitApp(rm)); + } + + // Finish first batch to allow the other batch to run + for (int i = 0; i < MAX_PARALLEL_APPS; i++) { + cs.handle(new AppAttemptRemovedSchedulerEvent(attemptIds.get(i), + RMAppAttemptState.FINISHED, true)); + } + + // Finish the remaining apps + for (int i = MAX_PARALLEL_APPS; i < 2 * MAX_PARALLEL_APPS; i++) { + cs.handle(new AppAttemptRemovedSchedulerEvent(attemptIds.get(i), + RMAppAttemptState.FINISHED, true)); + } + + Assert.assertEquals("No pending app should remain for root queue", 0, + cs.getRootQueueMetrics().getAppsPending()); + Assert.assertEquals("No running application should remain for root queue", 0, + cs.getRootQueueMetrics().getAppsRunning()); + + rm.stop(); + } + private ApplicationAttemptId submitApp(MockRM rm) throws Exception { // submit an app MockRMAppSubmissionData data = MockRMAppSubmissionData.Builder.createWithMemory(GB, rm) .withAppName("test-move-1") - .withUser("user_0") + .withUser(USER_0) .withAcls(null) .withQueue("a1") .withUnmanagedAM(false) .build(); RMApp app = MockRMAppSubmitter.submit(rm, data); - ApplicationAttemptId appAttemptId = - rm.getApplicationReport(app.getApplicationId()) - .getCurrentApplicationAttemptId(); + return rm.getApplicationReport(app.getApplicationId()) + .getCurrentApplicationAttemptId(); + } + + @Test + public void testMoveAllAppsInvalidDestination() throws Exception { + MockRM rm = setUpMove(); + ResourceScheduler scheduler = rm.getResourceScheduler(); + + // submit an app + ApplicationAttemptId appAttemptId = submitApp(rm); // check preconditions assertApps(scheduler, "root", appAttemptId); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/RouterMetrics.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/RouterMetrics.java index b02b3e155fa18..ac37c4ed1b9e6 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/RouterMetrics.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/RouterMetrics.java @@ -81,6 +81,8 @@ public final class RouterMetrics { private MutableGaugeInt numUpdateAppPriorityFailedRetrieved; @Metric("# of updateApplicationPriority failed to be retrieved") private MutableGaugeInt numUpdateAppTimeoutsFailedRetrieved; + @Metric("# of signalToContainer failed to be retrieved") + private MutableGaugeInt numSignalToContainerFailedRetrieved; // Aggregate metrics are shared, and don't have to be looked up per call @Metric("Total number of successful Submitted apps and latency(ms)") @@ -126,6 +128,8 @@ public final class RouterMetrics { private MutableRate totalSucceededUpdateAppPriorityRetrieved; @Metric("Total number of successful Retrieved updateApplicationTimeouts and latency(ms)") private MutableRate totalSucceededUpdateAppTimeoutsRetrieved; + @Metric("Total number of successful Retrieved signalToContainer and latency(ms)") + private MutableRate totalSucceededSignalToContainerRetrieved; /** * Provide quantile counters for all latencies. @@ -150,6 +154,7 @@ public final class RouterMetrics { private MutableQuantiles failAppAttemptLatency; private MutableQuantiles updateAppPriorityLatency; private MutableQuantiles updateAppTimeoutsLatency; + private MutableQuantiles signalToContainerLatency; private static volatile RouterMetrics instance = null; private static MetricsRegistry registry; @@ -228,6 +233,10 @@ private RouterMetrics() { updateAppTimeoutsLatency = registry.newQuantiles("updateApplicationTimeoutsLatency", "latency of update application timeouts", "ops", "latency", 10); + + signalToContainerLatency = + registry.newQuantiles("signalToContainerLatency", + "latency of signal to container timeouts", "ops", "latency", 10); } public static RouterMetrics getMetrics() { @@ -349,6 +358,11 @@ public long getNumSucceededUpdateAppTimeoutsRetrieved() { return totalSucceededUpdateAppTimeoutsRetrieved.lastStat().numSamples(); } + @VisibleForTesting + public long getNumSucceededSignalToContainerRetrieved() { + return totalSucceededSignalToContainerRetrieved.lastStat().numSamples(); + } + @VisibleForTesting public double getLatencySucceededAppsCreated() { return totalSucceededAppsCreated.lastStat().mean(); @@ -449,6 +463,11 @@ public double getLatencySucceededUpdateAppTimeoutsRetrieved() { return totalSucceededUpdateAppTimeoutsRetrieved.lastStat().mean(); } + @VisibleForTesting + public double getLatencySucceededSignalToContainerRetrieved() { + return totalSucceededSignalToContainerRetrieved.lastStat().mean(); + } + @VisibleForTesting public int getAppsFailedCreated() { return numAppsFailedCreated.value(); @@ -549,6 +568,11 @@ public int getUpdateApplicationTimeoutsFailedRetrieved() { return numUpdateAppTimeoutsFailedRetrieved.value(); } + @VisibleForTesting + public int getSignalToContainerFailedRetrieved() { + return numSignalToContainerFailedRetrieved.value(); + } + public void succeededAppsCreated(long duration) { totalSucceededAppsCreated.add(duration); getNewApplicationLatency.add(duration); @@ -649,6 +673,11 @@ public void succeededUpdateAppTimeoutsRetrieved(long duration) { updateAppTimeoutsLatency.add(duration); } + public void succeededSignalToContainerRetrieved(long duration) { + totalSucceededSignalToContainerRetrieved.add(duration); + signalToContainerLatency.add(duration); + } + public void incrAppsFailedCreated() { numAppsFailedCreated.incr(); } @@ -728,4 +757,8 @@ public void incrUpdateAppPriorityFailedRetrieved() { public void incrUpdateApplicationTimeoutsRetrieved() { numUpdateAppTimeoutsFailedRetrieved.incr(); } + + public void incrSignalToContainerFailedRetrieved() { + numSignalToContainerFailedRetrieved.incr(); + } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/clientrm/FederationClientInterceptor.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/clientrm/FederationClientInterceptor.java index fec62d4b0804f..6cc317242cd73 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/clientrm/FederationClientInterceptor.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/clientrm/FederationClientInterceptor.java @@ -1304,7 +1304,43 @@ public UpdateApplicationPriorityResponse updateApplicationPriority( @Override public SignalContainerResponse signalToContainer( SignalContainerRequest request) throws YarnException, IOException { - throw new NotImplementedException("Code is not implemented"); + if (request == null || request.getContainerId() == null + || request.getCommand() == null) { + routerMetrics.incrSignalToContainerFailedRetrieved(); + RouterServerUtil.logAndThrowException( + "Missing signalToContainer request or containerId " + + "or command information.", null); + } + + long startTime = clock.getTime(); + SubClusterId subClusterId = null; + ApplicationId applicationId = + request.getContainerId().getApplicationAttemptId().getApplicationId(); + try { + subClusterId = getApplicationHomeSubCluster(applicationId); + } catch (YarnException ex) { + routerMetrics.incrSignalToContainerFailedRetrieved(); + RouterServerUtil.logAndThrowException("Application " + applicationId + + " does not exist in FederationStateStore.", ex); + } + + ApplicationClientProtocol clientRMProxy = getClientRMProxyForSubCluster(subClusterId); + SignalContainerResponse response = null; + try { + response = clientRMProxy.signalToContainer(request); + } catch (Exception ex) { + RouterServerUtil.logAndThrowException("Unable to signal to container for " + + applicationId + " from SubCluster " + subClusterId.getId(), ex); + } + + if (response == null) { + LOG.error("No response when signal to container of " + + "the applicationId {} to SubCluster {}.", applicationId, subClusterId.getId()); + } + + long stopTime = clock.getTime(); + routerMetrics.succeededSignalToContainerRetrieved(stopTime - startTime); + return response; } @Override diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/TestRouterMetrics.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/TestRouterMetrics.java index 4b1049e8b647e..eddd2a0ab4816 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/TestRouterMetrics.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/TestRouterMetrics.java @@ -413,6 +413,11 @@ public void getUpdateApplicationTimeouts() { LOG.info("Mocked: failed updateApplicationTimeouts call"); metrics.incrUpdateApplicationTimeoutsRetrieved(); } + + public void getSignalContainer() { + LOG.info("Mocked: failed signalContainer call"); + metrics.incrSignalToContainerFailedRetrieved(); + } } // Records successes for all calls @@ -523,6 +528,11 @@ public void getUpdateApplicationTimeouts(long duration) { LOG.info("Mocked: successful updateApplicationTimeouts call with duration {}", duration); metrics.succeededUpdateAppTimeoutsRetrieved(duration); } + + public void getSignalToContainerTimeouts(long duration) { + LOG.info("Mocked: successful signalToContainer call with duration {}", duration); + metrics.succeededSignalToContainerRetrieved(duration); + } } @Test @@ -806,4 +816,27 @@ public void testUpdateAppTimeoutsFailed() { metrics.getUpdateApplicationTimeoutsFailedRetrieved()); } + @Test + public void testSucceededSignalToContainerRetrieved() { + long totalGoodBefore = metrics.getNumSucceededSignalToContainerRetrieved(); + goodSubCluster.getSignalToContainerTimeouts(150); + Assert.assertEquals(totalGoodBefore + 1, + metrics.getNumSucceededSignalToContainerRetrieved()); + Assert.assertEquals(150, + metrics.getLatencySucceededSignalToContainerRetrieved(), ASSERT_DOUBLE_DELTA); + goodSubCluster.getSignalToContainerTimeouts(300); + Assert.assertEquals(totalGoodBefore + 2, + metrics.getNumSucceededSignalToContainerRetrieved()); + Assert.assertEquals(225, + metrics.getLatencySucceededSignalToContainerRetrieved(), ASSERT_DOUBLE_DELTA); + } + + @Test + public void testSignalToContainerFailed() { + long totalBadBefore = metrics.getSignalToContainerFailedRetrieved(); + badSubCluster.getSignalContainer(); + Assert.assertEquals(totalBadBefore + 1, + metrics.getSignalToContainerFailedRetrieved()); + } + } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/clientrm/TestFederationClientInterceptor.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/clientrm/TestFederationClientInterceptor.java index 9ead9fbe721ed..3037738240266 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/clientrm/TestFederationClientInterceptor.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/clientrm/TestFederationClientInterceptor.java @@ -72,6 +72,8 @@ import org.apache.hadoop.yarn.api.protocolrecords.UpdateApplicationPriorityResponse; import org.apache.hadoop.yarn.api.protocolrecords.UpdateApplicationTimeoutsRequest; import org.apache.hadoop.yarn.api.protocolrecords.UpdateApplicationTimeoutsResponse; +import org.apache.hadoop.yarn.api.protocolrecords.SignalContainerRequest; +import org.apache.hadoop.yarn.api.protocolrecords.SignalContainerResponse; import org.apache.hadoop.yarn.api.records.ApplicationAttemptId; import org.apache.hadoop.yarn.api.records.ApplicationId; import org.apache.hadoop.yarn.api.records.ApplicationSubmissionContext; @@ -83,6 +85,7 @@ import org.apache.hadoop.yarn.api.records.ReservationId; import org.apache.hadoop.yarn.api.records.ContainerId; import org.apache.hadoop.yarn.api.records.ApplicationTimeoutType; +import org.apache.hadoop.yarn.api.records.SignalContainerCommand; import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.exceptions.YarnException; import org.apache.hadoop.yarn.server.federation.policies.manager.UniformBroadcastPolicyManager; @@ -91,6 +94,7 @@ import org.apache.hadoop.yarn.server.federation.utils.FederationStateStoreFacade; import org.apache.hadoop.yarn.server.federation.utils.FederationStateStoreTestUtil; import org.apache.hadoop.yarn.server.resourcemanager.MockRM; +import org.apache.hadoop.yarn.server.resourcemanager.MockNM; import org.apache.hadoop.yarn.server.resourcemanager.ResourceManager; import org.apache.hadoop.yarn.server.resourcemanager.rmapp.RMApp; import org.apache.hadoop.yarn.server.resourcemanager.rmapp.RMAppState; @@ -1056,4 +1060,45 @@ public void testUpdateApplicationTimeouts() throws Exception { Assert.assertNotNull(timeoutsResponse); Assert.assertEquals(appTimeout, responseTimeOut); } + + @Test + public void testSignalContainer() throws Exception { + LOG.info("Test FederationClientInterceptor : Signal Container request."); + + // null request + LambdaTestUtils.intercept(YarnException.class, "Missing signalToContainer request " + + "or containerId or command information.", () -> interceptor.signalToContainer(null)); + + // normal request + ApplicationId appId = + ApplicationId.newInstance(System.currentTimeMillis(), 1); + SubmitApplicationRequest request = mockSubmitApplicationRequest(appId); + + // Submit the application + SubmitApplicationResponse response = interceptor.submitApplication(request); + Assert.assertNotNull(response); + Assert.assertNotNull(stateStoreUtil.queryApplicationHomeSC(appId)); + + SubClusterId subClusterId = interceptor.getApplicationHomeSubCluster(appId); + Assert.assertNotNull(subClusterId); + + MockRM mockRM = interceptor.getMockRMs().get(subClusterId); + mockRM.waitForState(appId, RMAppState.ACCEPTED); + RMApp rmApp = mockRM.getRMContext().getRMApps().get(appId); + mockRM.waitForState(rmApp.getCurrentAppAttempt().getAppAttemptId(), + RMAppAttemptState.SCHEDULED); + MockNM nm = interceptor.getMockNMs().get(subClusterId); + nm.nodeHeartbeat(true); + mockRM.waitForState(rmApp.getCurrentAppAttempt(), RMAppAttemptState.ALLOCATED); + mockRM.sendAMLaunched(rmApp.getCurrentAppAttempt().getAppAttemptId()); + + ContainerId containerId = rmApp.getCurrentAppAttempt().getMasterContainer().getId(); + + SignalContainerRequest signalContainerRequest = + SignalContainerRequest.newInstance(containerId, SignalContainerCommand.GRACEFUL_SHUTDOWN); + SignalContainerResponse signalContainerResponse = + interceptor.signalToContainer(signalContainerRequest); + + Assert.assertNotNull(signalContainerResponse); + } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/clientrm/TestableFederationClientInterceptor.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/clientrm/TestableFederationClientInterceptor.java index 202a286696a21..af1f45924c19c 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/clientrm/TestableFederationClientInterceptor.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/clientrm/TestableFederationClientInterceptor.java @@ -33,6 +33,7 @@ import org.apache.hadoop.yarn.server.federation.store.records.SubClusterId; import org.apache.hadoop.yarn.server.resourcemanager.ClientRMService; import org.apache.hadoop.yarn.server.resourcemanager.MockRM; +import org.apache.hadoop.yarn.server.resourcemanager.MockNM; import org.apache.hadoop.yarn.server.resourcemanager.RMAppManager; import org.apache.hadoop.yarn.server.resourcemanager.RMContext; import org.apache.hadoop.yarn.server.resourcemanager.scheduler.YarnScheduler; @@ -51,6 +52,9 @@ public class TestableFederationClientInterceptor private ConcurrentHashMap mockRMs = new ConcurrentHashMap<>(); + private ConcurrentHashMap mockNMs = + new ConcurrentHashMap<>(); + private List badSubCluster = new ArrayList(); @Override @@ -71,7 +75,8 @@ protected ApplicationClientProtocol getClientRMProxyForSubCluster( mockRM.init(super.getConf()); mockRM.start(); try { - mockRM.registerNode("h1:1234", 1024); + MockNM nm = mockRM.registerNode("127.0.0.1:1234", 8*1024, 4); + mockNMs.put(subClusterId, nm); } catch (Exception e) { Assert.fail(e.getMessage()); } @@ -118,4 +123,8 @@ protected void registerBadSubCluster(SubClusterId badSC) throws IOException { public ConcurrentHashMap getMockRMs() { return mockRMs; } + + public ConcurrentHashMap getMockNMs() { + return mockNMs; + } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/webapp/TestRouterWebServicesREST.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/webapp/TestRouterWebServicesREST.java index 868a953e9adfd..b345ebdd90202 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/webapp/TestRouterWebServicesREST.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/webapp/TestRouterWebServicesREST.java @@ -20,7 +20,7 @@ import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.APPLICATION_XML; @@ -143,6 +143,8 @@ import net.jcip.annotations.NotThreadSafe; +import javax.servlet.http.HttpServletRequest; + /** * This test validate E2E the correctness of the RouterWebServices. It starts * Router, RM and NM in 3 different processes to avoid servlet conflicts. Each @@ -423,7 +425,7 @@ public void testSchedulerInfoXML() throws Exception { /** * This test validates the correctness of - * {@link RMWebServiceProtocol#getNodes()} inside Router. + * {@link RMWebServiceProtocol#getNodes(String)} inside Router. */ @Test(timeout = 2000) public void testNodesEmptyXML() throws Exception { @@ -444,7 +446,7 @@ public void testNodesEmptyXML() throws Exception { /** * This test validates the correctness of - * {@link RMWebServiceProtocol#getNodes()} inside Router. + * {@link RMWebServiceProtocol#getNodes(String)} inside Router. */ @Test(timeout = 2000) public void testNodesXML() throws Exception { @@ -465,7 +467,7 @@ public void testNodesXML() throws Exception { /** * This test validates the correctness of - * {@link RMWebServiceProtocol#getNode()} inside Router. + * {@link RMWebServiceProtocol#getNode(String)} inside Router. */ @Test(timeout = 2000) public void testNodeXML() throws Exception { @@ -528,7 +530,7 @@ public void testUpdateNodeResource() throws Exception { /** * This test validates the correctness of - * {@link RMWebServiceProtocol#getActivities()} inside Router. + * {@link RMWebServiceProtocol#getActivities(HttpServletRequest, String, String)} inside Router. */ @Test(timeout = 2000) public void testActiviesXML() throws Exception { @@ -600,7 +602,7 @@ public void testDumpSchedulerLogsXML() throws Exception { performCall(RM_WEB_SERVICE_PATH + SCHEDULER_LOGS, null, null, null, PUT); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method ClientResponse response = performCall( @@ -623,7 +625,7 @@ public void testNewApplicationXML() throws Exception { RM_WEB_SERVICE_PATH + APPS_NEW_APPLICATION, null, null, null, PUT); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method ClientResponse response = performCall( @@ -646,7 +648,7 @@ public void testSubmitApplicationXML() throws Exception { ClientResponse badResponse = performCall( RM_WEB_SERVICE_PATH + APPS, null, null, null, PUT); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method ApplicationSubmissionContextInfo context = @@ -771,7 +773,7 @@ public void testUpdateAppStateXML() throws Exception { ClientResponse badResponse = performCall( pathApp, null, null, null, POST); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method AppState appState = new AppState("KILLED"); @@ -820,7 +822,7 @@ public void testUpdateAppPriorityXML() throws Exception { RM_WEB_SERVICE_PATH + format(APPS_APPID_PRIORITY, appId), null, null, null, POST); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method AppPriority appPriority = new AppPriority(1); @@ -870,7 +872,7 @@ public void testUpdateAppQueueXML() throws Exception { RM_WEB_SERVICE_PATH + format(APPS_APPID_QUEUE, appId), null, null, null, POST); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method AppQueue appQueue = new AppQueue("default"); @@ -945,7 +947,7 @@ public void testUpdateAppTimeoutsXML() throws Exception { RM_WEB_SERVICE_PATH + format(APPS_TIMEOUT, appId), null, null, null, POST); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with a bad request AppTimeoutInfo appTimeoutInfo = new AppTimeoutInfo(); @@ -971,7 +973,7 @@ public void testNewReservationXML() throws Exception { RM_WEB_SERVICE_PATH + RESERVATION_NEW, null, null, null, PUT); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method ClientResponse response = performCall( @@ -995,7 +997,7 @@ public void testSubmitReservationXML() throws Exception { RM_WEB_SERVICE_PATH + RESERVATION_SUBMIT, null, null, null, PUT); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method ReservationSubmissionRequestInfo context = @@ -1022,7 +1024,7 @@ public void testUpdateReservationXML() throws Exception { ClientResponse badResponse = performCall( RM_WEB_SERVICE_PATH + RESERVATION_UPDATE, null, null, null, PUT); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method String reservationId = getNewReservationId().getReservationId(); @@ -1048,7 +1050,7 @@ public void testDeleteReservationXML() throws Exception { ClientResponse badResponse = performCall( RM_WEB_SERVICE_PATH + RESERVATION_DELETE, null, null, null, PUT); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method String reservationId = getNewReservationId().getReservationId(); @@ -1185,7 +1187,7 @@ public void testAddToClusterNodeLabelsXML() throws Exception { RM_WEB_SERVICE_PATH + ADD_NODE_LABELS, null, null, null, PUT); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method @@ -1213,7 +1215,7 @@ public void testRemoveFromClusterNodeLabelsXML() ClientResponse badResponse = performCall( RM_WEB_SERVICE_PATH + REMOVE_NODE_LABELS, null, null, null, PUT); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method addNodeLabel(); @@ -1238,7 +1240,7 @@ public void testReplaceLabelsOnNodesXML() throws Exception { ClientResponse badResponse = performCall( RM_WEB_SERVICE_PATH + REPLACE_NODE_TO_LABELS, null, null, null, PUT); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method addNodeLabel(); @@ -1267,7 +1269,7 @@ public void testReplaceLabelsOnNodeXML() throws Exception { ClientResponse badResponse = performCall( pathNode, null, null, null, PUT); - assertEquals(SC_INTERNAL_SERVER_ERROR, badResponse.getStatus()); + assertEquals(SC_SERVICE_UNAVAILABLE, badResponse.getStatus()); // Test with the correct HTTP method addNodeLabel(); diff --git a/pom.xml b/pom.xml index a51c5e29aa650..ed869625de18f 100644 --- a/pom.xml +++ b/pom.xml @@ -550,6 +550,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/x licenses-binary/** dev-support/docker/pkg-resolver/packages.json dev-support/docker/pkg-resolver/platforms.json + **/target/**