]> git.proxmox.com Git - ceph.git/blob - ceph/src/boost/libs/leaf/examples/asio_beast_leaf_rpc.cpp
import quincy beta 17.1.0
[ceph.git] / ceph / src / boost / libs / leaf / examples / asio_beast_leaf_rpc.cpp
1 // Copyright (c) 2019 Sorin Fetche
2
3 // Distributed under the Boost Software License, Version 1.0. (See accompanying
4 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
5
6 // PLEASE NOTE: This example requires the Boost 1.70 version of Asio and Beast, which at the time of this
7 // writing is in beta.
8
9 // Example of a composed asynchronous operation which uses the LEAF library for error handling and reporting.
10 //
11 // Examples of running:
12 // - in one terminal (re)run: ./asio_beast_leaf_rpc_v3 0.0.0.0 8080
13 // - in another run:
14 // curl localhost:8080 -v -d "sum 0 1 2 3"
15 // generating errors returned to the client:
16 // curl localhost:8080 -v -X DELETE -d ""
17 // curl localhost:8080 -v -d "mul 1 2x3"
18 // curl localhost:8080 -v -d "div 1 0"
19 // curl localhost:8080 -v -d "mod 1"
20 //
21 // Runs that showcase the error handling on the server side:
22 // - error starting the server:
23 // ./asio_beast_leaf_rpc_v3 0.0.0.0 80
24 // - error while running the server logic:
25 // ./asio_beast_leaf_rpc_v3 0.0.0.0 8080
26 // curl localhost:8080 -v -d "error-quit"
27 //
28 #include <boost/algorithm/string/replace.hpp>
29 #include <boost/asio/io_context.hpp>
30 #include <boost/asio/ip/tcp.hpp>
31 #include <boost/beast/core.hpp>
32 #include <boost/beast/http.hpp>
33 #include <boost/beast/version.hpp>
34 #include <boost/format.hpp>
35 #include <boost/leaf.hpp>
36 #include <boost/spirit/include/qi_numeric.hpp>
37 #include <boost/spirit/include/qi_parse.hpp>
38 #include <deque>
39 #include <iostream>
40 #include <list>
41 #include <optional>
42 #include <string>
43
44 namespace beast = boost::beast;
45 namespace http = beast::http;
46 namespace leaf = boost::leaf;
47 namespace net = boost::asio;
48
49 namespace {
50 using error_code = boost::system::error_code;
51 } // namespace
52
53 // The operation being performed when an error occurs.
54 struct e_last_operation {
55 std::string_view value;
56 };
57
58 // The HTTP request type.
59 using request_t = http::request<http::string_body>;
60 // The HTTP response type.
61 using response_t = http::response<http::string_body>;
62
63 response_t handle_request(request_t &&request);
64
65 // A composed asynchronous operation that implements a basic remote calculator over HTTP.
66 // It receives from the remote side commands such as:
67 // sum 1 2 3
68 // div 3 2
69 // mod 1 0
70 // in the body of POST requests and sends back the result.
71 //
72 // Besides the calculator related commands, it also offer a special command:
73 // - `error_quit` that asks the server to simulate a server side error that leads to the connection being dropped
74 // .
75 //
76 // From the error handling perspective there are three parts of the implementation:
77 // - the handling of an HTTP request and creating the response to send back
78 // (see handle_request)
79 // - the parsing and execution of the remote command we received as the body of an an HTTP POST request
80 // (see execute_command())
81 // - this composed asynchronous operation which calls them
82 // .
83 //
84 // This example operation is based on:
85 // - https://github.com/boostorg/beast/blob/b02f59ff9126c5a17f816852efbbd0ed20305930/example/echo-op/echo_op.cpp
86 // - part of
87 // https://github.com/boostorg/beast/blob/b02f59ff9126c5a17f816852efbbd0ed20305930/example/advanced/server/advanced_server.cpp
88 //
89 template <class AsyncStream, typename ErrorContext, typename CompletionToken>
90 auto async_demo_rpc(AsyncStream &stream, ErrorContext &error_context, CompletionToken &&token) ->
91 typename net::async_result<typename std::decay<CompletionToken>::type, void(leaf::result<void>)>::return_type {
92
93 static_assert(beast::is_async_stream<AsyncStream>::value, "AsyncStream requirements not met");
94
95 using handler_type =
96 typename net::async_completion<CompletionToken, void(leaf::result<void>)>::completion_handler_type;
97 using base_type = beast::stable_async_base<handler_type, beast::executor_type<AsyncStream>>;
98 struct internal_op : base_type {
99 // This object must have a stable address
100 struct temporary_data {
101 beast::flat_buffer buffer;
102 std::optional<http::request_parser<request_t::body_type>> parser;
103 std::optional<response_t> response;
104 };
105
106 AsyncStream &m_stream;
107 ErrorContext &m_error_context;
108 temporary_data &m_data;
109 bool m_write_and_quit;
110
111 internal_op(AsyncStream &stream, ErrorContext &error_context, handler_type &&handler)
112 : base_type{std::move(handler), stream.get_executor()}, m_stream{stream}, m_error_context{error_context},
113 m_data{beast::allocate_stable<temporary_data>(*this)}, m_write_and_quit{false} {
114 start_read_request();
115 }
116
117 void operator()(error_code ec, std::size_t /*bytes_transferred*/ = 0) {
118 leaf::result<bool> result_continue_execution;
119 {
120 auto active_context = activate_context(m_error_context);
121 auto load = leaf::on_error(e_last_operation{m_data.response ? "async_demo_rpc::continuation-write"
122 : "async_demo_rpc::continuation-read"});
123 if (ec == http::error::end_of_stream) {
124 // The remote side closed the connection.
125 result_continue_execution = false;
126 } else if (ec) {
127 result_continue_execution = leaf::new_error(ec);
128 } else {
129 result_continue_execution = leaf::exception_to_result([&]() -> leaf::result<bool> {
130 if (!m_data.response) {
131 // Process the request we received.
132 m_data.response = handle_request(std::move(m_data.parser->release()));
133 m_write_and_quit = m_data.response->need_eof();
134 http::async_write(m_stream, *m_data.response, std::move(*this));
135 return true;
136 }
137
138 // If getting here, we completed a write operation.
139 m_data.response.reset();
140 // And start reading a new message if not quitting
141 // (i.e. the message semantics of the last response we sent required an end of file)
142 if (!m_write_and_quit) {
143 start_read_request();
144 return true;
145 }
146
147 // We didn't initiate any new async operation above, so we will not continue the execution.
148 return false;
149 });
150 }
151 // The activation object and load_last_operation need to be reset before calling the completion handler
152 // This is because, in general, the completion handler may be called directly or posted and if posted,
153 // it could execute in another thread. This means that regardless of how the handler gets to be actually
154 // called we must ensure that it is not called with the error context active.
155 // Note: An error context cannot be activated twice
156 }
157 if (!result_continue_execution) {
158 // We don't continue the execution due to an error, calling the completion handler
159 this->complete_now(result_continue_execution.error());
160 } else if( !*result_continue_execution ) {
161 // We don't continue the execution due to the flag not being set, calling the completion handler
162 this->complete_now(leaf::result<void>{});
163 }
164 }
165
166 void start_read_request() {
167 m_data.parser.emplace();
168 m_data.parser->body_limit(1024);
169 http::async_read(m_stream, m_data.buffer, *m_data.parser, std::move(*this));
170 }
171 };
172
173 auto initiation = [](auto &&completion_handler, AsyncStream *stream, ErrorContext *error_context) {
174 internal_op op{*stream, *error_context, std::forward<decltype(completion_handler)>(completion_handler)};
175 };
176
177 // We are in the "initiation" part of the async operation.
178 [[maybe_unused]] auto load = leaf::on_error(e_last_operation{"async_demo_rpc::initiation"});
179 return net::async_initiate<CompletionToken, void(leaf::result<void>)>(initiation, token, &stream, &error_context);
180 }
181
182 // The location of a int64 parse error.
183 // It refers the range of characters from which the parsing was done.
184 struct e_parse_int64_error {
185 using location_base = std::pair<std::string_view const, std::string_view::const_iterator>;
186 struct location : public location_base {
187 using location_base::location_base;
188
189 friend std::ostream &operator<<(std::ostream &os, location const &value) {
190 auto const &sv = value.first;
191 std::size_t pos = std::distance(sv.begin(), value.second);
192 if (pos == 0) {
193 os << "->\"" << sv << "\"";
194 } else if (pos < sv.size()) {
195 os << "\"" << sv.substr(0, pos) << "\"->\"" << sv.substr(pos) << "\"";
196 } else {
197 os << "\"" << sv << "\"<-";
198 }
199 return os;
200 }
201 };
202
203 location value;
204 };
205
206 // Parses an integer from a string_view.
207 leaf::result<std::int64_t> parse_int64(std::string_view word) {
208 auto const begin = word.begin();
209 auto const end = word.end();
210 std::int64_t value = 0;
211 auto i = begin;
212 bool result = boost::spirit::qi::parse(i, end, boost::spirit::long_long, value);
213 if (!result || i != end) {
214 return leaf::new_error(e_parse_int64_error{std::make_pair(word, i)});
215 }
216 return value;
217 }
218
219 // The command being executed while we get an error.
220 // It refers the range of characters from which the command was extracted.
221 struct e_command {
222 std::string_view value;
223 };
224
225 // The details about an incorrect number of arguments error
226 // Some commands may accept a variable number of arguments (e.g. greater than 1 would mean [2, SIZE_MAX]).
227 struct e_unexpected_arg_count {
228 struct arg_info {
229 std::size_t count;
230 std::size_t min;
231 std::size_t max;
232
233 friend std::ostream &operator<<(std::ostream &os, arg_info const &value) {
234 os << value.count << " (required: ";
235 if (value.min == value.max) {
236 os << value.min;
237 } else if (value.max < SIZE_MAX) {
238 os << "[" << value.min << ", " << value.max << "]";
239 } else {
240 os << "[" << value.min << ", MAX]";
241 }
242 os << ")";
243 return os;
244 }
245 };
246
247 arg_info value;
248 };
249
250 // The HTTP status that should be returned in case we get into an error.
251 struct e_http_status {
252 http::status value;
253 };
254
255 // Unexpected HTTP method.
256 struct e_unexpected_http_method {
257 http::verb value;
258 };
259
260 // The E-type that describes the `error_quit` command as an error condition.
261 struct e_error_quit {
262 struct none_t {};
263 none_t value;
264 };
265
266 // Processes a remote command.
267 leaf::result<std::string> execute_command(std::string_view line) {
268 // Split the command in words.
269 std::list<std::string_view> words; // or std::deque<std::string_view> words;
270
271 char const *const ws = "\t \r\n";
272 auto skip_ws = [&](std::string_view &line) {
273 if (auto pos = line.find_first_not_of(ws); pos != std::string_view::npos) {
274 line = line.substr(pos);
275 } else {
276 line = std::string_view{};
277 }
278 };
279
280 skip_ws(line);
281 while (!line.empty()) {
282 std::string_view word;
283 if (auto pos = line.find_first_of(ws); pos != std::string_view::npos) {
284 word = line.substr(0, pos);
285 line = line.substr(pos + 1);
286 } else {
287 word = line;
288 line = std::string_view{};
289 }
290
291 if (!word.empty()) {
292 words.push_back(word);
293 }
294 skip_ws(line);
295 }
296
297 static char const *const help = "Help:\n"
298 " error-quit Simulated error to end the session\n"
299 " sum <int64>* Addition\n"
300 " sub <int64>+ Substraction\n"
301 " mul <int64>* Multiplication\n"
302 " div <int64>+ Division\n"
303 " mod <int64> <int64> Remainder\n"
304 " <anything else> This message";
305
306 if (words.empty()) {
307 return std::string(help);
308 }
309
310 auto command = words.front();
311 words.pop_front();
312
313 auto load_cmd = leaf::on_error(e_command{command}, e_http_status{http::status::bad_request});
314 std::string response;
315
316 if (command == "error-quit") {
317 return leaf::new_error(e_error_quit{});
318 } else if (command == "sum") {
319 std::int64_t sum = 0;
320 for (auto const &w : words) {
321 BOOST_LEAF_AUTO(i, parse_int64(w));
322 sum += i;
323 }
324 response = std::to_string(sum);
325 } else if (command == "sub") {
326 if (words.size() < 2) {
327 return leaf::new_error(e_unexpected_arg_count{words.size(), 2, SIZE_MAX});
328 }
329 BOOST_LEAF_AUTO(sub, parse_int64(words.front()));
330 words.pop_front();
331 for (auto const &w : words) {
332 BOOST_LEAF_AUTO(i, parse_int64(w));
333 sub -= i;
334 }
335 response = std::to_string(sub);
336 } else if (command == "mul") {
337 std::int64_t mul = 1;
338 for (auto const &w : words) {
339 BOOST_LEAF_AUTO(i, parse_int64(w));
340 mul *= i;
341 }
342 response = std::to_string(mul);
343 } else if (command == "div") {
344 if (words.size() < 2) {
345 return leaf::new_error(e_unexpected_arg_count{words.size(), 2, SIZE_MAX});
346 }
347 BOOST_LEAF_AUTO(div, parse_int64(words.front()));
348 words.pop_front();
349 for (auto const &w : words) {
350 BOOST_LEAF_AUTO(i, parse_int64(w));
351 if (i == 0) {
352 // In some cases this command execution function might throw, not just return an error.
353 throw std::runtime_error{"division by zero"};
354 }
355 div /= i;
356 }
357 response = std::to_string(div);
358 } else if (command == "mod") {
359 if (words.size() != 2) {
360 return leaf::new_error(e_unexpected_arg_count{words.size(), 2, 2});
361 }
362 BOOST_LEAF_AUTO(i1, parse_int64(words.front()));
363 words.pop_front();
364 BOOST_LEAF_AUTO(i2, parse_int64(words.front()));
365 words.pop_front();
366 if (i2 == 0) {
367 // In some cases this command execution function might throw, not just return an error.
368 throw leaf::exception(std::runtime_error{"division by zero"});
369 }
370 response = std::to_string(i1 % i2);
371 } else {
372 response = help;
373 }
374
375 return response;
376 }
377
378 std::string diagnostic_to_str(leaf::verbose_diagnostic_info const &diag) {
379 auto str = boost::str(boost::format("%1%") % diag);
380 boost::algorithm::replace_all(str, "\n", "\n ");
381 return "\nDetailed error diagnostic:\n----\n" + str + "\n----";
382 };
383
384 // Handles an HTTP request and returns the response to send back.
385 response_t handle_request(request_t &&request) {
386
387 auto msg_prefix = [](e_command const *cmd) {
388 if (cmd != nullptr) {
389 return boost::str(boost::format("Error (%1%):") % cmd->value);
390 }
391 return std::string("Error:");
392 };
393
394 auto make_sr = [](e_http_status const *status, std::string &&response) {
395 return std::make_pair(status != nullptr ? status->value : http::status::internal_server_error,
396 std::move(response));
397 };
398
399 // In this variant of the RPC example we execute the remote command and handle any errors coming from it
400 // in one place (using `leaf::try_handle_all`).
401 auto pair_status_response = leaf::try_handle_all(
402 [&]() -> leaf::result<std::pair<http::status, std::string>> {
403 if (request.method() != http::verb::post) {
404 return leaf::new_error(e_unexpected_http_method{http::verb::post},
405 e_http_status{http::status::bad_request});
406 }
407 BOOST_LEAF_AUTO(response, execute_command(request.body()));
408 return std::make_pair(http::status::ok, std::move(response));
409 },
410 // For the `error_quit` command and associated error condition we have the error handler itself fail
411 // (by throwing). This means that the server will not send any response to the client, it will just
412 // shutdown the connection.
413 // This implementation showcases two aspects:
414 // - that the implementation of error handling can fail, too
415 // - how the asynchronous operation calling this error handling function reacts to this failure.
416 [](e_error_quit const &) -> std::pair<http::status, std::string> { throw std::runtime_error("error_quit"); },
417 // For the rest of error conditions we just build a message to be sent to the remote client.
418 [&](e_parse_int64_error const &e, e_http_status const *status, e_command const *cmd,
419 leaf::verbose_diagnostic_info const &diag) {
420 return make_sr(status, boost::str(boost::format("%1% int64 parse error: %2%") % msg_prefix(cmd) % e.value) +
421 diagnostic_to_str(diag));
422 },
423 [&](e_unexpected_arg_count const &e, e_http_status const *status, e_command const *cmd,
424 leaf::verbose_diagnostic_info const &diag) {
425 return make_sr(status,
426 boost::str(boost::format("%1% wrong argument count: %2%") % msg_prefix(cmd) % e.value) +
427 diagnostic_to_str(diag));
428 },
429 [&](e_unexpected_http_method const &e, e_http_status const *status, e_command const *cmd,
430 leaf::verbose_diagnostic_info const &diag) {
431 return make_sr(status, boost::str(boost::format("%1% unexpected HTTP method. Expected: %2%") %
432 msg_prefix(cmd) % e.value) +
433 diagnostic_to_str(diag));
434 },
435 [&](std::exception const & e, e_http_status const *status, e_command const *cmd,
436 leaf::verbose_diagnostic_info const &diag) {
437 return make_sr(status, boost::str(boost::format("%1% %2%") % msg_prefix(cmd) % e.what()) +
438 diagnostic_to_str(diag));
439 },
440 [&](e_http_status const *status, e_command const *cmd, leaf::verbose_diagnostic_info const &diag) {
441 return make_sr(status, boost::str(boost::format("%1% unknown failure") % msg_prefix(cmd)) +
442 diagnostic_to_str(diag));
443 });
444 response_t response{pair_status_response.first, request.version()};
445 response.set(http::field::server, "Example-with-" BOOST_BEAST_VERSION_STRING);
446 response.set(http::field::content_type, "text/plain");
447 response.keep_alive(request.keep_alive());
448 pair_status_response.second += "\n";
449 response.body() = std::move(pair_status_response.second);
450 response.prepare_payload();
451 return response;
452 }
453
454 int main(int argc, char **argv) {
455 auto msg_prefix = [](e_last_operation const *op) {
456 if (op != nullptr) {
457 return boost::str(boost::format("Error (%1%): ") % op->value);
458 }
459 return std::string("Error: ");
460 };
461
462 // Error handler for internal server internal errors (not communicated to the remote client).
463 auto error_handlers = std::make_tuple(
464 [&](std::exception_ptr const &ep, e_last_operation const *op) {
465 return leaf::try_handle_all(
466 [&]() -> leaf::result<int> { std::rethrow_exception(ep); },
467 [&](std::exception const & e, leaf::verbose_diagnostic_info const &diag) {
468 std::cerr << msg_prefix(op) << e.what() << " (captured)" << diagnostic_to_str(diag)
469 << std::endl;
470 return -11;
471 },
472 [&](leaf::verbose_diagnostic_info const &diag) {
473 std::cerr << msg_prefix(op) << "unknown (captured)" << diagnostic_to_str(diag) << std::endl;
474 return -12;
475 });
476 },
477 [&](std::exception const & e, e_last_operation const *op, leaf::verbose_diagnostic_info const &diag) {
478 std::cerr << msg_prefix(op) << e.what() << diagnostic_to_str(diag) << std::endl;
479 return -21;
480 },
481 [&](error_code ec, leaf::verbose_diagnostic_info const &diag, e_last_operation const *op) {
482 std::cerr << msg_prefix(op) << ec << ":" << ec.message() << diagnostic_to_str(diag) << std::endl;
483 return -22;
484 },
485 [&](leaf::verbose_diagnostic_info const &diag, e_last_operation const *op) {
486 std::cerr << msg_prefix(op) << "unknown" << diagnostic_to_str(diag) << std::endl;
487 return -23;
488 });
489
490 // Top level try block and error handler.
491 // It will handle errors from starting the server for example failure to bind to a given port
492 // (e.g. ports less than 1024 if not running as root)
493 return leaf::try_handle_all(
494 [&]() -> leaf::result<int> {
495 auto load = leaf::on_error(e_last_operation{"main"});
496 if (argc != 3) {
497 std::cerr << "Usage: " << argv[0] << " <address> <port>" << std::endl;
498 std::cerr << "Example:\n " << argv[0] << " 0.0.0.0 8080" << std::endl;
499 return -1;
500 }
501
502 auto const address{net::ip::make_address(argv[1])};
503 auto const port{static_cast<std::uint16_t>(std::atoi(argv[2]))};
504 net::ip::tcp::endpoint const endpoint{address, port};
505
506 net::io_context io_context;
507
508 // Start the server acceptor and wait for a client.
509 net::ip::tcp::acceptor acceptor{io_context, endpoint};
510
511 auto local_endpoint = acceptor.local_endpoint();
512 auto address_try_msg = acceptor.local_endpoint().address().to_string();
513 if (address_try_msg == "0.0.0.0") {
514 address_try_msg = "localhost";
515 }
516 std::cout << "Server: Started on: " << local_endpoint << std::endl;
517 std::cout << "Try in a different terminal:\n"
518 << " curl " << address_try_msg << ":" << local_endpoint.port() << " -d \"\"\nor\n"
519 << " curl " << address_try_msg << ":" << local_endpoint.port() << " -d \"sum 1 2 3\""
520 << std::endl;
521
522 auto socket = acceptor.accept();
523 std::cout << "Server: Client connected: " << socket.remote_endpoint() << std::endl;
524
525 // The error context for the async operation.
526 auto error_context = leaf::make_context(error_handlers);
527 int rv = 0;
528 async_demo_rpc(socket, error_context, [&](leaf::result<void> result) {
529 // Note: In case we wanted to add some additional information to the error associated with the result
530 // we would need to activate the error-context
531 auto active_context = activate_context(error_context);
532 if (result) {
533 std::cout << "Server: Client work completed successfully" << std::endl;
534 rv = 0;
535 } else {
536 // Handle errors from running the server logic
537 leaf::result<int> result_int{result.error()};
538 rv = error_context.handle_error<int>(result_int.error(), error_handlers);
539 }
540 });
541 io_context.run();
542
543 // Let the remote side know we are shutting down.
544 error_code ignored;
545 socket.shutdown(net::ip::tcp::socket::shutdown_both, ignored);
546 return rv;
547 },
548 error_handlers);
549 }