jerryshao commented on code in PR #13506:
URL: https://github.com/apache/gravitino/pull/13506#discussion_r4355637283


##########
catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java:
##########
@@ -542,25 +543,58 @@ public Fileset createMultipleLocationFileset(
           });
     } else {
       try {
-        // formalize the path to avoid path without scheme, uri, authority, 
etc.
+        boolean allowExistingLocationAsManaged =
+            (boolean)
+                propertiesMetadata
+                    .filesetPropertiesMetadata()
+                    .getOrDefault(properties, 
PROPERTY_ALLOW_EXISTING_LOCATION_AS_MANAGED);
+        Map<String, Map<String, String>> fileSystemConfigurations = 
Maps.newHashMap();
+        // Formalize and validate every location before creating any 
directories. Otherwise,
+        // creating a child location can make another location in the request 
non-empty.
         for (Map.Entry<String, Path> entry : filesetPaths.entrySet()) {
           Map<String, String> fsConf =
               mergeUpLevelConfigurations(ident, properties, entry.getValue());
+          fileSystemConfigurations.put(entry.getKey(), fsConf);
           FileSystem tmpFs = getFileSystemWithCache(entry.getValue(), fsConf);
           Path formalizePath =
               entry.getValue().makeQualified(tmpFs.getUri(), 
tmpFs.getWorkingDirectory());
 
           filesetPathsBuilder.put(entry.getKey(), formalizePath);
           FileSystem fs = getFileSystemWithCache(formalizePath, fsConf);
 
-          if (fs.exists(formalizePath) && 
fs.getFileStatus(formalizePath).isFile()) {
-            throw new IllegalArgumentException(
-                "Fileset location cannot be a file: "
-                    + formalizePath
-                    + ", location name: "
-                    + entry.getKey());
+          if (fs.exists(formalizePath)) {
+            if (fs.getFileStatus(formalizePath).isFile()) {
+              throw new IllegalArgumentException(
+                  "Fileset location cannot be a file: "
+                      + formalizePath
+                      + ", location name: "
+                      + entry.getKey());
+            }
+            // A managed fileset takes its data with it on drop, so silently 
adopting a location
+            // that already holds files would put data Gravitino never created 
at risk.
+            if (type == Fileset.Type.MANAGED
+                && !allowExistingLocationAsManaged
+                && fs.listStatus(formalizePath).length > 0) {
+              throw new IllegalArgumentException(
+                  "Location "
+                      + formalizePath
+                      + " for managed fileset "
+                      + ident
+                      + " already exists and is not empty, location name: "
+                      + entry.getKey()
+                      + ". Register it as an "
+                      + Fileset.Type.EXTERNAL
+                      + " fileset, or set "
+                      + PROPERTY_ALLOW_EXISTING_LOCATION_AS_MANAGED
+                      + "=true to manage it anyway");
+            }
           }
+        }
 
+        for (Map.Entry<String, Path> entry : 
filesetPathsBuilder.build().entrySet()) {

Review Comment:
   The preflight above is not what gets stored. In this second loop, for an 
existing path we go to the `else` branch (line 614, "manages the existing 
location") without listing it again, and `mkdirs` also succeeds when the 
directory already exists. A path that was absent or empty during the first loop 
can hold data by the time we get here (another writer, or `mkdirs` of a nested 
location in this same request), and it will then be stored as `MANAGED` and 
recursively deleted on drop. Please treat "absent" / "empty" as a snapshot: on 
the adopt path, reject the request if the directory now has children that are 
not other locations from this call, and clean up directories this call already 
created. Note that a blanket re-check here would break 
`testCreateManagedFilesetWithNestedNewLocations` when the child is created 
first.



##########
catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java:
##########
@@ -542,25 +543,58 @@ public Fileset createMultipleLocationFileset(
           });
     } else {
       try {
-        // formalize the path to avoid path without scheme, uri, authority, 
etc.
+        boolean allowExistingLocationAsManaged =
+            (boolean)
+                propertiesMetadata
+                    .filesetPropertiesMetadata()
+                    .getOrDefault(properties, 
PROPERTY_ALLOW_EXISTING_LOCATION_AS_MANAGED);
+        Map<String, Map<String, String>> fileSystemConfigurations = 
Maps.newHashMap();
+        // Formalize and validate every location before creating any 
directories. Otherwise,
+        // creating a child location can make another location in the request 
non-empty.
         for (Map.Entry<String, Path> entry : filesetPaths.entrySet()) {
           Map<String, String> fsConf =
               mergeUpLevelConfigurations(ident, properties, entry.getValue());
+          fileSystemConfigurations.put(entry.getKey(), fsConf);
           FileSystem tmpFs = getFileSystemWithCache(entry.getValue(), fsConf);
           Path formalizePath =
               entry.getValue().makeQualified(tmpFs.getUri(), 
tmpFs.getWorkingDirectory());
 
           filesetPathsBuilder.put(entry.getKey(), formalizePath);
           FileSystem fs = getFileSystemWithCache(formalizePath, fsConf);
 
-          if (fs.exists(formalizePath) && 
fs.getFileStatus(formalizePath).isFile()) {
-            throw new IllegalArgumentException(
-                "Fileset location cannot be a file: "
-                    + formalizePath
-                    + ", location name: "
-                    + entry.getKey());
+          if (fs.exists(formalizePath)) {
+            if (fs.getFileStatus(formalizePath).isFile()) {
+              throw new IllegalArgumentException(
+                  "Fileset location cannot be a file: "
+                      + formalizePath
+                      + ", location name: "
+                      + entry.getKey());
+            }
+            // A managed fileset takes its data with it on drop, so silently 
adopting a location
+            // that already holds files would put data Gravitino never created 
at risk.
+            if (type == Fileset.Type.MANAGED
+                && !allowExistingLocationAsManaged
+                && fs.listStatus(formalizePath).length > 0) {

Review Comment:
   `listStatus().length > 0` materializes every child just to decide the 
directory is non-empty, and the directories this guard is meant to reject are 
exactly the large ones that can make this time out or OOM the server. Also, a 
listing failure (including `AccessControlException`) is wrapped as a generic 
"Failed to create fileset". Please use `listStatusIterator` and stop at the 
first real child, as `probeLocation` already does. Could you also confirm the 
behavior on S3/GCS/OSS/ABFS: `exists() == false` must not hide objects under 
that prefix, and a 0-byte directory marker alone should not count as content 
(otherwise pre-created empty prefixes would be rejected).



##########
api/src/main/java/org/apache/gravitino/file/Fileset.java:
##########
@@ -71,6 +71,13 @@ public interface Fileset extends Auditable {
   /** The property name for the default location name of the fileset. */
   String PROPERTY_DEFAULT_LOCATION_NAME = "default-location-name";
 
+  /**
+   * The property name that allows creating a managed fileset on a location 
that already exists and
+   * is not empty. Defaults to false, in which case such a location must be 
registered as an
+   * external fileset instead.
+   */
+  String PROPERTY_ALLOW_EXISTING_LOCATION_AS_MANAGED = 
"allow-existing-location-as-managed";
+

Review Comment:
   nit: please add the matching `PROPERTY_ALLOW_EXISTING_LOCATION_AS_MANAGED` 
constant next to the other fileset property constants in the Python client 
(`clients/client-python/gravitino/api/file/fileset.py`). Optional: 
`booleanPropertyEntry` uses `Boolean.valueOf`, so `yes` / `1` are silently 
treated as `false` (fail-closed, consistent with other boolean properties); 
consider rejecting values other than `true`/`false` for this property.



##########
docs/filesets.md:
##########
@@ -55,13 +55,15 @@ See [How to use GVFS](./how-to-use-gvfs.md).
 ### Managed and External
 
 A managed fileset belongs to Gravitino. Creating it creates the directory, and 
deleting it deletes

Review Comment:
   The quick start earlier on this page (line 48) still says that pointing at 
an existing path makes the fileset external. That is no longer true: the type 
stays whatever the caller sent, and creating `MANAGED` on a non-empty location 
now fails unless `allow-existing-location-as-managed=true`. The same sentence 
is in `docs/manage-fileset-metadata-using-gravitino.md:23`. Please update both, 
and also rewrite the PR description, since `delete-data-on-drop` / 
`deletesStorageOnDrop` are no longer part of this patch.



##########
catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java:
##########
@@ -1047,6 +1048,219 @@ public void testCreateFilesetWithExceptions() throws 
IOException {
     }
   }
 
+  /** Checks that managed filesets reject existing data without registering 
metadata. */

Review Comment:
   Please add tests for a managed fileset with no explicit storage location 
whose path is derived from the schema location (and the catalog-location 
equivalent), and for a `{{schema}}/{{fileset}}` placeholder path that resolves 
onto existing files. Also assert that `setProperty` / `removeProperty` of 
`allow-existing-location-as-managed` is rejected, and that only a boolean 
`true` (any case) opts in.



-- 
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]

Reply via email to