Skip to content

Commit a44b537

Browse files
HaochengLIUpitrou
andauthored
apacheGH-41493: [C++][S3] Add a new option to check existence before CreateDir (apache#41822)
### Rationale for this change I have a use case that thousands of jobs are writing hive partitioned parquet files daily to the same bucket via S3FS filesystem. The gist here is a lot of keys are being created at the same time hense jobs hits `AWS Error SLOW_DOWN. during Put Object operation: The object exceeded the rate limit for object mutation operations(create, update, and delete). Please reduce your rate request error.` frequently throughout the day since the code is creating directories pessimistically. ### What changes are included in this PR? Add a new S3Option to check the existence of the directory before creation in `CreateDir`. It's disabled by default. When it's enabled, the CreateDir function will check the existence of the directory first before creation. It ensures that the create operation is only acted when necessary. Though there are more I/O calls, but it avoids hitting the cloud vendor put object limit. ### Are these changes tested? Add test cases when the flag is set to true. Right on top of the mind i donno how to ensure it's working in these tests. But in our production environment, we have very similar code and it worked well. ### Are there any user-facing changes? * GitHub Issue: apache#41493 Lead-authored-by: Haocheng Liu <[email protected]> Co-authored-by: Antoine Pitrou <[email protected]> Signed-off-by: Antoine Pitrou <[email protected]>
1 parent d02a91b commit a44b537

File tree

3 files changed

+78
-7
lines changed

3 files changed

+78
-7
lines changed

cpp/src/arrow/filesystem/s3fs.cc

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2860,17 +2860,41 @@ Status S3FileSystem::CreateDir(const std::string& s, bool recursive) {
28602860
return impl_->CreateBucket(path.bucket);
28612861
}
28622862

2863+
FileInfo file_info;
28632864
// Create object
28642865
if (recursive) {
28652866
// Ensure bucket exists
28662867
ARROW_ASSIGN_OR_RAISE(bool bucket_exists, impl_->BucketExists(path.bucket));
28672868
if (!bucket_exists) {
28682869
RETURN_NOT_OK(impl_->CreateBucket(path.bucket));
28692870
}
2871+
2872+
auto key_i = path.key_parts.begin();
2873+
std::string parent_key{};
2874+
if (options().check_directory_existence_before_creation) {
2875+
// Walk up the directory first to find the first existing parent
2876+
for (const auto& part : path.key_parts) {
2877+
parent_key += part;
2878+
parent_key += kSep;
2879+
}
2880+
for (key_i = path.key_parts.end(); key_i-- != path.key_parts.begin();) {
2881+
ARROW_ASSIGN_OR_RAISE(file_info,
2882+
this->GetFileInfo(path.bucket + kSep + parent_key));
2883+
if (file_info.type() != FileType::NotFound) {
2884+
// Found!
2885+
break;
2886+
} else {
2887+
// remove the kSep and the part
2888+
parent_key.pop_back();
2889+
parent_key.erase(parent_key.end() - key_i->size(), parent_key.end());
2890+
}
2891+
}
2892+
key_i++; // Above for loop moves one extra iterator at the end
2893+
}
28702894
// Ensure that all parents exist, then the directory itself
2871-
std::string parent_key;
2872-
for (const auto& part : path.key_parts) {
2873-
parent_key += part;
2895+
// Create all missing directories
2896+
for (; key_i < path.key_parts.end(); ++key_i) {
2897+
parent_key += *key_i;
28742898
parent_key += kSep;
28752899
RETURN_NOT_OK(impl_->CreateEmptyDir(path.bucket, parent_key));
28762900
}
@@ -2888,11 +2912,18 @@ Status S3FileSystem::CreateDir(const std::string& s, bool recursive) {
28882912
"': parent directory does not exist");
28892913
}
28902914
}
2915+
}
28912916

2892-
// XXX Should we check that no non-directory entry exists?
2893-
// Minio does it for us, not sure about other S3 implementations.
2894-
return impl_->CreateEmptyDir(path.bucket, path.key);
2917+
// Check if the directory exists already
2918+
if (options().check_directory_existence_before_creation) {
2919+
ARROW_ASSIGN_OR_RAISE(file_info, this->GetFileInfo(path.full_path));
2920+
if (file_info.type() != FileType::NotFound) {
2921+
return Status::OK();
2922+
}
28952923
}
2924+
// XXX Should we check that no non-directory entry exists?
2925+
// Minio does it for us, not sure about other S3 implementations.
2926+
return impl_->CreateEmptyDir(path.bucket, path.key);
28962927
}
28972928

