From 091e70d35face9bc89db05c884364171dc9cd9f3 Mon Sep 17 00:00:00 2001 From: Vincenzo Eduardo Padulano Date: Sat, 1 Nov 2025 00:05:31 +0100 Subject: [PATCH] [io] Centralise XRootD EOS URL redirection The logic to extract the extended attribute 'eos.url.xroot' from a path to an EOS file on a FUSE mount is centralised in a separate header in RIO, with an internal function called GetEOSRedirectedXRootUrl. This function can be called anywhere needed in the rest of ROOT. With this commit, the following places use this functionality: - TFile::Open (done before this commit) - RLoopManager::ChangeSpec (done before this commit) - RRawFile::Create (introduced in this commit) In particular the last means introducing the redirection also for RNTupleReader, which follows a different logic to open the TFile. --- io/io/CMakeLists.txt | 3 ++ io/io/res/ROOT/InternalIOUtils.hxx | 24 +++++++++ io/io/src/InternalIOUtils.cxx | 82 +++++++++++++++++++++++++++++ io/io/src/RRawFile.cxx | 4 ++ io/io/src/TFile.cxx | 33 +++--------- tree/dataframe/src/RLoopManager.cxx | 46 +++------------- 6 files changed, 127 insertions(+), 65 deletions(-) create mode 100644 io/io/res/ROOT/InternalIOUtils.hxx create mode 100644 io/io/src/InternalIOUtils.cxx diff --git a/io/io/CMakeLists.txt b/io/io/CMakeLists.txt index 6f30d2d9ac96a..d14937b2e6284 100644 --- a/io/io/CMakeLists.txt +++ b/io/io/CMakeLists.txt @@ -21,6 +21,7 @@ if (uring) endif () ROOT_LINKER_LIBRARY(RIO + src/InternalIOUtils.cxx src/RConcurrentHashColl.cxx src/RFile.cxx src/RRawFile.cxx @@ -66,6 +67,8 @@ ROOT_LINKER_LIBRARY(RIO ) target_include_directories(RIO PRIVATE ${CMAKE_SOURCE_DIR}/core/clib/res) +# PUBLIC so that dependent libraries e.g. ROOTDataFrame may find the internal header too +target_include_directories(RIO PUBLIC $) target_link_libraries(RIO PUBLIC ${ROOT_ATOMIC_LIBS}) target_link_libraries(RIO PRIVATE nlohmann_json::nlohmann_json) diff --git a/io/io/res/ROOT/InternalIOUtils.hxx b/io/io/res/ROOT/InternalIOUtils.hxx new file mode 100644 index 0000000000000..9c62c835d9e83 --- /dev/null +++ b/io/io/res/ROOT/InternalIOUtils.hxx @@ -0,0 +1,24 @@ +// Author: Vincenzo Eduardo Padulano (CERN), 08/2026 + +#ifndef ROOT_IO_UTILS +#define ROOT_IO_UTILS + +#include +#include +#include + +namespace ROOT::Internal { + +/// \brief Get extended attribute value from path +/// \param path Path to the file to check +/// \param xattr Extended attribute to evaluate +/// \return The string containing the extended attribute value if found, std::nullopt otherwise +std::optional GetXAttrVal(std::string_view path, std::string_view xattr); + +/// \brief Redirects the input path to the equivalent XRootD URL on EOS +/// \param inputUrl The input path to redirect +/// \return The redirected URL in case of successful redirection, std::nullopt otherwise +std::optional GetEOSRedirectedXRootURL(std::string_view inputPath); +} // namespace ROOT::Internal + +#endif diff --git a/io/io/src/InternalIOUtils.cxx b/io/io/src/InternalIOUtils.cxx new file mode 100644 index 0000000000000..f494d9461d456 --- /dev/null +++ b/io/io/src/InternalIOUtils.cxx @@ -0,0 +1,82 @@ +#include "ROOT/InternalIOUtils.hxx" + +#include "ROOT/RConfig.hxx" // R__UNIX + +#ifdef R__UNIX + +#include + +ROOT::RLogChannel &InternalLogChannel() +{ + static ROOT::RLogChannel sLog("ROOT.Internal"); + return sLog; +} + +// getxattr +#ifdef R__FBSD +#include +#else +#include +#endif + +#ifdef R__MACOSX +/* On macOS getxattr takes two extra arguments that should be set to 0 */ +#define getxattr(path, name, value, size) getxattr(path, name, value, size, 0u, 0) +#endif + +#ifdef R__FBSD +#define getxattr(path, name, value, size) extattr_get_file(path, EXTATTR_NAMESPACE_USER, name, value, size) +#endif + +#include "ROOT/StringUtils.hxx" // ROOT::StartsWith +#include "TEnv.h" // TEnv::GetValue +#endif + +std::optional +ROOT::Internal::GetXAttrVal([[maybe_unused]] std::string_view path, [[maybe_unused]] std::string_view xattr) +{ +#ifdef R__UNIX + // First call to getxattr evaluates the length of the extended attribute value + if (auto len = getxattr(path.data(), xattr.data(), nullptr, 0); len >= 0) { + std::string xval(len, 0); + // Second call extracts the extended attribute value, checking it's of the correct length + if (getxattr(path.data(), xattr.data(), xval.data(), len) == len) + return xval; + } +#endif + return std::nullopt; +} + +std::optional ROOT::Internal::GetEOSRedirectedXRootURL([[maybe_unused]] std::string_view inputPath) +{ +#ifdef R__UNIX + if (inputPath.empty() || inputPath.back() == '/') + return std::nullopt; + + if (gEnv->GetValue("TFile.CrossProtocolRedirects", 1) != 1) + return std::nullopt; + + auto xurl = ROOT::Internal::GetXAttrVal(inputPath, "eos.url.xroot"); + if (!xurl) + return std::nullopt; + + auto baseName = inputPath.substr(inputPath.find_last_of("/") + 1); + // Sometimes the `getxattr` call may return an invalid URL due + // to the POSIX attribute not being yet completely filled by EOS. + if (!std::equal(baseName.crbegin(), baseName.crend(), xurl->crbegin())) { + R__LOG_WARNING(InternalLogChannel()) + << "Could not find path base name '" << baseName << "' in redirected URL '" << *xurl << "'."; + return std::nullopt; + } + + // Ensure the redirected URL actually starts with the XRootD protocol string + if (ROOT::StartsWith(*xurl, "root://") || ROOT::StartsWith(*xurl, "xroot://") || + ROOT::StartsWith(*xurl, "roots://") || ROOT::StartsWith(*xurl, "xroots://")) + return xurl; + else + R__LOG_WARNING(InternalLogChannel()) + << "Redirected URL '" << *xurl << "' does not begin with any valid XRootD protocol string."; + +#endif + return std::nullopt; +} diff --git a/io/io/src/RRawFile.cxx b/io/io/src/RRawFile.cxx index 1304f6b8dda72..58d54daacf064 100644 --- a/io/io/src/RRawFile.cxx +++ b/io/io/src/RRawFile.cxx @@ -20,6 +20,7 @@ #include "TError.h" #include "TPluginManager.h" #include "TROOT.h" +#include "ROOT/InternalIOUtils.hxx" #include #include // for towlower @@ -68,6 +69,9 @@ ROOT::Internal::RRawFile::Create(std::string_view url, ROptions options) #ifdef _WIN32 return std::unique_ptr(new RRawFileWin(url, options)); #else + // We're assuming the input url is null-terminated in the next call + if (auto xurl = ROOT::Internal::GetEOSRedirectedXRootURL(url)) + return Create(*xurl, options); return std::unique_ptr(new RRawFileUnix(url, options)); #endif } diff --git a/io/io/src/TFile.cxx b/io/io/src/TFile.cxx index 993c3f0cb6ab6..d3450dc3ee791 100644 --- a/io/io/src/TFile.cxx +++ b/io/io/src/TFile.cxx @@ -171,6 +171,7 @@ The structure of a directory is shown in TDirectoryFile::TDirectoryFile #include "TThreadSlots.h" #include "TGlobal.h" #include "ROOT/RConcurrentHashColl.hxx" +#include "ROOT/InternalIOUtils.hxx" #include #include @@ -3811,34 +3812,16 @@ TFile *TFile::Open(const char *url, Option_t *options, const char *ftitle, TString expandedUrl(url); gSystem->ExpandPathName(expandedUrl); -#ifdef R__UNIX - // If URL is a file on an EOS FUSE mount, attempt redirection to XRootD protocol. - if (gEnv->GetValue("TFile.CrossProtocolRedirects", 1) == 1) { - TUrl fileurl(expandedUrl, /* default is file */ kTRUE); - if (strcmp(fileurl.GetProtocol(), "file") == 0) { - ssize_t len = getxattr(fileurl.GetFile(), "eos.url.xroot", nullptr, 0); - if (len > 0) { - std::string xurl(len, 0); - std::string fileNameFromUrl{fileurl.GetFile()}; - if (getxattr(fileNameFromUrl.c_str(), "eos.url.xroot", &xurl[0], len) == len) { - // Sometimes the `getxattr` call may return an invalid URL due - // to the POSIX attribute not being yet completely filled by EOS. - if (auto baseName = fileNameFromUrl.substr(fileNameFromUrl.find_last_of("/") + 1); - std::equal(baseName.crbegin(), baseName.crend(), xurl.crbegin())) { - if ((f = TFile::Open(xurl.c_str(), options, ftitle, compress, netopt))) { - if (!f->IsZombie()) { - return f; - } else { - delete f; - f = nullptr; - } - } - } - } + if (auto xurl = ROOT::Internal::GetEOSRedirectedXRootURL(expandedUrl)) { + if ((f = TFile::Open(xurl->c_str(), options, ftitle, compress, netopt))) { + if (!f->IsZombie()) { + return f; + } else { + delete f; + f = nullptr; } } } -#endif // If a timeout has been specified extract the value and try to apply it (it requires // support for asynchronous open, though; the following is completely transparent if diff --git a/tree/dataframe/src/RLoopManager.cxx b/tree/dataframe/src/RLoopManager.cxx index 5a4b36330d0c8..591b3790666e9 100644 --- a/tree/dataframe/src/RLoopManager.cxx +++ b/tree/dataframe/src/RLoopManager.cxx @@ -43,23 +43,8 @@ #include "ROOT/RSlotStack.hxx" #endif -#ifdef R__UNIX -// Functions needed to perform EOS XRootD redirection in ChangeSpec -#include "TEnv.h" +#include "ROOT/InternalIOUtils.hxx" #include "TSystem.h" -#ifndef R__FBSD -#include -#else -#include -#endif -#ifdef R__MACOSX -/* On macOS getxattr takes two extra arguments that should be set to 0 */ -#define getxattr(path, name, value, size) getxattr(path, name, value, size, 0u, 0) -#endif -#ifdef R__FBSD -#define getxattr(path, name, value, size) extattr_get_file(path, EXTATTR_NAMESPACE_USER, name, value, size) -#endif -#endif #include #include @@ -370,7 +355,6 @@ RLoopManager::RLoopManager(ROOT::RDF::Experimental::RDatasetSpec &&spec) ChangeSpec(std::move(spec)); } -#ifdef R__UNIX namespace { std::optional GetRedirectedSampleId(std::string_view path, std::string_view datasetName) { @@ -378,29 +362,14 @@ std::optional GetRedirectedSampleId(std::string_view path, std::str // If so, we create a redirected sample ID with the full xroot URL. TString expandedUrl(path.data()); gSystem->ExpandPathName(expandedUrl); - if (gEnv->GetValue("TFile.CrossProtocolRedirects", 1) == 1) { - TUrl fileurl(expandedUrl, /* default is file */ kTRUE); - if (strcmp(fileurl.GetProtocol(), "file") == 0) { - ssize_t len = getxattr(fileurl.GetFile(), "eos.url.xroot", nullptr, 0); - if (len > 0) { - std::string xurl(len, 0); - std::string fileNameFromUrl{fileurl.GetFile()}; - if (getxattr(fileNameFromUrl.c_str(), "eos.url.xroot", &xurl[0], len) == len) { - // Sometimes the `getxattr` call may return an invalid URL due - // to the POSIX attribute not being yet completely filled by EOS. - if (auto baseName = fileNameFromUrl.substr(fileNameFromUrl.find_last_of("/") + 1); - std::equal(baseName.crbegin(), baseName.crend(), xurl.crbegin())) { - return xurl + '/' + datasetName.data(); - } - } - } - } + TUrl fileurl(expandedUrl, /* default is file */ kTRUE); + if (strcmp(fileurl.GetProtocol(), "file") == 0) { + if (auto xurl = ROOT::Internal::GetEOSRedirectedXRootURL(fileurl.GetFile())) + return *xurl + '/' + datasetName.data(); } - return std::nullopt; } } // namespace -#endif /** * @brief Changes the internal TTree held by the RLoopManager. @@ -451,11 +420,10 @@ void RLoopManager::ChangeSpec(ROOT::RDF::Experimental::RDatasetSpec &&spec) // is exposed to users via RSampleInfo and DefinePerSample). const auto sampleId = files[i] + '/' + trees[i]; fSampleMap.insert({sampleId, &sample}); -#ifdef R__UNIX + // Also add redirected EOS xroot URL when available if (auto redirectedSampleId = GetRedirectedSampleId(files[i], trees[i])) fSampleMap.insert({redirectedSampleId.value(), &sample}); -#endif } } fDataSource = std::make_unique(std::move(chain), spec.GetFriendInfo()); @@ -473,11 +441,9 @@ void RLoopManager::ChangeSpec(ROOT::RDF::Experimental::RDatasetSpec &&spec) fileNames.push_back(files[i]); rntupleNames.insert(trees[i]); -#ifdef R__UNIX // Also add redirected EOS xroot URL when available if (auto redirectedSampleId = GetRedirectedSampleId(files[i], trees[i])) fSampleMap.insert({redirectedSampleId.value(), &sample}); -#endif } }