Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
RPageStorageS3.cxx
Go to the documentation of this file.
1/// \file RPageStorageS3.cxx
2/// \author Jas Mehta <jasmehta805@gmail.com>
3/// \date 2026-06-01
4
5/*************************************************************************
6 * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. *
7 * All rights reserved. *
8 * *
9 * For the licensing terms see $ROOTSYS/LICENSE. *
10 * For the list of contributors see $ROOTSYS/README/CREDITS. *
11 *************************************************************************/
12
14
15#include <ROOT/RCluster.hxx>
16#include <ROOT/RCurlConnection.hxx>
17#include <ROOT/RLogger.hxx>
20#include <ROOT/RNTupleTypes.hxx>
21#include <ROOT/RNTupleUtils.hxx>
22#include <ROOT/RNTupleZip.hxx>
23#include <ROOT/RPage.hxx>
24#include <ROOT/StringUtils.hxx>
25
26#include <nlohmann/json.hpp>
27#include <xxhash.h>
28
29#include <algorithm>
30#include <cctype>
31#include <cstring>
32#include <mutex>
33#include <string>
34#include <utility>
35#include <vector>
36
41
42/// Field-by-field equality check across all data members.
54
55/// Serialize the anchor to a pretty-printed JSON string (2-space indent).
56/// The checksum is computed over the compact canonical form of the data fields;
57/// the stored JSON uses pretty-printing for readability.
59{
60 nlohmann::json jsonAnchor;
61 jsonAnchor["anchorVersion"] = fVersionAnchor;
62 jsonAnchor["formatVersionEpoch"] = fVersionEpoch;
63 jsonAnchor["formatVersionMajor"] = fVersionMajor;
64 jsonAnchor["formatVersionMinor"] = fVersionMinor;
65 jsonAnchor["formatVersionPatch"] = fVersionPatch;
66 jsonAnchor["urlTemplate"] = fUrlTemplate;
67 jsonAnchor["cloneTemplate"] = fCloneTemplate;
68 jsonAnchor["headerObjId"] = fHeaderObjId;
69 jsonAnchor["headerOffset"] = fHeaderOffset;
70 jsonAnchor["nBytesHeader"] = fNBytesHeader;
71 jsonAnchor["lenHeader"] = fLenHeader;
72 jsonAnchor["footerObjId"] = fFooterObjId;
73 jsonAnchor["footerOffset"] = fFooterOffset;
74 jsonAnchor["nBytesFooter"] = fNBytesFooter;
75 jsonAnchor["lenFooter"] = fLenFooter;
76
77 auto canonical = jsonAnchor.dump(-1);
78 jsonAnchor["checksum"] = XXH3_64bits(canonical.data(), canonical.size());
79 return jsonAnchor.dump(2);
80}
81
82/// Construct an anchor from a JSON string.
83/// The anchor version is checked first; if it does not match the current version,
84/// parsing fails immediately. All remaining fields are extracted with jsonAnchor.at()
85/// which throws on missing keys or type mismatches.
88{
89 nlohmann::json jsonAnchor;
90 try {
91 jsonAnchor = nlohmann::json::parse(json);
92 } catch (const nlohmann::json::parse_error &e) {
93 return R__FAIL("cannot parse S3 anchor JSON: " + std::string(e.what()));
94 }
95
96 RNTupleAnchorS3 anchor;
97
98 try {
99 anchor.fVersionAnchor = jsonAnchor.at("anchorVersion").get<std::uint32_t>();
100 } catch (const nlohmann::json::exception &e) {
101 return R__FAIL("missing or invalid 'anchorVersion' in S3 anchor: " + std::string(e.what()));
102 }
103
104 if (anchor.fVersionAnchor != RNTupleAnchorS3().fVersionAnchor)
105 return R__FAIL("unsupported S3 anchor version: " + std::to_string(anchor.fVersionAnchor));
106
107 try {
108 anchor.fVersionEpoch = jsonAnchor.at("formatVersionEpoch").get<std::uint16_t>();
109 anchor.fVersionMajor = jsonAnchor.at("formatVersionMajor").get<std::uint16_t>();
110 anchor.fVersionMinor = jsonAnchor.at("formatVersionMinor").get<std::uint16_t>();
111 anchor.fVersionPatch = jsonAnchor.at("formatVersionPatch").get<std::uint16_t>();
112 anchor.fUrlTemplate = jsonAnchor.at("urlTemplate").get<std::string>();
113 anchor.fCloneTemplate = jsonAnchor.at("cloneTemplate").get<std::string>();
114 anchor.fHeaderObjId = jsonAnchor.at("headerObjId").get<std::uint64_t>();
115 anchor.fHeaderOffset = jsonAnchor.at("headerOffset").get<std::uint64_t>();
116 anchor.fNBytesHeader = jsonAnchor.at("nBytesHeader").get<std::uint64_t>();
117 anchor.fLenHeader = jsonAnchor.at("lenHeader").get<std::uint64_t>();
118 anchor.fFooterObjId = jsonAnchor.at("footerObjId").get<std::uint64_t>();
119 anchor.fFooterOffset = jsonAnchor.at("footerOffset").get<std::uint64_t>();
120 anchor.fNBytesFooter = jsonAnchor.at("nBytesFooter").get<std::uint64_t>();
121 anchor.fLenFooter = jsonAnchor.at("lenFooter").get<std::uint64_t>();
122 } catch (const nlohmann::json::exception &e) {
123 return R__FAIL("missing or invalid field in S3 anchor: " + std::string(e.what()));
124 }
125
126 if (!jsonAnchor.contains("checksum"))
127 return R__FAIL("missing 'checksum' field in S3 anchor");
128
129 std::uint64_t storedChecksum;
130 try {
131 storedChecksum = jsonAnchor.at("checksum").get<std::uint64_t>();
132 } catch (const nlohmann::json::exception &e) {
133 return R__FAIL("invalid 'checksum' field in S3 anchor: " + std::string(e.what()));
134 }
135
136 jsonAnchor.erase("checksum");
137 auto canonical = jsonAnchor.dump(-1);
138 auto computedChecksum = XXH3_64bits(canonical.data(), canonical.size());
139
140 if (storedChecksum != computedChecksum)
141 return R__FAIL("S3 anchor checksum mismatch");
142
143 return anchor;
144}
145
146// S3 URI parsing
147
149{
150 const std::string uriStr(uri);
151
152 // The base URL is a plain bucket/path prefix (MakeObjectUrl() appends "/<id>") and S3 authentication
153 // comes from the environment via SigV4, not from the URL. Reject embedded userinfo, query strings,
154 // and fragments rather than silently mishandling them.
155 if (uriStr.find_first_of("@?#") != std::string::npos)
156 return R__FAIL("S3 URI must not contain userinfo ('@'), a query ('?') or a fragment ('#'): " + uriStr);
157
158 // The dedicated ntpl+s3 scheme marks an RNTuple stored natively as S3 objects, distinguishing it
159 // from a ROOT file stored on S3 (which is opened through the S3 handler for s3:// URLs). Use
160 // ntpl+s3+https:// in production; ntpl+s3+http:// targets local/testing endpoints such as MinIO and
161 // transmits data unencrypted. The scheme is matched case-insensitively (RFC 3986), but the host,
162 // bucket and key are kept verbatim because they are case-sensitive.
163 std::string schemeLower;
164 for (std::size_t i = 0; i < uriStr.size() && i < std::strlen("ntpl+s3+https://"); ++i)
165 schemeLower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(uriStr[i]))));
166
167 std::string httpScheme;
168 std::size_t schemeLen = 0;
169 if (ROOT::StartsWith(schemeLower, "ntpl+s3+https://")) {
170 httpScheme = "https";
171 schemeLen = std::strlen("ntpl+s3+https://");
172 } else if (ROOT::StartsWith(schemeLower, "ntpl+s3+http://")) {
173 httpScheme = "http";
174 schemeLen = std::strlen("ntpl+s3+http://");
175 } else {
176 return R__FAIL("invalid S3 URI (expected ntpl+s3+http:// or ntpl+s3+https://): " + uriStr);
177 }
178
179 std::string hostAndPath = uriStr.substr(schemeLen);
180 // Drop trailing slashes so MakeObjectUrl() never produces "//" in an object key and the anchor key
181 // (the base URL itself) is not left ending in '/'.
182 while (!hostAndPath.empty() && hostAndPath.back() == '/')
183 hostAndPath.pop_back();
184
185 // There must be a host after the scheme; check for emptiness once the trailing slashes are removed,
186 // so a URI that is only slashes after the scheme (e.g. "ntpl+s3+http:///") is rejected as well.
187 if (hostAndPath.empty())
188 return R__FAIL("S3 URI has no host: " + uriStr);
189
190 return httpScheme + "://" + hostAndPath;
191}
192
193// RPageSinkS3
194
195ROOT::Experimental::Internal::RPageSinkS3::RPageSinkS3(std::string_view ntupleName, std::string_view uri,
196 const ROOT::RNTupleWriteOptions &options)
197 : RPageSinkS3(ntupleName, ParseS3Url(uri).Unwrap(), options, RFromBaseUrl{})
198{
199}
200
201ROOT::Experimental::Internal::RPageSinkS3::RPageSinkS3(std::string_view ntupleName, std::string_view baseUrl,
203 : RPagePersistentSink(ntupleName, options), fBaseUrl(baseUrl), fConnection(fBaseUrl)
204{
205 static std::once_flag once;
206 std::call_once(once, []() {
207 R__LOG_WARNING(ROOT::Internal::NTupleLog()) << "The S3 backend is experimental and still under development. "
208 << "Do not store real data with this version of RNTuple!";
209 });
210 fConnection.SetCredentialsFromEnvironment();
211 EnableDefaultMetrics("RPageSinkS3");
212}
213
215
217{
218 return fBaseUrl + "/" + std::to_string(objId);
219}
220
221void ROOT::Experimental::Internal::RPageSinkS3::PutObject(const std::string &url, const unsigned char *data,
222 std::size_t size)
223{
224 // All objects share fConnection; retarget it to this object's URL (via SetUrl) so curl can keep
225 // the connection alive across uploads to the same host.
226 fConnection.SetUrl(url).ThrowOnError();
227 auto status = fConnection.SendPutReq(data, size);
228 if (!status)
229 throw ROOT::RException(R__FAIL("S3 PUT failed for " + url + ": " + status.fStatusMsg));
230}
231
232void ROOT::Experimental::Internal::RPageSinkS3::InitImpl(unsigned char *serializedHeader, std::uint32_t length)
233{
234 // fAnchor.fUrlTemplate keeps its default ("${baseurl}/${objid}").
235
236 auto zipBuffer = MakeUninitArray<unsigned char>(length);
237 auto szZipHeader =
238 RNTupleCompressor::Zip(serializedHeader, length, GetWriteOptions().GetCompression(), zipBuffer.get());
239
240 const auto headerObjId = fObjectId++;
241 {
242 Detail::RNTupleAtomicTimer timer(fCounters->fTimeWallWrite, fCounters->fTimeCpuWrite);
243 PutObject(MakeObjectUrl(headerObjId), zipBuffer.get(), szZipHeader);
244 }
245
246 fAnchor.fHeaderObjId = headerObjId;
247 fAnchor.fHeaderOffset = 0;
248 fAnchor.fNBytesHeader = szZipHeader;
249 fAnchor.fLenHeader = length;
250}
251
254 const RPageStorage::RSealedPage &sealedPage)
255{
256 // Mode B: one S3 object per sealed page, located by a kTypeObject64 locator
257 const auto pageObjId = fObjectId++;
258 {
259 Detail::RNTupleAtomicTimer timer(fCounters->fTimeWallWrite, fCounters->fTimeCpuWrite);
260 PutObject(MakeObjectUrl(pageObjId), reinterpret_cast<const unsigned char *>(sealedPage.GetBuffer()),
261 sealedPage.GetBufferSize());
262 }
263
266 result.SetNBytesOnStorage(sealedPage.GetDataSize());
267 result.SetPosition(ROOT::RNTupleLocatorObject64{pageObjId});
268 fCounters->fNPageCommitted.Inc();
269 fCounters->fSzWritePayload.Add(sealedPage.GetBufferSize());
270 fNBytesCurrentCluster += sealedPage.GetBufferSize();
271 return result;
272}
273
275{
276 return std::exchange(fNBytesCurrentCluster, 0);
277}
278
281 std::uint32_t length)
282{
283 auto bufPageListZip = MakeUninitArray<unsigned char>(length);
284 auto szPageListZip =
285 RNTupleCompressor::Zip(serializedPageList, length, GetWriteOptions().GetCompression(), bufPageListZip.get());
286
287 const auto objId = fObjectId++;
288 {
289 Detail::RNTupleAtomicTimer timer(fCounters->fTimeWallWrite, fCounters->fTimeCpuWrite);
290 PutObject(MakeObjectUrl(objId), bufPageListZip.get(), szPageListZip);
291 }
292
295 result.SetNBytesOnStorage(szPageListZip);
296 result.SetPosition(ROOT::RNTupleLocatorObject64{objId});
297 fCounters->fSzWritePayload.Add(static_cast<std::int64_t>(szPageListZip));
298 return result;
299}
300
302ROOT::Experimental::Internal::RPageSinkS3::CommitDatasetImpl(unsigned char *serializedFooter, std::uint32_t length)
303{
304 auto bufFooterZip = MakeUninitArray<unsigned char>(length);
305 auto szFooterZip =
306 RNTupleCompressor::Zip(serializedFooter, length, GetWriteOptions().GetCompression(), bufFooterZip.get());
307
308 const auto footerObjId = fObjectId++;
309 {
310 Detail::RNTupleAtomicTimer timer(fCounters->fTimeWallWrite, fCounters->fTimeCpuWrite);
311 PutObject(MakeObjectUrl(footerObjId), bufFooterZip.get(), szFooterZip);
312 }
313
314 fAnchor.fFooterObjId = footerObjId;
315 fAnchor.fFooterOffset = 0;
316 fAnchor.fNBytesFooter = szFooterZip;
317 fAnchor.fLenFooter = length;
318
319 // Upload the anchor LAST: once it exists at the base URL, a reader can assume the whole ntuple
320 // is complete. Never upload it before all other objects are in place.
321 const auto anchorJson = fAnchor.ToJSON();
322 PutObject(fBaseUrl, reinterpret_cast<const unsigned char *>(anchorJson.data()), anchorJson.size());
323
324 // An S3 ntuple is self-locating: its anchor always lives at the base URL, so there is no anchor
325 // link to hand back here.
326 return {};
327}
328
329std::unique_ptr<ROOT::Internal::RPageSink>
331 const ROOT::RNTupleWriteOptions &opts) const
332{
333 // Resolve the clone template so the hidden ntuple's objects and anchor live under a sub-prefix
334 // that cannot collide with the main ntuple's numeric object keys.
335 std::string cloneBaseUrl = fAnchor.fCloneTemplate;
336
337 auto pos = cloneBaseUrl.find("${baseurl}");
338 if (pos != std::string::npos)
339 cloneBaseUrl.replace(pos, std::strlen("${baseurl}"), fBaseUrl);
340
341 pos = cloneBaseUrl.find("${name}");
342 if (pos != std::string::npos)
343 cloneBaseUrl.replace(pos, std::strlen("${name}"), name);
344
345 return std::unique_ptr<ROOT::Internal::RPageSink>(new RPageSinkS3(name, cloneBaseUrl, opts, RFromBaseUrl{}));
346}
347
348// RPageSourceS3
349
350namespace {
351/// How much of the anchor to ask for without knowing its size. Its schema is fixed and only the two URL
352/// templates vary in length, so 8 KiB covers any realistic anchor in a single request; a larger one is
353/// re-read at kMaxAnchorSize.
354constexpr std::uint64_t kAnchorReadSize = 8 * 1024;
355
356/// Ceiling on the anchor size, since the length ultimately comes from the remote endpoint and is used
357/// as an allocation size.
358constexpr std::uint64_t kMaxAnchorSize = 1024 * 1024;
359
360/// The binary format stores an envelope's size in 48 bits (the low 16 of the same word hold the envelope
361/// type), so nothing a writer can produce exceeds this. Sizes are clamped here before being summed into
362/// a buffer length, no matter how large the internal MaxEnvelopeSize read option is set.
363constexpr std::uint64_t kFormatMaxEnvelopeSize = 1ULL << 48;
364} // anonymous namespace
365
366ROOT::Experimental::Internal::RPageSourceS3::RPageSourceS3(std::string_view ntupleName, std::string_view uri,
367 const ROOT::RNTupleReadOptions &options)
368 : RPageSourceS3(ntupleName, ParseS3Url(uri).Unwrap(), options, RFromBaseUrl{})
369{
370}
371
372ROOT::Experimental::Internal::RPageSourceS3::RPageSourceS3(std::string_view ntupleName, std::string_view baseUrl,
374 : RPageSource(ntupleName, options), fBaseUrl(baseUrl), fMainConnection(fBaseUrl), fClusterConnection(fBaseUrl)
375{
376 fMainConnection.SetCredentialsFromEnvironment();
377 fClusterConnection.SetCredentialsFromEnvironment();
378 // Enable the counters before any I/O happens, so that LoadStructureImpl() can already use them.
379 EnableDefaultMetrics("RPageSourceS3");
380}
381
383{
384 // The cluster pool's I/O thread calls back into this source, so it has to be joined before any of
385 // the members below (in particular fMainConnection) are destroyed.
386 StopClusterPoolBackgroundThread();
387}
388
390{
391 // ${baseurl} was already substituted once, when the anchor was read; only the per-object part is
392 // left to do here.
393 std::string url = fResolvedUrlTemplate;
394
395 const auto pos = url.find("${objid}");
396 if (pos != std::string::npos)
397 url.replace(pos, std::strlen("${objid}"), std::to_string(objId));
398
399 return url;
400}
401
402void ROOT::Experimental::Internal::RPageSourceS3::GetObject(ROOT::Internal::RCurlConnection &connection,
403 const std::string &url, unsigned char *buffer,
404 std::size_t size)
405{
406 // Retarget the connection rather than opening a new one, so curl keeps it alive across objects. The
407 // caller picks which one: a curl easy handle serves one thread at a time.
408 connection.SetUrl(url).ThrowOnError();
409
410 // One range from offset 0 covers the whole object. RCurlConnection also copes with a server that
411 // ignores the range and replies 200 with the full body.
412 ROOT::Internal::RCurlConnection::RUserRange range;
413 range.fDestination = buffer;
414 range.fOffset = 0;
415 range.fLength = size;
416
417 auto status = connection.SendRangesReq(1, &range);
418 if (!status)
419 throw ROOT::RException(R__FAIL("S3 GET failed for " + url + ": " + status.fStatusMsg));
420 if (range.fNBytesRecv != size) {
421 throw ROOT::RException(R__FAIL("S3 GET for " + url + " returned " + std::to_string(range.fNBytesRecv) +
422 " bytes, expected " + std::to_string(size)));
423 }
424}
425
426std::size_t ROOT::Experimental::Internal::RPageSourceS3::GetShortObject(ROOT::Internal::RCurlConnection &connection,
427 const std::string &url, unsigned char *buffer,
428 std::size_t size)
429{
430 connection.SetUrl(url).ThrowOnError();
431
432 // A range that runs past the end of an object is valid and simply returns fewer bytes, so asking for
433 // more than the object holds is how its size is learned in a single request. Unlike GetObject(),
434 // a short read is the expected outcome rather than an error.
435 ROOT::Internal::RCurlConnection::RUserRange range;
436 range.fDestination = buffer;
437 range.fOffset = 0;
438 range.fLength = size;
439
440 auto status = connection.SendRangesReq(1, &range);
441 if (!status)
442 throw ROOT::RException(R__FAIL("S3 GET failed for " + url + ": " + status.fStatusMsg));
443 return range.fNBytesRecv;
444}
445
447{
448 // The anchor lives at the base URL, and its size is not known upfront. Rather than a HEAD to learn the
449 // size followed by a GET to fetch it, over-request in a single GET: the reply is truncated at the end
450 // of the object, so the number of bytes received is the anchor size.
451 auto anchorBuffer = MakeUninitArray<unsigned char>(kAnchorReadSize);
452 auto anchorSize = GetShortObject(fMainConnection, fBaseUrl, anchorBuffer.get(), kAnchorReadSize);
453
454 if (anchorSize == kAnchorReadSize) {
455 // The anchor filled the request, so it may have been cut short. Re-read it once at the largest size
456 // we are prepared to accept.
457 anchorBuffer = MakeUninitArray<unsigned char>(kMaxAnchorSize);
458 anchorSize = GetShortObject(fMainConnection, fBaseUrl, anchorBuffer.get(), kMaxAnchorSize);
459 if (anchorSize == kMaxAnchorSize) {
460 throw ROOT::RException(R__FAIL("S3 anchor at " + fBaseUrl + " is implausibly large (>= " +
461 std::to_string(kMaxAnchorSize) + " bytes); refusing to read it"));
462 }
463 }
464
465 const std::string anchorJson(reinterpret_cast<const char *>(anchorBuffer.get()), anchorSize);
466 auto anchorResult = RNTupleAnchorS3::CreateFromJSON(anchorJson);
467 if (!anchorResult)
468 throw ROOT::RException(R__FORWARD_ERROR(anchorResult));
469 fAnchor = anchorResult.Inspect();
470
471 // ${baseurl} is fixed for the lifetime of this source, so resolve it once here rather than on every
472 // object lookup. Mirrors the substitution in RPageSinkS3::CloneAsHidden().
473 fResolvedUrlTemplate = fAnchor.GetUrlTemplate();
474 if (const auto pos = fResolvedUrlTemplate.find("${baseurl}"); pos != std::string::npos)
475 fResolvedUrlTemplate.replace(pos, std::strlen("${baseurl}"), fBaseUrl);
476
477 // The envelope sizes are used to size a buffer and to offset into it, so bound them before summing.
478 // The limit is an internal read option so that it can be raised for a data set with an unusually
479 // large schema; it is capped at what the format itself can express, so that the sum cannot overflow
480 // no matter what it is set to.
481 const auto maxEnvelopeSize =
482 std::min(ROOT::Internal::RNTupleReadOptionsManip::GetMaxEnvelopeSize(fOptions), kFormatMaxEnvelopeSize);
483 if (fAnchor.GetNBytesHeader() > maxEnvelopeSize || fAnchor.GetNBytesFooter() > maxEnvelopeSize ||
484 fAnchor.GetLenHeader() > maxEnvelopeSize || fAnchor.GetLenFooter() > maxEnvelopeSize) {
485 throw ROOT::RException(R__FAIL("S3 anchor at " + fBaseUrl + " declares a header or footer larger than " +
486 std::to_string(maxEnvelopeSize) + " bytes"));
487 }
488
489 fDescriptorBuilder.SetVersion(fAnchor.GetVersionEpoch(), fAnchor.GetVersionMajor(), fAnchor.GetVersionMinor(),
490 fAnchor.GetVersionPatch());
491 fDescriptorBuilder.SetOnDiskHeaderSize(fAnchor.GetNBytesHeader());
492 fDescriptorBuilder.AddToOnDiskFooterSize(fAnchor.GetNBytesFooter());
493
494 // Reserve enough space for the compressed and the uncompressed header/footer (see AttachImpl)
495 const auto bufSize =
496 fAnchor.GetNBytesHeader() + fAnchor.GetNBytesFooter() + std::max(fAnchor.GetLenHeader(), fAnchor.GetLenFooter());
497 fStructureBuffer.fBuffer = MakeUninitArray<unsigned char>(bufSize);
498 fStructureBuffer.fPtrHeader = fStructureBuffer.fBuffer.get();
499 fStructureBuffer.fPtrFooter = fStructureBuffer.fBuffer.get() + fAnchor.GetNBytesHeader();
500
501 {
502 Detail::RNTupleAtomicTimer timer(fCounters->fTimeWallRead, fCounters->fTimeCpuRead);
503 GetObject(fMainConnection, MakeObjectUrl(fAnchor.GetHeaderObjId()),
504 reinterpret_cast<unsigned char *>(fStructureBuffer.fPtrHeader), fAnchor.GetNBytesHeader());
505 GetObject(fMainConnection, MakeObjectUrl(fAnchor.GetFooterObjId()),
506 reinterpret_cast<unsigned char *>(fStructureBuffer.fPtrFooter), fAnchor.GetNBytesFooter());
507 // One range read each for the anchor, the header and the footer.
508 fCounters->fNRead.Add(3);
509 }
510}
511
513{
514 auto unzipBuf = reinterpret_cast<unsigned char *>(fStructureBuffer.fPtrFooter) + fAnchor.GetNBytesFooter();
515
516 RNTupleDecompressor::Unzip(fStructureBuffer.fPtrHeader, fAnchor.GetNBytesHeader(), fAnchor.GetLenHeader(), unzipBuf);
517 RNTupleSerializer::DeserializeHeader(unzipBuf, fAnchor.GetLenHeader(), fDescriptorBuilder);
518
519 // The name locates nothing here, but comparing it asserts that the URL points at the data set the
520 // caller meant. An empty name opts out.
521 const auto &storedName = fDescriptorBuilder.GetDescriptor().GetName();
522 if (!fNTupleName.empty() && storedName != fNTupleName) {
523 throw ROOT::RException(
524 R__FAIL("the S3 ntuple at " + fBaseUrl + " is named '" + storedName + "', not '" + fNTupleName + "'"));
525 }
526
527 RNTupleDecompressor::Unzip(fStructureBuffer.fPtrFooter, fAnchor.GetNBytesFooter(), fAnchor.GetLenFooter(), unzipBuf);
528 RNTupleSerializer::DeserializeFooter(unzipBuf, fAnchor.GetLenFooter(), fDescriptorBuilder);
529
530 return fDescriptorBuilder.MoveDescriptor();
531}
532
534{
535 const auto objId = locator.GetPosition<ROOT::RNTupleLocatorObject64>().GetLocation();
536 GetObject(fMainConnection, MakeObjectUrl(objId), buffer, locator.GetNBytesOnStorage());
537}
538
540 RSealedPage &sealedPage)
541{
542 Detail::RNTupleAtomicTimer timer(fCounters->fTimeWallRead, fCounters->fTimeCpuRead);
543 const auto objId = locator.GetPosition<ROOT::RNTupleLocatorObject64>().GetLocation();
544 // The const_cast is safe: the buffer belongs to the caller, who provided it for us to fill.
545 auto buffer = static_cast<unsigned char *>(const_cast<void *>(sealedPage.GetBuffer()));
546 GetObject(fMainConnection, MakeObjectUrl(objId), buffer, sealedPage.GetBufferSize());
547}
548
549std::vector<std::unique_ptr<ROOT::Internal::RCluster>>
550ROOT::Experimental::Internal::RPageSourceS3::LoadClusters(std::span<ROOT::Internal::RCluster::RKey> clusterKeys)
551{
552 /// Where one page of a cluster lives in S3 and how much space it takes in the cluster buffer.
553 struct RS3SealedPageLocator {
554 ROOT::DescriptorId_t fColumnId = 0;
555 ROOT::NTupleSize_t fPageNo = 0;
556 std::uint64_t fObjId = 0;
557 std::uint64_t fBufferSize = 0; ///< page payload + checksum (if available)
558 };
559
560 fCounters->fNClusterLoaded.Add(clusterKeys.size());
561
562 std::vector<std::unique_ptr<ROOT::Internal::RCluster>> clusters;
563
564 for (const auto &clusterKey : clusterKeys) {
565 std::vector<RS3SealedPageLocator> onDiskPages;
566 std::uint64_t clusterBufSize = 0;
567
568 auto pageZeroMap = std::make_unique<ROOT::Internal::ROnDiskPageMap>();
569 PrepareLoadCluster(
570 clusterKey, *pageZeroMap,
571 [&](ROOT::DescriptorId_t physicalColumnId, ROOT::NTupleSize_t pageNo,
572 const ROOT::RClusterDescriptor::RPageInfo &pageInfo) {
573 const auto &pageLocator = pageInfo.GetLocator();
574 const auto objId = pageLocator.GetPosition<ROOT::RNTupleLocatorObject64>().GetLocation();
575 const auto pageBufferSize = pageLocator.GetNBytesOnStorage() + pageInfo.HasChecksum() * kNBytesPageChecksum;
576 onDiskPages.emplace_back(RS3SealedPageLocator{physicalColumnId, pageNo, objId, pageBufferSize});
577 clusterBufSize += pageBufferSize;
578 });
579
580 auto clusterBuffer = new unsigned char[clusterBufSize];
581 auto pageMap =
582 std::make_unique<ROOT::Internal::ROnDiskPageMapHeap>(std::unique_ptr<unsigned char[]>(clusterBuffer));
583
584 {
585 // One GET per page in Mode B; batching them concurrently is a follow-up. This runs on the
586 // cluster pool's I/O thread, hence fClusterConnection: the caller may be using fMainConnection.
587 Detail::RNTupleAtomicTimer timer(fCounters->fTimeWallRead, fCounters->fTimeCpuRead);
588 auto pageBuffer = clusterBuffer;
589 for (const auto &sealedLoc : onDiskPages) {
590 ROOT::Internal::ROnDiskPage::Key key(sealedLoc.fColumnId, sealedLoc.fPageNo);
591 pageMap->Register(key, ROOT::Internal::ROnDiskPage(pageBuffer, sealedLoc.fBufferSize));
592 GetObject(fClusterConnection, MakeObjectUrl(sealedLoc.fObjId), pageBuffer, sealedLoc.fBufferSize);
593 pageBuffer += sealedLoc.fBufferSize;
594 }
595 }
596
597 fCounters->fNPageRead.Add(onDiskPages.size());
598 fCounters->fSzReadPayload.Add(clusterBufSize);
599 fCounters->fNRead.Add(onDiskPages.size());
600
601 auto cluster = std::make_unique<ROOT::Internal::RCluster>(clusterKey.fClusterId);
602 cluster->Adopt(std::move(pageMap));
603 cluster->Adopt(std::move(pageZeroMap));
604 for (auto colId : clusterKey.fPhysicalColumnSet)
605 cluster->SetColumnAvailable(colId);
606 clusters.emplace_back(std::move(cluster));
607 }
608
609 return clusters;
610}
611
612std::unique_ptr<ROOT::Internal::RPageSource> ROOT::Experimental::Internal::RPageSourceS3::CloneImpl() const
613{
614 // The clone opens its own connections, so clones can be read from concurrently.
615 auto clone = std::unique_ptr<RPageSourceS3>(new RPageSourceS3(fNTupleName, fBaseUrl, fOptions, RFromBaseUrl{}));
616 // Carry the anchor over: an attached clone skips LoadStructureImpl(), so without it MakeObjectUrl()
617 // would resolve against a default-constructed template.
618 clone->fAnchor = fAnchor;
619 clone->fResolvedUrlTemplate = fResolvedUrlTemplate;
620 return clone;
621}
622
623std::unique_ptr<ROOT::Internal::RPageSource>
626{
627 // An S3 ntuple is self-locating (its anchor is at the base URL), so there is no anchor link to follow
628 // within the same storage container the way the file backend does.
629 throw ROOT::RException(R__FAIL("OpenWithDifferentAnchor is not implemented for the S3 backend"));
630}
nlohmann::json json
#define R__FORWARD_ERROR(res)
Short-hand to return an RResult<T> in an error state (i.e. after checking)
Definition RError.hxx:326
#define R__FAIL(msg)
Short-hand to return an RResult<T> in an error state; the RError is implicitly converted into RResult...
Definition RError.hxx:322
#define R__LOG_WARNING(...)
Definition RLogger.hxx:357
#define e(i)
Definition RSha256.hxx:103
size_t size(const MatrixT &matrix)
retrieve the size of a square matrix
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void data
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t Float_t Float_t Int_t Int_t UInt_t UInt_t Rectangle_t result
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t Float_t Float_t Int_t Int_t UInt_t UInt_t Rectangle_t Int_t Int_t Window_t TString Int_t GCValues_t GetPrimarySelectionOwner GetDisplay GetScreen GetColormap GetNativeEvent const char const char dpyName wid window const char font_name cursor keysym reg const char only_if_exist regb h Point_t winding char text const char depth char const char Int_t count const char ColorStruct_t color const char Pixmap_t Pixmap_t PictureAttributes_t attr const char char ret_data h unsigned char height h length
char name[80]
Definition TGX11.cxx:142
Record wall time and CPU time between construction and destruction.
Entry point for an RNTuple stored in S3-compatible object storage.
bool operator==(const RNTupleAnchorS3 &other) const
Field-by-field equality check across all data members.
std::uint64_t fHeaderObjId
Object ID and byte offset of the compressed header within the S3 object.
std::string fUrlTemplate
Pattern for resolving object IDs to full S3 URLs.
std::uint32_t fVersionAnchor
Allows evolving the anchor JSON schema in future versions.
std::uint16_t fVersionEpoch
Version of the RNTuple binary format supported by the writer.
std::string fCloneTemplate
Pattern for resolving clone (attribute-set) names to base URLs.
std::uint64_t fNBytesHeader
Compressed and uncompressed sizes of the header envelope.
std::string ToJSON() const
Serialize the anchor to a JSON string suitable for storage at the base URL.
std::uint64_t fNBytesFooter
Compressed and uncompressed sizes of the footer envelope.
static RResult< RNTupleAnchorS3 > CreateFromJSON(const std::string &json)
Deserialize the anchor from a JSON string. Returns an error on malformed or incompatible input.
std::uint64_t fFooterObjId
Object ID and byte offset of the compressed footer within the S3 object.
Storage provider that writes ntuple pages into S3-compatible object storage.
ROOT::Internal::RCurlConnection fConnection
One HTTP connection reused for every upload, so curl keeps it alive across objects on the same host i...
void InitImpl(unsigned char *serializedHeader, std::uint32_t length) final
std::uint64_t StageClusterImpl() final
Returns the number of bytes written to storage (excluding metadata)
RNTupleLocator CommitClusterGroupImpl(unsigned char *serializedPageList, std::uint32_t length) final
Returns the locator of the page list envelope of the given buffer that contains the serialized page l...
std::unique_ptr< ROOT::Internal::RPageSink > CloneAsHidden(std::string_view name, const ROOT::RNTupleWriteOptions &opts) const final
Creates a new sink with the same underlying storage as this but writing to a different RNTuple named ...
std::string MakeObjectUrl(std::uint64_t objId) const
Resolve a numeric object ID to its full HTTP URL.
RNTupleLocator CommitSealedPageImpl(ROOT::DescriptorId_t physicalColumnId, const RPageStorage::RSealedPage &sealedPage) final
RPageSinkS3(std::string_view ntupleName, std::string_view baseUrl, const ROOT::RNTupleWriteOptions &options, RFromBaseUrl)
Internal constructor used by CloneAsHidden: the public constructor derives the base URL by parsing an...
void PutObject(const std::string &url, const unsigned char *data, std::size_t size)
Upload raw bytes to the given S3 URL via an HTTP PUT request.
Storage provider that reads ntuple pages from S3-compatible object storage.
ROOT::RNTupleDescriptor AttachImpl() final
LoadStructureImpl() has been called before AttachImpl() is called
void LoadSealedPageImpl(const RNTupleLocator &locator, RSealedPage &sealedPage) final
RPageSourceS3(std::string_view ntupleName, std::string_view baseUrl, const ROOT::RNTupleReadOptions &options, RFromBaseUrl)
Internal constructor used by CloneImpl: the public constructor derives the base URL by parsing an s3 ...
ROOT::Internal::RCurlConnection fClusterConnection
Connection used exclusively by LoadClusters(), which the cluster pool calls on its own I/O thread.
void LoadStructureImpl() final
Fills fStructureBuffer with the compressed header and footer.
void LoadPageListImpl(const RNTupleLocator &locator, unsigned char *buffer) final
std::unique_ptr< RPageSource > OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &anchorLink, const ROOT::RNTupleReadOptions &options={}) final
Creates a new PageSource using the same underlying file as this but referring to a different RNTuple,...
void GetObject(ROOT::Internal::RCurlConnection &connection, const std::string &url, unsigned char *buffer, std::size_t size)
Download size bytes from url into the caller-provided buffer via an HTTP GET request.
std::string MakeObjectUrl(std::uint64_t objId) const
Resolve a numeric object ID to its full HTTP URL through the anchor's URL template.
std::size_t GetShortObject(ROOT::Internal::RCurlConnection &connection, const std::string &url, unsigned char *buffer, std::size_t size)
Download at most size bytes of url, returning how many arrived.
ROOT::Internal::RCurlConnection fMainConnection
Connection used by everything that runs on the calling thread: the anchor, header and footer in LoadS...
std::vector< std::unique_ptr< ROOT::Internal::RCluster > > LoadClusters(std::span< ROOT::Internal::RCluster::RKey > clusterKeys) final
Populates all the pages of the given cluster ids and columns; it is possible that some columns do not...
std::unique_ptr< RPageSource > CloneImpl() const final
The cloned page source opens its own pair of HTTP connections to the same base URL.
Helper class to compress data blocks in the ROOT compression frame format.
static std::size_t Zip(const void *from, std::size_t nbytes, int compression, void *to)
Returns the size of the compressed data, written into the provided output buffer.
Helper class to uncompress data blocks in the ROOT compression frame format.
static void Unzip(const void *from, size_t nbytes, size_t dataLen, void *to)
The nbytes parameter provides the size ls of the from buffer.
static std::uint64_t GetMaxEnvelopeSize(const RNTupleReadOptions &options)
A helper class for serializing and deserialization of the RNTuple binary format.
static RResult< void > DeserializeFooter(const void *buffer, std::uint64_t bufSize, ROOT::Internal::RNTupleDescriptorBuilder &descBuilder)
static RResult< void > DeserializeHeader(const void *buffer, std::uint64_t bufSize, ROOT::Internal::RNTupleDescriptorBuilder &descBuilder)
A page as being stored on disk, that is packed and compressed.
Definition RCluster.hxx:40
Base class for a sink with a physical storage backend.
void EnableDefaultMetrics(const std::string &prefix)
Enables the default set of metrics provided by RPageSink.
Abstract interface to read data from an ntuple.
void EnableDefaultMetrics(const std::string &prefix)
Enables the default set of metrics provided by RPageSource.
Base class for all ROOT issued exceptions.
Definition RError.hxx:78
The on-storage metadata of an RNTuple.
RNTupleLocator payload that is common for object stores using 64bit location information.
Generic information about the physical location of data.
std::uint64_t GetNBytesOnStorage() const
T GetPosition() const
Note that for GetPosition() / SetPosition(), the locator type must correspond (kTypeFile,...
Common user-tunable settings for reading RNTuples.
Common user-tunable settings for storing RNTuples.
The class is used as a return type for operations that can fail; wraps a value of type T or an RError...
Definition RError.hxx:222
RResult< std::string > ParseS3Url(std::string_view uri)
Translate an ntpl+s3 URI into its plain HTTP(S) equivalent.
ROOT::RLogChannel & NTupleLog()
Log channel for RNTuple diagnostics.
std::unique_ptr< T[]> MakeUninitArray(std::size_t size)
Make an array of default-initialized elements.
std::uint64_t DescriptorId_t
Distriniguishes elements of the same type within a descriptor, e.g. different fields.
bool StartsWith(std::string_view string, std::string_view prefix)
std::uint64_t NTupleSize_t
Integer type long enough to hold the maximum number of entries in a column.
Tag to select the internal constructor that takes an already-resolved base URL.
Tag to select the internal constructor that takes an already-resolved base URL.
On-disk pages within a page source are identified by the column and page number.
Definition RCluster.hxx:50
A sealed page contains the bytes of a page as written to storage (packed & compressed).
Information about a single page in the context of a cluster's page range.
const RNTupleLocator & GetLocator() const