-
Notifications
You must be signed in to change notification settings - Fork 129
refactor(io): hand out S3 delegates by shared_ptr and build them off the lock #898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0eb0820
bc689cc
460ced6
851673a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,9 +17,12 @@ | |
| * under the License. | ||
| */ | ||
|
|
||
| #include <algorithm> | ||
| #include <cstdlib> | ||
| #include <memory> | ||
| #include <mutex> | ||
| #include <optional> | ||
| #include <shared_mutex> | ||
| #include <string> | ||
| #include <string_view> | ||
| #include <unordered_map> | ||
|
|
@@ -179,7 +182,7 @@ class ArrowS3FileIO final : public FileIO, public SupportsStorageCredentials { | |
| public: | ||
| ArrowS3FileIO(std::shared_ptr<::arrow::fs::FileSystem> arrow_fs, | ||
| std::unordered_map<std::string, std::string> default_properties) | ||
| : default_file_io_(std::move(arrow_fs)), | ||
| : default_file_io_(std::make_shared<ArrowFileSystemFileIO>(std::move(arrow_fs))), | ||
| default_properties_(std::move(default_properties)) {} | ||
|
|
||
| Result<std::unique_ptr<InputFile>> NewInputFile(std::string file_location) override; | ||
|
|
@@ -196,27 +199,67 @@ class ArrowS3FileIO final : public FileIO, public SupportsStorageCredentials { | |
| Status SetStorageCredentials( | ||
| const std::vector<StorageCredential>& storage_credentials) override; | ||
|
|
||
| const std::vector<StorageCredential>& credentials() const override { | ||
| std::vector<StorageCredential> credentials() const override { | ||
| std::shared_lock lock(mutex_); | ||
| return storage_credentials_; | ||
| } | ||
|
|
||
| SupportsStorageCredentials* AsSupportsStorageCredentials() override { return this; } | ||
|
|
||
| private: | ||
| ArrowFileSystemFileIO& FileIOForPath(std::string_view location); | ||
|
|
||
| ArrowFileSystemFileIO default_file_io_; | ||
| /// \brief Delegate serving `location`, pinned by the caller against a | ||
| /// concurrent credential install. | ||
| std::shared_ptr<ArrowFileSystemFileIO> FileIOForPath(std::string_view location); | ||
|
|
||
| using DelegatesByPrefix = | ||
| std::vector<std::pair<std::string, std::shared_ptr<ArrowFileSystemFileIO>>>; | ||
|
|
||
| /// \brief Longest-prefix match against one consistent view of the delegates. | ||
| static std::shared_ptr<ArrowFileSystemFileIO> MatchDelegate( | ||
| const std::shared_ptr<ArrowFileSystemFileIO>& fallback, | ||
| const DelegatesByPrefix& by_prefix, std::string_view location); | ||
|
|
||
| /// \brief Build a delegate for each credential this FileIO can serve. | ||
| /// | ||
| /// Runs without holding `mutex_`: building an S3 client can block (without | ||
| /// static keys the AWS SDK may wait on the EC2 metadata service), which would | ||
| /// stall every concurrent operation. Reads no mutable member state. | ||
| Result<DelegatesByPrefix> BuildDelegates( | ||
| const std::vector<StorageCredential>& storage_credentials) const; | ||
|
|
||
| /// \brief Swap in credentials and delegates, handing back the retired ones. | ||
| /// | ||
| /// Callers must hold `mutex_` exclusively and let the returned generation | ||
| /// destruct only after releasing it: tearing down an S3 client can block on | ||
| /// in-flight requests, which would stall every operation. | ||
| void InstallCredentials(std::vector<StorageCredential>& storage_credentials, | ||
| DelegatesByPrefix& delegates); | ||
|
|
||
| std::shared_ptr<ArrowFileSystemFileIO> default_file_io_; | ||
| std::unordered_map<std::string, std::string> default_properties_; | ||
| // Guards everything below; shared because reads happen per file operation. | ||
| mutable std::shared_mutex mutex_; | ||
| std::vector<StorageCredential> storage_credentials_; | ||
| std::vector<std::pair<std::string, std::unique_ptr<ArrowFileSystemFileIO>>> | ||
| file_io_by_prefix_; | ||
| DelegatesByPrefix file_io_by_prefix_; | ||
| }; | ||
|
|
||
| Status ArrowS3FileIO::SetStorageCredentials( | ||
| const std::vector<StorageCredential>& storage_credentials) { | ||
| std::vector<std::pair<std::string, std::unique_ptr<ArrowFileSystemFileIO>>> | ||
| file_io_by_prefix; | ||
| file_io_by_prefix.reserve(storage_credentials.size()); | ||
| ICEBERG_ASSIGN_OR_RAISE(auto delegates, BuildDelegates(storage_credentials)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. REST installs credentials only at FileIO creation. C++ has no refresh path. What production path reinstalls them on a live S3 FileIO?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| auto credentials = storage_credentials; | ||
| { | ||
| std::unique_lock lock(mutex_); | ||
| InstallCredentials(credentials, delegates); | ||
| } | ||
| // `credentials` and `delegates` now hold the retired generation and destruct | ||
| // here, outside the lock. | ||
| return {}; | ||
| } | ||
|
|
||
| Result<ArrowS3FileIO::DelegatesByPrefix> ArrowS3FileIO::BuildDelegates( | ||
| const std::vector<StorageCredential>& storage_credentials) const { | ||
| DelegatesByPrefix delegates; | ||
| delegates.reserve(storage_credentials.size()); | ||
| // TODO(gangwu): Refresh vended credentials via credentials.uri before tokens expire. | ||
| for (const auto& credential : storage_credentials) { | ||
| ICEBERG_RETURN_UNEXPECTED(credential.Validate()); | ||
|
|
@@ -231,62 +274,91 @@ Status ArrowS3FileIO::SetStorageCredentials( | |
| properties[key] = value; | ||
| } | ||
| ICEBERG_ASSIGN_OR_RAISE(auto fs, BuildArrowS3FileSystem(properties)); | ||
| file_io_by_prefix.emplace_back( | ||
| CanonicalizeS3Scheme(credential.prefix), | ||
| std::make_unique<ArrowFileSystemFileIO>(std::move(fs))); | ||
| delegates.emplace_back(CanonicalizeS3Scheme(credential.prefix), | ||
| std::make_shared<ArrowFileSystemFileIO>(std::move(fs))); | ||
| } | ||
| if (file_io_by_prefix.empty() && !storage_credentials.empty()) { | ||
| if (delegates.empty() && !storage_credentials.empty()) { | ||
| // Silent skipping of every vended credential is hard to diagnose: S3 access | ||
| // would proceed with the default credentials and fail only at IO time. | ||
| ICEBERG_LOG_WARN( | ||
| "None of the {} vended storage credential(s) has an S3-compatible prefix; " | ||
| "S3 access will use the default credentials", | ||
| storage_credentials.size()); | ||
| } | ||
| file_io_by_prefix_ = std::move(file_io_by_prefix); | ||
| storage_credentials_ = storage_credentials; | ||
| return {}; | ||
| return delegates; | ||
| } | ||
|
|
||
| ArrowFileSystemFileIO& ArrowS3FileIO::FileIOForPath(std::string_view location) { | ||
| if (file_io_by_prefix_.empty()) { | ||
| return default_file_io_; | ||
| void ArrowS3FileIO::InstallCredentials( | ||
| std::vector<StorageCredential>& storage_credentials, DelegatesByPrefix& delegates) { | ||
| file_io_by_prefix_.swap(delegates); | ||
| storage_credentials_.swap(storage_credentials); | ||
| } | ||
|
|
||
| std::shared_ptr<ArrowFileSystemFileIO> ArrowS3FileIO::MatchDelegate( | ||
| const std::shared_ptr<ArrowFileSystemFileIO>& fallback, | ||
| const DelegatesByPrefix& by_prefix, std::string_view location) { | ||
| if (by_prefix.empty()) { | ||
| return fallback; | ||
| } | ||
| const std::string canonical = CanonicalizeS3Scheme(location); | ||
| ArrowFileSystemFileIO* best = &default_file_io_; | ||
| auto best = fallback; | ||
| size_t best_len = 0; | ||
| for (const auto& [prefix, file_io] : file_io_by_prefix_) { | ||
| for (const auto& [prefix, file_io] : by_prefix) { | ||
| if (prefix.size() > best_len && canonical.starts_with(prefix)) { | ||
| best = file_io.get(); | ||
| best = file_io; | ||
| best_len = prefix.size(); | ||
| } | ||
| } | ||
| return *best; | ||
| return best; | ||
| } | ||
|
|
||
| std::shared_ptr<ArrowFileSystemFileIO> ArrowS3FileIO::FileIOForPath( | ||
| std::string_view location) { | ||
| std::shared_lock lock(mutex_); | ||
| return MatchDelegate(default_file_io_, file_io_by_prefix_, location); | ||
| } | ||
|
|
||
| Result<std::unique_ptr<InputFile>> ArrowS3FileIO::NewInputFile( | ||
| std::string file_location) { | ||
| return FileIOForPath(file_location).NewInputFile(std::move(file_location)); | ||
| return FileIOForPath(file_location)->NewInputFile(std::move(file_location)); | ||
| } | ||
|
|
||
| Result<std::unique_ptr<InputFile>> ArrowS3FileIO::NewInputFile(std::string file_location, | ||
| size_t length) { | ||
| return FileIOForPath(file_location).NewInputFile(std::move(file_location), length); | ||
| return FileIOForPath(file_location)->NewInputFile(std::move(file_location), length); | ||
| } | ||
|
|
||
| Result<std::unique_ptr<OutputFile>> ArrowS3FileIO::NewOutputFile( | ||
| std::string file_location) { | ||
| return FileIOForPath(file_location).NewOutputFile(std::move(file_location)); | ||
| return FileIOForPath(file_location)->NewOutputFile(std::move(file_location)); | ||
| } | ||
|
|
||
| Status ArrowS3FileIO::DeleteFile(const std::string& file_location) { | ||
| return FileIOForPath(file_location).DeleteFile(file_location); | ||
| return FileIOForPath(file_location)->DeleteFile(file_location); | ||
| } | ||
|
|
||
| Status ArrowS3FileIO::DeleteFiles(const std::vector<std::string>& file_locations) { | ||
| std::unordered_map<ArrowFileSystemFileIO*, std::vector<std::string>> locations_by_io; | ||
| // One snapshot so the whole batch matches the same delegate generation; only | ||
| // ever a handful of delegates, so a linear scan beats hashing. | ||
| std::shared_ptr<ArrowFileSystemFileIO> fallback; | ||
| DelegatesByPrefix by_prefix; | ||
| { | ||
| std::shared_lock lock(mutex_); | ||
| fallback = default_file_io_; | ||
| by_prefix = file_io_by_prefix_; | ||
| } | ||
| std::vector<std::pair<std::shared_ptr<ArrowFileSystemFileIO>, std::vector<std::string>>> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This retains the pre-existing fail-fast behavior. Java attempts all batches and counts failures. Is matching that behavior in scope?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| locations_by_io; | ||
| for (const auto& file_location : file_locations) { | ||
| locations_by_io[&FileIOForPath(file_location)].push_back(file_location); | ||
| auto file_io = MatchDelegate(fallback, by_prefix, file_location); | ||
| auto it = std::ranges::find_if( | ||
| locations_by_io, [&](const auto& entry) { return entry.first == file_io; }); | ||
| if (it == locations_by_io.end()) { | ||
| locations_by_io.emplace_back(std::move(file_io), | ||
| std::vector<std::string>{file_location}); | ||
| } else { | ||
| it->second.push_back(file_location); | ||
| } | ||
| } | ||
| for (auto& [file_io, locations] : locations_by_io) { | ||
| ICEBERG_RETURN_UNEXPECTED(file_io->DeleteFiles(locations)); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to change this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wgtmac MatchDelegate now returns a shared_ptr, so the default delegate is one too, which keeps a single owning return type.