algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
http_client.hpp
1#pragma once
2
8#define ATL_TRANSPORT_LAYER_PRESENT 1
9
10#include "./rate_limit.hpp"
11
12#include <algotradelib/core>
13
14#include <boost/asio/any_completion_handler.hpp>
15#include <boost/asio/async_result.hpp>
16#include <boost/asio/associated_executor.hpp>
17#include <boost/asio/dispatch.hpp>
18#include <boost/asio/ip/tcp.hpp>
19#include <boost/asio/post.hpp>
20#include <boost/asio/ssl.hpp>
21#include <boost/asio/strand.hpp>
22#include <boost/beast/core.hpp>
23#include <boost/beast/http.hpp>
24#include <boost/beast/ssl.hpp>
25
26#include <chrono>
27#include <cstdint>
28#include <deque>
29#include <map>
30#include <memory>
31#include <optional>
32#include <string>
33#include <utility>
34#include <vector>
35
36namespace algotradelib {
37
38namespace beast = boost::beast;
39namespace http = boost::beast::http;
40namespace net = boost::asio;
41namespace ssl = net::ssl;
43using tcp = net::ip::tcp;
44
45namespace network {
46
48using HttpResponse = http::response<http::string_body>;
49
58 http::verb method_{http::verb::get};
60 std::string target_{"/"};
62 std::string body_;
64 std::vector<std::pair<std::string, std::string>> headers_;
66 int version_{11};
74};
75
82 std::chrono::seconds timeout_{30};
84 std::size_t body_limit_{8 * 1024 * 1024};
89 std::size_t queue_limit_{1024};
90
95 std::optional<RateLimitBudget> rate_limit_;
96
107 std::chrono::milliseconds unreplayed_idle_limit_{1000};
108};
109
117inline BrokerError transport_error(std::string stage, beast::error_code ec) {
118 BrokerError error;
119 error.category_ = ec == net::error::operation_aborted ?
122 error.broker_code_ = std::move(stage);
123 error.message_ = ec.message();
124 return error;
125}
126
133inline std::string make_host_header(const std::string& host, const std::string& port) {
134 return port == "443" ? host : host + ":" + port;
135}
136
146inline void shutdown_ssl_stream(beast::ssl_stream<beast::tcp_stream>& stream, std::chrono::seconds timeout) noexcept {
147 beast::error_code ignored;
148 beast::get_lowest_layer(stream).expires_after(timeout);
149 stream.shutdown(ignored);
150 beast::get_lowest_layer(stream).socket().shutdown(tcp::socket::shutdown_both, ignored);
151 beast::get_lowest_layer(stream).socket().close(ignored);
152}
153
159inline void close_ssl_stream(beast::ssl_stream<beast::tcp_stream>& stream) noexcept {
160 beast::error_code ignored;
161 beast::get_lowest_layer(stream).cancel();
162 beast::get_lowest_layer(stream).socket().shutdown(tcp::socket::shutdown_both, ignored);
163 beast::get_lowest_layer(stream).socket().close(ignored);
164}
165
173public:
185 net::io_context& ioc,
186 ssl::context& context,
187 std::string host,
188 std::string port,
189 HttpClientOptions options = {}
190 )
191 : resolver_(ioc), context_(context), host_(std::move(host)), port_(std::move(port)),
192 options_(options) {}
193
202 const std::string& target,
203 const std::map<std::string, std::string>& headers = {}
204 ) {
205 return request(http::verb::get, target, "", headers);
206 }
207
217 const std::string& target,
218 const std::string& body,
219 const std::map<std::string, std::string>& headers = {}
220 ) {
221 return request(http::verb::post, target, body, headers);
222 }
223
234 http::verb method,
235 const std::string& target,
236 const std::string& body = {},
237 const std::map<std::string, std::string>& headers = {}
238 ) {
239 for (int attempt = 0;; ++attempt) {
240 beast::error_code error;
241 // Never replayed, so never written into a connection the server may have closed while it was
242 // idle; see `HttpClientOptions::unreplayed_idle_limit_`.
243 if (method != http::verb::get && idle_since_ &&
244 std::chrono::steady_clock::now() - *idle_since_ > options_.unreplayed_idle_limit_) {
245 // Dropped without waiting for the peer's close notification: a synchronous TLS shutdown is not
246 // bounded by the stream's deadline, and a live server may never answer one.
247 close_ssl_stream(*stream_);
248 stream_.reset();
249 buffer_.consume(buffer_.size());
250 idle_since_.reset();
251 }
252 ensure_connected(error);
253 if (!error) {
254 http::request<http::string_body> request_value{method, target, 11};
255 request_value.set(http::field::host, host_header());
256 request_value.set(http::field::user_agent, "algotradelib");
257 request_value.keep_alive(true);
258 for (const auto& header : headers) {
259 request_value.set(header.first, header.second);
260 }
261 request_value.body() = body;
262 if (!body.empty()) {
263 request_value.prepare_payload();
264 }
265
266 beast::get_lowest_layer(*stream_).expires_after(options_.timeout_);
267 http::write(*stream_, request_value, error);
268 }
269
270 http::response_parser<http::string_body> parser;
271 if (!error) {
272 parser.body_limit(options_.body_limit_);
273 http::read(*stream_, buffer_, parser, error);
274 }
275
276 if (!error) {
277 auto response = parser.release();
278 if (!response.keep_alive() || response.need_eof()) {
279 disconnect();
280 } else {
281 idle_since_ = std::chrono::steady_clock::now();
282 }
283 return response;
284 }
285
286 disconnect();
287 if (method != http::verb::get || attempt > 0) {
288 throw beast::system_error(error);
289 }
290 }
291 }
292
294 void close() { disconnect(); }
295
296private:
297 tcp::resolver resolver_;
298 ssl::context& context_;
299 std::string host_;
300 std::string port_;
301 HttpClientOptions options_;
302 std::unique_ptr<beast::ssl_stream<beast::tcp_stream>> stream_;
303 beast::flat_buffer buffer_;
304 // When the connection last finished a response and was kept alive; empty while there is none.
305 std::optional<std::chrono::steady_clock::time_point> idle_since_;
306
307 void ensure_connected(beast::error_code& error) {
308 if (stream_ && beast::get_lowest_layer(*stream_).socket().is_open()) {
309 return;
310 }
311 stream_ = std::make_unique<beast::ssl_stream<beast::tcp_stream>>(resolver_.get_executor(), context_);
312 if (!SSL_set_tlsext_host_name(stream_->native_handle(), host_.c_str())) {
313 error = beast::error_code(
314 static_cast<int>(::ERR_get_error()), net::error::get_ssl_category()
315 );
316 return;
317 }
318 stream_->set_verify_callback(ssl::host_name_verification(host_));
319 const auto endpoints = resolver_.resolve(host_, port_, error);
320 if (error) {
321 return;
322 }
323 beast::get_lowest_layer(*stream_).expires_after(options_.timeout_);
324 beast::get_lowest_layer(*stream_).connect(endpoints, error);
325 if (error) {
326 return;
327 }
328 beast::get_lowest_layer(*stream_).expires_after(options_.timeout_);
329 stream_->handshake(ssl::stream_base::client, error);
330 }
331
332 std::string host_header() const { return make_host_header(host_, port_); }
333
334 void disconnect() noexcept {
335 if (!stream_) {
336 return;
337 }
338 shutdown_ssl_stream(*stream_, std::chrono::seconds(1));
339 stream_.reset();
340 buffer_.consume(buffer_.size());
341 idle_since_.reset();
342 }
343};
344
367class AsyncHttpClient : public std::enable_shared_from_this<AsyncHttpClient> {
368public:
370 using CompletionHandler = net::any_completion_handler<void(Result<HttpResponse>)>;
371
384 net::any_io_executor executor,
385 ssl::context& context,
386 std::string host,
387 std::string port,
388 HttpClientOptions options = {}
389 )
390 : strand_(net::make_strand(std::move(executor))), resolver_(strand_), resolve_timer_(strand_),
391 admission_timer_(strand_), context_(context), host_(std::move(host)),
392 port_(std::move(port)), options_(options) {
393 if (options_.rate_limit_) {
394 limiter_.emplace(*options_.rate_limit_);
395 }
396 }
397
407 return limiter_ ? &*limiter_ : nullptr;
408 }
409
417 const RateLimitController* rate_limits() const noexcept {
418 return limiter_ ? &*limiter_ : nullptr;
419 }
420
430 std::uint64_t admission_waits() const noexcept { return admission_timer_.arms(); }
431
445 template <typename CompletionToken>
446 auto async_request(HttpRequest request_value, CompletionToken&& token) {
447 return net::async_initiate<CompletionToken, void(Result<HttpResponse>)>(
448 [self = shared_from_this(),
449 request_value = std::move(request_value)](auto&& handler) mutable {
450 Pending pending(
451 std::move(request_value), CompletionHandler(std::forward<decltype(handler)>(handler))
452 );
453 net::post(self->strand_, [self, pending = std::move(pending)]() mutable {
454 self->enqueue(std::move(pending));
455 });
456 },
457 token
458 );
459 }
460
465 void close() {
466 net::post(strand_, [self = shared_from_this()] {
467 self->stopped_ = true;
468 self->resolver_.cancel();
469 self->resolve_timer_.cancel();
470 self->admission_timer_.cancel();
471 self->disconnect();
472 BrokerError error;
474 error.broker_code_ = "close";
475 error.message_ = "HTTP client closed";
476 // Each entry answers for itself, which is why `sent_` lives on the request rather than being
477 // inferred from the connection: a caller has to be able to tell an order that provably does
478 // not exist -- no reconciliation owed, no request spent -- from one that may be on the wire.
479 // Reading the client's state here would answer for none of them, since `disconnect()` has
480 // just cleared it.
481 while (!self->queue_.empty()) {
482 auto pending = std::move(self->queue_.front());
483 self->queue_.pop_front();
484 BrokerError reported = error;
485 reported.reached_broker_ = pending.sent_;
486 self->complete(std::move(pending.handler_), Result<HttpResponse>(std::move(reported)));
487 }
488 });
489 }
490
491private:
492 enum class State {
493 DISCONNECTED,
494 RESOLVING,
495 CONNECTING,
496 HANDSHAKING,
497 READY,
498 WRITING,
499 READING,
500 };
501
502 struct Pending {
503 HttpRequest request_;
504 CompletionHandler handler_;
505 bool retried_{false};
506 // Whether this request already went to a fresh connection because the last one sat idle. Once is
507 // enough, and it bounds the rule: a fresh connection starts its own idle clock, which with a limit
508 // of zero or below would otherwise send it straight back for another.
509 bool refreshed_{false};
510 // Whether any attempt at this request has begun writing it to the socket.
511 //
512 // The honest record of what a caller may be told about the broker having seen it, and
513 // deliberately not `retried_`, which counts a replay rather than a send: a GET that fails to
514 // *connect* is retried without a byte having left, and reading the retry as evidence would
515 // report an unsent request as one the broker may hold. Sticky once set, because a second
516 // attempt cannot unsend the first.
517 bool sent_{false};
518 // When this request began waiting for rate-limit admission; empty while it is not waiting. Kept
519 // on the request, not the client: a request inserted ahead of a waiting one is admitted on its
520 // own terms, and must neither be charged the other's wait nor restart it.
521 std::optional<std::chrono::steady_clock::time_point> waiting_since_;
522
523 Pending(HttpRequest request_value, CompletionHandler completion)
524 : request_(std::move(request_value)), handler_(std::move(completion)) {}
525 };
526
527 net::strand<net::any_io_executor> strand_;
528 tcp::resolver resolver_;
529 net::steady_timer resolve_timer_;
530 // The admission timer, wrapped so that arming it is counted by construction.
531 //
532 // **A counter beside the timer is not enough, and that is not hypothetical.** The first version of
533 // this observable incremented a member at the one call site that armed the timer, on the paced
534 // branch. `pump()` returns before that site when there is no budget, so a regression arming this
535 // timer on the *unpaced* branch would have delayed every request on an unmetered API while the
536 // counter still read zero -- and the test asserting zero would have passed.
537 //
538 // Making the count a property of the timer removes the bypass rather than documenting it: there
539 // is no way to arm *this* timer without `expires_after`, so there is no way to wait on it without
540 // saying so. What that does not cover is a delay built on some other timer; see
541 // `admission_waits()`.
542 //
543 // **A narrow wrapper for two call sites, not a general Asio timer substitute.** It discards the
544 // completion-token return values its callers do not use, and it counts an expiry reset even when
545 // no wait follows -- which is exact for the one place that arms this timer and immediately waits,
546 // and would need revisiting for any other use. Changing an expiry cancels a pending wait, whose
547 // handler then receives `operation_aborted`; this client's handler already returns on any error.
548 class CountedTimer {
549 public:
550 explicit CountedTimer(net::strand<net::any_io_executor>& strand) : timer_(strand) {}
551
552 // Arms the timer for `delay`, and counts the arm.
553 template <typename Rep, typename Period>
554 void expires_after(const std::chrono::duration<Rep, Period>& delay) {
555 timer_.expires_after(delay);
556 // Counted after the call, so this records an arm that happened rather than one attempted.
557 ++arms_;
558 }
559
560 // Waits, forwarding to the underlying timer.
561 template <typename Handler>
562 void async_wait(Handler&& handler) {
563 timer_.async_wait(std::forward<Handler>(handler));
564 }
565
566 // Cancels any outstanding wait. Not counted: a cancellation is not an admission wait.
567 std::size_t cancel() { return timer_.cancel(); }
568
569 // Returns how many times this timer has been armed.
570 std::uint64_t arms() const noexcept { return arms_; }
571
572 private:
573 net::steady_timer timer_;
574 std::uint64_t arms_{0};
575 };
576
577 CountedTimer admission_timer_;
578 ssl::context& context_;
579 std::unique_ptr<beast::ssl_stream<beast::tcp_stream>> stream_;
580 beast::flat_buffer buffer_;
581 std::string host_;
582 std::string port_;
583 HttpClientOptions options_;
584 State state_{State::DISCONNECTED};
585 bool stopped_{false};
586 bool resolve_timed_out_{false};
587 std::deque<Pending> queue_;
588 std::optional<RateLimitController> limiter_;
589 std::optional<http::request<http::string_body>> active_request_;
590 std::optional<http::response_parser<http::string_body>> parser_;
591 // When the connection last finished a response and was kept alive; empty while there is none.
592 std::optional<std::chrono::steady_clock::time_point> idle_since_;
593
594 void enqueue(Pending pending) {
595 if (stopped_) {
596 BrokerError error;
597 error.category_ = ErrorCategory::CANCELLED;
598 // Never queued, let alone sent: the instruction reached nobody.
599 error.reached_broker_ = false;
600 error.broker_code_ = "enqueue";
601 error.message_ = "HTTP client is closed";
602 complete(std::move(pending.handler_), Result<HttpResponse>(std::move(error)));
603 return;
604 }
605 if (queue_.size() >= options_.queue_limit_) {
606 if (limiter_) {
607 limiter_->record_shed(pending.request_.group_);
608 }
609 BrokerError error;
610 error.category_ = ErrorCategory::TRANSPORT;
611 // Refused at admission, so nothing was sent. `TRANSPORT` would otherwise read as a request
612 // that may be on the wire, and a runner would withhold the instrument to reconcile an order
613 // that was never built.
614 error.reached_broker_ = false;
615 error.broker_code_ = "queue_full";
616 error.message_ = "HTTP request queue limit exceeded";
617 complete(std::move(pending.handler_), Result<HttpResponse>(std::move(error)));
618 return;
619 }
620 queue_.insert(insertion_point(pending.request_.group_), std::move(pending));
621 pump();
622 }
623
624 // Returns where a request of `group` belongs in the pending queue.
625 //
626 // Higher priority is served first, and equal priority keeps arrival order, so
627 // a queue carrying one traffic class behaves exactly as a FIFO queue does.
628 //
629 // The front request is skipped while it is on the wire. Reordering around an
630 // in-flight request would detach it from the response being read for it, which
631 // is the one thing priority must never be allowed to do.
632 std::deque<Pending>::iterator insertion_point(RateLimitGroup group) {
633 auto position = queue_.begin();
634 if (front_in_flight() && position != queue_.end()) {
635 ++position;
636 }
637 const int priority = rate_limit_priority(group);
638 while (position != queue_.end() &&
639 rate_limit_priority(position->request_.group_) >= priority) {
640 ++position;
641 }
642 return position;
643 }
644
645 // Returns whether the front request has already been committed to the wire.
646 bool front_in_flight() const noexcept {
647 switch (state_) {
648 case State::WRITING:
649 case State::READING:
650 return true;
651 case State::DISCONNECTED:
652 case State::RESOLVING:
653 case State::CONNECTING:
654 case State::HANDSHAKING:
655 case State::READY:
656 return false;
657 }
658 return true;
659 }
660
661 void pump() {
662 if (stopped_ || queue_.empty()) {
663 return;
664 }
665 if (state_ == State::DISCONNECTED) {
666 start_resolve();
667 return;
668 }
669 if (state_ != State::READY) {
670 return;
671 }
672 // A request that is never replayed is not written into a connection the server may have closed
673 // while it sat idle; see `HttpClientOptions::unreplayed_idle_limit_`. Before admission, so the
674 // reconnect spends no rate-limit slot of its own.
675 if (queue_.front().request_.method_ != http::verb::get && !queue_.front().refreshed_ && idle_since_ &&
676 std::chrono::steady_clock::now() - *idle_since_ > options_.unreplayed_idle_limit_) {
677 queue_.front().refreshed_ = true;
678 disconnect();
679 // The handshake is not admission delay; the wait, if any, is measured again from here.
680 queue_.front().waiting_since_.reset();
681 start_resolve();
682 return;
683 }
684 if (!limiter_) {
685 start_write();
686 return;
687 }
688
689 // Admission is evaluated for whichever request is at the front *now*, so a
690 // trading request that arrives while a polling request is waiting is
691 // reconsidered immediately rather than inheriting the wait it did not earn.
692 const auto group = queue_.front().request_.group_;
693 const auto delay = limiter_->delay_until(group);
694 if (delay > RateLimitController::Duration::zero()) {
695 // Waiting longer than the operation's own deadline would queue a request
696 // only to time it out on the wire. Failing now says the same thing sooner,
697 // and says it before the request is sent rather than after.
698 if (delay > std::chrono::duration_cast<RateLimitController::Duration>(options_.timeout_)) {
699 limiter_->record_shed(group);
700 auto pending = std::move(queue_.front());
701 queue_.pop_front();
702 BrokerError error;
703 error.category_ = ErrorCategory::CANCELLED;
704 // Ordinarily nothing was sent -- admission is evaluated only while the connection is
705 // `READY`, never while the front request is in flight. A GET being replayed is the
706 // exception: it is admitted again, and its first attempt did reach the wire.
707 error.reached_broker_ = pending.sent_;
708 error.broker_code_ = "admission_deadline";
709 error.message_ = "request would wait for rate-limit admission beyond its deadline";
710 complete(std::move(pending.handler_), Result<HttpResponse>(std::move(error)));
711 pump();
712 return;
713 }
714 if (!queue_.front().waiting_since_) {
715 queue_.front().waiting_since_ = std::chrono::steady_clock::now();
716 }
717 admission_timer_.expires_after(delay);
718 admission_timer_.async_wait([self = shared_from_this()](beast::error_code ec) {
719 if (ec) {
720 return;
721 }
722 self->pump();
723 });
724 return;
725 }
726
727 if (auto& waiting_since = queue_.front().waiting_since_) {
728 limiter_->record_delay(
729 group,
730 std::chrono::duration_cast<RateLimitController::Duration>(
731 std::chrono::steady_clock::now() - *waiting_since
732 )
733 );
734 waiting_since.reset();
735 }
736 admission_timer_.cancel();
737 limiter_->acquire(group);
738 start_write();
739 }
740
741 void start_resolve() {
742 stream_ = std::make_unique<beast::ssl_stream<beast::tcp_stream>>(strand_, context_);
743 state_ = State::RESOLVING;
744 resolve_timed_out_ = false;
745 resolve_timer_.expires_after(options_.timeout_);
746 resolve_timer_.async_wait([self = shared_from_this()](beast::error_code ec) {
747 if (!ec && self->state_ == State::RESOLVING) {
748 self->resolve_timed_out_ = true;
749 self->resolver_.cancel();
750 }
751 });
752 resolver_.async_resolve(
753 host_,
754 port_,
755 [self = shared_from_this()](beast::error_code ec, tcp::resolver::results_type endpoints) {
756 self->resolve_timer_.cancel();
757 // `close()` completes every queued request and releases the stream, so a
758 // continuation that arrives afterwards has nothing left to advance.
759 if (self->stopped_ || !self->stream_) {
760 return;
761 }
762 if (ec) {
763 if (self->resolve_timed_out_) {
764 ec = beast::error_code(net::error::timed_out);
765 }
766 self->fail("resolve", ec);
767 return;
768 }
769 self->state_ = State::CONNECTING;
770 beast::get_lowest_layer(*self->stream_).expires_after(self->options_.timeout_);
771 beast::get_lowest_layer(*self->stream_)
772 .async_connect(
773 endpoints,
774 [self](beast::error_code connect_ec, tcp::resolver::results_type::endpoint_type) {
775 if (self->stopped_ || !self->stream_) {
776 return;
777 }
778 if (connect_ec) {
779 self->fail("connect", connect_ec);
780 return;
781 }
782 self->start_handshake();
783 }
784 );
785 }
786 );
787 }
788
789 void start_handshake() {
790 if (!SSL_set_tlsext_host_name(stream_->native_handle(), host_.c_str())) {
791 fail(
792 "sni",
793 beast::error_code(static_cast<int>(::ERR_get_error()), net::error::get_ssl_category())
794 );
795 return;
796 }
797 stream_->set_verify_callback(ssl::host_name_verification(host_));
798 state_ = State::HANDSHAKING;
799 beast::get_lowest_layer(*stream_).expires_after(options_.timeout_);
800 stream_->async_handshake(
801 ssl::stream_base::client, [self = shared_from_this()](beast::error_code ec) {
802 if (self->stopped_ || !self->stream_) {
803 return;
804 }
805 if (ec) {
806 self->fail("tls_handshake", ec);
807 return;
808 }
809 self->state_ = State::READY;
810 // The idle clock starts here too: a request can wait for admission on a connection that has
811 // carried nothing yet, and the server's idle timeout runs all the same.
812 self->idle_since_ = std::chrono::steady_clock::now();
813 self->pump();
814 }
815 );
816 }
817
818 void start_write() {
819 // Marked here, which is the only moment this can be said truthfully: everything before is
820 // resolving, connecting and handshaking, none of which puts the request anywhere.
821 queue_.front().sent_ = true;
822 const auto& request_value = queue_.front().request_;
823 active_request_.emplace(request_value.method_, request_value.target_, request_value.version_);
824 active_request_->set(http::field::host, host_header());
825 active_request_->set(http::field::user_agent, "algotradelib");
826 active_request_->keep_alive(true);
827 for (const auto& header : request_value.headers_) {
828 active_request_->set(header.first, header.second);
829 }
830 active_request_->body() = request_value.body_;
831 if (!request_value.body_.empty()) {
832 active_request_->prepare_payload();
833 }
834
835 state_ = State::WRITING;
836 beast::get_lowest_layer(*stream_).expires_after(options_.timeout_);
837 http::async_write(
838 *stream_, *active_request_, [self = shared_from_this()](beast::error_code ec, std::size_t) {
839 if (self->stopped_ || self->queue_.empty()) {
840 return;
841 }
842 if (ec) {
843 self->fail("write", ec);
844 return;
845 }
846 self->start_read();
847 }
848 );
849 }
850
851 void start_read() {
852 parser_.emplace();
853 parser_->body_limit(options_.body_limit_);
854 state_ = State::READING;
855 beast::get_lowest_layer(*stream_).expires_after(options_.timeout_);
856 http::async_read(
857 *stream_, buffer_, *parser_, [self = shared_from_this()](beast::error_code ec, std::size_t) {
858 if (self->stopped_ || self->queue_.empty()) {
859 return;
860 }
861 if (ec) {
862 self->fail("read", ec);
863 return;
864 }
865 auto response = self->parser_->release();
866 const bool keep_alive = response.keep_alive() && !response.need_eof();
867 self->parser_.reset();
868 self->active_request_.reset();
869 auto pending = std::move(self->queue_.front());
870 self->queue_.pop_front();
871 if (keep_alive) {
872 self->state_ = State::READY;
873 self->idle_since_ = std::chrono::steady_clock::now();
874 } else {
875 self->disconnect();
876 }
877 self->complete(std::move(pending.handler_), Result<HttpResponse>(std::move(response)));
878 self->pump();
879 }
880 );
881 }
882
883 void fail(const char* stage, beast::error_code ec) {
884 // **Asked of the request, not of the connection** (`sent_` is kept on each pending request), for
885 // the reason `close()` gives: `disconnect()` resets the connection's state, and asking it afterwards
886 // would answer "not in flight" for every failure -- including one that happened halfway through
887 // writing an order. This is the same question from the other
888 // side: a resolve, connect, or handshake failure happens *before* `start_write()`, so an order
889 // has certainly not been sent, and reporting it as indeterminate spends a rate-limit slot and
890 // withholds the instrument to reconcile an order that does not exist.
891 disconnect();
892 if (queue_.empty()) {
893 return;
894 }
895 auto& pending = queue_.front();
896 if (
897 pending.request_.method_ == http::verb::get && !pending.retried_ &&
898 ec != net::error::operation_aborted
899 ) {
900 pending.retried_ = true;
901 pump();
902 return;
903 }
904 auto completed = std::move(pending);
905 queue_.pop_front();
906 auto error = transport_error(stage, ec);
907 error.reached_broker_ = completed.sent_;
908 complete(std::move(completed.handler_), Result<HttpResponse>(std::move(error)));
909 pump();
910 }
911
912 void disconnect() noexcept {
913 resolve_timer_.cancel();
914 if (stream_) {
915 close_ssl_stream(*stream_);
916 stream_.reset();
917 }
918 buffer_.consume(buffer_.size());
919 parser_.reset();
920 active_request_.reset();
921 state_ = State::DISCONNECTED;
922 idle_since_.reset();
923 }
924
925 std::string host_header() const { return make_host_header(host_, port_); }
926
927 void complete(CompletionHandler handler, Result<HttpResponse> result) {
928 auto executor = net::get_associated_executor(handler, strand_);
929 net::dispatch(executor, [handler = std::move(handler), result = std::move(result)]() mutable {
930 std::move(handler)(std::move(result));
931 });
932 }
933};
934
935} // namespace network
936} // namespace algotradelib
Either a value or a BrokerError.
Definition types.hpp:797
auto async_request(HttpRequest request_value, CompletionToken &&token)
Queues a request; completes with the response (any status) or an error.
AsyncHttpClient(net::any_io_executor executor, ssl::context &context, std::string host, std::string port, HttpClientOptions options={})
Constructs a client; it connects on the first request.
RateLimitController * rate_limits() noexcept
Returns the connection's rate limiter.
net::any_completion_handler< void(Result< HttpResponse >)> CompletionHandler
The completion handler type: called once with the response or an error.
void close()
Closes the client: cancels the connection and completes every queued request with CANCELLED....
std::uint64_t admission_waits() const noexcept
Returns how many times the client started a rate-limit admission wait.
const RateLimitController * rate_limits() const noexcept
Returns the connection's rate limiter (read-only).
HttpResponse get(const std::string &target, const std::map< std::string, std::string > &headers={})
Sends a GET request and waits for the response.
HttpClient(net::io_context &ioc, ssl::context &context, std::string host, std::string port, HttpClientOptions options={})
Constructs a client; it connects on the first request.
void close()
Closes the connection; the next request opens a new one.
HttpResponse post(const std::string &target, const std::string &body, const std::map< std::string, std::string > &headers={})
Sends a POST request and waits for the response; never retried.
HttpResponse request(http::verb method, const std::string &target, const std::string &body={}, const std::map< std::string, std::string > &headers={})
Sends a request and waits for the response; a GET that fails is sent once more.
The rate limiter of one connection (one app key): a RateLimitBucket on the steady clock,...
HTTP and WebSocket transport, OAuth tokens, and request pacing.
RateLimitGroup
The traffic class of a request, for rate limiting and queue priority. The broker adapter that builds ...
@ REFERENCE
Instrument reference data and trading calendars.
constexpr int rate_limit_priority(RateLimitGroup group) noexcept
Returns a traffic class's queue priority.
BrokerError transport_error(std::string stage, beast::error_code ec)
Converts a transport error into a BrokerError.
void shutdown_ssl_stream(beast::ssl_stream< beast::tcp_stream > &stream, std::chrono::seconds timeout) noexcept
Shuts a TLS stream down (best effort) and closes its socket.
void close_ssl_stream(beast::ssl_stream< beast::tcp_stream > &stream) noexcept
Cancels a TLS stream's operations and closes its socket, without waiting for the TLS shutdown.
std::string make_host_header(const std::string &host, const std::string &port)
Builds an HTTP Host header value.
http::response< http::string_body > HttpResponse
The HTTP response type the transport clients return.
The library: broker-neutral types (core), and the modules below.
@ CANCELLED
The operation was cancelled, such as by closing the client or a local deadline.
Definition types.hpp:689
@ TRANSPORT
The connection failed (DNS, connect, TLS, write or read).
Definition types.hpp:691
net::ip::tcp tcp
Short for boost::asio::ip::tcp.
The error a broker or transport operation returns.
Definition types.hpp:742
std::string message_
A human-readable description, for logs; do not parse it.
Definition types.hpp:772
ErrorCategory category_
The kind of failure; the field to branch on.
Definition types.hpp:744
std::string broker_code_
The broker's own error code, verbatim (for example KIS's EGW00201).
Definition types.hpp:761
bool reached_broker_
Whether a request was sent before this error.
Definition types.hpp:753
Deadlines, size limits and pacing for an HTTP client.
std::optional< RateLimitBudget > rate_limit_
The rate-limit budget that paces this connection's requests, or empty (the default) for no pacing (As...
std::chrono::seconds timeout_
The deadline for each of DNS lookup, connect, TLS handshake, write and read, in AsyncHttpClient; Http...
std::chrono::milliseconds unreplayed_idle_limit_
How long a kept-alive connection may sit idle and still carry a request other than a GET (one second ...
std::size_t body_limit_
The largest response body accepted, in bytes.
std::size_t queue_limit_
How many requests the queue may hold, the one being sent included (AsyncHttpClient); one more fails w...
An HTTP request for AsyncHttpClient.
RateLimitGroup group_
The traffic class, for rate limiting and queue priority.
std::string target_
The request target: path and query.
std::vector< std::pair< std::string, std::string > > headers_
Additional headers, in the order they are sent.
std::string body_
The request body, empty for a GET.
http::verb method_
The HTTP method. A GET may be sent again once after a connection failure; nothing else is.
int version_
The HTTP version, 11 for HTTP/1.1.