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>
@@ -179,7 +182,7 @@ class ArrowS3FileIO final : public FileIO, public SupportsStorageCredentials {
179182 public:
180183 ArrowS3FileIO (std::shared_ptr<::arrow::fs::FileSystem> arrow_fs,
181184 std::unordered_map<std::string, std::string> default_properties)
182- : default_file_io_(std::move(arrow_fs)),
185+ : default_file_io_(std::make_shared<ArrowFileSystemFileIO>(std:: move(arrow_fs) )),
183186 default_properties_ (std::move(default_properties)) {}
184187
185188 Result<std::unique_ptr<InputFile>> NewInputFile (std::string file_location) override ;
@@ -196,27 +199,67 @@ class ArrowS3FileIO final : public FileIO, public SupportsStorageCredentials {
196199 Status SetStorageCredentials (
197200 const std::vector<StorageCredential>& storage_credentials) override ;
198201
199- const std::vector<StorageCredential>& credentials () const override {
202+ std::vector<StorageCredential> credentials () const override {
203+ std::shared_lock lock (mutex_);
200204 return storage_credentials_;
201205 }
202206
203207 SupportsStorageCredentials* AsSupportsStorageCredentials () override { return this ; }
204208
205209 private:
206- ArrowFileSystemFileIO& FileIOForPath (std::string_view location);
207-
208- ArrowFileSystemFileIO default_file_io_;
210+ // / \brief Delegate serving `location`, pinned by the caller against a
211+ // / concurrent credential install.
212+ std::shared_ptr<ArrowFileSystemFileIO> FileIOForPath (std::string_view location);
213+
214+ using DelegatesByPrefix =
215+ std::vector<std::pair<std::string, std::shared_ptr<ArrowFileSystemFileIO>>>;
216+
217+ // / \brief Longest-prefix match against one consistent view of the delegates.
218+ static std::shared_ptr<ArrowFileSystemFileIO> MatchDelegate (
219+ const std::shared_ptr<ArrowFileSystemFileIO>& fallback,
220+ const DelegatesByPrefix& by_prefix, std::string_view location);
221+
222+ // / \brief Build a delegate for each credential this FileIO can serve.
223+ // /
224+ // / Lock-free on purpose: building an S3 client can reach out to discover a
225+ // / bucket region, which would stall every concurrent operation. Reads no
226+ // / mutable member state.
227+ Result<DelegatesByPrefix> BuildDelegates (
228+ const std::vector<StorageCredential>& storage_credentials) const ;
229+
230+ // / \brief Swap in credentials and delegates, handing back the retired ones.
231+ // /
232+ // / Callers must hold `mutex_` exclusively and let the returned generation
233+ // / destruct only after releasing it: tearing down an S3 client can block on
234+ // / in-flight requests, which would stall every operation.
235+ void InstallCredentials (std::vector<StorageCredential>& storage_credentials,
236+ DelegatesByPrefix& delegates);
237+
238+ std::shared_ptr<ArrowFileSystemFileIO> default_file_io_;
209239 std::unordered_map<std::string, std::string> default_properties_;
240+ // Guards everything below; shared because reads happen per file operation.
241+ mutable std::shared_mutex mutex_;
210242 std::vector<StorageCredential> storage_credentials_;
211- std::vector<std::pair<std::string, std::unique_ptr<ArrowFileSystemFileIO>>>
212- file_io_by_prefix_;
243+ DelegatesByPrefix file_io_by_prefix_;
213244};
214245
215246Status ArrowS3FileIO::SetStorageCredentials (
216247 const std::vector<StorageCredential>& storage_credentials) {
217- std::vector<std::pair<std::string, std::unique_ptr<ArrowFileSystemFileIO>>>
218- file_io_by_prefix;
219- file_io_by_prefix.reserve (storage_credentials.size ());
248+ ICEBERG_ASSIGN_OR_RAISE (auto delegates, BuildDelegates (storage_credentials));
249+ auto credentials = storage_credentials;
250+ {
251+ std::unique_lock lock (mutex_);
252+ InstallCredentials (credentials, delegates);
253+ }
254+ // `credentials` and `delegates` now hold the retired generation and destruct
255+ // here, outside the lock.
256+ return {};
257+ }
258+
259+ Result<ArrowS3FileIO::DelegatesByPrefix> ArrowS3FileIO::BuildDelegates (
260+ const std::vector<StorageCredential>& storage_credentials) const {
261+ DelegatesByPrefix delegates;
262+ delegates.reserve (storage_credentials.size ());
220263 // TODO(gangwu): Refresh vended credentials via credentials.uri before tokens expire.
221264 for (const auto & credential : storage_credentials) {
222265 ICEBERG_RETURN_UNEXPECTED (credential.Validate ());
@@ -231,62 +274,91 @@ Status ArrowS3FileIO::SetStorageCredentials(
231274 properties[key] = value;
232275 }
233276 ICEBERG_ASSIGN_OR_RAISE (auto fs, BuildArrowS3FileSystem (properties));
234- file_io_by_prefix.emplace_back (
235- CanonicalizeS3Scheme (credential.prefix ),
236- std::make_unique<ArrowFileSystemFileIO>(std::move (fs)));
277+ delegates.emplace_back (CanonicalizeS3Scheme (credential.prefix ),
278+ std::make_shared<ArrowFileSystemFileIO>(std::move (fs)));
237279 }
238- if (file_io_by_prefix .empty () && !storage_credentials.empty ()) {
280+ if (delegates .empty () && !storage_credentials.empty ()) {
239281 // Silent skipping of every vended credential is hard to diagnose: S3 access
240282 // would proceed with the default credentials and fail only at IO time.
241283 ICEBERG_LOG_WARN (
242284 " None of the {} vended storage credential(s) has an S3-compatible prefix; "
243285 " S3 access will use the default credentials" ,
244286 storage_credentials.size ());
245287 }
246- file_io_by_prefix_ = std::move (file_io_by_prefix);
247- storage_credentials_ = storage_credentials;
248- return {};
288+ return delegates;
249289}
250290
251- ArrowFileSystemFileIO& ArrowS3FileIO::FileIOForPath (std::string_view location) {
252- if (file_io_by_prefix_.empty ()) {
253- return default_file_io_;
291+ void ArrowS3FileIO::InstallCredentials (
292+ std::vector<StorageCredential>& storage_credentials, DelegatesByPrefix& delegates) {
293+ file_io_by_prefix_.swap (delegates);
294+ storage_credentials_.swap (storage_credentials);
295+ }
296+
297+ std::shared_ptr<ArrowFileSystemFileIO> ArrowS3FileIO::MatchDelegate (
298+ const std::shared_ptr<ArrowFileSystemFileIO>& fallback,
299+ const DelegatesByPrefix& by_prefix, std::string_view location) {
300+ if (by_prefix.empty ()) {
301+ return fallback;
254302 }
255303 const std::string canonical = CanonicalizeS3Scheme (location);
256- ArrowFileSystemFileIO* best = &default_file_io_ ;
304+ auto best = fallback ;
257305 size_t best_len = 0 ;
258- for (const auto & [prefix, file_io] : file_io_by_prefix_ ) {
306+ for (const auto & [prefix, file_io] : by_prefix ) {
259307 if (prefix.size () > best_len && canonical.starts_with (prefix)) {
260- best = file_io. get () ;
308+ best = file_io;
261309 best_len = prefix.size ();
262310 }
263311 }
264- return *best;
312+ return best;
313+ }
314+
315+ std::shared_ptr<ArrowFileSystemFileIO> ArrowS3FileIO::FileIOForPath (
316+ std::string_view location) {
317+ std::shared_lock lock (mutex_);
318+ return MatchDelegate (default_file_io_, file_io_by_prefix_, location);
265319}
266320
267321Result<std::unique_ptr<InputFile>> ArrowS3FileIO::NewInputFile (
268322 std::string file_location) {
269- return FileIOForPath (file_location). NewInputFile (std::move (file_location));
323+ return FileIOForPath (file_location)-> NewInputFile (std::move (file_location));
270324}
271325
272326Result<std::unique_ptr<InputFile>> ArrowS3FileIO::NewInputFile (std::string file_location,
273327 size_t length) {
274- return FileIOForPath (file_location). NewInputFile (std::move (file_location), length);
328+ return FileIOForPath (file_location)-> NewInputFile (std::move (file_location), length);
275329}
276330
277331Result<std::unique_ptr<OutputFile>> ArrowS3FileIO::NewOutputFile (
278332 std::string file_location) {
279- return FileIOForPath (file_location). NewOutputFile (std::move (file_location));
333+ return FileIOForPath (file_location)-> NewOutputFile (std::move (file_location));
280334}
281335
282336Status ArrowS3FileIO::DeleteFile (const std::string& file_location) {
283- return FileIOForPath (file_location). DeleteFile (file_location);
337+ return FileIOForPath (file_location)-> DeleteFile (file_location);
284338}
285339
286340Status ArrowS3FileIO::DeleteFiles (const std::vector<std::string>& file_locations) {
287- std::unordered_map<ArrowFileSystemFileIO*, std::vector<std::string>> locations_by_io;
341+ // One snapshot so the whole batch matches the same delegate generation; only
342+ // ever a handful of delegates, so a linear scan beats hashing.
343+ std::shared_ptr<ArrowFileSystemFileIO> fallback;
344+ DelegatesByPrefix by_prefix;
345+ {
346+ std::shared_lock lock (mutex_);
347+ fallback = default_file_io_;
348+ by_prefix = file_io_by_prefix_;
349+ }
350+ std::vector<std::pair<std::shared_ptr<ArrowFileSystemFileIO>, std::vector<std::string>>>
351+ locations_by_io;
288352 for (const auto & file_location : file_locations) {
289- locations_by_io[&FileIOForPath (file_location)].push_back (file_location);
353+ auto file_io = MatchDelegate (fallback, by_prefix, file_location);
354+ auto it = std::ranges::find_if (
355+ locations_by_io, [&](const auto & entry) { return entry.first == file_io; });
356+ if (it == locations_by_io.end ()) {
357+ locations_by_io.emplace_back (std::move (file_io),
358+ std::vector<std::string>{file_location});
359+ } else {
360+ it->second .push_back (file_location);
361+ }
290362 }
291363 for (auto & [file_io, locations] : locations_by_io) {
292364 ICEBERG_RETURN_UNEXPECTED (file_io->DeleteFiles (locations));
0 commit comments