-
Notifications
You must be signed in to change notification settings - Fork 9.1k
HADOOP-17022 Tune S3AFileSystem.listFiles() api. #2038
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fa8093e
35ff50e
25ccf2d
36aec4d
5b631fa
abdbbbb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4181,79 +4181,126 @@ private RemoteIterator<S3ALocatedFileStatus> innerListFiles( | |
Path path = qualify(f); | ||
LOG.debug("listFiles({}, {})", path, recursive); | ||
try { | ||
// if a status was given, that is used, otherwise | ||
// call getFileStatus, which triggers an existence check | ||
final S3AFileStatus fileStatus = status != null | ||
? status | ||
: (S3AFileStatus) getFileStatus(path); | ||
if (fileStatus.isFile()) { | ||
// if a status was given and it is a file. | ||
if (status != null && status.isFile()) { | ||
// simple case: File | ||
LOG.debug("Path is a file"); | ||
return new Listing.SingleStatusRemoteIterator( | ||
toLocatedFileStatus(fileStatus)); | ||
} else { | ||
// directory: do a bulk operation | ||
String key = maybeAddTrailingSlash(pathToKey(path)); | ||
String delimiter = recursive ? null : "/"; | ||
LOG.debug("Requesting all entries under {} with delimiter '{}'", | ||
key, delimiter); | ||
final RemoteIterator<S3AFileStatus> cachedFilesIterator; | ||
final Set<Path> tombstones; | ||
boolean allowAuthoritative = allowAuthoritative(f); | ||
if (recursive) { | ||
final PathMetadata pm = metadataStore.get(path, true); | ||
// shouldn't need to check pm.isDeleted() because that will have | ||
// been caught by getFileStatus above. | ||
MetadataStoreListFilesIterator metadataStoreListFilesIterator = | ||
new MetadataStoreListFilesIterator(metadataStore, pm, | ||
allowAuthoritative); | ||
tombstones = metadataStoreListFilesIterator.listTombstones(); | ||
// if all of the below is true | ||
// - authoritative access is allowed for this metadatastore for this directory, | ||
// - all the directory listings are authoritative on the client | ||
// - the caller does not force non-authoritative access | ||
// return the listing without any further s3 access | ||
if (!forceNonAuthoritativeMS && | ||
allowAuthoritative && | ||
metadataStoreListFilesIterator.isRecursivelyAuthoritative()) { | ||
S3AFileStatus[] statuses = S3Guard.iteratorToStatuses( | ||
metadataStoreListFilesIterator, tombstones); | ||
cachedFilesIterator = listing.createProvidedFileStatusIterator( | ||
statuses, ACCEPT_ALL, acceptor); | ||
return listing.createLocatedFileStatusIterator(cachedFilesIterator); | ||
} | ||
cachedFilesIterator = metadataStoreListFilesIterator; | ||
} else { | ||
DirListingMetadata meta = | ||
S3Guard.listChildrenWithTtl(metadataStore, path, ttlTimeProvider, | ||
allowAuthoritative); | ||
if (meta != null) { | ||
tombstones = meta.listTombstones(); | ||
} else { | ||
tombstones = null; | ||
} | ||
cachedFilesIterator = listing.createProvidedFileStatusIterator( | ||
S3Guard.dirMetaToStatuses(meta), ACCEPT_ALL, acceptor); | ||
if (allowAuthoritative && meta != null && meta.isAuthoritative()) { | ||
// metadata listing is authoritative, so return it directly | ||
return listing.createLocatedFileStatusIterator(cachedFilesIterator); | ||
} | ||
toLocatedFileStatus(status)); | ||
} | ||
// Assuming the path to be a directory | ||
// do a bulk operation. | ||
RemoteIterator<S3ALocatedFileStatus> listFilesAssumingDir = | ||
getListFilesAssumingDir(path, | ||
recursive, | ||
acceptor, | ||
collectTombstones, | ||
forceNonAuthoritativeMS); | ||
// If there are no list entries present, we | ||
// fallback to file existence check as the path | ||
// can be a file or empty directory. | ||
if (!listFilesAssumingDir.hasNext()) { | ||
// If file status was already passed, reuse it. | ||
final S3AFileStatus fileStatus = status != null | ||
? status | ||
: (S3AFileStatus) getFileStatus(path); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's go to innerGetFileStatus here so the invocation count is consistent and we can declare we don't want to fall back to the list operation, just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This won't work as explained in #2038 (comment) |
||
if (fileStatus.isFile()) { | ||
return new Listing.SingleStatusRemoteIterator( | ||
toLocatedFileStatus(fileStatus)); | ||
} | ||
return listing.createTombstoneReconcilingIterator( | ||
listing.createLocatedFileStatusIterator( | ||
listing.createFileStatusListingIterator(path, | ||
createListObjectsRequest(key, delimiter), | ||
ACCEPT_ALL, | ||
acceptor, | ||
cachedFilesIterator)), | ||
collectTombstones ? tombstones : null); | ||
} | ||
// If we have reached here, it means either there are files | ||
// in this directory or it is empty. | ||
return listFilesAssumingDir; | ||
} catch (AmazonClientException e) { | ||
// TODO S3Guard: retry on file not found exception | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to consider this and see if the TODO is already done. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any retries in listFiles(). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lets cut that TODO line |
||
throw translateException("listFiles", path, e); | ||
} | ||
} | ||
|
||
/** | ||
* List files under a path assuming the path to be a directory. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i wonder if we can/should move this into the Listing class. in #2046 I'm looking at doing that for openfile, just to pull something complex out of the main fs. I know it hurts maintenance a bit (all subsequent patches will need this patch before cherry picking), but your work is big enough that will happen anyway. imagine we made the of course, one thing this will make harder is for my directory marker changes, but that shouldn't be a blocker. |
||
* @param path input path. | ||
* @param recursive recursive listing? | ||
* @param acceptor file status filter | ||
* @param collectTombstones should tombstones be collected from S3Guard? | ||
* @param forceNonAuthoritativeMS forces metadata store to act like non | ||
* authoritative. This is useful when | ||
* listFiles output is used by import tool. | ||
* @return an iterator over listing. | ||
* @throws IOException any exception. | ||
*/ | ||
private RemoteIterator<S3ALocatedFileStatus> getListFilesAssumingDir( | ||
Path path, | ||
boolean recursive, Listing.FileStatusAcceptor acceptor, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add on a new line |
||
boolean collectTombstones, | ||
boolean forceNonAuthoritativeMS) throws IOException { | ||
|
||
String key = maybeAddTrailingSlash(pathToKey(path)); | ||
String delimiter = recursive ? null : "/"; | ||
LOG.debug("Requesting all entries under {} with delimiter '{}'", | ||
key, delimiter); | ||
final RemoteIterator<S3AFileStatus> cachedFilesIterator; | ||
final Set<Path> tombstones; | ||
boolean allowAuthoritative = allowAuthoritative(path); | ||
if (recursive) { | ||
final PathMetadata pm = metadataStore.get(path, true); | ||
if (pm != null) { | ||
if (pm.isDeleted()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this a copy/paste of the other use of this? if so, what about we pull lines 4250-4255 into a new static method in org.apache.hadoop.fs.s3a.s3guard.S3Guard and use in both places. could even have a test, maybe? |
||
OffsetDateTime deletedAt = OffsetDateTime | ||
.ofInstant(Instant.ofEpochMilli( | ||
pm.getFileStatus().getModificationTime()), | ||
ZoneOffset.UTC); | ||
throw new FileNotFoundException("Path " + path + " is recorded as " + | ||
"deleted by S3Guard at " + deletedAt); | ||
} | ||
} | ||
MetadataStoreListFilesIterator metadataStoreListFilesIterator = | ||
new MetadataStoreListFilesIterator(metadataStore, pm, | ||
allowAuthoritative); | ||
tombstones = metadataStoreListFilesIterator.listTombstones(); | ||
// if all of the below is true | ||
// - authoritative access is allowed for this metadatastore | ||
// for this directory, | ||
// - all the directory listings are authoritative on the client | ||
// - the caller does not force non-authoritative access | ||
// return the listing without any further s3 access | ||
if (!forceNonAuthoritativeMS && | ||
allowAuthoritative && | ||
metadataStoreListFilesIterator.isRecursivelyAuthoritative()) { | ||
S3AFileStatus[] statuses = S3Guard.iteratorToStatuses( | ||
metadataStoreListFilesIterator, tombstones); | ||
cachedFilesIterator = listing.createProvidedFileStatusIterator( | ||
statuses, ACCEPT_ALL, acceptor); | ||
return listing.createLocatedFileStatusIterator(cachedFilesIterator); | ||
} | ||
cachedFilesIterator = metadataStoreListFilesIterator; | ||
} else { | ||
DirListingMetadata meta = | ||
S3Guard.listChildrenWithTtl(metadataStore, path, ttlTimeProvider, | ||
allowAuthoritative); | ||
if (meta != null) { | ||
tombstones = meta.listTombstones(); | ||
} else { | ||
tombstones = null; | ||
} | ||
cachedFilesIterator = listing.createProvidedFileStatusIterator( | ||
S3Guard.dirMetaToStatuses(meta), ACCEPT_ALL, acceptor); | ||
if (allowAuthoritative && meta != null && meta.isAuthoritative()) { | ||
// metadata listing is authoritative, so return it directly | ||
return listing.createLocatedFileStatusIterator(cachedFilesIterator); | ||
} | ||
} | ||
return listing.createTombstoneReconcilingIterator( | ||
listing.createLocatedFileStatusIterator( | ||
listing.createFileStatusListingIterator(path, | ||
createListObjectsRequest(key, delimiter), | ||
ACCEPT_ALL, | ||
acceptor, | ||
cachedFilesIterator)), | ||
collectTombstones ? tombstones : null); | ||
} | ||
|
||
/** | ||
* Override superclass so as to add statistic collection. | ||
* {@inheritDoc} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -168,6 +168,76 @@ public void testCostOfListLocatedStatusOnNonEmptyDir() throws Throwable { | |
} | ||
} | ||
|
||
@Test | ||
public void testCostOfListFilesOnFile() throws Throwable { | ||
describe("Performing listFiles() on a file"); | ||
Path file = path(getMethodName() + ".txt"); | ||
S3AFileSystem fs = getFileSystem(); | ||
touch(fs, file); | ||
resetMetricDiffs(); | ||
fs.listFiles(file, true); | ||
if (!fs.hasMetadataStore()) { | ||
metadataRequests.assertDiffEquals(1); | ||
} else { | ||
if (fs.allowAuthoritative(file)) { | ||
listRequests.assertDiffEquals(0); | ||
} else { | ||
listRequests.assertDiffEquals(1); | ||
} | ||
} | ||
} | ||
|
||
@Test | ||
public void testCostOfListFilesOnEmptyDir() throws Throwable { | ||
describe("Performing listFiles() on an empty dir"); | ||
Path dir = path(getMethodName()); | ||
S3AFileSystem fs = getFileSystem(); | ||
fs.mkdirs(dir); | ||
resetMetricDiffs(); | ||
fs.listFiles(dir, true); | ||
if (!fs.hasMetadataStore()) { | ||
verifyOperationCount(2, 1); | ||
} else { | ||
if (fs.allowAuthoritative(dir)) { | ||
verifyOperationCount(0, 0); | ||
} else { | ||
verifyOperationCount(0, 1); | ||
} | ||
} | ||
} | ||
|
||
@Test | ||
public void testCostOfListFilesOnNonEmptyDir() throws Throwable { | ||
mukund-thakur marked this conversation as resolved.
Show resolved
Hide resolved
|
||
describe("Performing listFiles() on a non empty dir"); | ||
Path dir = path(getMethodName()); | ||
S3AFileSystem fs = getFileSystem(); | ||
fs.mkdirs(dir); | ||
Path file = new Path(dir, "file.txt"); | ||
touch(fs, file); | ||
resetMetricDiffs(); | ||
fs.listFiles(dir, true); | ||
if (!fs.hasMetadataStore()) { | ||
verifyOperationCount(0, 1); | ||
} else { | ||
if (fs.allowAuthoritative(dir)) { | ||
verifyOperationCount(0, 0); | ||
} else { | ||
verifyOperationCount(0, 1); | ||
} | ||
} | ||
} | ||
|
||
@Test | ||
public void testCostOfListFilesOnNonExistingDir() throws Throwable { | ||
describe("Performing listFiles() on a non existing dir"); | ||
Path dir = path(getMethodName()); | ||
S3AFileSystem fs = getFileSystem(); | ||
resetMetricDiffs(); | ||
intercept(FileNotFoundException.class, | ||
() -> fs.listFiles(dir, true)); | ||
verifyOperationCount(2, 2); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. with a move to innerGetFileStatus and only do the file/marker probes, we could get cost to (2, 1) |
||
} | ||
|
||
@Test | ||
public void testCostOfGetFileStatusOnFile() throws Throwable { | ||
describe("performing getFileStatus on a file"); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we are keeping this, include the path f in the log message, just to make more sense of it in a busy log