2
0
mirror of https://github.com/boostorg/redis.git synced 2026-01-19 04:42:09 +00:00
Files
redis/test/test_run_fsm.cpp
Anarthal (Rubén Pérez) f683e368dd Implements async_run as a FSM and adds tests (#330)
* Implements async_run as a FSM and adds tests
* Places all sans-io variables in connection_impl in a connection_state struct

Entails no functional change.
2025-10-13 22:19:39 +02:00

476 lines
16 KiB
C++

//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/run_fsm.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/logger.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/local/basic_endpoint.hpp> // for BOOST_ASIO_HAS_LOCAL_SOCKETS
#include <boost/core/lightweight_test.hpp>
#include <boost/system/error_code.hpp>
#include "sansio_utils.hpp"
#include <ostream>
#include <string_view>
using namespace boost::redis;
namespace asio = boost::asio;
using detail::run_fsm;
using detail::multiplexer;
using detail::run_action_type;
using detail::run_action;
using boost::system::error_code;
using boost::asio::cancellation_type_t;
using detail::connection_logger;
using namespace std::chrono_literals;
// Operators
static const char* to_string(run_action_type value)
{
switch (value) {
case run_action_type::done: return "run_action_type::done";
case run_action_type::immediate: return "run_action_type::immediate";
case run_action_type::connect: return "run_action_type::connect";
case run_action_type::parallel_group: return "run_action_type::parallel_group";
case run_action_type::cancel_receive: return "run_action_type::cancel_receive";
case run_action_type::wait_for_reconnection: return "run_action_type::wait_for_reconnection";
default: return "<unknown run_action_type>";
}
}
namespace boost::redis::detail {
std::ostream& operator<<(std::ostream& os, run_action_type type)
{
os << to_string(type);
return os;
}
bool operator==(const run_action& lhs, const run_action& rhs) noexcept
{
return lhs.type == rhs.type && lhs.ec == rhs.ec;
}
std::ostream& operator<<(std::ostream& os, const run_action& act)
{
os << "run_action{ .type=" << act.type;
if (act.type == run_action_type::done)
os << ", .error=" << act.ec;
return os << " }";
}
} // namespace boost::redis::detail
namespace {
struct fixture : detail::log_fixture {
detail::connection_state st;
run_fsm fsm;
fixture(config&& cfg = {})
: st{make_logger(), std::move(cfg)}
{ }
};
config config_no_reconnect()
{
config res;
res.reconnect_wait_interval = 0s;
return res;
}
// Config errors
#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS
void test_config_error_unix()
{
// Setup
config cfg;
cfg.unix_socket = "/var/sock";
fixture fix{std::move(cfg)};
// Launching the operation fails immediately
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::immediate);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::unix_sockets_unsupported));
// Log
fix.check_log({
{logger::level::err,
"Invalid configuration: The configuration specified a UNIX socket address, but UNIX sockets "
"are not supported by the system. [boost.redis:24]"},
});
}
#endif
void test_config_error_unix_ssl()
{
// Setup
config cfg;
cfg.use_ssl = true;
cfg.unix_socket = "/var/sock";
fixture fix{std::move(cfg)};
// Launching the operation fails immediately
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::immediate);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::unix_sockets_ssl_unsupported));
// Log
fix.check_log({
{logger::level::err,
"Invalid configuration: The configuration specified UNIX sockets with SSL, which is not "
"supported. [boost.redis:25]"},
});
}
// An error in connect with reconnection enabled triggers a reconnection
void test_connect_error()
{
// Setup
fixture fix;
// Launch the operation
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
// Connect errors. We sleep and try to connect again
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
// This time we succeed and we launch the parallel group
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// An error in connect without reconnection enabled makes the operation finish
void test_connect_error_no_reconnect()
{
// Setup
fixture fix{config_no_reconnect()};
// Launch the operation
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
// Connect errors. The operation finishes
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::connect_timeout));
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// A cancellation in connect makes the operation finish even with reconnection enabled
void test_connect_cancel()
{
// Setup
fixture fix;
// Launch the operation
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
// Connect cancelled. The operation finishes
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (1)"}
});
}
// Same, but only the cancellation is set
void test_connect_cancel_edge()
{
// Setup
fixture fix;
// Launch the operation
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
// Connect cancelled. The operation finishes
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (1)"}
});
}
// An error in the parallel group triggers a reconnection
// (the parallel group always exits with an error)
void test_parallel_group_error()
{
// Setup
fixture fix;
// Run the operation. We connect and launch the tasks
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// This exits with an error. We sleep and connect again
act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// An error in the parallel group makes the operation exit if reconnection is disabled
void test_parallel_group_error_no_reconnect()
{
// Setup
fixture fix{config_no_reconnect()};
// Run the operation. We connect and launch the tasks
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// This exits with an error. We cancel the receive operation and exit
act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, error_code(error::empty_field));
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// A cancellation in the parallel group makes it exit, even if reconnection is enabled.
// Parallel group tasks always exit with an error, so there is no edge case here
void test_parallel_group_cancel()
{
// Setup
fixture fix;
// Run the operation. We connect and launch the tasks
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// This exits because the operation gets cancelled. Any receive operation gets cancelled
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (2)"}
});
}
void test_parallel_group_cancel_no_reconnect()
{
// Setup
fixture fix{config_no_reconnect()};
// Run the operation. We connect and launch the tasks
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// This exits because the operation gets cancelled. Any receive operation gets cancelled
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (2)"}
});
}
// If the reconnection wait gets cancelled, we exit
void test_wait_cancel()
{
// Setup
fixture fix;
// Run the operation. We connect and launch the tasks
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// This exits with an error. We sleep
act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
// We get cancelled during the sleep
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (3)"}
});
}
void test_wait_cancel_edge()
{
// Setup
fixture fix;
// Run the operation. We connect and launch the tasks
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// This exits with an error. We sleep
act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
// We get cancelled during the sleep
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// We log on cancellation only
fix.check_log({
{logger::level::debug, "Run: cancelled (3)"}
});
}
void test_several_reconnections()
{
// Setup
fixture fix;
// Run the operation. Connect errors and we sleep
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
// Connect again, this time successfully. We launch the tasks
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// This exits with an error. We sleep and connect again
act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// Exit with cancellation
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
// The cancellation was logged
fix.check_log({
{logger::level::debug, "Run: cancelled (2)"}
});
}
// Setup and ping requests are only composed once at startup
void test_setup_ping_requests()
{
// Setup
config cfg;
cfg.health_check_id = "some_value";
cfg.username = "foo";
cfg.password = "bar";
cfg.clientname = "";
fixture fix{std::move(cfg)};
// Run the operation. We connect and launch the tasks
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// At this point, the requests are set up
const std::string_view expected_ping = "*2\r\n$4\r\nPING\r\n$10\r\nsome_value\r\n";
const std::string_view
expected_setup = "*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
BOOST_TEST_EQ(fix.st.ping_req.payload(), expected_ping);
BOOST_TEST_EQ(fix.st.cfg.setup.payload(), expected_setup);
// Reconnect
act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::connect);
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
BOOST_TEST_EQ(act, run_action_type::parallel_group);
// The requests haven't been modified
BOOST_TEST_EQ(fix.st.ping_req.payload(), expected_ping);
BOOST_TEST_EQ(fix.st.cfg.setup.payload(), expected_setup);
}
} // namespace
int main()
{
#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS
test_config_error_unix();
#endif
test_config_error_unix_ssl();
test_connect_error();
test_connect_error_no_reconnect();
test_connect_cancel();
test_connect_cancel_edge();
test_parallel_group_error();
test_parallel_group_error_no_reconnect();
test_parallel_group_cancel();
test_parallel_group_cancel_no_reconnect();
test_wait_cancel();
test_wait_cancel_edge();
test_several_reconnections();
test_setup_ping_requests();
return boost::report_errors();
}