28982929
Status S3FileSystem::DeleteDir(const std::string& s) {

cpp/src/arrow/filesystem/s3fs.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,17 @@ struct ARROW_EXPORT S3Options {
166166
/// Whether to allow deletion of buckets
167167
bool allow_bucket_deletion = false;
168168

169+
/// Whether to allow pessimistic directory creation in CreateDir function
170+
///
171+
/// By default, CreateDir function will try to create the directory without checking its
172+
/// existence. It's an optimization to try directory creation and catch the error,
173+
/// rather than issue two dependent I/O calls.
174+
/// Though for key/value storage like Google Cloud Storage, too many creation calls will
175+
/// breach the rate limit for object mutation operations and cause serious consequences.
176+
/// It's also possible you don't have creation access for the parent directory. Set it
177+
/// to be true to address these scenarios.
178+
bool check_directory_existence_before_creation = false;
179+
169180
/// \brief Default metadata for OpenOutputStream.
170181
///
171182
/// This will be ignored if non-empty metadata is passed to OpenOutputStream.

cpp/src/arrow/filesystem/s3fs_test.cc

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -922,9 +922,13 @@ TEST_F(TestS3FS, CreateDir) {
922922

923923
// New "directory"
924924
AssertFileInfo(fs_.get(), "bucket/newdir", FileType::NotFound);
925-
ASSERT_OK(fs_->CreateDir("bucket/newdir"));
925+
ASSERT_OK(fs_->CreateDir("bucket/newdir", /*recursive=*/false));
926926
AssertFileInfo(fs_.get(), "bucket/newdir", FileType::Directory);
927927

928+
// By default CreateDir uses recursvie mode, make it explictly to be false
929+
ASSERT_RAISES(IOError,
930+
fs_->CreateDir("bucket/newdir/newsub/newsubsub", /*recursive=*/false));
931+
928932
// New "directory", recursive
929933
ASSERT_OK(fs_->CreateDir("bucket/newdir/newsub/newsubsub", /*recursive=*/true));
930934
AssertFileInfo(fs_.get(), "bucket/newdir/newsub", FileType::Directory);
@@ -939,6 +943,31 @@ TEST_F(TestS3FS, CreateDir) {
939943
// Extraneous slashes
940944
ASSERT_RAISES(Invalid, fs_->CreateDir("bucket//somedir"));
941945
ASSERT_RAISES(Invalid, fs_->CreateDir("bucket/somedir//newdir"));
946+
947+
// check existing before creation
948+
options_.check_directory_existence_before_creation = true;
949+
MakeFileSystem();
950+
// New "directory" again
951+
AssertFileInfo(fs_.get(), "bucket/checknewdir", FileType::NotFound);
952+
ASSERT_OK(fs_->CreateDir("bucket/checknewdir"));
953+
AssertFileInfo(fs_.get(), "bucket/checknewdir", FileType::Directory);
954+
955+
ASSERT_RAISES(IOError, fs_->CreateDir("bucket/checknewdir/newsub/newsubsub/newsubsub/",
956+
/*recursive=*/false));
957+
958+
// New "directory" again, recursive
959+
ASSERT_OK(fs_->CreateDir("bucket/checknewdir/newsub/newsubsub", /*recursive=*/true));
960+
AssertFileInfo(fs_.get(), "bucket/checknewdir/newsub", FileType::Directory);
961+
AssertFileInfo(fs_.get(), "bucket/checknewdir/newsub/newsubsub", FileType::Directory);
962+
AssertFileInfo(fs_.get(), "bucket/checknewdir/newsub/newsubsub/newsubsub",
963+
FileType::NotFound);
964+
// Try creation with the same name
965+
ASSERT_OK(fs_->CreateDir("bucket/checknewdir/newsub/newsubsub/newsubsub/",
966+
/*recursive=*/true));
967+
AssertFileInfo(fs_.get(), "bucket/checknewdir/newsub", FileType::Directory);
968+
AssertFileInfo(fs_.get(), "bucket/checknewdir/newsub/newsubsub", FileType::Directory);
969+
AssertFileInfo(fs_.get(), "bucket/checknewdir/newsub/newsubsub/newsubsub",
970+
FileType::Directory);
942971
}
943972

944973
TEST_F(TestS3FS, DeleteFile) {

0 commit comments

Comments
 (0)