FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
rest_channel.hpp
Go to the documentation of this file.
1#pragma once
2// RestChannel: reactor-driven keep-alive HTTP(S) client with lazy (re)connection, for the
3// venue control requests that run on the net thread (depth snapshots, openOrders, listenKey
4// keepalive, order-entry REST fallback). net::HttpClient is single-connection and does not
5// reconnect; this wrapper recreates it on demand and re-resolves the host when a connect
6// fails. Requests are std::function/std::string based: control path only.
7#include "fastmm/net/dns.hpp"
10#include "fastmm/net/source_address.hpp"
11#include "fastmm/net/tls_stream.hpp"
12#include "fastmm/net/url.hpp"
13
14#include <cstdint>
15#include <memory>
16#include <stdexcept>
17#include <string>
18#include <string_view>
19#include <utility>
20#include <vector>
21
22namespace fastmm::venues {
23
24struct RestChannelConfig {
25 std::string base_url; // https://host[:port][/prefix]
26 std::string ca_file;
27 bool insecure_tls = false;
28 std::uint32_t timeout_ms = 5000;
29 std::size_t max_queue = 8;
30 net::SourceAddress source; // where the connection leaves this host from; empty: anywhere
31};
33// Queue room for a connector's REST channel with `symbols` subscribed: every connector fans some
34// requests out per symbol (cancel-all, the USDⓈ-M countdown, execution and funding replays), and
35// those can overlap, on top of a fixed set (reconciliation, clock, listen key, the dead man's
36// switch, orders falling back to REST). The queue is a deque: room costs nothing until used.
37[[nodiscard]] constexpr std::size_t rest_queue_for(std::size_t symbols) noexcept {
38 return 32 + 4 * symbols;
40
41// The RestChannelConfig every connector config spells the same way.
42template <class Cfg>
43[[nodiscard]] RestChannelConfig rest_channel_config(const Cfg& cfg, std::size_t symbols) {
44 RestChannelConfig rc;
45 rc.base_url = cfg.rest_url;
46 rc.ca_file = cfg.ca_file;
47 rc.insecure_tls = cfg.insecure_tls;
48 rc.timeout_ms = cfg.http_timeout_ms;
49 rc.max_queue = rest_queue_for(symbols);
50 if constexpr (requires { cfg.source; }) rc.source = cfg.source;
51 return rc;
52}
53
54class RestChannel {
55 public:
61 : reactor_(reactor), cfg_(std::move(cfg)) {
62 const auto url = net::Url::parse(cfg_.base_url);
63 if (!url) throw std::invalid_argument("RestChannel: bad base url '" + cfg_.base_url + "'");
64 host_ = std::string(url->host);
65 port_ = url->port;
66 use_tls_ = url->tls;
67 prefix_ = url->path == "/" ? std::string{} : std::string(url->path);
68 while (!prefix_.empty() && prefix_.back() == '/') prefix_.pop_back();
69 if (use_tls_) {
70 tls_ctx_ = std::make_unique<net::TlsContext>();
71 if (!cfg_.ca_file.empty()) tls_ctx_->set_ca_file(cfg_.ca_file);
72 tls_ctx_->set_insecure(cfg_.insecure_tls);
73 }
74 }
75 ~RestChannel() { reset(); }
76 RestChannel(const RestChannel&) = delete;
79 // `target` is the path plus query without the base prefix. The callback runs on the
80 // reactor thread; a transport failure arrives as HttpResponse::error != None. Returns
81 // false when the request could not even be queued (resolve/connect failure, queue full).
82 bool request(std::string_view method,
83 std::string_view target,
84 std::string_view extra_headers,
85 std::string_view body,
86 Callback cb) {
87 if (!ensure_client()) return false;
88 const std::string full = prefix_ + std::string(target);
89 ++requests_;
90 auto wrapped = wrap(std::move(cb));
91 const bool ok = plain_ ? plain_->request(method, full, extra_headers, body, wrapped)
92 : tls_->request(method, full, extra_headers, body, wrapped);
93 if (!ok) ++errors_;
94 return ok;
95 }
96
97 // The same, with the target (without the base prefix) made when the request is written, not
98 // when it is queued: a signed query is stamped and signed then, however long it waited behind
99 // the requests ahead of it on the one connection (net::HttpClient). Empty: not sent, the
100 // callback gets NetError::Canceled.
101 bool request(std::string_view method,
102 net::HttpTargetMaker make_target,
103 std::string_view extra_headers,
104 std::string_view body,
105 Callback cb) {
106 if (!make_target || !ensure_client()) return false;
107 ++requests_;
108 net::HttpTargetMaker full = [prefix = prefix_, make = std::move(make_target)] {
109 std::string target = make();
110 return target.empty() ? target : prefix + target;
111 };
112 auto wrapped = wrap(std::move(cb));
113 const bool ok = plain_ ? plain_->request(method, full, extra_headers, body, wrapped)
114 : tls_->request(method, full, extra_headers, body, wrapped);
115 if (!ok) ++errors_;
116 return ok;
117 }
118
119 // Drops the connection (deferred to the reactor loop so it is safe from callbacks).
120 void schedule_reset() {
121 if (reset_pending_) return;
122 reset_pending_ = true;
123 std::weak_ptr<int> alive = alive_;
124 reactor_.post([this, alive] {
125 if (alive.expired()) return;
126 reset_pending_ = false;
127 reset();
128 });
129 }
130 void reset() noexcept {
131 plain_.reset();
132 tls_.reset();
133 }
134
135 // For a subscription that grew after connect: see rest_queue_for().
136 void set_max_queue(std::size_t n) noexcept {
137 cfg_.max_queue = n;
138 if (plain_) plain_->set_max_queue(n);
139 if (tls_) tls_->set_max_queue(n);
140 }
141
142 [[nodiscard]] bool connected() const noexcept { return plain_ || tls_; }
143 // Requests queued on the connection, the one in flight included.
144 [[nodiscard]] std::size_t queued() const noexcept {
145 if (plain_) return plain_->queued();
146 return tls_ ? tls_->queued() : 0;
147 }
148 [[nodiscard]] std::size_t max_queue() const noexcept { return cfg_.max_queue; }
149 [[nodiscard]] std::uint64_t requests() const noexcept { return requests_; }
150 [[nodiscard]] std::uint64_t errors() const noexcept { return errors_; }
151 [[nodiscard]] const std::string& host() const noexcept { return host_; }
152 [[nodiscard]] bool tls() const noexcept { return use_tls_; }
154 private:
155 Callback wrap(Callback cb) {
156 return [this, cb = std::move(cb)](const net::HttpResponse& r) {
157 if (r.error != net::NetError::None) {
158 ++errors_;
159 // Canceled: the client closed already, or a withdrawn target that left it usable.
160 if (r.error != net::NetError::Canceled) schedule_reset();
161 }
162 if (cb) cb(r);
163 };
164 }
165
166 bool ensure_client() {
167 if (plain_ && plain_->state() == net::HttpClientState::Closed) plain_.reset();
168 if (tls_ && tls_->state() == net::HttpClientState::Closed) tls_.reset();
169 if (plain_ || tls_) return true;
170 if (addrs_.empty() || addr_gen_ != resolve_gen_) {
171 // Blocking resolve on the net thread: only on (re)connect, cached afterwards.
172 const net::ResolveResult r = net::resolve_sync(host_, port_);
173 if (!r.ok()) {
174 ++errors_;
175 return false;
176 }
177 addrs_ = r.addrs;
178 net::keep_reachable(cfg_.source, addrs_);
179 if (addrs_.empty()) {
180 ++errors_;
181 return false;
182 }
183 addr_gen_ = resolve_gen_;
184 }
185 net::TcpSocket sock;
186 const net::SockAddr& addr = addrs_[addr_index_++ % addrs_.size()];
187 const std::optional<net::SockAddr> local = net::source_bind_address(cfg_.source, addr.family());
188 if (sock.connect(addr, local ? &*local : nullptr) == net::ConnectStatus::Error) {
189 ++errors_;
190 ++resolve_gen_; // re-resolve next time
191 return false;
192 }
193 sock.set_nodelay(true);
194 net::HttpClientConfig hc;
195 hc.timeout_ms = cfg_.timeout_ms;
196 hc.max_queue = cfg_.max_queue;
197 if (use_tls_) {
198 tls_ = std::make_unique<TlsClient>(
199 reactor_,
200 net::TlsStream<net::PlainStream>(*tls_ctx_, net::PlainStream(std::move(sock)), host_),
201 host_,
202 port_,
203 true,
204 hc);
205 if (!tls_->start()) {
206 tls_.reset();
207 return false;
208 }
209 } else {
210 plain_ = std::make_unique<PlainClient>(
211 reactor_, net::PlainStream(std::move(sock)), host_, port_, false, hc);
212 if (!plain_->start()) {
213 plain_.reset();
214 return false;
215 }
216 }
217 return true;
218 }
219
220 net::Reactor& reactor_;
221 RestChannelConfig cfg_;
222 std::string host_;
223 std::uint16_t port_ = 0;
224 bool use_tls_ = false;
225 std::string prefix_;
226 std::unique_ptr<net::TlsContext> tls_ctx_;
227 std::unique_ptr<PlainClient> plain_;
228 std::unique_ptr<TlsClient> tls_;
229 std::vector<net::SockAddr> addrs_;
230 std::size_t addr_index_ = 0;
231 std::uint64_t resolve_gen_ = 0;
232 std::uint64_t addr_gen_ = ~0ULL;
233 std::shared_ptr<int> alive_ = std::make_shared<int>(0);
234 bool reset_pending_ = false;
235 std::uint64_t requests_ = 0;
236 std::uint64_t errors_ = 0;
237};
238
239} // namespace fastmm::venues
Definition http_client.hpp:118
Definition reactor.hpp:72
void post(Task task)
Thread-safe: queue a task for the reactor thread and wake it.
Definition rest_channel.hpp:56
const std::string & host() const noexcept
Definition rest_channel.hpp:153
std::size_t max_queue() const noexcept
Definition rest_channel.hpp:150
bool tls() const noexcept
Definition rest_channel.hpp:154
std::uint64_t requests() const noexcept
Definition rest_channel.hpp:151
net::HttpResponseCallback Callback
Definition rest_channel.hpp:58
bool connected() const noexcept
Definition rest_channel.hpp:144
std::size_t queued() const noexcept
Requests queued on the connection, the one in flight included.
Definition rest_channel.hpp:146
RestChannel(net::Reactor &reactor, RestChannelConfig cfg)
Definition rest_channel.hpp:62
void reset() noexcept
Definition rest_channel.hpp:132
void schedule_reset()
Drops the connection (deferred to the reactor loop so it is safe from callbacks).
Definition rest_channel.hpp:122
std::uint64_t errors() const noexcept
Definition rest_channel.hpp:152
RestChannel & operator=(const RestChannel &)=delete
~RestChannel()
Definition rest_channel.hpp:77
bool request(std::string_view method, std::string_view target, std::string_view extra_headers, std::string_view body, Callback cb)
target is the path plus query without the base prefix.
Definition rest_channel.hpp:84
void set_max_queue(std::size_t n) noexcept
For a subscription that grew after connect: see rest_queue_for().
Definition rest_channel.hpp:138
Keep-alive HTTP/1.1 client over any ByteStream: one request in flight per connection plus a small FIF...
std::function< void(const HttpResponse &)> HttpResponseCallback
Definition http_client.hpp:41
std::function< std::string()> HttpTargetMaker
Makes a request's target (path and query) when the request is written.
Definition http_client.hpp:44
Definition venue.hpp:38
RestChannelConfig rest_channel_config(const Cfg &cfg, std::size_t symbols)
The RestChannelConfig every connector config spells the same way.
Definition rest_channel.hpp:45
constexpr std::size_t rest_queue_for(std::size_t symbols) noexcept
Queue room for a connector's REST channel with symbols subscribed: every connector fans some requests...
Definition rest_channel.hpp:39
@ Error
venue-level error payload
Single-threaded event loop with a timer min-heap and a thread-safe mailbox (post/wake via eventfd),...
Definition http_client.hpp:29
static std::optional< Url > parse(std::string_view s) noexcept
Accepts scheme://host[:port][/path][?query].
Definition url.hpp:38
Definition rest_channel.hpp:26
std::string base_url
https://host[:port][/prefix]
Definition rest_channel.hpp:27
std::string ca_file
Definition rest_channel.hpp:28
bool insecure_tls
Definition rest_channel.hpp:29
net::SourceAddress source
where the connection leaves this host from; empty: anywhere
Definition rest_channel.hpp:32
std::size_t max_queue
Definition rest_channel.hpp:31
std::uint32_t timeout_ms
Definition rest_channel.hpp:30
Minimal URL parsing / query building for venue endpoints.