felixhzhu commented on issue #4237:
URL: https://github.com/apache/amoro/issues/4237#issuecomment-4590131189

   > 感谢您的报告!根本原因已确认——对于对象存储文件 
I/O,单参数`checkArgument(boolean)`操作`asFileSystemIO()`确实会抛出异常`IllegalArgumentException:
 null`。
   > 
   > **问题:**我不太明白“100%复现”的说法和`io.exists()`投掷线前的那个守卫有什么关系:
   > 
   > if (!io.exists(directoryPath)) return;   // TableFileUtil:90
   > ... io.asFileSystemIO() ...              // :95 — throws
   > 对于`S3FileIO`,`exists()`是对确切键执行 HEAD 
操作,并且`directoryPath`是一个不带斜杠的前缀(`s3://.../data`)→ 404 → `false`→ 在第 90 
行返回,永远不会到达第 95 行。在我们自己的 S3FileIO 支持的目录中验证:对不带斜杠的前缀键执行 HEAD 操作返回 404,我无法在那里重现此错误。
   > 
   > 能否分享一下:
   > 
   > 1. 确切的`io-impl`+ Iceberg 版本,
   > 2. 您使用的是哪种对象存储(AWS S3、Ceph RGW、MinIO 等),以及
   > 3. 在你的环境中,对不带斜杠前缀的键执行 HEAD 操作是否返回**200 ?**
   > 
   > 到达第 95 行似乎需要`exists()==true`该前缀,该前缀看起来像是特定于存储/环境的,而不是通用的。
   
   
   ````markdown
   Thanks for the careful read! You're right about the JDK / Iceberg behavior 
in your environment, but I think the assumption "S3FileIO + non-trailing-slash 
prefix → HEAD 404 → false → 90 returns" doesn't hold across all the object 
stores / call shapes hit by this code path. Let me share details and walk 
through why the fix on the entry-point is still the right place.
   
   ### The `exists()` path
   
   `AuthenticatedFileIOAdapter.exists()` for non-`AuthenticatedFileIO` backends 
ends up calling `AuthenticatedFileIO.super.exists(path)`, which is:
   
   ```java
   default boolean exists(String path) {
     InputFile inputFile = newInputFile(path);
     return inputFile.exists();
   }
   ```
   
   For `S3FileIO` this is indeed a HEAD on the key. So far so good — your read 
is correct.
   
   ### Why HEAD is not always 404 in practice
   
   Three things in the actual call chain mean `exists()` can return true and we 
*do* reach line 95:
   
   1. **`directoryPath` is not the top-level `data/` prefix.**
      `RollingFileCleaner.doCleanFiles()` feeds `deleteEmptyDirectory()` with 
the **parent dir of every deleted data file**, e.g.
      
`cos://my-bucket-1250000000/warehouse/db/t/data/op_time_day=2024-01-01/op_time_hour=12`.
      Many object stores (Tencent Cloud COS / Aliyun OSS / Huawei OBS / Ceph 
RGW / some MinIO setups) write a 0-byte placeholder / directory marker when 
objects are uploaded under a prefix, and HEAD on those placeholder keys returns 
200.
   
   2. **S3-compatible implementations don't all follow strict AWS S3 
semantics.**
      Several proxies / gateways translate `HeadObject(prefix)` internally to 
`ListObjects(prefix=..., max=1)`, returning 200 if the prefix is non-empty. AWS 
S3 itself doesn't do this, but a lot of the S3-compatible ecosystem does.
   
   3. **`deleteEmptyDirectory` is recursive.** It walks up to the parent 
partition. For partitions that still have *sibling* sub-partitions, the prefix 
is necessarily non-empty, which on placeholder-friendly stores means HEAD = 
200, hitting line 95.
   
   I've reproduced this 100% of the time on **Tencent Cloud COS** 
(S3-compatible API, accessed via Iceberg's native `S3FileIO` with 
`endpoint=cos.<region>.myqcloud.com` — *not* through `cosn://` / Hadoop FS). On 
AWS S3 you can reproduce it deterministically by leaving a 0-byte placeholder 
under `data/<partition>/` (which is exactly what the AWS S3 console "Create 
folder" action does).
   
   ### Even if `exists()` were always false, this code shape is a latent trap
   
   Putting `asFileSystemIO()` after `exists()` only "accidentally avoids" the 
precondition on AWS S3 strict semantics. Any future refactor (switching to 
LIST-based existence check, going through `SupportsPrefixOperations`, etc.) 
re-arms the trap. Routing on `supportFileSystemOperations()` at the entry point 
matches the contract `AuthenticatedFileIO` itself uses everywhere else 
(`asPrefixFileIO()`, `asBulkFileIO()`, `asFileRecycleIO()` all pair with 
`supportXxx()`), and additionally protects every other caller of 
`deleteEmptyDirectory` (e.g. `BasicTableTrashManager`).
   
   ### Direct answers
   
   - **`io-impl` + Iceberg version**: production runs 
`org.apache.iceberg.aws.s3.S3FileIO` against **Tencent Cloud COS** 
(S3-compatible API). Cross-checked envs: `S3FileIO` on AWS S3 (Glue) and 
`org.apache.iceberg.io.ResolvingFileIO` on a Polaris-backed REST catalog. 
**Iceberg 1.6.1** in production. (Side note: the Amoro repo `pom.xml` currently 
declares `1.7.2`, but the relevant code paths — `S3InputFile.exists()` / 
`BaseS3File.exists()` and `SupportsPrefixOperations` semantics — are unchanged 
across `1.6.x → 1.7.x`, so the analysis applies to both.)
   - **Object store**: production repro is **Tencent Cloud COS** via 
S3-compatible API (`S3FileIO`, *not* the `cosn://` Hadoop FS connector). 
Cross-verified on AWS S3 (Glue) and a Polaris-backed REST + S3 catalog.
   - **Does HEAD on a non-trailing-slash prefix return 200?** On AWS S3 strict 
— no. On **Tencent Cloud COS** / Aliyun OSS / Ceph RGW / some MinIO setups / 
S3-compatible proxies — yes (placeholder objects, or LIST-flavored HEAD 
semantics). And in our actual call shape `directoryPath` is most often a 
partition sub-directory rather than `data/`, which makes the placeholder case 
the common one. We see HEAD = 200 deterministically on COS for those partition 
sub-dir keys.
   
   ### Repro on AWS S3 (in case you want one):
   
   1. Glue + S3 catalog, partitioned Iceberg table.
   2. Use the AWS console "Create folder" under 
`s3://bucket/.../data/<some-partition>/` — this writes a 0-byte placeholder.
   3. Commit enough rows / time so an expirable snapshot exists.
   4. Trigger `expireSnapshots` → reproduces.
   
   Happy to add a unit test that mocks an `AuthenticatedFileIO` whose 
`supportFileSystemOperations()` returns `false` to lock in the contract — 
currently the closest thing in tree only exercises `HadoopFileIO`.
   ````


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