1 /* 2 // Copyright (c) 2020 Intel Corporation 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 */ 16 #pragma once 17 #include <boost/asio/ip/address.hpp> 18 #include <boost/asio/ip/basic_endpoint.hpp> 19 #include <boost/asio/steady_timer.hpp> 20 #include <boost/beast/core/flat_buffer.hpp> 21 #include <boost/beast/core/tcp_stream.hpp> 22 #include <boost/beast/http/message.hpp> 23 #include <boost/beast/version.hpp> 24 #include <boost/container/devector.hpp> 25 #include <include/async_resolve.hpp> 26 27 #include <cstdlib> 28 #include <functional> 29 #include <iostream> 30 #include <memory> 31 #include <queue> 32 #include <string> 33 34 namespace crow 35 { 36 37 // It is assumed that the BMC should be able to handle 4 parallel connections 38 constexpr uint8_t maxPoolSize = 4; 39 constexpr uint8_t maxRequestQueueSize = 50; 40 constexpr unsigned int httpReadBodyLimit = 8192; 41 42 enum class ConnState 43 { 44 initialized, 45 resolveInProgress, 46 resolveFailed, 47 connectInProgress, 48 connectFailed, 49 connected, 50 sendInProgress, 51 sendFailed, 52 recvInProgress, 53 recvFailed, 54 idle, 55 closeInProgress, 56 closed, 57 suspended, 58 terminated, 59 abortConnection, 60 retry 61 }; 62 63 // We need to allow retry information to be set before a message has been sent 64 // and a connection pool has been created 65 struct RetryPolicyData 66 { 67 uint32_t maxRetryAttempts = 5; 68 std::chrono::seconds retryIntervalSecs = std::chrono::seconds(0); 69 std::string retryPolicyAction = "TerminateAfterRetries"; 70 std::string name; 71 }; 72 73 struct PendingRequest 74 { 75 boost::beast::http::request<boost::beast::http::string_body> req; 76 std::function<void(bool, uint32_t, Response&)> callback; 77 RetryPolicyData retryPolicy; 78 PendingRequest( 79 boost::beast::http::request<boost::beast::http::string_body>&& req, 80 const std::function<void(bool, uint32_t, Response&)>& callback, 81 const RetryPolicyData& retryPolicy) : 82 req(std::move(req)), 83 callback(callback), retryPolicy(retryPolicy) 84 {} 85 }; 86 87 class ConnectionInfo : public std::enable_shared_from_this<ConnectionInfo> 88 { 89 private: 90 ConnState state = ConnState::initialized; 91 uint32_t retryCount = 0; 92 bool runningTimer = false; 93 std::string subId; 94 std::string host; 95 uint16_t port; 96 uint32_t connId; 97 98 // Retry policy information 99 // This should be updated before each message is sent 100 RetryPolicyData retryPolicy; 101 102 // Data buffers 103 boost::beast::http::request<boost::beast::http::string_body> req; 104 std::optional< 105 boost::beast::http::response_parser<boost::beast::http::string_body>> 106 parser; 107 boost::beast::flat_static_buffer<httpReadBodyLimit> buffer; 108 Response res; 109 110 // Ascync callables 111 std::function<void(bool, uint32_t, Response&)> callback; 112 crow::async_resolve::Resolver resolver; 113 boost::beast::tcp_stream conn; 114 boost::asio::steady_timer timer; 115 116 friend class ConnectionPool; 117 118 void doResolve() 119 { 120 state = ConnState::resolveInProgress; 121 BMCWEB_LOG_DEBUG << "Trying to resolve: " << host << ":" 122 << std::to_string(port) 123 << ", id: " << std::to_string(connId); 124 125 auto respHandler = 126 [self(shared_from_this())]( 127 const boost::beast::error_code ec, 128 const std::vector<boost::asio::ip::tcp::endpoint>& 129 endpointList) { 130 if (ec || (endpointList.empty())) 131 { 132 BMCWEB_LOG_ERROR << "Resolve failed: " << ec.message(); 133 self->state = ConnState::resolveFailed; 134 self->waitAndRetry(); 135 return; 136 } 137 BMCWEB_LOG_DEBUG << "Resolved " << self->host << ":" 138 << std::to_string(self->port) 139 << ", id: " << std::to_string(self->connId); 140 self->doConnect(endpointList); 141 }; 142 143 resolver.asyncResolve(host, port, std::move(respHandler)); 144 } 145 146 void doConnect( 147 const std::vector<boost::asio::ip::tcp::endpoint>& endpointList) 148 { 149 state = ConnState::connectInProgress; 150 151 BMCWEB_LOG_DEBUG << "Trying to connect to: " << host << ":" 152 << std::to_string(port) 153 << ", id: " << std::to_string(connId); 154 155 conn.expires_after(std::chrono::seconds(30)); 156 conn.async_connect(endpointList, 157 [self(shared_from_this())]( 158 const boost::beast::error_code ec, 159 const boost::asio::ip::tcp::endpoint& endpoint) { 160 if (ec) 161 { 162 BMCWEB_LOG_ERROR << "Connect " << endpoint.address().to_string() 163 << ":" << std::to_string(endpoint.port()) 164 << ", id: " << std::to_string(self->connId) 165 << " failed: " << ec.message(); 166 self->state = ConnState::connectFailed; 167 self->waitAndRetry(); 168 return; 169 } 170 BMCWEB_LOG_DEBUG 171 << "Connected to: " << endpoint.address().to_string() << ":" 172 << std::to_string(endpoint.port()) 173 << ", id: " << std::to_string(self->connId); 174 self->state = ConnState::connected; 175 self->sendMessage(); 176 }); 177 } 178 179 void sendMessage() 180 { 181 state = ConnState::sendInProgress; 182 183 // Set a timeout on the operation 184 conn.expires_after(std::chrono::seconds(30)); 185 186 // Send the HTTP request to the remote host 187 boost::beast::http::async_write( 188 conn, req, 189 [self(shared_from_this())](const boost::beast::error_code& ec, 190 const std::size_t& bytesTransferred) { 191 if (ec) 192 { 193 BMCWEB_LOG_ERROR << "sendMessage() failed: " << ec.message(); 194 self->state = ConnState::sendFailed; 195 self->waitAndRetry(); 196 return; 197 } 198 BMCWEB_LOG_DEBUG << "sendMessage() bytes transferred: " 199 << bytesTransferred; 200 boost::ignore_unused(bytesTransferred); 201 202 self->recvMessage(); 203 }); 204 } 205 206 void recvMessage() 207 { 208 state = ConnState::recvInProgress; 209 210 parser.emplace(std::piecewise_construct, std::make_tuple()); 211 parser->body_limit(httpReadBodyLimit); 212 213 // Receive the HTTP response 214 boost::beast::http::async_read( 215 conn, buffer, *parser, 216 [self(shared_from_this())](const boost::beast::error_code& ec, 217 const std::size_t& bytesTransferred) { 218 if (ec) 219 { 220 BMCWEB_LOG_ERROR << "recvMessage() failed: " << ec.message(); 221 self->state = ConnState::recvFailed; 222 self->waitAndRetry(); 223 return; 224 } 225 BMCWEB_LOG_DEBUG << "recvMessage() bytes transferred: " 226 << bytesTransferred; 227 BMCWEB_LOG_DEBUG << "recvMessage() data: " 228 << self->parser->get().body(); 229 230 unsigned int respCode = self->parser->get().result_int(); 231 BMCWEB_LOG_DEBUG << "recvMessage() Header Response Code: " 232 << respCode; 233 234 // 2XX response is considered to be successful 235 if ((respCode < 200) || (respCode >= 300)) 236 { 237 // The listener failed to receive the Sent-Event 238 BMCWEB_LOG_ERROR << "recvMessage() Listener Failed to " 239 "receive Sent-Event. Header Response Code: " 240 << respCode; 241 self->state = ConnState::recvFailed; 242 self->waitAndRetry(); 243 return; 244 } 245 246 // Send is successful 247 // Reset the counter just in case this was after retrying 248 self->retryCount = 0; 249 250 // Keep the connection alive if server supports it 251 // Else close the connection 252 BMCWEB_LOG_DEBUG << "recvMessage() keepalive : " 253 << self->parser->keep_alive(); 254 255 // Copy the response into a Response object so that it can be 256 // processed by the callback function. 257 self->res.clear(); 258 self->res.stringResponse = self->parser->release(); 259 self->callback(self->parser->keep_alive(), self->connId, self->res); 260 }); 261 } 262 263 void waitAndRetry() 264 { 265 if (retryCount >= retryPolicy.maxRetryAttempts) 266 { 267 BMCWEB_LOG_ERROR << "Maximum number of retries reached."; 268 BMCWEB_LOG_DEBUG << "Retry policy: " 269 << retryPolicy.retryPolicyAction; 270 271 // We want to return a 502 to indicate there was an error with the 272 // external server 273 res.clear(); 274 redfish::messages::operationFailed(res); 275 276 if (retryPolicy.retryPolicyAction == "TerminateAfterRetries") 277 { 278 // TODO: delete subscription 279 state = ConnState::terminated; 280 callback(false, connId, res); 281 } 282 if (retryPolicy.retryPolicyAction == "SuspendRetries") 283 { 284 state = ConnState::suspended; 285 callback(false, connId, res); 286 } 287 // Reset the retrycount to zero so that client can try connecting 288 // again if needed 289 retryCount = 0; 290 return; 291 } 292 293 if (runningTimer) 294 { 295 BMCWEB_LOG_DEBUG << "Retry timer is already running."; 296 return; 297 } 298 runningTimer = true; 299 300 retryCount++; 301 302 BMCWEB_LOG_DEBUG << "Attempt retry after " 303 << std::to_string( 304 retryPolicy.retryIntervalSecs.count()) 305 << " seconds. RetryCount = " << retryCount; 306 timer.expires_after(retryPolicy.retryIntervalSecs); 307 timer.async_wait( 308 [self(shared_from_this())](const boost::system::error_code ec) { 309 if (ec == boost::asio::error::operation_aborted) 310 { 311 BMCWEB_LOG_DEBUG 312 << "async_wait failed since the operation is aborted" 313 << ec.message(); 314 } 315 else if (ec) 316 { 317 BMCWEB_LOG_ERROR << "async_wait failed: " << ec.message(); 318 // Ignore the error and continue the retry loop to attempt 319 // sending the event as per the retry policy 320 } 321 self->runningTimer = false; 322 323 // Let's close the connection and restart from resolve. 324 self->doCloseAndRetry(); 325 }); 326 } 327 328 void doClose() 329 { 330 state = ConnState::closeInProgress; 331 boost::beast::error_code ec; 332 conn.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 333 conn.close(); 334 335 // not_connected happens sometimes so don't bother reporting it. 336 if (ec && ec != boost::beast::errc::not_connected) 337 { 338 BMCWEB_LOG_ERROR << host << ":" << std::to_string(port) 339 << ", id: " << std::to_string(connId) 340 << "shutdown failed: " << ec.message(); 341 return; 342 } 343 BMCWEB_LOG_DEBUG << host << ":" << std::to_string(port) 344 << ", id: " << std::to_string(connId) 345 << " closed gracefully"; 346 if ((state != ConnState::suspended) && (state != ConnState::terminated)) 347 { 348 state = ConnState::closed; 349 } 350 } 351 352 void doCloseAndRetry() 353 { 354 state = ConnState::closeInProgress; 355 boost::beast::error_code ec; 356 conn.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 357 conn.close(); 358 359 // not_connected happens sometimes so don't bother reporting it. 360 if (ec && ec != boost::beast::errc::not_connected) 361 { 362 BMCWEB_LOG_ERROR << host << ":" << std::to_string(port) 363 << ", id: " << std::to_string(connId) 364 << "shutdown failed: " << ec.message(); 365 return; 366 } 367 BMCWEB_LOG_DEBUG << host << ":" << std::to_string(port) 368 << ", id: " << std::to_string(connId) 369 << " closed gracefully"; 370 if ((state != ConnState::suspended) && (state != ConnState::terminated)) 371 { 372 // Now let's try to resend the data 373 state = ConnState::retry; 374 this->doResolve(); 375 } 376 } 377 378 public: 379 explicit ConnectionInfo(boost::asio::io_context& ioc, const std::string& id, 380 const std::string& destIP, const uint16_t destPort, 381 const unsigned int connId) : 382 subId(id), 383 host(destIP), port(destPort), connId(connId), conn(ioc), timer(ioc) 384 {} 385 }; 386 387 class ConnectionPool : public std::enable_shared_from_this<ConnectionPool> 388 { 389 private: 390 boost::asio::io_context& ioc; 391 const std::string id; 392 const std::string destIP; 393 const uint16_t destPort; 394 std::vector<std::shared_ptr<ConnectionInfo>> connections; 395 boost::container::devector<PendingRequest> requestQueue; 396 397 friend class HttpClient; 398 399 // Configure a connections's request, callback, and retry info in 400 // preparation to begin sending the request 401 void setConnProps(ConnectionInfo& conn) 402 { 403 if (requestQueue.empty()) 404 { 405 BMCWEB_LOG_ERROR 406 << "setConnProps() should not have been called when requestQueue is empty"; 407 return; 408 } 409 410 auto nextReq = requestQueue.front(); 411 conn.retryPolicy = std::move(nextReq.retryPolicy); 412 conn.req = std::move(nextReq.req); 413 conn.callback = std::move(nextReq.callback); 414 415 BMCWEB_LOG_DEBUG << "Setting properties for connection " << conn.host 416 << ":" << std::to_string(conn.port) 417 << ", id: " << std::to_string(conn.connId) 418 << ", retry policy is \"" << conn.retryPolicy.name 419 << "\""; 420 421 // We can remove the request from the queue at this point 422 requestQueue.pop_front(); 423 } 424 425 // Configures a connection to use the specific retry policy. 426 inline void setConnRetryPolicy(ConnectionInfo& conn, 427 const RetryPolicyData& retryPolicy) 428 { 429 BMCWEB_LOG_DEBUG << destIP << ":" << std::to_string(destPort) 430 << ", id: " << std::to_string(conn.connId) 431 << " using retry policy \"" << retryPolicy.name 432 << "\""; 433 434 conn.retryPolicy = retryPolicy; 435 } 436 437 // Gets called as part of callback after request is sent 438 // Reuses the connection if there are any requests waiting to be sent 439 // Otherwise closes the connection if it is not a keep-alive 440 void sendNext(bool keepAlive, uint32_t connId) 441 { 442 auto conn = connections[connId]; 443 // Reuse the connection to send the next request in the queue 444 if (!requestQueue.empty()) 445 { 446 BMCWEB_LOG_DEBUG << std::to_string(requestQueue.size()) 447 << " requests remaining in queue for " << destIP 448 << ":" << std::to_string(destPort) 449 << ", reusing connnection " 450 << std::to_string(connId); 451 452 setConnProps(*conn); 453 454 if (keepAlive) 455 { 456 conn->sendMessage(); 457 } 458 else 459 { 460 // Server is not keep-alive enabled so we need to close the 461 // connection and then start over from resolve 462 conn->doClose(); 463 conn->doResolve(); 464 } 465 return; 466 } 467 468 // No more messages to send so close the connection if necessary 469 if (keepAlive) 470 { 471 conn->state = ConnState::idle; 472 } 473 else 474 { 475 // Abort the connection since server is not keep-alive enabled 476 conn->state = ConnState::abortConnection; 477 conn->doClose(); 478 } 479 } 480 481 void sendData(std::string& data, const std::string& destUri, 482 const boost::beast::http::fields& httpHeader, 483 const boost::beast::http::verb verb, 484 const RetryPolicyData& retryPolicy, 485 std::function<void(Response&)>& resHandler) 486 { 487 std::weak_ptr<ConnectionPool> weakSelf = weak_from_this(); 488 489 // Callback to be called once the request has been sent 490 auto cb = [weakSelf, resHandler](bool keepAlive, uint32_t connId, 491 Response& res) { 492 // Allow provided callback to perform additional processing of the 493 // request 494 resHandler(res); 495 496 // If requests remain in the queue then we want to reuse this 497 // connection to send the next request 498 std::shared_ptr<ConnectionPool> self = weakSelf.lock(); 499 if (!self) 500 { 501 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection"; 502 return; 503 } 504 505 self->sendNext(keepAlive, connId); 506 }; 507 508 // Construct the request to be sent 509 boost::beast::http::request<boost::beast::http::string_body> thisReq( 510 verb, destUri, 11, "", httpHeader); 511 thisReq.set(boost::beast::http::field::host, destIP); 512 thisReq.keep_alive(true); 513 thisReq.body() = std::move(data); 514 thisReq.prepare_payload(); 515 516 // Reuse an existing connection if one is available 517 for (unsigned int i = 0; i < connections.size(); i++) 518 { 519 auto conn = connections[i]; 520 if ((conn->state == ConnState::idle) || 521 (conn->state == ConnState::initialized) || 522 (conn->state == ConnState::closed)) 523 { 524 conn->req = std::move(thisReq); 525 conn->callback = std::move(cb); 526 setConnRetryPolicy(*conn, retryPolicy); 527 std::string commonMsg = std::to_string(i) + " from pool " + 528 destIP + ":" + std::to_string(destPort); 529 530 if (conn->state == ConnState::idle) 531 { 532 BMCWEB_LOG_DEBUG << "Grabbing idle connection " 533 << commonMsg; 534 conn->sendMessage(); 535 } 536 else 537 { 538 BMCWEB_LOG_DEBUG << "Reusing existing connection " 539 << commonMsg; 540 conn->doResolve(); 541 } 542 return; 543 } 544 } 545 546 // All connections in use so create a new connection or add request to 547 // the queue 548 if (connections.size() < maxPoolSize) 549 { 550 BMCWEB_LOG_DEBUG << "Adding new connection to pool " << destIP 551 << ":" << std::to_string(destPort); 552 auto conn = addConnection(); 553 conn->req = std::move(thisReq); 554 conn->callback = std::move(cb); 555 setConnRetryPolicy(*conn, retryPolicy); 556 conn->doResolve(); 557 } 558 else if (requestQueue.size() < maxRequestQueueSize) 559 { 560 BMCWEB_LOG_ERROR << "Max pool size reached. Adding data to queue."; 561 requestQueue.emplace_back(std::move(thisReq), std::move(cb), 562 retryPolicy); 563 } 564 else 565 { 566 BMCWEB_LOG_ERROR << destIP << ":" << std::to_string(destPort) 567 << " request queue full. Dropping request."; 568 } 569 } 570 571 std::shared_ptr<ConnectionInfo>& addConnection() 572 { 573 unsigned int newId = static_cast<unsigned int>(connections.size()); 574 575 auto& ret = connections.emplace_back( 576 std::make_shared<ConnectionInfo>(ioc, id, destIP, destPort, newId)); 577 578 BMCWEB_LOG_DEBUG << "Added connection " 579 << std::to_string(connections.size() - 1) 580 << " to pool " << destIP << ":" 581 << std::to_string(destPort); 582 583 return ret; 584 } 585 586 public: 587 explicit ConnectionPool(boost::asio::io_context& ioc, const std::string& id, 588 const std::string& destIP, 589 const uint16_t destPort) : 590 ioc(ioc), 591 id(id), destIP(destIP), destPort(destPort) 592 { 593 std::string clientKey = destIP + ":" + std::to_string(destPort); 594 BMCWEB_LOG_DEBUG << "Initializing connection pool for " << destIP << ":" 595 << std::to_string(destPort); 596 597 // Initialize the pool with a single connection 598 addConnection(); 599 } 600 }; 601 602 class HttpClient 603 { 604 private: 605 std::unordered_map<std::string, std::shared_ptr<ConnectionPool>> 606 connectionPools; 607 boost::asio::io_context& ioc = 608 crow::connections::systemBus->get_io_context(); 609 std::unordered_map<std::string, RetryPolicyData> retryInfo; 610 HttpClient() = default; 611 612 // Used as a dummy callback by sendData() in order to call 613 // sendDataWithCallback() 614 static void genericResHandler(Response& res) 615 { 616 BMCWEB_LOG_DEBUG << "Response handled with return code: " 617 << std::to_string(res.resultInt()); 618 }; 619 620 public: 621 HttpClient(const HttpClient&) = delete; 622 HttpClient& operator=(const HttpClient&) = delete; 623 HttpClient(HttpClient&&) = delete; 624 HttpClient& operator=(HttpClient&&) = delete; 625 ~HttpClient() = default; 626 627 static HttpClient& getInstance() 628 { 629 static HttpClient handler; 630 return handler; 631 } 632 633 // Send a request to destIP:destPort where additional processing of the 634 // result is not required 635 void sendData(std::string& data, const std::string& id, 636 const std::string& destIP, const uint16_t destPort, 637 const std::string& destUri, 638 const boost::beast::http::fields& httpHeader, 639 const boost::beast::http::verb verb, 640 const std::string& retryPolicyName) 641 { 642 std::function<void(Response&)> cb = genericResHandler; 643 sendDataWithCallback(data, id, destIP, destPort, destUri, httpHeader, 644 verb, retryPolicyName, cb); 645 } 646 647 // Send request to destIP:destPort and use the provided callback to 648 // handle the response 649 void sendDataWithCallback(std::string& data, const std::string& id, 650 const std::string& destIP, 651 const uint16_t destPort, 652 const std::string& destUri, 653 const boost::beast::http::fields& httpHeader, 654 const boost::beast::http::verb verb, 655 const std::string& retryPolicyName, 656 std::function<void(Response&)>& resHandler) 657 { 658 std::string clientKey = destIP + ":" + std::to_string(destPort); 659 // Use nullptr to avoid creating a ConnectionPool each time 660 auto result = connectionPools.try_emplace(clientKey, nullptr); 661 if (result.second) 662 { 663 // Now actually create the ConnectionPool shared_ptr since it does 664 // not already exist 665 result.first->second = 666 std::make_shared<ConnectionPool>(ioc, id, destIP, destPort); 667 BMCWEB_LOG_DEBUG << "Created connection pool for " << clientKey; 668 } 669 else 670 { 671 BMCWEB_LOG_DEBUG << "Using existing connection pool for " 672 << clientKey; 673 } 674 675 // Get the associated retry policy 676 auto policy = retryInfo.try_emplace(retryPolicyName); 677 if (policy.second) 678 { 679 BMCWEB_LOG_DEBUG << "Creating retry policy \"" << retryPolicyName 680 << "\" with default values"; 681 policy.first->second.name = retryPolicyName; 682 } 683 684 // Send the data using either the existing connection pool or the newly 685 // created connection pool 686 result.first->second->sendData(data, destUri, httpHeader, verb, 687 policy.first->second, resHandler); 688 } 689 690 void setRetryConfig(const uint32_t retryAttempts, 691 const uint32_t retryTimeoutInterval, 692 const std::string& retryPolicyName) 693 { 694 // We need to create the retry policy if one does not already exist for 695 // the given retryPolicyName 696 auto result = retryInfo.try_emplace(retryPolicyName); 697 if (result.second) 698 { 699 BMCWEB_LOG_DEBUG << "setRetryConfig(): Creating new retry policy \"" 700 << retryPolicyName << "\""; 701 result.first->second.name = retryPolicyName; 702 } 703 else 704 { 705 BMCWEB_LOG_DEBUG << "setRetryConfig(): Updating retry info for \"" 706 << retryPolicyName << "\""; 707 } 708 709 result.first->second.maxRetryAttempts = retryAttempts; 710 result.first->second.retryIntervalSecs = 711 std::chrono::seconds(retryTimeoutInterval); 712 } 713 714 void setRetryPolicy(const std::string& retryPolicy, 715 const std::string& retryPolicyName) 716 { 717 // We need to create the retry policy if one does not already exist for 718 // the given retryPolicyName 719 auto result = retryInfo.try_emplace(retryPolicyName); 720 if (result.second) 721 { 722 BMCWEB_LOG_DEBUG << "setRetryPolicy(): Creating new retry policy \"" 723 << retryPolicyName << "\""; 724 result.first->second.name = retryPolicyName; 725 } 726 else 727 { 728 BMCWEB_LOG_DEBUG << "setRetryPolicy(): Updating retry policy for \"" 729 << retryPolicyName << "\""; 730 } 731 732 result.first->second.retryPolicyAction = retryPolicy; 733 } 734 }; 735 } // namespace crow 736