wombatu-kun opened a new pull request, #8962:
URL: https://github.com/apache/paimon/pull/8962

   ### Purpose
   
   Closes #8548. With the Hadoop FileSystem cache disabled 
(`fs.<scheme>.impl.disable.cache=true`, commonly used to isolate per-tenant 
credentials), every `HadoopFileIO` creates its own `FileSystem`, and nothing 
ever released them. The reporter sees unbounded JVM thread growth because each 
unclosed `AliyunOSSFileSystem` keeps its transfer executor threads alive.
   
   **The ownership rule.** `HadoopFileIO.createFileSystem` calls 
`path.getFileSystem(conf)`, which is Hadoop's `FileSystem.get(URI, 
Configuration)`:
   
   ```java
   String disableCacheName = String.format("fs.%s.impl.disable.cache", scheme);
   if (conf.getBoolean(disableCacheName, false)) {
       return createFileSystem(uri, conf);   // a fresh instance nobody else 
can reach
   }
   return CACHE.get(uri, conf);              // shared, owned by Hadoop's 
global cache
   ```
   
   So a `FileIO` owns a `FileSystem` if and only if that property is set for 
the scheme, and `close()` releases exactly those. When the global cache is 
enabled the instance is shared with every other user in the JVM, Hadoop 
releases it through `FileSystem.closeAll()` on its shutdown hook, and closing 
it here would surface as `IOException: Filesystem closed` in unrelated readers. 
This answers the two design questions the reporter asked on the issue.
   
   Two details are load bearing, both checked against the Hadoop bytecode 
(2.6.2 and 3.1.0) rather than assumed. The scheme comes from the **path**, not 
from `FileSystem#getUri()`, because that is what Hadoop consulted when it 
decided whether to cache. And it is matched **as written**, with no lower 
casing, because Hadoop does not lower case it either: a path spelled 
`OSS://bucket` is served from the shared cache, so a case-insensitive check 
would find `fs.oss.impl.disable.cache=true` and close an instance it does not 
own.
   
   **The close never reached `HadoopFileIO`.** Real deployments hold a 
`ResolvingFileIO` or a `PluginFileIO`, and neither forwarded `close()` to its 
delegates, so a `HadoopFileIO.close()` alone would have been dead code. 
`CachingFileIO.close()` already forwards, which is the pattern the other two 
now follow. Notably this also means `OSSFileIO.close()`, which the project 
already wrote to release uncached file systems, has never actually run in 
production, and it targets exactly the reporter's symptom. 
`HadoopSecuredFileSystem` likewise extended `FileSystem` without overriding 
`close()`, so on a Kerberised deployment the wrapped instance was never closed.
   
   **A second, larger leak on the creation path.** `FileIO.checkAccess` probes 
a loader by loading a throwaway `FileIO` and calling `exists()` on it, then 
discards it. With the cache disabled that probe materialises a fully 
initialised `FileSystem`, connector threads included, on **every** 
`FileIO.get()` call for any scheme served by the universal `HadoopFileIOLoader` 
fallback. This one leaks without anyone calling `close()` at all, and a 
measurement of the reporter's configuration shows two file systems created and 
one closed per `FileIO.get()`.
   
   **Close is terminal.** Without a closed flag each wrapper silently 
re-created its delegate after `close()`, restoring the very leak being fixed, 
and a `FileSystem` published concurrently with a `close()` could land in a 
drained map where nothing would ever release it. `HadoopFileIO` now publishes 
under the same monitor `close()` takes, and use after close fails with a clear 
`IOException` instead of leaking quietly.
   
   **Out of scope, stated explicitly.** The seven per-module 
`HadoopCompliantFileIO` copies under `paimon-filesystems` obtain their file 
systems from module-level static caches (for example `S3FileIO.CACHE`), which 
are process wide by design, so releasing those per instance needs a separate 
refcounting design.
   
   **One behaviour change reviewers should weigh.** `RESTTokenFileIO` keeps a 
static cache of `FileIO`s whose removal listener already calls 
`IOUtils.closeQuietly`. That call has been landing on the empty default 
`close()`; now that `close()` does real work, a size based eviction can in 
principle release a file system another table is still reading through. Two 
things keep it narrow: the cache evicts on ten hour idle access or above 1000 
entries, and the ownership gate means only instances created under 
`disable.cache=true` are ever closed, which is the configuration where the 
operator already opted into one file system per `FileIO`. `OSSFileIO` carries 
the same exposure today, so this is new reach rather than a new class of 
hazard. The `FileIO.close()` javadoc, which previously told implementers to 
override the method to empty, is updated to match.
   
   ### Tests
   
   New `HadoopFileIOTest` (16 tests) plus additions to `ResolvingFileIOTest`, 
`PluginFileIOTest` and `HadoopSecuredFileSystemTest`, built on a recording 
`FileSystem` registered under synthetic schemes. Coverage: an owned file system 
is closed and a Hadoop cached one is not (proved by pulling the same instance 
back through a second `FileIO`, not just by a counter); ownership is decided 
per scheme and follows the path rather than `getUri()`; the scheme is matched 
as written; a failing `close()` does not stop the others and still propagates; 
close is idempotent and safe before any use, on a deserialized instance, and on 
an externally injected file system the caller still owns; the creation race 
releases the losing instance while never closing a shared one; a file system 
created while closing is released; a malformed `fs.defaultFS` cannot throw out 
of `close()`; use after close is rejected rather than leaking again; and 
`FileIO.get` leaves no unclosed access probe.
   
   Verified by reverting the implementation while keeping the API: the tests 
asserting new behaviour fail, while those guarding the dangerous half (a shared 
file system must stay open) keep passing.
   
   Regression runs on JDK 8: full `paimon-common` suite 12198 tests green, 
`paimon-core` rest, catalog and fs packages 342 tests green, checkstyle and 
spotless clean.
   
   Audited before submitting, with independent reviewers on ownership 
semantics, concurrency and test adequacy, and every finding verified against 
the code. That pass found and fixed a defect in my own first draft: making 
`close()` null the `PluginFileIO` delegate turned `fileIO()` into a method that 
could return null to a caller about to dereference it. It also found the 
`checkAccess` probe leak described above.
   


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