279 if (m_recv_all_headers) {
280 m_recv_all_headers =
false;
281 m_recv_status_line =
false;
284 if (!m_recv_status_line) {
285 m_recv_status_line =
true;
287 std::stringstream ss(header_line);
289 if (!std::getline(ss, item,
' '))
return false;
290 m_resp_protocol = item;
291 if (!std::getline(ss, item,
' '))
return false;
293 m_status_code = std::stol(item);
297 if (m_status_code < 100 || m_status_code >= 600) {
300 if (!std::getline(ss, item,
'\n'))
return false;
301 auto cr_loc = item.find(
'\r');
302 if (cr_loc != std::string::npos) {
303 m_resp_message = item.substr(0, cr_loc);
305 m_resp_message = item;
310 if (header_line.empty() || header_line ==
"\n" || header_line ==
"\r\n") {
311 m_recv_all_headers =
true;
315 auto found = header_line.find(
":");
316 if (found == std::string::npos) {
320 std::string header_name = header_line.substr(0, found);
326 while (found < header_line.size()) {
327 if (header_line[found] !=
' ') {
break;}
330 std::string header_value = header_line.substr(found);
333 header_value.erase(header_value.find_last_not_of(
" \r\n\t") + 1);
337 auto iter = m_headers.find(header_name);
338 if (iter == m_headers.end()) {
339 m_headers.insert(iter, {header_name, {header_value}});
341 iter->second.push_back(header_value);
344 if (header_name ==
"Allow") {
345 std::string_view val(header_value);
346 while (!val.empty()) {
347 auto found = val.find(
',');
348 auto method = val.substr(0, found);
349 if (method ==
"PROPFIND") {
353 if (found == std::string_view::npos)
break;
354 val = val.substr(found + 1);
359 }
else if (header_name ==
"Content-Length") {
361 m_content_length = std::stoll(header_value);
366 else if (header_name ==
"Content-Type") {
367 std::string_view val(header_value);
368 auto found = val.find(
";");
369 auto first_type = val.substr(0, found);
370 m_multipart_byteranges = first_type ==
"multipart/byteranges";
371 if (m_multipart_byteranges) {
372 auto remainder = val.substr(found + 1);
373 found = remainder.find(
"boundary=");
374 if (found != std::string_view::npos) {
379 else if (header_name ==
"Content-Range") {
380 auto found = header_value.find(
" ");
381 if (found == std::string::npos) {
384 std::string range_unit = header_value.substr(0, found);
385 if (range_unit !=
"bytes") {
388 auto range_resp = header_value.substr(found + 1);
389 found = range_resp.find(
"/");
390 if (found == std::string::npos) {
393 auto incl_range = range_resp.substr(0, found);
394 found = incl_range.find(
"-");
395 if (found == std::string::npos) {
398 auto first_pos = incl_range.substr(0, found);
400 m_response_offset = std::stoll(first_pos);
404 auto last_pos = incl_range.substr(found + 1);
407 last_byte = std::stoll(last_pos);
411 m_content_length = last_byte - m_response_offset + 1;
413 else if (header_name ==
"Location") {
414 m_location = header_value;
415 }
else if (header_name ==
"Digest") {
418 else if (header_name ==
"Etag")
422 m_etag = header_value;
423 m_etag.erase(remove(m_etag.begin(), m_etag.end(),
'\"'), m_etag.end());
425 else if (header_name ==
"Cache-Control")
427 m_cache_control = header_value;
923 auto now = std::chrono::system_clock::now().time_since_epoch().count();
924 auto oldest_op = now;
925 auto oldest_cycle = now;
927 std::unique_lock lk(m_worker_stats_mutex);
928 for (
const auto &entry : m_workers_last_completed_cycle) {
929 if (!entry) {
continue;}
930 auto cycle = entry->load(std::memory_order_relaxed);
931 if (cycle < oldest_cycle) oldest_cycle = cycle;
933 for (
const auto &entry : m_workers_oldest_op) {
934 if (!entry) {
continue;}
935 auto op = entry->load(std::memory_order_relaxed);
936 if (op < oldest_op) oldest_op = op;
939 auto oldest_op_dbl = std::chrono::duration<double>(std::chrono::system_clock::time_point(std::chrono::system_clock::duration(oldest_op)).time_since_epoch()).count();
940 auto oldest_cycle_dbl = std::chrono::duration<double>(std::chrono::system_clock::time_point(std::chrono::system_clock::duration(oldest_cycle)).time_since_epoch()).count();
941 std::string retval =
"{"
942 "\"oldest_op\":" + std::to_string(oldest_op_dbl) +
","
943 "\"oldest_cycle\":" + std::to_string(oldest_cycle_dbl) +
","
948 for (
size_t op_idx = 0; op_idx < 402; op_idx++) {
949 if (op_idx == 401)
continue;
951 auto &op_stats = m_ops[verb_idx][op_idx];
952 auto duration = op_stats.m_duration.load(std::memory_order_relaxed);
953 if (duration == 0)
continue;
955 std::string prefix =
"http_" + verb_str +
"_" + ((op_idx == 402) ?
"invalid" : std::to_string(200 + op_idx)) +
"_";
957 auto duration_dbl = std::chrono::duration<double>(std::chrono::steady_clock::duration(duration)).count();
958 retval +=
"\"" + prefix +
"duration\":" + std::to_string(duration_dbl) +
",";
960 duration = op_stats.m_pause_duration.load(std::memory_order_relaxed);
962 duration_dbl = std::chrono::duration<double>(std::chrono::steady_clock::duration(duration)).count();
963 retval +=
"\"" + prefix +
"pause_duration\":" + std::to_string(duration_dbl) +
",";
966 auto count = op_stats.m_bytes.load(std::memory_order_relaxed);
967 if (count) retval +=
"\"" + prefix +
"bytes\":" + std::to_string(count) +
",";
968 count = op_stats.m_error.load(std::memory_order_relaxed);
969 if (count) retval +=
"\"" + prefix +
"error\":" + std::to_string(count) +
",";
970 count = op_stats.m_finished.load(std::memory_order_relaxed);
971 if (count) retval +=
"\"" + prefix +
"finished\":" + std::to_string(count) +
",";
972 count = op_stats.m_client_timeout.load(std::memory_order_relaxed);
973 if (count) retval +=
"\"" + prefix +
"client_timeout\":" + std::to_string(count) +
",";
974 count = op_stats.m_server_timeout.load(std::memory_order_relaxed);
975 if (count) retval +=
"\"" + prefix +
"server_timeout\":" + std::to_string(count) +
",";
978 auto &op_stats = m_ops[verb_idx][401];
979 auto duration = op_stats.m_duration.load(std::memory_order_relaxed);
980 if (duration == 0)
continue;
982 std::string prefix =
"http_" + verb_str +
"_";
984 auto duration_dbl = std::chrono::duration<double>(std::chrono::steady_clock::duration(duration)).count();
985 retval +=
"\"" + prefix +
"preheader_duration\":" + std::to_string(duration_dbl) +
",";
987 auto count = op_stats.m_started.load(std::memory_order_relaxed);
988 if (count) retval +=
"\"" + prefix +
"started\":" + std::to_string(count) +
",";
989 count = op_stats.m_error.load(std::memory_order_relaxed);
990 if (count) retval +=
"\"" + prefix +
"preheader_error\":" + std::to_string(count) +
",";
991 count = op_stats.m_finished.load(std::memory_order_relaxed);
992 if (count) retval +=
"\"" + prefix +
"preheader_finished\":" + std::to_string(count) +
",";
993 count = op_stats.m_server_timeout.load(std::memory_order_relaxed);
994 if (count) retval +=
"\"" + prefix +
"preheader_timeout\":" + std::to_string(count) +
",";
995 count = op_stats.m_conncall_timeout.load(std::memory_order_relaxed);
996 if (count) retval +=
"\"" + prefix +
"conncall_timeout\":" + std::to_string(count) +
",";
1001 "\"conncall_error\":" + std::to_string(m_conncall_errors.load(std::memory_order_relaxed)) +
","
1002 "\"conncall_started\":" + std::to_string(m_conncall_req.load(std::memory_order_relaxed)) +
","
1003 "\"conncall_success\":" + std::to_string(m_conncall_success.load(std::memory_order_relaxed)) +
","
1004 "\"conncall_timeout\":" + std::to_string(m_conncall_timeout.load(std::memory_order_relaxed)) +
1091 int max_pending = 50;
1094 auto &queue = *m_queue.get();
1097 CURLM *multi_handle = curl_multi_init();
1098 if (multi_handle ==
nullptr) {
1099 throw std::runtime_error(
"Failed to create curl multi-handle");
1102 int running_handles = 0;
1103 time_t last_maintenance = time(NULL);
1104 CURLMcode mres = CURLM_OK;
1108 std::unordered_map<int, WaitingForBroker> broker_reqs;
1109 std::vector<struct curl_waitfd> waitfds;
1111 bool want_shutdown =
false;
1112 while (!want_shutdown) {
1113 m_last_completed_cycle.store(std::chrono::system_clock::now().time_since_epoch().count());
1114 auto oldest_op = std::chrono::system_clock::now();
1115 for (
const auto &entry : m_op_map) {
1116 OpRecord(*entry.second.first, OpKind::Update);
1117 if (entry.second.second < oldest_op) {
1118 oldest_op = entry.second.second;
1121 m_oldest_op.store(oldest_op.time_since_epoch().count());
1125 auto op = m_continue_queue->TryConsume();
1132 m_logger->Debug(
kLogXrdClHttp,
"Ignoring continuation of operation that has already completed");
1135 m_logger->Debug(
kLogXrdClHttp,
"Continuing the curl handle from op %p on thread %d", op.get(), getthreadid());
1139 OpRecord(*op, OpKind::Error);
1142 curl_multi_remove_handle(multi_handle, curl);
1143 curl_easy_cleanup(curl);
1144 m_op_map.erase(curl);
1146 running_handles -= 1;
1149 auto iter = m_op_map.find(curl);
1150 if (iter != m_op_map.end()) iter->second.second = std::chrono::system_clock::now();
1154 while (running_handles <
static_cast<int>(m_max_ops)) {
1155 auto op = running_handles == 0 ? queue.Consume(std::chrono::seconds(1)) : queue.TryConsume();
1159 auto curl = queue.GetHandle();
1160 if (curl ==
nullptr) {
1161 m_logger->Debug(
kLogXrdClHttp,
"Unable to allocate a curl handle");
1166 auto rv = op->
Setup(curl, *
this);
1168 m_logger->Debug(
kLogXrdClHttp,
"Failed to setup the curl handle");
1173 m_logger->Debug(
kLogXrdClHttp,
"Failed to finish setup of the curl handle");
1178 m_logger->Debug(
kLogXrdClHttp,
"Unable to setup the curl handle");
1187 m_op_map[curl] = {op, std::chrono::system_clock::now()};
1192 std::string modified_url;
1193 std::shared_ptr<CurlOptionsOp> options_op(
1205 curl = queue.GetHandle();
1206 if (curl ==
nullptr) {
1207 m_logger->Debug(
kLogXrdClHttp,
"Unable to allocate a curl handle");
1209 OpRecord(*op, OpKind::Error);
1212 auto rv = options_op->Setup(curl, *
this);
1214 m_logger->Debug(
kLogXrdClHttp,
"Failed to allocate a curl handle for OPTIONS");
1217 m_op_map[curl] = {options_op, std::chrono::system_clock::now()};
1218 OpRecord(*options_op, OpKind::Start);
1219 running_handles += 1;
1221 OpRecord(*op, OpKind::Start);
1224 auto mres = curl_multi_add_handle(multi_handle, curl);
1225 if (mres != CURLM_OK) {
1226 m_logger->Debug(
kLogXrdClHttp,
"Unable to add operation to the curl multi-handle");
1228 OpRecord(*op, OpKind::Error);
1231 m_logger->Debug(
kLogXrdClHttp,
"Added request for URL %s to worker thread for processing", op->
GetUrl().c_str());
1232 running_handles += 1;
1237 time_t now = time(NULL);
1238 time_t next_maintenance = last_maintenance + m_maintenance_period.load(std::memory_order_relaxed);
1239 if (now >= next_maintenance) {
1241 m_continue_queue->Expire();
1242 m_logger->Debug(
kLogXrdClHttp,
"Curl worker thread %d is running %d operations",
1243 getthreadid(), running_handles);
1244 last_maintenance = now;
1247 std::vector<std::pair<int, CURL *>> expired_ops;
1248 for (
const auto &entry : broker_reqs) {
1249 if (entry.second.expiry < now) {
1250 expired_ops.emplace_back(entry.first, entry.second.curl);
1253 for (
const auto &entry : expired_ops) {
1254 auto iter = m_op_map.find(entry.second);
1255 if (iter == m_op_map.end()) {
1256 m_logger->Warning(
kLogXrdClHttp,
"Found an expired curl handle with no corresponding operation!");
1260 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(iter->second.first.get())) !=
nullptr) {
1262 bool parent_op_failed =
false;
1263 if (parent_op->IsRedirect()) {
1267 if (iter != m_op_map.end()) {
1268 OpRecord(*iter->second.first, OpKind::Error);
1270 m_op_map.erase(iter);
1271 running_handles -= 1;
1273 parent_op_failed =
true;
1275 OpRecord(*parent_op, OpKind::Start);
1278 OpRecord(*parent_op, OpKind::Start);
1280 if (!parent_op_failed){
1286 iter->second.first->ReleaseHandle();
1287 OpRecord(*(iter->second.first), OpKind::ConncallTimeout);
1288 m_op_map.erase(entry.second);
1289 curl_easy_cleanup(entry.second);
1290 running_handles -= 1;
1292 broker_reqs.erase(entry.first);
1293 m_conncall_timeout.fetch_add(1, std::memory_order_relaxed);
1301 waitfds.resize(3 + broker_reqs.size());
1303 waitfds[0].fd = queue.PollFD();
1304 waitfds[0].events = CURL_WAIT_POLLIN;
1305 waitfds[0].revents = 0;
1306 waitfds[1].fd = m_continue_queue->PollFD();
1307 waitfds[1].events = CURL_WAIT_POLLIN;
1308 waitfds[1].revents = 0;
1309 waitfds[2].fd = m_shutdown_pipe_r;
1310 waitfds[2].revents = 0;
1311 waitfds[2].events = CURL_WAIT_POLLIN | CURL_WAIT_POLLPRI;
1314 for (
const auto &entry : broker_reqs) {
1315 waitfds[idx].fd = entry.first;
1316 waitfds[idx].events = CURL_WAIT_POLLIN|CURL_WAIT_POLLPRI;
1317 waitfds[idx].revents = 0;
1322 curl_multi_timeout(multi_handle, &timeo);
1326 if (running_handles && timeo == -1) {
1331 mres = curl_multi_wait(multi_handle, &waitfds[0], waitfds.size(), 50,
nullptr);
1337 mres = curl_multi_wait(multi_handle, &waitfds[0], waitfds.size(), 50,
nullptr);
1339 if (mres != CURLM_OK) {
1340 m_logger->Warning(
kLogXrdClHttp,
"Failed to wait on multi-handle: %d", mres);
1344 for (
const auto &entry : waitfds) {
1346 if (waitfds[0].fd == entry.fd || waitfds[1].fd == entry.fd) {
1350 if ((waitfds[2].fd == entry.fd) && entry.revents) {
1351 want_shutdown =
true;
1354 if ((entry.revents & CURL_WAIT_POLLIN) != CURL_WAIT_POLLIN) {
1357 auto handle = broker_reqs[entry.fd].curl;
1358 auto iter = m_op_map.find(handle);
1359 if (iter == m_op_map.end()) {
1360 m_logger->Warning(
kLogXrdClHttp,
"Internal error: broker responded on FD %d but no corresponding curl operation", entry.fd);
1361 broker_reqs.erase(entry.fd);
1362 m_conncall_errors.fetch_add(1, std::memory_order_relaxed);
1366 auto result = iter->second.first->WaitSocketCallback(err);
1368 m_logger->Warning(
kLogXrdClHttp,
"Error when invoking the broker callback: %s", err.c_str());
1371 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(iter->second.first.get())) !=
nullptr) {
1373 bool parent_op_failed =
false;
1374 if (parent_op->IsRedirect()) {
1378 if (iter != m_op_map.end()) {
1379 OpRecord(*iter->second.first, OpKind::Error);
1381 m_op_map.erase(iter);
1382 running_handles -= 1;
1384 parent_op_failed =
true;
1386 OpRecord(*parent_op, OpKind::Start);
1389 OpRecord(*parent_op, OpKind::Start);
1391 if (!parent_op_failed){
1397 OpRecord(*iter->second.first, OpKind::Error);
1398 m_op_map.erase(handle);
1399 broker_reqs.erase(entry.fd);
1400 m_conncall_errors.fetch_add(1, std::memory_order_relaxed);
1401 running_handles -= 1;
1403 broker_reqs.erase(entry.fd);
1404 curl_multi_add_handle(multi_handle, handle);
1405 m_conncall_success.fetch_add(1, std::memory_order_relaxed);
1411 auto mres = curl_multi_perform(multi_handle, &still_running);
1412 if (mres == CURLM_CALL_MULTI_PERFORM) {
1414 }
else if (mres != CURLM_OK) {
1415 m_logger->Warning(
kLogXrdClHttp,
"Failed to perform multi-handle operation: %d", mres);
1422 msg = curl_multi_info_read(multi_handle, &msgq);
1423 if (msg && (msg->msg == CURLMSG_DONE)) {
1424 if (!msg->easy_handle) {
1425 m_logger->Warning(
kLogXrdClHttp,
"Logic error: got a callback for a null handle");
1426 mres = CURLM_BAD_EASY_HANDLE;
1429 auto iter = m_op_map.find(msg->easy_handle);
1430 if (iter == m_op_map.end()) {
1431 m_logger->Error(
kLogXrdClHttp,
"Logic error: got a callback for an entry that doesn't exist");
1432 mres = CURLM_BAD_EASY_HANDLE;
1435 auto op = iter->second.first;
1436 auto res = msg->data.result;
1437 bool keep_handle =
false;
1438 bool waiting_on_callout =
false;
1439 if (res == CURLE_OK) {
1441 OpRecord(*op, OpKind::Finish);
1450 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(op.get())) !=
nullptr) {
1452 bool parent_op_failed =
false;
1453 if (parent_op->IsRedirect()) {
1456 OpRecord(*parent_op, OpKind::Error);
1458 running_handles -= 1;
1459 parent_op_failed =
true;
1461 OpRecord(*parent_op, OpKind::Start);
1464 OpRecord(*parent_op, OpKind::Start);
1467 if (!parent_op_failed) {
1472 queue.RecycleHandle(iter->first);
1476 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(op.get()))) {
1482 OpRecord(*op, OpKind::Start);
1484 curl_multi_remove_handle(multi_handle, iter->first);
1485 queue.RecycleHandle(iter->first);
1500 OpRecord(*op, OpKind::Error);
1502 keep_handle =
false;
1510 OpRecord(*op, OpKind::Start);
1520 std::string modified_url;
1523 std::shared_ptr<CurlOperation> new_op(options_op);
1524 auto curl = queue.GetHandle();
1525 if (curl ==
nullptr) {
1526 m_logger->Debug(
kLogXrdClHttp,
"Unable to allocate a curl handle");
1528 keep_handle =
false;
1529 options_op =
nullptr;
1532 OpRecord(*new_op, OpKind::Start);
1534 auto rv = new_op->Setup(curl, *
this);
1536 m_logger->Debug(
kLogXrdClHttp,
"Unable to configure a curl handle for OPTIONS");
1537 keep_handle =
false;
1538 options_op =
nullptr;
1542 m_logger->Debug(
kLogXrdClHttp,
"Unable to setup the curl handle for the OPTIONS operation");
1543 new_op->Fail(
XrdCl::errInternal, ENOMEM,
"Failed to setup the curl handle for the OPTIONS operation");
1544 OpRecord(*new_op, OpKind::Error);
1545 keep_handle =
false;
1548 new_op->SetContinueQueue(m_continue_queue);
1549 m_op_map[curl] = {new_op, std::chrono::system_clock::now()};
1550 auto mres = curl_multi_add_handle(multi_handle, curl);
1551 if (mres != CURLM_OK) {
1552 m_logger->Debug(
kLogXrdClHttp,
"Unable to add OPTIONS operation to the curl multi-handle: %s", curl_multi_strerror(mres));
1554 OpRecord(*new_op, OpKind::Error);
1557 running_handles += 1;
1558 m_logger->Debug(
kLogXrdClHttp,
"Invoking the OPTIONS operation before redirect to %s", target.c_str());
1565 if ((waiting_on_callout = callout_socket >= 0)) {
1566 auto expiry = time(
nullptr) + 20;
1567 m_logger->Debug(
kLogXrdClHttp,
"Creating a callout wait request on socket %d", callout_socket);
1568 broker_reqs[callout_socket] = {iter->first, expiry};
1569 m_conncall_req.fetch_add(1, std::memory_order_relaxed);
1571 }
else if (options_op) {
1576 curl_multi_remove_handle(multi_handle, iter->first);
1577 if (!waiting_on_callout && !options_op) {
1578 curl_multi_add_handle(multi_handle, iter->first);
1580 }
else if (!options_op) {
1584 queue.RecycleHandle(iter->first);
1593 int wait_socket = -1;
1595 m_logger->Error(
kLogXrdClHttp,
"Failed to start broker-based connection: %s", err.c_str());
1597 keep_handle =
false;
1599 curl_multi_remove_handle(multi_handle, iter->first);
1600 auto expiry = time(
nullptr) + 20;
1601 m_logger->Debug(
kLogXrdClHttp,
"Curl operation requires a new TCP socket; waiting for callout to respond on socket %d", wait_socket);
1602 broker_reqs[wait_socket] = {iter->first, expiry};
1603 m_conncall_req.fetch_add(1, std::memory_order_relaxed);
1606 if (res == CURLE_ABORTED_BY_CALLBACK || res == CURLE_WRITE_ERROR) {
1611#ifdef HAVE_XPROTOCOL_TIMEREXPIRED
1616 OpRecord(*op, OpKind::Error);
1621 OpRecord(*op, OpKind::Error);
1626 OpRecord(*op, op->
IsPaused() ? OpKind::ClientTimeout : OpKind::ServerTimeout);
1630 OpRecord(*op, OpKind::ServerTimeout);
1634 OpRecord(*op, OpKind::ClientTimeout);
1638 OpRecord(*op, OpKind::ServerTimeout);
1642 OpRecord(*op, OpKind::Error);
1646 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(op.get())) !=
nullptr) {
1648 bool parent_op_failed =
false;
1649 if (parent_op->IsRedirect()) {
1653 if (iter != m_op_map.end()) {
1654 OpRecord(*iter->second.first, OpKind::Error);
1656 m_op_map.erase(iter);
1657 running_handles -= 1;
1659 parent_op_failed =
true;
1661 OpRecord(*parent_op, OpKind::Start);
1664 OpRecord(*parent_op, OpKind::Start);
1666 if (!parent_op_failed){
1673 const char *curl_easy_err = curl_easy_strerror(res);
1674 const std::string fail_err = !curl_err.empty() ? curl_err : curl_easy_err;
1675 m_logger->Debug(
kLogXrdClHttp,
"Curl generated an error: %s (%d)", fail_err.c_str(), res);
1676 op->
Fail(xrdCode.first, xrdCode.second, fail_err);
1677 OpRecord(*op, OpKind::Error);
1679 if ((options_op =
dynamic_cast<CurlOptionsOp*
>(op.get())) !=
nullptr) {
1681 bool parent_op_failed =
false;
1682 if (parent_op->IsRedirect()) {
1686 if (iter != m_op_map.end()) {
1687 OpRecord(*iter->second.first, OpKind::Error);
1689 m_op_map.erase(iter);
1690 running_handles -= 1;
1692 parent_op_failed =
true;
1695 if (!parent_op_failed){
1703 curl_multi_remove_handle(multi_handle, iter->first);
1704 if (res != CURLE_OK) {
1705 curl_easy_cleanup(iter->first);
1707 for (
auto &req : broker_reqs) {
1708 if (req.second.curl == iter->first) {
1709 m_logger->Warning(
kLogXrdClHttp,
"Curl handle finished while a broker operation was outstanding");
1710 m_conncall_errors.fetch_add(1, std::memory_order_relaxed);
1713 m_op_map.erase(iter);
1714 running_handles -= 1;
1720 for (
auto map_entry : m_op_map) {
1723 OpRecord(*map_entry.second.first, OpKind::Error);
1725 if (multi_handle && map_entry.first) curl_multi_remove_handle(multi_handle, map_entry.first);
1728 m_queue->ReleaseHandles();
1729 curl_multi_cleanup(multi_handle);