XRootD
Loading...
Searching...
No Matches
XrdHttpTpcMultistream.cc
Go to the documentation of this file.
1
4
5#include "XrdHttpTpcTPC.hh"
6#include "XrdHttpTpcState.hh"
7
9
10#include <curl/curl.h>
11
12#include <algorithm>
13#include <sstream>
14#include <stdexcept>
15
16
17using namespace TPC;
18
19class CurlHandlerSetupError : public std::runtime_error {
20public:
21 CurlHandlerSetupError(const std::string &msg) :
22 std::runtime_error(msg)
23 {}
24
25 virtual ~CurlHandlerSetupError() noexcept {}
26};
27
28namespace {
29class MultiCurlHandler {
30public:
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
188private:
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
281int 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
559int 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)
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.