xiangfu0 commented on code in PR #19863:
URL: https://github.com/apache/pinot/pull/19863#discussion_r4278287625


##########
pinot-plugins/pinot-file-system/pinot-s3/src/main/java/org/apache/pinot/plugin/filesystem/S3PinotFS.java:
##########
@@ -797,6 +799,67 @@ public List<FileMetadata> listFilesWithMetadata(final URI 
fileUri, final boolean
     return result;
   }
 
+  @Override
+  public boolean supportsListingAfter() {
+    return true;
+  }
+
+  @Override
+  public List<FileMetadata> listFilesWithMetadataAfter(final URI fileUri, 
@Nullable final String startAfter,
+      final int maxResults)
+      throws IOException {
+    final List<FileMetadata> result = new ArrayList<>();
+    if (maxResults <= 0) {
+      return result;
+    }
+    final String scheme = fileUri.getScheme();
+    Preconditions.checkArgument(scheme.equals(S3_SCHEME) || 
scheme.equals(S3A_SCHEME));
+    final String pathPrefix = scheme + SCHEME_SEPARATOR + fileUri.getHost() + 
DELIMITER;
+    Preconditions.checkArgument(startAfter == null || 
startAfter.startsWith(pathPrefix),
+        "startAfter %s is not under %s", startAfter, pathPrefix);
+    try {
+      final String prefix = normalizeToDirectoryPrefix(fileUri);
+      String continuationToken = null;
+      boolean isDone = false;
+      while (!isDone && result.size() < maxResults) {
+        final ListObjectsV2Request.Builder builder = 
ListObjectsV2Request.builder().bucket(fileUri.getHost())
+            .maxKeys(Math.min(maxResults - result.size(), LIST_MAX_KEYS));
+        if (!prefix.equals(DELIMITER)) {
+          builder.prefix(prefix);
+        }
+        if (continuationToken != null) {
+          builder.continuationToken(continuationToken);
+        } else if (startAfter != null) {
+          // StartAfter is ignored once a continuation token is set.

Review Comment:
   Good catch handling the "StartAfter is ignored once a continuation token is 
set" case — the `testContinuationReplacesStartAfter` test locks this in. This 
is the kind of S3 subtlety that's easy to get wrong, so thanks for the explicit 
comment and coverage.



##########
pinot-plugins/pinot-file-system/pinot-gcs/src/main/java/org/apache/pinot/plugin/filesystem/GcsPinotFS.java:
##########
@@ -316,6 +313,72 @@ public List<FileMetadata> listFilesWithMetadata(final URI 
fileUri, final boolean
     return result;
   }
 
+  @Override
+  public boolean supportsListingAfter() {
+    return true;
+  }
+
+  /// GCS lists objects by name and file paths are those names URI-escaped, so 
`startAfter` is decoded back to the
+  /// object name it spells.
+  @Override
+  public List<FileMetadata> listFilesWithMetadataAfter(final URI fileUri, 
@Nullable final String startAfter,
+      final int maxResults)
+      throws IOException {
+    final List<FileMetadata> result = new ArrayList<>();
+    if (maxResults <= 0) {
+      return result;
+    }
+    final GcsUri gcsFileUri = new GcsUri(fileUri);
+    final String bucketName = gcsFileUri.getBucketName();
+    final String prefix = gcsFileUri.getPrefix();
+    String startName = null;
+    if (startAfter != null) {
+      final URI start = URI.create(startAfter);
+      checkArgument(GcsUri.SCHEME.equals(start.getScheme()) && 
bucketName.equals(start.getAuthority()),
+          "startAfter %s is not in bucket %s", startAfter, bucketName);
+      startName = start.getPath().substring(1);

Review Comment:
   `start.getPath().substring(1)` decodes the escaped path back to the raw 
object name, which is exactly what `startOffset` wants — nice. One edge case 
worth a thought: if a caller ever passes `gs://bucket/` (empty path), 
`getPath()` is empty and `substring(1)` throws 
`StringIndexOutOfBoundsException`. Probably not a real caller shape, but a 
guard or a quick test would make the contract robust.



##########
pinot-plugins/pinot-file-system/pinot-gcs/src/main/java/org/apache/pinot/plugin/filesystem/GcsPinotFS.java:
##########
@@ -316,6 +313,72 @@ public List<FileMetadata> listFilesWithMetadata(final URI 
fileUri, final boolean
     return result;
   }
 
+  @Override
+  public boolean supportsListingAfter() {
+    return true;
+  }
+
+  /// GCS lists objects by name and file paths are those names URI-escaped, so 
`startAfter` is decoded back to the
+  /// object name it spells.
+  @Override
+  public List<FileMetadata> listFilesWithMetadataAfter(final URI fileUri, 
@Nullable final String startAfter,
+      final int maxResults)
+      throws IOException {
+    final List<FileMetadata> result = new ArrayList<>();
+    if (maxResults <= 0) {
+      return result;
+    }
+    final GcsUri gcsFileUri = new GcsUri(fileUri);
+    final String bucketName = gcsFileUri.getBucketName();
+    final String prefix = gcsFileUri.getPrefix();
+    String startName = null;
+    if (startAfter != null) {
+      final URI start = URI.create(startAfter);
+      checkArgument(GcsUri.SCHEME.equals(start.getScheme()) && 
bucketName.equals(start.getAuthority()),
+          "startAfter %s is not in bucket %s", startAfter, bucketName);
+      startName = start.getPath().substring(1);
+    }
+    try {
+      final List<Storage.BlobListOption> options = new ArrayList<>();
+      options.add(Storage.BlobListOption.prefix(prefix));
+      options.add(Storage.BlobListOption.pageSize(Math.min(maxResults, 
LIST_PAGE_SIZE)));

Review Comment:
   Nit: the page size is capped against the original `maxResults`, while the S3 
path uses the remaining count (`maxResults - result.size()`). After skipping 
the `startOffset` match and directory markers, this can fetch a slightly 
larger-than-needed page on continuation. Not a correctness issue since the loop 
stops at `maxResults`, but matching S3's `maxResults - result.size()` would 
keep the two implementations consistent.



##########
pinot-spi/src/main/java/org/apache/pinot/spi/filesystem/PinotFS.java:
##########
@@ -188,6 +189,27 @@ default List<FileMetadata> listFilesWithMetadata(URI 
fileUri, boolean recursive,
     return result;
   }
 
+  /// Whether [#listFilesWithMetadataAfter] is supported.
+  default boolean supportsListingAfter() {
+    return false;
+  }
+
+  /// Lists the files (never directories) under `fileUri` recursively, in the 
store's key order, starting strictly
+  /// after the key `startAfter` names. That key need not exist, so a caller 
can skip a range of keys with one request
+  /// instead of paging through it. Only stores whose listing is sorted and 
can start at an arbitrary key implement
+  /// this; see [#supportsListingAfter]. Where file paths escape the key (GCS 
URI-escapes object names), path order

Review Comment:
   The javadoc is clear about path-order vs key-order differing for GCS — 
that's the one caveat a consumer really needs. Could be worth one concrete 
sentence that callers computing `startAfter` should derive it from a 
previously-returned `getFilePath()` value (not a hand-built key), so the 
escaping matches on both sides.



##########
pinot-plugins/pinot-file-system/pinot-s3/src/main/java/org/apache/pinot/plugin/filesystem/S3PinotFS.java:
##########
@@ -797,6 +799,67 @@ public List<FileMetadata> listFilesWithMetadata(final URI 
fileUri, final boolean
     return result;
   }
 
+  @Override
+  public boolean supportsListingAfter() {
+    return true;
+  }
+
+  @Override
+  public List<FileMetadata> listFilesWithMetadataAfter(final URI fileUri, 
@Nullable final String startAfter,
+      final int maxResults)
+      throws IOException {
+    final List<FileMetadata> result = new ArrayList<>();
+    if (maxResults <= 0) {
+      return result;
+    }
+    final String scheme = fileUri.getScheme();
+    Preconditions.checkArgument(scheme.equals(S3_SCHEME) || 
scheme.equals(S3A_SCHEME));
+    final String pathPrefix = scheme + SCHEME_SEPARATOR + fileUri.getHost() + 
DELIMITER;
+    Preconditions.checkArgument(startAfter == null || 
startAfter.startsWith(pathPrefix),

Review Comment:
   The validation only checks that `startAfter` shares the `s3://bucket/` 
prefix, not that it falls under the listing directory prefix. That's fine 
functionally (S3's prefix filter still constrains results), but it means a 
`startAfter` pointing at a sibling directory silently behaves like a seek 
rather than an error. Worth a one-line javadoc note that `startAfter` only 
needs to be in the same bucket, not necessarily under `fileUri`.



##########
pinot-spi/src/main/java/org/apache/pinot/spi/filesystem/PinotFS.java:
##########
@@ -188,6 +189,27 @@ default List<FileMetadata> listFilesWithMetadata(URI 
fileUri, boolean recursive,
     return result;
   }
 
+  /// Whether [#listFilesWithMetadataAfter] is supported.
+  default boolean supportsListingAfter() {
+    return false;
+  }
+
+  /// Lists the files (never directories) under `fileUri` recursively, in the 
store's key order, starting strictly
+  /// after the key `startAfter` names. That key need not exist, so a caller 
can skip a range of keys with one request
+  /// instead of paging through it. Only stores whose listing is sorted and 
can start at an arbitrary key implement
+  /// this; see [#supportsListingAfter]. Where file paths escape the key (GCS 
URI-escapes object names), path order
+  /// and key order can differ.
+  ///
+  /// @param fileUri location to list files from
+  /// @param startAfter a path in the form [FileMetadata#getFilePath] returns, 
or null to start at the first file
+  /// @param maxResults maximum number of files to return; fewer are returned 
only when no files remain
+  /// @return the next files in order, at most `maxResults`
+  /// @throws IOException on IO failure
+  default List<FileMetadata> listFilesWithMetadataAfter(URI fileUri, @Nullable 
String startAfter, int maxResults)

Review Comment:
   The default throwing `UnsupportedOperationException` with no message is a 
bit opaque when it surfaces. Consider a short message (e.g. the class name plus 
"does not support listing after a key; check supportsListingAfter()") so a 
caller that forgot the capability check gets an actionable error.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to