XRootD
XrdHttpTpcMultistream.cc
Go to the documentation of this file.
1 
5 #include "XrdHttpTpcTPC.hh"
6 #include "XrdHttpTpcState.hh"
7 
8 #include "XrdSys/XrdSysError.hh"
9 
10 #include <curl/curl.h>
11 
12 #include <algorithm>
13 #include <sstream>
14 #include <stdexcept>
15 
16 
17 using namespace TPC;
18 
19 class CurlHandlerSetupError : public std::runtime_error {
20 public:
21  CurlHandlerSetupError(const std::string &msg) :
22  std::runtime_error(msg)
23  {}
24 
25  virtual ~CurlHandlerSetupError() noexcept {}
26 };
27 
28 namespace {
29 class MultiCurlHandler {
30 public:
31  MultiCurlHandler(std::vector<State*> &states, XrdSysError &log) :
32  m_handle(curl_multi_init()),
33  m_states(states),
34  m_log(log),
35  m_bytes_transferred(0),
36  m_error_code(0),
37  m_status_code(0)
38  {
39  if (m_handle == NULL) {
40  throw CurlHandlerSetupError("Failed to initialize a libcurl multi-handle");
41  }
42  m_avail_handles.reserve(states.size());
43  m_active_handles.reserve(states.size());
44  for (std::vector<State*>::const_iterator state_iter = states.begin();
45  state_iter != states.end();
46  state_iter++) {
47  m_avail_handles.push_back((*state_iter)->GetHandle());
48  }
49  }
50 
51  ~MultiCurlHandler()
52  {
53  if (!m_handle) {return;}
54  for (std::vector<CURL *>::const_iterator it = m_active_handles.begin();
55  it != m_active_handles.end();
56  it++) {
57  curl_multi_remove_handle(m_handle, *it);
58  }
59  curl_multi_cleanup(m_handle);
60  }
61 
62  MultiCurlHandler(const MultiCurlHandler &) = delete;
63 
64  CURLM *Get() const {return m_handle;}
65 
66  void FinishCurlXfer(CURL *curl) {
67  CURLMcode mres = curl_multi_remove_handle(m_handle, curl);
68  if (mres) {
69  std::stringstream ss;
70  ss << "Failed to remove transfer from set: "
71  << curl_multi_strerror(mres);
72  throw std::runtime_error(ss.str());
73  }
74  for (std::vector<State*>::iterator state_iter = m_states.begin();
75  state_iter != m_states.end();
76  state_iter++) {
77  if (curl == (*state_iter)->GetHandle()) {
78  m_bytes_transferred += (*state_iter)->BytesTransferred();
79  int error_code = (*state_iter)->GetErrorCode();
80  if (error_code && !m_error_code) {
81  m_error_code = error_code;
82  m_error_message = (*state_iter)->GetErrorMessage();
83  }
84  int status_code = (*state_iter)->GetStatusCode();
85  if (status_code >= 400 && !m_status_code) {
86  m_status_code = status_code;
87  m_error_message = (*state_iter)->GetErrorMessage();
88  }
89  (*state_iter)->ResetAfterRequest();
90  break;
91  }
92  }
93  for (std::vector<CURL *>::iterator iter = m_active_handles.begin();
94  iter != m_active_handles.end();
95  ++iter)
96  {
97  if (*iter == curl) {
98  m_active_handles.erase(iter);
99  break;
100  }
101  }
102  m_avail_handles.push_back(curl);
103  }
104 
105  off_t StartTransfers(off_t current_offset, off_t content_length, size_t block_size,
106  int &running_handles) {
107  bool started_new_xfer = false;
108  do {
109  size_t xfer_size = std::min(content_length - current_offset, static_cast<off_t>(block_size));
110  if (xfer_size == 0) {return current_offset;}
111  if (!(started_new_xfer = StartTransfer(current_offset, xfer_size))) {
112  // In this case, we need to start new transfers but weren't able to.
113  if (running_handles == 0) {
114  if (!CanStartTransfer(true)) {
115  m_log.Emsg("StartTransfers", "Unable to start transfers.");
116  }
117  }
118  break;
119  } else {
120  running_handles += 1;
121  }
122  current_offset += xfer_size;
123  } while (true);
124  return current_offset;
125  }
126 
127  // Flush every stream to the local filesystem. Returns State::errNone and
128  // leaves error_msg untouched if all the streams could be flushed; otherwise
129  // returns the error code of the first failure encountered and sets error_msg
130  // to the corresponding error message.
131  int Flush(std::string &error_msg) {
132  int error_code = State::errNone;
133  for (std::vector<State*>::iterator state_it = m_states.begin();
134  state_it != m_states.end();
135  state_it++)
136  {
137  if (((*state_it)->Flush() == -1) && !error_code) {
138  error_code = State::errFlush;
139  error_msg = (*state_it)->GetFinalizeErrorMessage();
140  if (error_msg.empty()) {error_msg = "(no error message provided)";}
141  }
142  }
143  return error_code;
144  }
145 
146  off_t BytesTransferred() const {
147  return m_bytes_transferred;
148  }
149 
150  // Number of bytes that have actually been transferred so far: the bytes of
151  // the requests that already completed plus the bytes of the requests that
152  // are still in flight. The two are disjoint: FinishCurlXfer() accumulates
153  // the counter of a state into m_bytes_transferred and then zeroes it via
154  // State::ResetAfterRequest(), so no byte is counted twice.
155  // Note this is not the same quantity as the scheduling offset maintained by
156  // StartTransfers(), which is advanced as soon as a range request is handed
157  // over to libcurl, hence before any byte of that range has been received.
158  off_t BytesInFlightAndTransferred() const {
159  off_t bytes = m_bytes_transferred;
160  for (std::vector<State*>::const_iterator state_iter = m_states.begin();
161  state_iter != m_states.end();
162  state_iter++) {
163  bytes += (*state_iter)->BytesTransferred();
164  }
165  return bytes;
166  }
167 
168  int GetStatusCode() const {
169  return m_status_code;
170  }
171 
172  int GetErrorCode() const {
173  return m_error_code;
174  }
175 
176  void SetErrorCode(int error_code) {
177  m_error_code = error_code;
178  }
179 
180  std::string GetErrorMessage() const {
181  return m_error_message;
182  }
183 
184  void SetErrorMessage(const std::string &error_msg) {
185  m_error_message = error_msg;
186  }
187 
188 private:
189 
190  bool StartTransfer(off_t offset, size_t size) {
191  if (!CanStartTransfer(false)) {return false;}
192  for (std::vector<CURL*>::const_iterator handle_it = m_avail_handles.begin();
193  handle_it != m_avail_handles.end();
194  handle_it++) {
195  for (std::vector<State*>::iterator state_it = m_states.begin();
196  state_it != m_states.end();
197  state_it++) {
198  if ((*state_it)->GetHandle() == *handle_it) { // This state object represents an idle handle.
199  (*state_it)->SetTransferParameters(offset, size);
200  ActivateHandle(**state_it);
201  return true;
202  }
203  }
204  }
205  return false;
206  }
207 
208  void ActivateHandle(State &state) {
209  CURL *curl = state.GetHandle();
210  m_active_handles.push_back(curl);
211  CURLMcode mres;
212  mres = curl_multi_add_handle(m_handle, curl);
213  if (mres) {
214  std::stringstream ss;
215  ss << "Failed to add transfer to libcurl multi-handle"
216  << curl_multi_strerror(mres);
217  throw std::runtime_error(ss.str());
218  }
219  for (auto iter = m_avail_handles.begin();
220  iter != m_avail_handles.end();
221  ++iter)
222  {
223  if (*iter == curl) {
224  m_avail_handles.erase(iter);
225  break;
226  }
227  }
228  }
229 
230  bool CanStartTransfer(bool log_reason) const {
231  size_t idle_handles = m_avail_handles.size();
232  size_t transfer_in_progress = 0;
233  for (std::vector<State*>::const_iterator state_iter = m_states.begin();
234  state_iter != m_states.end();
235  state_iter++) {
236  for (std::vector<CURL*>::const_iterator handle_iter = m_active_handles.begin();
237  handle_iter != m_active_handles.end();
238  handle_iter++) {
239  if (*handle_iter == (*state_iter)->GetHandle()) {
240  transfer_in_progress += (*state_iter)->BodyTransferInProgress();
241  break;
242  }
243  }
244  }
245  if (!idle_handles) {
246  if (log_reason) {
247  m_log.Emsg("CanStartTransfer", "Unable to start transfers as no idle CURL handles are available.");
248  }
249  return false;
250  }
251  ssize_t available_buffers = m_states[0]->AvailableBuffers();
252  // To be conservative, set aside buffers for any transfers that have been activated
253  // but don't have their first responses back yet.
254  available_buffers -= (m_active_handles.size() - transfer_in_progress);
255  if (log_reason && (available_buffers == 0)) {
256  std::stringstream ss;
257  ss << "Unable to start transfers as no buffers are available. Available buffers: " <<
258  m_states[0]->AvailableBuffers() << ", Active curl handles: " << m_active_handles.size()
259  << ", Transfers in progress: " << transfer_in_progress;
260  m_log.Emsg("CanStartTransfer", ss.str().c_str());
261  if (m_states[0]->AvailableBuffers() == 0) {
262  m_states[0]->DumpBuffers();
263  }
264  }
265  return available_buffers > 0;
266  }
267 
268  CURLM *m_handle;
269  std::vector<CURL *> m_avail_handles;
270  std::vector<CURL *> m_active_handles;
271  std::vector<State*> &m_states;
272  XrdSysError &m_log;
273  off_t m_bytes_transferred;
274  int m_error_code;
275  int m_status_code;
276  std::string m_error_message;
277 };
278 }
279 
280 
281 int TPCHandler::RunCurlWithStreamsImpl(XrdHttpExtReq &req, State &state,
282  size_t streams, std::vector<State*> &handles,
283  std::vector<ManagedCurlHandle> &curl_handles, TPCLogRecord &rec)
284 {
285  bool success;
286  // The content-length was set thanks to the call to GetContentLengthTPCPull() before calling this function
287  off_t content_size = state.GetContentLength();
288  off_t current_offset = 0;
289 
290  size_t concurrency = streams * m_pipelining_multiplier;
291 
292  handles.reserve(concurrency);
293  handles.push_back(new State());
294  handles[0]->Move(state);
295  for (size_t idx = 1; idx < concurrency; idx++) {
296  handles.push_back(handles[0]->Duplicate());
297  curl_handles.emplace_back(handles.back()->GetHandle());
298  }
299 
300  // Notify the packet marking manager that the transfer will start after this point
301  rec.pmarkManager.startTransfer();
302 
303  // Create the multi-handle and add in the current transfer to it.
304  MultiCurlHandler mch(handles, m_log);
305  CURLM *multi_handle = mch.Get();
306 
307  curl_multi_setopt(multi_handle, CURLMOPT_PIPELINING, 1);
308  curl_multi_setopt(multi_handle, CURLMOPT_MAX_HOST_CONNECTIONS, streams);
309 
310  // Start response to client prior to the first call to curl_multi_perform
311  int retval = req.StartChunkedResp(201, "Created", "Content-Type: text/plain");
312  if (retval) {
313  logTransferEvent(LogMask::Error, rec, "RESPONSE_FAIL",
314  "Failed to send the initial response to the TPC client");
315  return retval;
316  } else {
317  logTransferEvent(LogMask::Debug, rec, "RESPONSE_START",
318  "Initial transfer response sent to the TPC client");
319  }
320 
321  // Start assigning transfers
322  int running_handles = 0;
323  current_offset = mch.StartTransfers(current_offset, content_size, m_block_size, running_handles);
324 
325  // Transfer loop: use curl to actually run the transfer, but periodically
326  // interrupt things to send back performance updates to the client.
327  time_t last_marker = 0;
328  // Track the time since the transfer last made progress
329  off_t last_advance_bytes = 0;
330  time_t last_advance_time = time(NULL);
331  time_t transfer_start = last_advance_time;
332  CURLcode res = static_cast<CURLcode>(-1);
333  CURLMcode mres = CURLM_OK;
334  do {
335  time_t now = time(NULL);
336  time_t next_marker = last_marker + m_marker_period;
337  if (now >= next_marker) {
338  // Report - and watch for progress on - the bytes that have really
339  // been transferred, not the offset up to which the range requests
340  // have been scheduled: the latter runs ahead of the transfer by up
341  // to concurrency * m_block_size bytes.
342  const off_t bytes_transferred = mch.BytesInFlightAndTransferred();
343  if (bytes_transferred > last_advance_bytes) {
344  last_advance_bytes = bytes_transferred;
345  last_advance_time = now;
346  }
347  if (SendPerfMarker(req, rec, handles, bytes_transferred)) {
348  logTransferEvent(LogMask::Error, rec, "PERFMARKER_FAIL",
349  "Failed to send a perf marker to the TPC client");
350  return -1;
351  }
352  int timeout = (transfer_start == last_advance_time) ? m_first_timeout : m_timeout;
353  if (now > last_advance_time + timeout) {
354  const char *log_prefix = rec.log_prefix.c_str();
355  bool tpc_pull = strncmp("Pull", log_prefix, 4) == 0;
356 
357  mch.SetErrorCode(State::errTimeout);
358  std::stringstream ss;
359  ss << "Transfer failed because no bytes have been "
360  << (tpc_pull ? "received from the source (pull mode) in "
361  : "transmitted to the destination (push mode) in ") << timeout << " seconds.";
362  mch.SetErrorMessage(ss.str());
363  break;
364  }
365  last_marker = now;
366  }
367 
368  mres = curl_multi_perform(multi_handle, &running_handles);
369  if (mres == CURLM_CALL_MULTI_PERFORM) {
370  // curl_multi_perform should be called again immediately. On newer
371  // versions of curl, this is no longer used.
372  continue;
373  } else if (mres != CURLM_OK) {
374  break;
375  }
376 
377  rec.pmarkManager.beginPMarks();
378 
379 
380  // Harvest any messages, looking for CURLMSG_DONE.
381  CURLMsg *msg;
382  do {
383  int msgq = 0;
384  msg = curl_multi_info_read(multi_handle, &msgq);
385  if (msg && (msg->msg == CURLMSG_DONE)) {
386  CURL *easy_handle = msg->easy_handle;
387  res = msg->data.result;
388  mch.FinishCurlXfer(easy_handle);
389  // If any requests fail, cut off the entire transfer.
390  if (res != CURLE_OK) {
391  break;
392  }
393  }
394  } while (msg);
395  if (res != static_cast<CURLcode>(-1) && res != CURLE_OK) {
396  std::stringstream ss;
397  ss << "Breaking loop due to failed curl transfer: " << curl_easy_strerror(res);
398  logTransferEvent(LogMask::Debug, rec, "MULTISTREAM_CURL_FAILURE",
399  ss.str());
400  break;
401  }
402 
403  if (running_handles < static_cast<int>(concurrency)) {
404  // Issue new transfers if there is still pending work to do.
405  // Otherwise, continue running until there are no handles left.
406  if (current_offset != content_size) {
407  current_offset = mch.StartTransfers(current_offset, content_size,
408  m_block_size, running_handles);
409  if (!running_handles) {
410  std::stringstream ss;
411  ss << "No handles are able to run. Streams=" << streams << ", concurrency="
412  << concurrency;
413 
414  logTransferEvent(LogMask::Debug, rec, "MULTISTREAM_IDLE", ss.str());
415  }
416  } else if (running_handles == 0) {
417  logTransferEvent(LogMask::Debug, rec, "MULTISTREAM_IDLE",
418  "All the ranges have been scheduled and all the handles are done; ending the transfer loop.");
419  break;
420  }
421  }
422 
423  int64_t max_sleep_time = next_marker - time(NULL);
424  if (max_sleep_time <= 0) {
425  continue;
426  }
427  int fd_count;
428  mres = curl_multi_wait(multi_handle, NULL, 0, max_sleep_time*1000,
429  &fd_count);
430  if (mres != CURLM_OK) {
431  break;
432  }
433  } while (running_handles);
434 
435  if (mres != CURLM_OK) {
436  std::stringstream ss;
437  ss << "Internal libcurl multi-handle error: "
438  << curl_multi_strerror(mres);
439  logTransferEvent(LogMask::Error, rec, "MULTISTREAM_ERROR", ss.str());
440  throw std::runtime_error(ss.str());
441  }
442 
443  // Harvest any messages, looking for CURLMSG_DONE.
444  CURLMsg *msg;
445  do {
446  int msgq = 0;
447  msg = curl_multi_info_read(multi_handle, &msgq);
448  if (msg && (msg->msg == CURLMSG_DONE)) {
449  CURL *easy_handle = msg->easy_handle;
450  mch.FinishCurlXfer(easy_handle);
451  if (res == CURLE_OK || res == static_cast<CURLcode>(-1))
452  res = msg->data.result; // Transfer result will be examined below.
453  }
454  } while (msg);
455 
456  if (!state.GetErrorCode() && res == static_cast<CURLcode>(-1)) { // No transfers returned?!?
457  logTransferEvent(LogMask::Error, rec, "MULTISTREAM_ERROR",
458  "Internal state error in libcurl");
459  throw std::runtime_error("Internal state error in libcurl");
460  }
461 
462  // A failure to flush the file to the local filesystem is always logged and is
463  // appended to the error reported to the client, but it never replaces the
464  // transfer failure itself: the flush failure is usually a consequence of it.
465  std::string flushErrorMsg;
466  const int flushErrorCode = mch.Flush(flushErrorMsg);
467  std::string flushErrorSuffix;
468  if (flushErrorCode) {
469  std::replace(flushErrorMsg.begin(), flushErrorMsg.end(), '\n', ' ');
470  flushErrorMsg = "Failed to flush the file to the local filesystem. " + flushErrorMsg;
471  logTransferEvent(LogMask::Error, rec, "FLUSH_FAIL", flushErrorMsg);
472  flushErrorSuffix = "; " + flushErrorMsg;
473  }
474 
475  rec.bytes_transferred = mch.BytesTransferred();
476  rec.tpc_status = mch.GetStatusCode();
477 
478  // Generate the final response back to the client.
479  std::stringstream ss;
480  success = false;
481  if (mch.GetStatusCode() >= 400) {
482  std::string err = mch.GetErrorMessage();
483  std::stringstream ss2;
484  ss2 << "Remote side failed with status code " << mch.GetStatusCode();
485  if (!err.empty()) {
486  std::replace(err.begin(), err.end(), '\n', ' ');
487  ss2 << "; error message: \"" << err << "\"";
488  }
489  logTransferEvent(LogMask::Error, rec, "MULTISTREAM_FAIL", ss2.str());
490  ss2 << flushErrorSuffix;
491  ss << generateClientErr(ss2, rec);
492  } else if (mch.GetErrorCode() == State::errTimeout) {
493  // The stall detector fired; its message already describes precisely
494  // what happened, report it as-is.
495  std::stringstream ss2;
496  ss2 << mch.GetErrorMessage();
497  logTransferEvent(LogMask::Error, rec, "MULTISTREAM_FAIL", ss2.str());
498  ss2 << flushErrorSuffix;
499  ss << generateClientErr(ss2, rec);
500  } else if (mch.GetErrorCode()) {
501  std::string err = mch.GetErrorMessage();
502  if (err.empty()) {err = "(no error message provided)";}
503  else {std::replace(err.begin(), err.end(), '\n', ' ');}
504  std::stringstream ss2;
505  ss2 << "Error when interacting with local filesystem: " << err;
506  logTransferEvent(LogMask::Error, rec, "MULTISTREAM_FAIL", ss2.str());
507  ss2 << flushErrorSuffix;
508  ss << generateClientErr(ss2, rec);
509  } else if (res != CURLE_OK) {
510  std::stringstream ss2;
511  ss2 << "Request failed when processing";
512  std::stringstream ss3;
513  ss3 << ss2.str() << ":" << curl_easy_strerror(res);
514  logTransferEvent(LogMask::Error, rec, "MULTISTREAM_FAIL", ss3.str());
515  ss2 << flushErrorSuffix;
516  ss << generateClientErr(ss2, rec, res);
517  } else if (current_offset != content_size) {
518  std::stringstream ss2;
519  ss2 << "Internal logic error led to early abort; current offset is " <<
520  current_offset << " while full size is " << content_size;
521  logTransferEvent(LogMask::Error, rec, "MULTISTREAM_FAIL", ss2.str());
522  ss2 << flushErrorSuffix;
523  ss << generateClientErr(ss2, rec);
524  } else if (flushErrorCode) {
525  // Nothing else went wrong: the flush failure is the reason of the failure.
526  std::stringstream ss2;
527  ss2 << flushErrorMsg;
528  ss << generateClientErr(ss2, rec);
529  } else {
530  if (!handles[0]->Finalize()) {
531  std::stringstream ss2;
532  ss2 << "Failed to finalize and close file handle.";
533  std::string handleErrMsg = handles[0]->GetFinalizeErrorMessage();
534  if(handleErrMsg.size()) {
535  std::replace(handleErrMsg.begin(), handleErrMsg.end(), '\n', ' ');
536  ss2 << " " << handleErrMsg;
537  }
538  ss << generateClientErr(ss2, rec);
539  logTransferEvent(LogMask::Error, rec, "MULTISTREAM_ERROR",
540  ss2.str());
541  } else {
542  ss << "success: Created";
543  success = true;
544  }
545  }
546 
547  if ((retval = req.ChunkResp(ss.str().c_str(), 0))) {
548  logTransferEvent(LogMask::Error, rec, "TRANSFER_ERROR",
549  "Failed to send last update to remote client");
550  return retval;
551  } else if (success) {
552  logTransferEvent(LogMask::Info, rec, "TRANSFER_SUCCESS");
553  rec.status = 0;
554  }
555  return req.ChunkResp(NULL, 0);
556 }
557 
558 
559 int TPCHandler::RunCurlWithStreams(XrdHttpExtReq &req, State &state,
560  size_t streams, TPCLogRecord &rec)
561 {
562  std::vector<ManagedCurlHandle> curl_handles;
563  std::vector<State*> handles;
564  std::stringstream err_ss;
565  try {
566  int retval = RunCurlWithStreamsImpl(req, state, streams, handles, curl_handles, rec);
567  for (std::vector<State*>::iterator state_iter = handles.begin();
568  state_iter != handles.end();
569  state_iter++) {
570  delete *state_iter;
571  }
572  return retval;
573  } catch (CurlHandlerSetupError &e) {
574  for (std::vector<State*>::iterator state_iter = handles.begin();
575  state_iter != handles.end();
576  state_iter++) {
577  delete *state_iter;
578  }
579 
580  rec.status = 500;
581  logTransferEvent(LogMask::Error, rec, "MULTISTREAM_ERROR", e.what());
582  std::stringstream ss;
583  ss << e.what();
584  err_ss << generateClientErr(ss, rec);
585  return req.SendSimpleResp(rec.status, NULL, NULL, e.what(), 0);
586  } catch (std::runtime_error &e) {
587  for (std::vector<State*>::iterator state_iter = handles.begin();
588  state_iter != handles.end();
589  state_iter++) {
590  delete *state_iter;
591  }
592 
593  logTransferEvent(LogMask::Error, rec, "MULTISTREAM_ERROR", e.what());
594  std::stringstream ss;
595  ss << e.what();
596  err_ss << generateClientErr(ss, rec);
597  int retval;
598  if ((retval = req.ChunkResp(err_ss.str().c_str(), 0))) {
599  return retval;
600  }
601  return req.ChunkResp(NULL, 0);
602  }
603 }
void CURL
#define Duplicate(x, y)
bool Debug
@ Error
CurlHandlerSetupError(const std::string &msg)
virtual ~CurlHandlerSetupError() noexcept
CURL * GetHandle() const
int GetErrorCode() const
off_t GetContentLength() const
int ChunkResp(const char *body, long long bodylen)
Send a (potentially partial) body in a chunked response; invoking with NULL body.
int StartChunkedResp(int code, const char *desc, const char *header_to_add)
Starts a chunked response; body of request is sent over multiple parts using the SendChunkResp.
int SendSimpleResp(int code, const char *desc, const char *header_to_add, const char *body, long long bodylen)
Sends a basic response. If the length is < 0 then it is calculated internally.