-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[ntuple] Add RPageSinkS3 for the basic S3 write path (Mode B) #22653
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: master
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -13,11 +13,15 @@ | |
| #ifndef ROOT_RPageStorageS3 | ||
| #define ROOT_RPageStorageS3 | ||
|
|
||
| #include <ROOT/RCurlConnection.hxx> | ||
| #include <ROOT/RError.hxx> | ||
| #include <ROOT/RNTuple.hxx> | ||
| #include <ROOT/RPageStorage.hxx> | ||
|
|
||
| #include <cstdint> | ||
| #include <memory> | ||
| #include <string> | ||
| #include <string_view> | ||
|
|
||
| namespace ROOT { | ||
| namespace Experimental { | ||
|
|
@@ -43,9 +47,10 @@ struct RNTupleAnchorS3 { | |
| std::uint16_t fVersionMajor = RNTuple::kVersionMajor; | ||
| std::uint16_t fVersionMinor = RNTuple::kVersionMinor; | ||
| std::uint16_t fVersionPatch = RNTuple::kVersionPatch; | ||
| /// Pattern for resolving object IDs to full S3 URLs. | ||
| /// ${baseurl} is replaced with the anchor URL, ${objid} with the numeric object ID. | ||
| std::string fUrlTemplate; | ||
| /// Pattern for resolving object IDs to full S3 URLs. ${baseurl} is replaced with the anchor URL, | ||
| /// ${objid} with the numeric object ID. Defaults to the scheme this writer uses; the reader | ||
| /// overrides it from the stored anchor. | ||
| std::string fUrlTemplate = "${baseurl}/${objid}"; | ||
| /// Object ID and byte offset of the compressed header within the S3 object | ||
| std::uint64_t fHeaderObjId = 0; | ||
| std::uint64_t fHeaderOffset = 0; | ||
|
|
@@ -67,6 +72,68 @@ struct RNTupleAnchorS3 { | |
| static RResult<RNTupleAnchorS3> CreateFromJSON(const std::string &json); | ||
| }; | ||
|
|
||
| /// \brief Translate an ntpl+s3 URI into its plain HTTP(S) equivalent. | ||
| /// | ||
| /// Accepts `ntpl+s3+http://host/bucket/path` and `ntpl+s3+https://host/bucket/path`, returning the | ||
| /// URL with the scheme replaced by http or https respectively. Throws RException on any other scheme. | ||
| std::string ParseS3Url(std::string_view uri); | ||
|
|
||
| // clang-format off | ||
| /** | ||
| \class ROOT::Experimental::Internal::RPageSinkS3 | ||
| \ingroup NTuple | ||
| \brief Storage provider that writes ntuple pages into S3-compatible object storage. | ||
|
|
||
| Currently implements Mode B (one sealed page per S3 object, kTypeObject64 locators). | ||
| Mode A (multiple packed pages per object, kTypeMulti locators) will be added separately. | ||
|
|
||
| \warning The S3 backend is experimental and under active development. | ||
| */ | ||
| // clang-format on | ||
| class RPageSinkS3 : public ROOT::Internal::RPagePersistentSink { | ||
| private: | ||
| /// HTTP base URL for this ntuple (derived from the s3 scheme URI); never has a trailing slash | ||
| std::string fBaseUrl; | ||
| /// One HTTP connection reused for every upload, so curl keeps it alive across objects on the same | ||
| /// host instead of re-handshaking per object. | ||
| ROOT::Internal::RCurlConnection fConnection; | ||
| /// Object ID counter; incremented for each object written. | ||
| std::uint64_t fObjectId{0}; | ||
| /// Tracks the number of bytes committed to the current cluster (reset in StageClusterImpl) | ||
| std::uint64_t fNBytesCurrentCluster{0}; | ||
| /// Anchor metadata populated during the write path and uploaded last in CommitDatasetImpl | ||
| RNTupleAnchorS3 fAnchor; | ||
|
|
||
| /// Resolve a numeric object ID to its full HTTP URL | ||
| std::string MakeObjectUrl(std::uint64_t objId) const; | ||
| /// Upload raw bytes to the given S3 URL via an HTTP PUT request | ||
| void PutObject(const std::string &url, const unsigned char *data, std::size_t size); | ||
|
|
||
| /// Tag to select the internal constructor that takes an already-resolved base URL. | ||
| struct RFromBaseUrl {}; | ||
| /// Internal constructor used by CloneAsHidden: the public constructor derives the base URL by parsing | ||
| /// an s3 scheme URI, whereas a clone already has a resolved base URL to write under. | ||
| RPageSinkS3(std::string_view ntupleName, std::string baseUrl, const ROOT::RNTupleWriteOptions &options, | ||
|
Contributor
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. Any reason why |
||
| RFromBaseUrl); | ||
|
|
||
| protected: | ||
| using RPagePersistentSink::InitImpl; | ||
| void InitImpl(unsigned char *serializedHeader, std::uint32_t length) final; | ||
| RNTupleLocator | ||
| CommitSealedPageImpl(ROOT::DescriptorId_t physicalColumnId, const RPageStorage::RSealedPage &sealedPage) final; | ||
| std::uint64_t StageClusterImpl() final; | ||
| RNTupleLocator CommitClusterGroupImpl(unsigned char *serializedPageList, std::uint32_t length) final; | ||
| using RPagePersistentSink::CommitDatasetImpl; | ||
| ROOT::Internal::RNTupleLink CommitDatasetImpl(unsigned char *serializedFooter, std::uint32_t length) final; | ||
|
|
||
| public: | ||
| RPageSinkS3(std::string_view ntupleName, std::string_view uri, const ROOT::RNTupleWriteOptions &options); | ||
| ~RPageSinkS3() override; | ||
|
|
||
| std::unique_ptr<ROOT::Internal::RPageSink> | ||
| CloneAsHidden(std::string_view name, const ROOT::RNTupleWriteOptions &opts) const final; | ||
| }; // class RPageSinkS3 | ||
|
|
||
| } // namespace Internal | ||
| } // namespace Experimental | ||
| } // namespace ROOT | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,9 +23,13 @@ | |
| #include <ROOT/RNTupleZip.hxx> | ||
| #include <ROOT/RPageAllocator.hxx> | ||
| #include <ROOT/RPageSinkBuf.hxx> | ||
| #include <ROOT/StringUtils.hxx> | ||
| #ifdef R__ENABLE_DAOS | ||
| #include <ROOT/RPageStorageDaos.hxx> | ||
| #endif | ||
| #ifdef R__ENABLE_S3 | ||
| #include <ROOT/RPageStorageS3.hxx> | ||
|
Contributor
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 should probably be added in the next commit |
||
| #endif | ||
|
|
||
| #include <Compression.h> | ||
| #include <TError.h> | ||
|
|
@@ -188,6 +192,9 @@ ROOT::Internal::RPageSource::Create(std::string_view ntupleName, std::string_vie | |
| throw RException(R__FAIL("This RNTuple build does not support DAOS.")); | ||
| #endif | ||
|
|
||
| if (ROOT::StartsWith(location, "ntpl+s3+http://") || ROOT::StartsWith(location, "ntpl+s3+https://")) | ||
| throw RException(R__FAIL("S3 read support is not yet implemented.")); | ||
|
|
||
| return std::make_unique<ROOT::Internal::RPageSourceFile>(ntupleName, location, options); | ||
| } | ||
|
|
||
|
|
@@ -920,6 +927,14 @@ ROOT::Internal::RPagePersistentSink::Create(std::string_view ntupleName, std::st | |
| #endif | ||
| } | ||
|
|
||
| if (ROOT::StartsWith(location, "ntpl+s3+http://") || ROOT::StartsWith(location, "ntpl+s3+https://")) { | ||
| #ifdef R__ENABLE_S3 | ||
| return std::make_unique<ROOT::Experimental::Internal::RPageSinkS3>(ntupleName, location, options); | ||
|
jblomer marked this conversation as resolved.
|
||
| #else | ||
| throw RException(R__FAIL("This RNTuple build does not support S3.")); | ||
|
Contributor
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. We should perhaps add instructions on how to build with s3 support |
||
| #endif | ||
| } | ||
|
|
||
| // Otherwise assume that the user wants us to create a file. | ||
| return std::make_unique<ROOT::Internal::RPageSinkFile>(ntupleName, location, options); | ||
| } | ||
|
|
@@ -1169,9 +1184,17 @@ void ROOT::Internal::RPagePersistentSink::CommitPage(ColumnHandle_t columnHandle | |
| { | ||
| fOpenColumnRanges.at(columnHandle.fPhysicalId).IncrementNElements(page.GetNElements()); | ||
|
|
||
| auto element = columnHandle.fColumn->GetElement(); | ||
| RPageStorage::RSealedPage sealedPage; | ||
| { | ||
| RNTupleAtomicTimer timer(fCounters->fTimeWallZip, fCounters->fTimeCpuZip); | ||
| sealedPage = SealPage(page, *element); | ||
| } | ||
| fCounters->fSzZip.Add(page.GetNBytes()); | ||
|
|
||
| ROOT::RClusterDescriptor::RPageInfo pageInfo; | ||
| pageInfo.SetNElements(page.GetNElements()); | ||
| pageInfo.SetLocator(CommitPageImpl(columnHandle, page)); | ||
| pageInfo.SetLocator(CommitSealedPageImpl(columnHandle.fPhysicalId, sealedPage)); | ||
| pageInfo.SetHasChecksum(GetWriteOptions().GetEnablePageChecksums()); | ||
| fOpenPageRanges.at(columnHandle.fPhysicalId).GetPageInfos().emplace_back(pageInfo); | ||
| } | ||
|
|
||
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.
For a parsing function on an unknown input I would rather return a
RResultthan throwing (this is the pattern we also use in the RNTuple serializer)