1717 * under the License.
1818 */
1919
20+ #include < algorithm>
2021#include < cstdlib>
2122#include < memory>
23+ #include < mutex>
2224#include < optional>
25+ #include < shared_mutex>
2326#include < string>
2427#include < string_view>
2528#include < unordered_map>
@@ -190,7 +193,7 @@ class ArrowS3FileIO final : public FileIO, public SupportsStorageCredentials {
190193 public:
191194 ArrowS3FileIO (std::shared_ptr<::arrow::fs::FileSystem> arrow_fs,
192195 std::unordered_map<std::string, std::string> default_properties)
193- : default_file_io_(std::move(arrow_fs)),
196+ : default_file_io_(std::make_shared<ArrowFileSystemFileIO>(std:: move(arrow_fs) )),
194197 default_properties_ (std::move(default_properties)) {}
195198
196199 Result<std::unique_ptr<InputFile>> NewInputFile (std::string file_location) override ;
@@ -207,27 +210,67 @@ class ArrowS3FileIO final : public FileIO, public SupportsStorageCredentials {
207210 Status SetStorageCredentials (
208211 const std::vector<StorageCredential>& storage_credentials) override ;
209212
210- const std::vector<StorageCredential>& credentials () const override {
213+ std::vector<StorageCredential> credentials () const override {
214+ std::shared_lock lock (mutex_);
211215 return storage_credentials_;
212216 }
213217
214218 SupportsStorageCredentials* AsSupportsStorageCredentials () override { return this ; }
215219
216220 private:
217- ArrowFileSystemFileIO& FileIOForPath (std::string_view location);
218-
219- ArrowFileSystemFileIO default_file_io_;
221+ // / \brief Delegate serving `location`, pinned by the caller against a
222+ // / concurrent credential install.
223+ std::shared_ptr<ArrowFileSystemFileIO> FileIOForPath (std::string_view location);
224+
225+ using DelegatesByPrefix =
226+ std::vector<std::pair<std::string, std::shared_ptr<ArrowFileSystemFileIO>>>;
227+
228+ // / \brief Longest-prefix match against one consistent view of the delegates.
229+ static std::shared_ptr<ArrowFileSystemFileIO> MatchDelegate (
230+ const std::shared_ptr<ArrowFileSystemFileIO>& fallback,
231+ const DelegatesByPrefix& by_prefix, std::string_view location);
232+
233+ // / \brief Build a delegate for each credential this FileIO can serve.
234+ // /
235+ // / Lock-free on purpose: building an S3 client can reach out to discover a
236+ // / bucket region, which would stall every concurrent operation. Reads no
237+ // / mutable member state.
238+ Result<DelegatesByPrefix> BuildDelegates (
239+ const std::vector<StorageCredential>& storage_credentials) const ;
240+
241+ // / \brief Swap in credentials and delegates, handing back the retired ones.
242+ // /
243+ // / Callers must hold `mutex_` exclusively and let the returned generation
244+ // / destruct only after releasing it: tearing down an S3 client can block on
245+ // / in-flight requests, which would stall every operation.
246+ void InstallCredentials (std::vector<StorageCredential>& storage_credentials,
247+ DelegatesByPrefix& delegates);
248+
249+ std::shared_ptr<ArrowFileSystemFileIO> default_file_io_;
220250 std::unordered_map<std::string, std::string> default_properties_;
251+ // Guards everything below; shared because reads happen per file operation.
252+ mutable std::shared_mutex mutex_;
221253 std::vector<StorageCredential> storage_credentials_;
222- std::vector<std::pair<std::string, std::unique_ptr<ArrowFileSystemFileIO>>>
223- file_io_by_prefix_;
254+ DelegatesByPrefix file_io_by_prefix_;
224255};
225256
226257Status ArrowS3FileIO::SetStorageCredentials (
227258 const std::vector<StorageCredential>& storage_credentials) {
228- std::vector<std::pair<std::string, std::unique_ptr<ArrowFileSystemFileIO>>>
229- file_io_by_prefix;
230- file_io_by_prefix.reserve (storage_credentials.size ());
259+ ICEBERG_ASSIGN_OR_RAISE (auto delegates, BuildDelegates (storage_credentials));
260+ auto credentials = storage_credentials;
261+ {
262+ std::unique_lock lock (mutex_);
263+ InstallCredentials (credentials, delegates);
264+ }
265+ // `credentials` and `delegates` now hold the retired generation and destruct
266+ // here, outside the lock.
267+ return {};
268+ }
269+
270+ Result<ArrowS3FileIO::DelegatesByPrefix> ArrowS3FileIO::BuildDelegates (
271+ const std::vector<StorageCredential>& storage_credentials) const {
272+ DelegatesByPrefix delegates;
273+ delegates.reserve (storage_credentials.size ());
231274 // TODO(gangwu): Refresh vended credentials via credentials.uri before tokens expire.
232275 for (const auto & credential : storage_credentials) {
233276 ICEBERG_RETURN_UNEXPECTED (credential.Validate ());
@@ -242,62 +285,91 @@ Status ArrowS3FileIO::SetStorageCredentials(
242285 properties[key] = value;
243286 }
244287 ICEBERG_ASSIGN_OR_RAISE (auto fs, BuildArrowS3FileSystem (properties));
245- file_io_by_prefix.emplace_back (
246- CanonicalizeS3Scheme (credential.prefix ),
247- std::make_unique<ArrowFileSystemFileIO>(std::move (fs)));
288+ delegates.emplace_back (CanonicalizeS3Scheme (credential.prefix ),
289+ std::make_shared<ArrowFileSystemFileIO>(std::move (fs)));
248290 }
249- if (file_io_by_prefix .empty () && !storage_credentials.empty ()) {
291+ if (delegates .empty () && !storage_credentials.empty ()) {
250292 // Silent skipping of every vended credential is hard to diagnose: S3 access
251293 // would proceed with the default credentials and fail only at IO time.
252294 ICEBERG_LOG_WARN (
253295 " None of the {} vended storage credential(s) has an S3-compatible prefix; "
254296 " S3 access will use the default credentials" ,
255297 storage_credentials.size ());
256298 }
257- file_io_by_prefix_ = std::move (file_io_by_prefix);
258- storage_credentials_ = storage_credentials;
259- return {};
299+ return delegates;
260300}
261301
262- ArrowFileSystemFileIO& ArrowS3FileIO::FileIOForPath (std::string_view location) {
263- if (file_io_by_prefix_.empty ()) {
264- return default_file_io_;
302+ void ArrowS3FileIO::InstallCredentials (
303+ std::vector<StorageCredential>& storage_credentials, DelegatesByPrefix& delegates) {
304+ file_io_by_prefix_.swap (delegates);
305+ storage_credentials_.swap (storage_credentials);
306+ }
307+
308+ std::shared_ptr<ArrowFileSystemFileIO> ArrowS3FileIO::MatchDelegate (
309+ const std::shared_ptr<ArrowFileSystemFileIO>& fallback,
310+ const DelegatesByPrefix& by_prefix, std::string_view location) {
311+ if (by_prefix.empty ()) {
312+ return fallback;
265313 }
266314 const std::string canonical = CanonicalizeS3Scheme (location);
267- ArrowFileSystemFileIO* best = &default_file_io_ ;
315+ auto best = fallback ;
268316 size_t best_len = 0 ;
269- for (const auto & [prefix, file_io] : file_io_by_prefix_ ) {
317+ for (const auto & [prefix, file_io] : by_prefix ) {
270318 if (prefix.size () > best_len && canonical.starts_with (prefix)) {
271- best = file_io. get () ;
319+ best = file_io;
272320 best_len = prefix.size ();
273321 }
274322 }
275- return *best;
323+ return best;
324+ }
325+
326+ std::shared_ptr<ArrowFileSystemFileIO> ArrowS3FileIO::FileIOForPath (
327+ std::string_view location) {
328+ std::shared_lock lock (mutex_);
329+ return MatchDelegate (default_file_io_, file_io_by_prefix_, location);
276330}
277331
278332Result<std::unique_ptr<InputFile>> ArrowS3FileIO::NewInputFile (
279333 std::string file_location) {
280- return FileIOForPath (file_location). NewInputFile (std::move (file_location));
334+ return FileIOForPath (file_location)-> NewInputFile (std::move (file_location));
281335}
282336
283337Result<std::unique_ptr<InputFile>> ArrowS3FileIO::NewInputFile (std::string file_location,
284338 size_t length) {
285- return FileIOForPath (file_location). NewInputFile (std::move (file_location), length);
339+ return FileIOForPath (file_location)-> NewInputFile (std::move (file_location), length);
286340}
287341
288342Result<std::unique_ptr<OutputFile>> ArrowS3FileIO::NewOutputFile (
289343 std::string file_location) {
290- return FileIOForPath (file_location). NewOutputFile (std::move (file_location));
344+ return FileIOForPath (file_location)-> NewOutputFile (std::move (file_location));
291345}
292346
293347Status ArrowS3FileIO::DeleteFile (const std::string& file_location) {
294- return FileIOForPath (file_location). DeleteFile (file_location);
348+ return FileIOForPath (file_location)-> DeleteFile (file_location);
295349}
296350
297351Status ArrowS3FileIO::DeleteFiles (const std::vector<std::string>& file_locations) {
298- std::unordered_map<ArrowFileSystemFileIO*, std::vector<std::string>> locations_by_io;
352+ // One snapshot so the whole batch matches the same delegate generation; only
353+ // ever a handful of delegates, so a linear scan beats hashing.
354+ std::shared_ptr<ArrowFileSystemFileIO> fallback;
355+ DelegatesByPrefix by_prefix;
356+ {
357+ std::shared_lock lock (mutex_);
358+ fallback = default_file_io_;
359+ by_prefix = file_io_by_prefix_;
360+ }
361+ std::vector<std::pair<std::shared_ptr<ArrowFileSystemFileIO>, std::vector<std::string>>>
362+ locations_by_io;
299363 for (const auto & file_location : file_locations) {
300- locations_by_io[&FileIOForPath (file_location)].push_back (file_location);
364+ auto file_io = MatchDelegate (fallback, by_prefix, file_location);
365+ auto it = std::ranges::find_if (
366+ locations_by_io, [&](const auto & entry) { return entry.first == file_io; });
367+ if (it == locations_by_io.end ()) {
368+ locations_by_io.emplace_back (std::move (file_io),
369+ std::vector<std::string>{file_location});
370+ } else {
371+ it->second .push_back (file_location);
372+ }
301373 }
302374 for (auto & [file_io, locations] : locations_by_io) {
303375 ICEBERG_RETURN_UNEXPECTED (file_io->DeleteFiles (locations));
0 commit comments