blobstore: implement async directory operations for Ali OSS - #460
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #460 +/- ##
============================================
+ Coverage 81.65% 81.71% +0.05%
Complexity 652 652
============================================
Files 204 204
Lines 13613 13784 +171
Branches 1809 1830 +21
============================================
+ Hits 11116 11263 +147
- Misses 1701 1711 +10
- Partials 796 810 +14
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
iamabhilaksh
left a comment
There was a problem hiding this comment.
Couple of blockers from reading the diff:
1. doDeleteDirectory data race on futures (AliAsyncBlobStore.java:736)
List<CompletableFuture<Void>> futures = new ArrayList<>();
Consumer<ListBlobsBatch> consumer = batch -> {
...
futures.add(doDelete(...)); // mutated from completion thread
};
CompletableFuture<Void> listFuture = doList(..., consumer);
futures.add(listFuture); // mutated from calling thread
return listFuture.thenCompose(v -> CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])));futures is a plain ArrayList mutated from the consumer callback (runs on doList's completion thread) and from the calling thread. That's a JMM data race. The thenCompose also assumes all consumer-added futures are visible by the time listFuture completes — true today depending on how doList schedules the consumer, but fragile.
You already used synchronized (blobInfos) in doDownloadDirectory for the same pattern — same care needed here. Simplest fix: Collections.synchronizedList(new ArrayList<>()) or CopyOnWriteArrayList.
2. Missing path-traversal check in doDownloadDirectory (AliAsyncBlobStore.java:575)
String relative = (prefix != null && key.startsWith(prefix)) ? key.substring(prefix.length()) : key;
Path destination = targetDir.resolve(relative).normalize();PR #422 just added a safeTargetDir.startsWith() guard to GCP for exactly this — a malicious key like prefix/../../../../etc/passwd resolves outside targetDir. Same vector here, no guard. Should match the GCP pattern.
Nits:
isExcluded(line 624) uses rawkey.startsWith(excludePrefix). If excludes containdata/logs(no trailing slash), it'll false-positive ondata/logs2/x. Either normalize trailing slash on entry or document the contract.partitionListandtoBlobIdentifiersonAliTransformer(newly added) should bestatic+ package-private — no instance state, not part of the public API surface.testUploadDirectoryWithSubFoldersforincludeSubFolders=falseonly checksgetFailedTransfers().size() == 0— doesn't assertputObjectAsyncwas called exactly once forroot.txt. The behavior is implicit; an explicitverify(..., times(1))would catch a regression where nested files leak in.
Rest is solid — upload/download/exclusion logic and pagination test coverage are good. Happy to re-review once the race + traversal are addressed.
Regarding pt 1, I will push a commit to add |
b833828 to
395914e
Compare
| ListBlobsRequest listRequest = ListBlobsRequest.builder() | ||
| .withPrefix(prefix) | ||
| .build(); | ||
| doList(listRequest, batch -> { |
There was a problem hiding this comment.
Nested blocking .join() inside CompletableFuture.supplyAsync(): You call doList(...).join() (blocking) inside a supplyAsync lambda, and then later call CompletableFuture.allOf(...).join() again. This means one executor thread is blocked for the entire duration of listing + all downloads, which could limit throughput if the executor has few threads.
There was a problem hiding this comment.
Ack. Will refactor this logic here in both upload/download to alleviate this
395914e to
05d937f
Compare
iamabhilaksh
left a comment
There was a problem hiding this comment.
Solid work overall — directory ops finally landing for Ali async is great. A few things I'd like to see resolved before we merge:
[Blocker] Path-traversal check missing in doDownloadDirectory — at the destination resolution site (~line 558), we still need the targetDir.resolve(relative).normalize().startsWith(targetDir) check. The sync side has it; without it, a crafted object key with ../ segments could escape the target dir. Same pattern Barry already enforced in #422.
[Thinking out loud] Blocking I/O on SDK completion threads (commit 05d937f) — the new Files.createDirectories(parent) and the double filePath.toFile().length() calls run on the SDK's async-completion thread, not on our executor. Under load that can starve the SDK's callback pool. One option is to wrap them in CompletableFuture.runAsync(..., executor); another is to do the directory creation up-front. Curious what you think — happy either way as long as we're aware of it.
Nits still open from earlier round (non-blocking, fine to chase in a follow-up):
@Threadsvalue still hardcoded- TearDown ordering on the param state
Once the path-traversal one is in I'll re-approve.
05d937f to
4b47e83
Compare
I have added code to do the path traversal check, as well as tweaked the thread logic around the file I/O to make sure the I/Os are executed in our executor thread, as well as creating/collecting the file info up front to avoid these ops inside the OSS SDK callback thread pool. PTAL |
4b47e83 to
124fa7f
Compare
iamabhilaksh
left a comment
There was a problem hiding this comment.
LGTM 👍 — all three blockers from earlier rounds resolved cleanly:
synchronizedListonfuturesindoDeleteDirectory- path-traversal guard in
doDownloadDirectory - directory-creation + file-size stat moved off SDK completion threads via
thenComposeAsync(executorService)
Earlier nits (excludePrefix trailing-slash semantics, static helpers on AliTransformer, mockito times(1) on the includeSubFolders=false test) are non-blocking — fine to chase in follow-up if you want.
Manual implementation using parallel file uploads since OSS v2 SDK has no native directory transfer manager. Supports includeSubFolders, followSymbolicLinks, tags, objectLock, and totalBytesTransferred progress tracking. Includes unit tests.
Replace the supplyAsync + .join() pattern in doUploadDirectory and doDownloadDirectory with composed CompletableFuture stages (runAsync/ supplyAsync for short blocking fs prep, then thenCompose for listing and the per-file transfer fan-out). This stops parking an executor thread for the entire listing + transfer duration, matching the already-non-blocking doDeleteDirectory pattern. Shared accumulators use synchronizedList since per-file callbacks now run concurrently on SDK completion threads.
…reads - Download: pre-create all destination directories before fanning out per-file downloads (was previously inside the per-file stream mapping). - Upload: compute file sizes during Stage 1 directory walk (on our executorService), eliminating double filePath.toFile().length() calls from Stage 2 stream mapping and the thenAccept callback. Both changes ensure filesystem I/O runs on our own executor thread pool rather than on whatever thread completes the preceding async stage.
124fa7f to
d9c8876
Compare
Summary
Add async directory related operations support for Ali blobstore.
Includes unit tests with mocked SDK client. Performed manual integration testing against live Ali environment to verify functionality.
Some conventions to follow
docstore:for document store module,blobstorefor Blob Store moduletest:perf: