XRootD
XrdClS3DownloadHandler.cc
Go to the documentation of this file.
1 /******************************************************************************/
2 /* Copyright (C) 2025, Pelican Project, Morgridge Institute for Research */
3 /* */
4 /* This file is part of the XrdClS3 client plugin for XRootD. */
5 /* */
6 /* XRootD is free software: you can redistribute it and/or modify it under */
7 /* the terms of the GNU Lesser General Public License as published by the */
8 /* Free Software Foundation, either version 3 of the License, or (at your */
9 /* option) any later version. */
10 /* */
11 /* XRootD is distributed in the hope that it will be useful, but WITHOUT */
12 /* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
13 /* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public */
14 /* License for more details. */
15 /* */
16 /* The copyright holder's institutional names and contributor's names may not */
17 /* be used to endorse or promote products derived from this software without */
18 /* specific prior written permission of the institution or contributor. */
19 /******************************************************************************/
20 
22 #include "XrdClS3Filesystem.hh"
23 
24 #include <XrdCl/XrdClConstants.hh>
25 #include <XrdCl/XrdClDefaultEnv.hh>
26 #include <XrdCl/XrdClFile.hh>
27 
28 #include <charconv>
29 
30 using namespace XrdClS3;
31 
32 namespace {
33 
34 class S3DownloadHandler : public XrdCl::ResponseHandler {
35 public:
36  S3DownloadHandler(std::unique_ptr<XrdCl::File> file, XrdCl::ResponseHandler *handler, time_t timeout)
37  : m_expiry(time(NULL) + timeout), m_file(std::move(file)), m_handler(handler), m_buffer(new XrdCl::Buffer(kReadSize))
38  {
39  if (timeout == 0) {
41  XrdCl::DefaultEnv::GetEnv()->GetInt( "RequestTimeout", val );
42  m_expiry += val;
43  }
44  }
45 
46  virtual ~S3DownloadHandler() noexcept = default;
47 
48  virtual void HandleResponse(XrdCl::XRootDStatus *status, XrdCl::AnyObject *response) override;
49 
50 private:
51  time_t m_expiry; // Expiration time for the download operation
52  std::unique_ptr<XrdCl::File> m_file; // File we are reading from
53  XrdCl::ResponseHandler *m_handler; // Handler to call with the final result buffer (or failure).
54  std::unique_ptr<XrdCl::Buffer> m_buffer; // Buffer to hold the data read from the file
55  static constexpr size_t kReadSize = 32 * 1024; // Size of each read operation (32 KB)
56 
57  std::pair<time_t, bool> GetTimeout() const {
58  // Calculate the timeout based on the current time and the expiry time
59  time_t now = time(NULL);
60  if (now >= m_expiry) {
61  return {0, false}; // No time left, return 0 timeout
62  }
63  return {m_expiry - now, true};
64  }
65 
66  class ReadHandler : public XrdCl::ResponseHandler {
67  public:
68  ReadHandler(std::unique_ptr<S3DownloadHandler> parent) : m_parent(std::move(parent)) {}
69  virtual ~ReadHandler() noexcept = default;
70 
71  virtual void HandleResponse(XrdCl::XRootDStatus *status, XrdCl::AnyObject *response) override;
72 
73  // Release ownership of the parent handler back to the caller
74  std::unique_ptr<S3DownloadHandler> TakeParent() { return std::move(m_parent); }
75  private:
76  std::unique_ptr<S3DownloadHandler> m_parent; // Pointer to the parent handler to access its members
77  };
78 
79  class CloseHandler : public XrdCl::ResponseHandler {
80  public:
81  CloseHandler(std::unique_ptr<S3DownloadHandler> parent, std::unique_ptr<XrdCl::XRootDStatus> status) : m_parent(std::move(parent)), m_read_status(std::move(status)) {}
82  virtual ~CloseHandler() noexcept = default;
83 
84  virtual void HandleResponse(XrdCl::XRootDStatus *status, XrdCl::AnyObject *response) override;
85 
86  private:
87  std::unique_ptr<S3DownloadHandler> m_parent; // Pointer to the parent handler to access its members
88  std::unique_ptr<XrdCl::XRootDStatus> m_read_status; // Status from the read operation; if nullptr, the read was successful
89  };
90 };
91 
92 void
93 S3DownloadHandler::HandleResponse(XrdCl::XRootDStatus *status_raw, XrdCl::AnyObject *response_raw)
94 {
95  std::unique_ptr<S3DownloadHandler> self(this);
96  std::unique_ptr<XrdCl::XRootDStatus> status(status_raw);
97  std::unique_ptr<XrdCl::AnyObject> response(response_raw);
98 
99  // If the open failed, we pass the status up the chain.
100  if (!status || !status->IsOK()) {
101  if (m_handler) m_handler->HandleResponse(status.release(), response.release());
102  return;
103  }
104  auto [timeout, ok] = GetTimeout();
105  if (!ok) {
106  // If we have no time left, we cannot proceed with the read.
107  if (m_handler) {
108  m_handler->HandleResponse(new XrdCl::XRootDStatus(XrdCl::stError, XrdCl::errOperationExpired, 0, "Download operation timed out"), nullptr);
109  }
110  return;
111  }
112 
113  // Open succeeded, so we can now read the file.
114  std::unique_ptr<ReadHandler> readHandler(new ReadHandler(std::move(self)));
115  auto st = m_file->Read(0, S3DownloadHandler::kReadSize, m_buffer->GetBufferAtCursor(), readHandler.get(), timeout);
116 
117  if (!st.IsOK()) {
118  // The read request failed; take ownership of 'this' back
119  self = readHandler->TakeParent();
120  // We close the file and return the error
121  std::unique_ptr<CloseHandler> closeHandler(new CloseHandler(std::move(self), std::unique_ptr<XrdCl::XRootDStatus>(new XrdCl::XRootDStatus(st))));
122  auto close_st = m_file->Close(closeHandler.get(), timeout);
123  if (close_st.IsOK()) {
124  closeHandler.release(); // The close handler now owns itself
125  } else {
126  if (m_handler) {
127  m_handler->HandleResponse(new XrdCl::XRootDStatus(close_st), nullptr);
128  }
129  }
130  return;
131  }
132 
133  // if read succeeds relinquish ownership
134  readHandler.release(); // Read now owns the handler
135 }
136 
137 void
138 S3DownloadHandler::ReadHandler::HandleResponse(XrdCl::XRootDStatus *status_raw, XrdCl::AnyObject *response_raw) {
139  std::unique_ptr<ReadHandler> self(this);
140  std::unique_ptr<XrdCl::XRootDStatus> status(status_raw);
141  std::unique_ptr<XrdCl::AnyObject> response(response_raw);
142 
143  auto [timeout, ok] = m_parent->GetTimeout();
144  if (!ok) {
145  // If we have no time left, we cannot proceed with the read.
146  if (m_parent->m_handler) {
147  m_parent->m_handler->HandleResponse(new XrdCl::XRootDStatus(XrdCl::stError, XrdCl::errOperationExpired, 0, "Download operation timed out"), nullptr);
148  }
149  return;
150  }
151 
152  if (!status || !status->IsOK()) {
153  auto parent = m_parent.get();
154  std::unique_ptr<CloseHandler> closeHandler(new CloseHandler(std::move(m_parent), std::move(status)));
155  auto st = parent->m_file->Close(closeHandler.get(), timeout);
156  if (st.IsOK()) {
157  closeHandler.release();
158  } else if (parent->m_handler) {
159  parent->m_handler->HandleResponse(new XrdCl::XRootDStatus(st), nullptr);
160  }
161  return;
162  }
163 
164  XrdCl::ChunkInfo *chunkInfo = nullptr;
165  response->Get(chunkInfo);
166  if (!chunkInfo) {
167  // If we didn't get a chunk, we can close the file and return.
168  auto parent = m_parent.get();
169  std::unique_ptr<CloseHandler> closeHandler(new CloseHandler(std::move(m_parent),
170  std::unique_ptr<XrdCl::XRootDStatus>(new XrdCl::XRootDStatus(XrdCl::stError, XrdCl::errInternal, 0, "No chunk info received"))));
171  auto st = parent->m_file->Close(closeHandler.get(), timeout);
172  if (st.IsOK()) {
173  closeHandler.release();
174  } else if (parent->m_handler) {
175  parent->m_handler->HandleResponse(new XrdCl::XRootDStatus(st), nullptr);
176  }
177  return;
178  }
179 
180  // If we got a chunk but the length is zero, that is the end of the file;
181  // we can close the file and return.
182  if (chunkInfo->GetLength() == 0) {
183  m_parent->m_buffer->ReAllocate(m_parent->m_buffer->GetCursor());
184  auto parent = m_parent.get();
185  std::unique_ptr<CloseHandler> closeHandler(new CloseHandler(std::move(m_parent), nullptr));
186  auto st = parent->m_file->Close(closeHandler.get(), timeout);
187  if (st.IsOK()) {
188  closeHandler.release();
189  } else if (parent->m_handler) {
190  parent->m_handler->HandleResponse(new XrdCl::XRootDStatus(st), nullptr);
191  }
192  return;
193  }
194 
195  // Read was successful; read additional data if available.
196  m_parent->m_buffer->AdvanceCursor(chunkInfo->GetLength());
197  m_parent->m_buffer->ReAllocate(m_parent->m_buffer->GetCursor() + S3DownloadHandler::kReadSize);
198  // Pass to Read a non-owning pointer
199  auto st = m_parent->m_file->Read(m_parent->m_buffer->GetCursor(), kReadSize, m_parent->m_buffer->GetBufferAtCursor(), self.get(), timeout);
200  if (!st.IsOK()) {
201  // If the read request failed, close or delete the parent handler.
202  // We still own the handler, pass it to close handler
203  auto parent = m_parent.get();
204  std::unique_ptr<CloseHandler> closeHandler(new CloseHandler(std::move(m_parent), nullptr));
205  auto close_st = parent->m_file->Close(closeHandler.get(), timeout);
206  if (close_st.IsOK()) {
207  closeHandler.release();
208  } else if (parent->m_handler) {
209  parent->m_handler->HandleResponse(new XrdCl::XRootDStatus(close_st), nullptr);
210  }
211  return;
212  }
213 
214  // Release ownership if Read returns successfully
215  self.release();
216 }
217 
218 void
219 S3DownloadHandler::CloseHandler::HandleResponse(XrdCl::XRootDStatus *status_raw, XrdCl::AnyObject *response_raw) {
220  std::unique_ptr<CloseHandler> self(this);
221  std::unique_ptr<XrdCl::XRootDStatus> status(status_raw);
222  std::unique_ptr<XrdCl::AnyObject> response(response_raw);
223 
224  // If there was a read error, then we report that to the handler and ignore the close status.
225  if (m_read_status) {
226  // If we had a read status, we pass it up the chain.
227  if (m_parent->m_handler) {
228  m_parent->m_handler->HandleResponse(m_read_status.release(), nullptr);
229  }
230  return;
231  }
232 
233  if (!status || !status->IsOK()) {
234  if (m_parent->m_handler) {
235  m_parent->m_handler->HandleResponse(status.release(), nullptr);
236  }
237  return;
238  }
239 
240  // If the close was successful, we can pass the buffer to the handler.
241  response.reset(new XrdCl::AnyObject());
242  response->Set(m_parent->m_buffer.release(), true); // Take ownership of the buffer
243  if (m_parent->m_handler) {
244  m_parent->m_handler->HandleResponse(status.release(), response.release());
245  }
246 }
247 
248 } // namespace
249 
251 XrdClS3::DownloadUrl(const std::string &url, XrdClHttp::HeaderCallout *header_callout, XrdCl::ResponseHandler *handler, time_t timeout)
252 {
253  std::unique_ptr<XrdCl::File> http_file(new XrdCl::File(url));
254 
255  if (header_callout) {
256  auto callout_loc = reinterpret_cast<long long>(header_callout);
257  size_t buf_size = 16;
258  char callout_buf[buf_size];
259  std::to_chars_result result = std::to_chars(callout_buf, callout_buf + buf_size - 1, callout_loc, 16);
260  if (result.ec == std::errc{}) {
261  std::string callout_str(callout_buf, result.ptr - callout_buf);
262  http_file->SetProperty("XrdClHttpHeaderCallout", callout_str);
263  }
264  }
265  http_file->SetProperty("XrdClHttpFullDownload", "true");
266 
267  auto http_file_raw = http_file.get();
268  S3DownloadHandler *downloadHandler = new S3DownloadHandler(std::move(http_file), handler, timeout);
269 
270  return http_file_raw->Open(url, XrdCl::OpenFlags::Read, XrdCl::Access::None, downloadHandler, timeout);
271 }
static void parent()
static Env * GetEnv()
Get default client environment.
bool GetInt(const std::string &key, int &value)
Definition: XrdClEnv.cc:115
A file.
Definition: XrdClFile.hh:52
Handle an async response.
XrdCl::XRootDStatus DownloadUrl(const std::string &url, XrdClHttp::HeaderCallout *header_callout, XrdCl::ResponseHandler *handler, time_t timeout)
const uint16_t errOperationExpired
Definition: XrdClStatus.hh:90
const uint16_t stError
An error occurred that could potentially be retried.
Definition: XrdClStatus.hh:32
const uint16_t errInternal
Internal error.
Definition: XrdClStatus.hh:56
const int DefaultRequestTimeout
Describe a data chunk for vector read.
uint32_t GetLength() const
Get the data length.
@ Read
Open only for reading.