xrpld
Loading...
Searching...
No Matches
short_read_test.cpp
1#include <test/jtx/envconfig.h>
2
3#include <xrpl/basics/make_SSLContext.h>
4#include <xrpl/beast/core/CurrentThreadName.h>
5#include <xrpl/beast/unit_test/suite.h>
6
7#include <boost/asio/basic_waitable_timer.hpp>
8#include <boost/asio/bind_executor.hpp>
9#include <boost/asio/buffer.hpp>
10#include <boost/asio/error.hpp>
11#include <boost/asio/executor_work_guard.hpp>
12#include <boost/asio/io_context.hpp>
13#include <boost/asio/ip/address.hpp>
14#include <boost/asio/ip/tcp.hpp>
15#include <boost/asio/post.hpp>
16#include <boost/asio/read_until.hpp>
17#include <boost/asio/ssl/context.hpp>
18#include <boost/asio/ssl/stream.hpp>
19#include <boost/asio/strand.hpp>
20#include <boost/asio/streambuf.hpp>
21#include <boost/asio/write.hpp>
22#include <boost/optional/optional.hpp> // IWYU pragma: keep
23#include <boost/system/detail/error_code.hpp>
24
25#include <cassert>
26#include <chrono>
27#include <condition_variable>
28#include <cstddef>
29#include <map>
30#include <memory>
31#include <mutex>
32#include <ostream>
33#include <string>
34#include <thread>
35#include <utility>
36#include <vector>
37
38namespace xrpl {
39/*
40
41Findings from the test:
42
43If the remote host calls async_shutdown then the local host's
44async_read will complete with eof.
45
46If both hosts call async_shutdown then the calls to async_shutdown
47will complete with eof.
48
49*/
50
52{
53private:
54 using io_context_type = boost::asio::io_context;
55 using strand_type = boost::asio::strand<io_context_type::executor_type>;
56 using timer_type = boost::asio::basic_waitable_timer<std::chrono::steady_clock>;
57 using acceptor_type = boost::asio::ip::tcp::acceptor;
58 using socket_type = boost::asio::ip::tcp::socket;
59 using stream_type = boost::asio::ssl::stream<socket_type&>;
60 using error_code = boost::system::error_code;
61 using endpoint_type = boost::asio::ip::tcp::endpoint;
62 using address_type = boost::asio::ip::address;
63
65 boost::optional<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>> work_;
68
69 template <class Streambuf>
70 static void
71 write(Streambuf& sb, std::string const& s)
72 {
73 using boost::asio::buffer;
74 using boost::asio::buffer_copy;
75 using boost::asio::buffer_size;
76 boost::asio::const_buffer const buf(s.data(), s.size());
77 sb.commit(buffer_copy(sb.prepare(buffer_size(buf)), buf));
78 }
79
80 //--------------------------------------------------------------------------
81
82 class Base
83 {
84 protected:
85 class Child
86 {
87 private:
89
90 public:
91 explicit Child(Base& base) : base_(base)
92 {
93 }
94
95 virtual ~Child()
96 {
97 base_.remove(this);
98 }
99
100 virtual void
101 close() = 0;
102 };
103
104 private:
108 bool closed_ = false;
109
110 public:
112 {
113 // Derived class must call wait() in the destructor
114 assert(list_.empty());
115 }
116
117 void
119 {
120 std::scoped_lock const lock(mutex_);
121 list_.emplace(child.get(), child);
122 }
123
124 void
125 remove(Child* child)
126 {
127 std::scoped_lock const lock(mutex_);
128 list_.erase(child);
129 if (list_.empty())
130 cond_.notify_one();
131 }
132
133 void
135 {
137 {
138 std::scoped_lock const lock(mutex_);
139 v.reserve(list_.size());
140 if (closed_)
141 return;
142 closed_ = true;
143 for (auto const& c : list_)
144 {
145 if (auto p = c.second.lock())
146 {
147 p->close();
148 // Must destroy shared_ptr outside the
149 // lock otherwise deadlock from the
150 // managed object's destructor.
151 v.emplace_back(std::move(p));
152 }
153 }
154 }
155 }
156
157 void
159 {
161 while (!list_.empty())
162 cond_.wait(lock);
163 }
164 };
165
166 //--------------------------------------------------------------------------
167
168 class Server : public Base
169 {
170 private:
173
175 {
181
183 : Child(server)
184 , server(server)
185 , test(server.test_)
186 , acceptor(
188 endpoint_type(boost::asio::ip::make_address(test::getEnvLocalhostAddr()), 0))
190 , strand(boost::asio::make_strand(test.ioContext_))
191 {
192 acceptor.listen();
193 server.endpoint_ = acceptor.local_endpoint();
194 }
195
196 void
197 close() override
198 {
199 if (!strand.running_in_this_thread())
200 {
201 post(strand, [self = shared_from_this()] { self->close(); });
202 return;
203 }
204 acceptor.close();
205 }
206
207 void
209 {
210 acceptor.async_accept(
211 socket,
212 bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
213 self->onAccept(ec);
214 }));
215 }
216
217 void
218 fail(std::string const& what, error_code ec)
219 {
220 if (acceptor.is_open())
221 {
222 if (ec != boost::asio::error::operation_aborted)
223 test.log << what << ": " << ec.message() << std::endl;
224 acceptor.close();
225 }
226 }
227
228 void
230 {
231 if (ec)
232 {
233 fail("accept", ec);
234 return;
235 }
236 auto const p = std::make_shared<Connection>(server, std::move(socket));
237 server.add(p);
238 p->run();
239 acceptor.async_accept(
240 socket,
241 bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
242 self->onAccept(ec);
243 }));
244 }
245 };
246
248 {
255 boost::asio::streambuf buf;
256
257 Connection(Server& inServer, socket_type&& inSocket)
258 : Child(inServer)
259 , server(inServer)
260 , test(server.test_)
261 , socket(std::move(inSocket))
263 , strand(boost::asio::make_strand(test.ioContext_))
265 {
266 }
267
268 void
269 close() override
270 {
271 if (!strand.running_in_this_thread())
272 {
273 post(strand, [self = shared_from_this()] { self->close(); });
274 return;
275 }
276 if (socket.is_open())
277 {
278 socket.close();
279 timer.cancel();
280 }
281 }
282
283 void
285 {
286 timer.expires_after(std::chrono::seconds(3));
287 timer.async_wait(bind_executor(
288 strand,
289 [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); }));
290 stream.async_handshake(
291 stream_type::server,
292 bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
293 self->onHandshake(ec);
294 }));
295 }
296
297 void
298 fail(std::string const& what, error_code ec)
299 {
300 if (socket.is_open())
301 {
302 if (ec != boost::asio::error::operation_aborted)
303 test.log << "[server] " << what << ": " << ec.message() << std::endl;
304 socket.close();
305 timer.cancel();
306 }
307 }
308
309 void
311 {
312 if (ec == boost::asio::error::operation_aborted)
313 return;
314 if (ec)
315 {
316 fail("timer", ec);
317 return;
318 }
319 test.log << "[server] timeout" << std::endl;
320 socket.close();
321 }
322
323 void
325 {
326 if (ec)
327 {
328 fail("handshake", ec);
329 return;
330 }
331 boost::asio::async_read_until(
332 stream,
333 buf,
334 "\n",
335 bind_executor(
336 strand,
337 [self = shared_from_this()](
338 error_code const& ec, std::size_t bytesTransferred) {
339 self->onRead(ec, bytesTransferred);
340 }));
341 }
342
343 void
344 onRead(error_code ec, std::size_t bytesTransferred)
345 {
346 if (ec == boost::asio::error::eof)
347 {
348 server.test_.log << "[server] read: EOF" << std::endl;
349 stream.async_shutdown(
350 bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
351 self->onShutdown(ec);
352 }));
353 return;
354 }
355 if (ec)
356 {
357 fail("read", ec);
358 return;
359 }
360
361 buf.commit(bytesTransferred);
362 buf.consume(bytesTransferred);
363 write(buf, "BYE\n");
364 boost::asio::async_write(
365 stream,
366 buf.data(),
367 bind_executor(
368 strand,
369 [self = shared_from_this()](
370 error_code const& ec, std::size_t bytesTransferred) {
371 self->onWrite(ec, bytesTransferred);
372 }));
373 }
374
375 void
376 onWrite(error_code ec, std::size_t bytesTransferred)
377 {
378 buf.consume(bytesTransferred);
379 if (ec)
380 {
381 fail("write", ec);
382 return;
383 }
384 stream.async_shutdown(bind_executor(
385 strand,
386 [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); }));
387 }
388
389 void
391 {
392 if (ec)
393 {
394 fail("shutdown", ec);
395 return;
396 }
397 socket.close();
398 timer.cancel();
399 }
400 };
401
402 public:
404 {
405 auto const p = std::make_shared<Acceptor>(*this);
406 add(p);
407 p->run();
408 }
409
411 {
412 close();
413 wait();
414 }
415
416 [[nodiscard]] endpoint_type const&
417 endpoint() const
418 {
419 return endpoint_;
420 }
421 };
422
423 //--------------------------------------------------------------------------
424 class Client : public Base
425
426 {
427 private:
429
431 {
438 boost::asio::streambuf buf;
440
442 : Child(client)
443 , client(client)
444 , test(client.test_)
447 , strand(boost::asio::make_strand(test.ioContext_))
449 , ep(ep)
450 {
451 }
452
453 void
454 close() override
455 {
456 if (!strand.running_in_this_thread())
457 {
458 post(strand, [self = shared_from_this()] { self->close(); });
459 return;
460 }
461 if (socket.is_open())
462 {
463 socket.close();
464 timer.cancel();
465 }
466 }
467
468 void
470 {
471 timer.expires_after(std::chrono::seconds(3));
472 timer.async_wait(bind_executor(
473 strand,
474 [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); }));
475 socket.async_connect(
476 ep, bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
477 self->onConnect(ec);
478 }));
479 }
480
481 void
482 fail(std::string const& what, error_code ec)
483 {
484 if (socket.is_open())
485 {
486 if (ec != boost::asio::error::operation_aborted)
487 test.log << "[client] " << what << ": " << ec.message() << std::endl;
488 socket.close();
489 timer.cancel();
490 }
491 }
492
493 void
495 {
496 if (ec == boost::asio::error::operation_aborted)
497 return;
498 if (ec)
499 {
500 fail("timer", ec);
501 return;
502 }
503 test.log << "[client] timeout";
504 socket.close();
505 }
506
507 void
509 {
510 if (ec)
511 {
512 fail("connect", ec);
513 return;
514 }
515 stream.async_handshake(
516 stream_type::client,
517 bind_executor(strand, [self = shared_from_this()](error_code const& ec) {
518 self->onHandshake(ec);
519 }));
520 }
521
522 void
524 {
525 if (ec)
526 {
527 fail("handshake", ec);
528 return;
529 }
530 write(buf, "HELLO\n");
531
532 boost::asio::async_write(
533 stream,
534 buf.data(),
535 bind_executor(
536 strand,
537 [self = shared_from_this()](
538 error_code const& ec, std::size_t bytesTransferred) {
539 self->onWrite(ec, bytesTransferred);
540 }));
541 }
542
543 void
544 onWrite(error_code ec, std::size_t bytesTransferred)
545 {
546 buf.consume(bytesTransferred);
547 if (ec)
548 {
549 fail("write", ec);
550 return;
551 }
552 boost::asio::async_read_until(
553 stream,
554 buf,
555 "\n",
556 bind_executor(
557 strand,
558 [self = shared_from_this()](
559 error_code const& ec, std::size_t bytesTransferred) {
560 self->onRead(ec, bytesTransferred);
561 }));
562 }
563
564 void
565 onRead(error_code ec, std::size_t bytesTransferred)
566 {
567 if (ec)
568 {
569 fail("read", ec);
570 return;
571 }
572 buf.commit(bytesTransferred);
573 stream.async_shutdown(bind_executor(
574 strand,
575 [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); }));
576 }
577
578 void
580 {
581 if (ec)
582 {
583 fail("shutdown", ec);
584 return;
585 }
586 socket.close();
587 timer.cancel();
588 }
589 };
590
591 public:
593 {
594 auto const p = std::make_shared<Connection>(*this, ep);
595 add(p);
596 p->run(ep);
597 }
598
600 {
601 close();
602 wait();
603 }
604 };
605
606public:
608 : work_(ioContext_.get_executor())
609 , thread_(std::thread([this]() {
610 beast::setCurrentThreadName("io_context");
611 this->ioContext_.run();
612 }))
614 {
615 }
616
618 {
619 work_.reset();
620 thread_.join();
621 }
622
623 void
624 run() override
625 {
626 Server const s(*this);
627 Client c(*this, s.endpoint());
628 c.wait();
629 pass();
630 }
631};
632
633BEAST_DEFINE_TESTSUITE(short_read, overlay, xrpl);
634
635} // namespace xrpl
A testsuite class.
Definition suite.h:52
void pass()
Record a successful test condition.
Definition suite.h:532
std::map< Child *, std::weak_ptr< Child > > list_
std::condition_variable cond_
void add(std::shared_ptr< Child > const &child)
Client(short_read_test &test, endpoint_type const &ep)
Server(short_read_test &test)
endpoint_type const & endpoint() const
io_context_type ioContext_
boost::asio::ip::tcp::endpoint endpoint_type
boost::asio::io_context io_context_type
boost::asio::ip::address address_type
boost::asio::strand< io_context_type::executor_type > strand_type
void run() override
Runs the suite.
boost::asio::ip::tcp::socket socket_type
boost::system::error_code error_code
std::shared_ptr< boost::asio::ssl::context > context_
boost::asio::ip::tcp::acceptor acceptor_type
boost::asio::basic_waitable_timer< std::chrono::steady_clock > timer_type
boost::asio::ssl::stream< socket_type & > stream_type
static void write(Streambuf &sb, std::string const &s)
boost::optional< boost::asio::executor_work_guard< boost::asio::io_context::executor_type > > work_
T data(T... args)
T emplace_back(T... args)
T endl(T... args)
T make_shared(T... args)
void setCurrentThreadName(std::string_view newThreadName)
Changes the name of the caller thread.
STL namespace.
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
std::shared_ptr< boost::asio::ssl::context > makeSslContext(std::string const &cipherList)
Create a self-signed SSL context that allows anonymous Diffie Hellman.
BEAST_DEFINE_TESTSUITE(AccountTxPaging, app, xrpl)
T reserve(T... args)
T size(T... args)
void onRead(error_code ec, std::size_t bytesTransferred)
Connection(Client &client, endpoint_type const &ep)
void onWrite(error_code ec, std::size_t bytesTransferred)
void fail(std::string const &what, error_code ec)
void fail(std::string const &what, error_code ec)
Connection(Server &inServer, socket_type &&inSocket)
void onRead(error_code ec, std::size_t bytesTransferred)
void fail(std::string const &what, error_code ec)
void onWrite(error_code ec, std::size_t bytesTransferred)