Skip to content

Added any_connection::connection_id #409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions include/boost/mysql/any_connection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/assert.hpp>
#include <boost/optional/optional.hpp>
#include <boost/system/result.hpp>

#include <cstddef>
#include <cstdint>
#include <memory>
#include <type_traits>
#include <utility>
Expand Down Expand Up @@ -351,6 +353,41 @@ class any_connection
*/
void set_meta_mode(metadata_mode v) noexcept { impl_.set_meta_mode(v); }

/**
* \brief Retrieves the connection id associated to the current session.
* \details
* If a session has been established, returns its associated connection id.
* If no session has been established (i.e. \ref async_connect hasn't been called yet)
* or the session has been terminated (i.e. \ref async_close has been called), an empty
* optional is returned.
*
* The connection id is a 4 byte value that uniquely identifies a client session
* at a given point in time. It can be used with the
* <a href="https://dev.mysql.com/doc/refman/8.4/en/kill.html">`KILL`</a> SQL statement
* to cancel queries and terminate connections.
*
* The server sends the connection id assigned to the current session as part of the
* handshake process. The value is stored and made available through this function.
* The same id can also be obtained by calling the
* <a
* href="https://dev.mysql.com/doc/refman/8.4/en/information-functions.html#function_connection-id">CONNECTION_ID()</a>
* SQL function. However, this function is faster and more reliable, since it does not entail
* communication with the server.
*
* This function is equivalent to the
* <a href="https://dev.mysql.com/doc/c-api/8.0/en/mysql-thread-id.html">`mysql_thread_id`</a> function
* in the C connector. This function works properly in 64-bit systems, as opposed to what
* the official docs suggest (see
* <a href="https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-5.html">this changelog</a>).
*
* It is safe to call this function while an async operation is outstanding, except for \ref async_connect
* and \ref async_close.
*
* \par Exception safety
* No-throw guarantee.
*/
boost::optional<std::uint32_t> connection_id() const noexcept { return impl_.connection_id(); }

/**
* \brief Establishes a connection to a MySQL server.
* \details
Expand Down
3 changes: 3 additions & 0 deletions include/boost/mysql/detail/connection_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@
#include <boost/mysql/detail/intermediate_handler.hpp>

#include <boost/asio/any_io_executor.hpp>
#include <boost/optional/optional.hpp>
#include <boost/system/result.hpp>

#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#include <type_traits>
Expand Down Expand Up @@ -334,6 +336,7 @@ class connection_impl
BOOST_MYSQL_DECL bool ssl_active() const;
BOOST_MYSQL_DECL bool backslash_escapes() const;
BOOST_MYSQL_DECL system::result<character_set> current_character_set() const;
BOOST_MYSQL_DECL boost::optional<std::uint32_t> connection_id() const;
BOOST_MYSQL_DECL diagnostics& shared_diag();

engine& get_engine()
Expand Down
9 changes: 9 additions & 0 deletions include/boost/mysql/impl/connection_impl.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ boost::system::result<boost::mysql::character_set> boost::mysql::detail::connect
return charset;
}

boost::optional<std::uint32_t> boost::mysql::detail::connection_impl::connection_id() const
{
const auto& data = st_->data();
if (!data.is_connected)
return {};
else
return data.connection_id;
}

boost::mysql::detail::run_pipeline_algo_params boost::mysql::detail::connection_impl::make_params_pipeline(
const pipeline_request& req,
std::vector<stage_response>& response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ struct server_hello
{
using auth_buffer_type = static_buffer<8 + 0xff>;
db_flavor server;
std::uint32_t connection_id{};
auth_buffer_type auth_plugin_data;
capabilities server_capabilities{};
string_view auth_plugin_name;
Expand Down Expand Up @@ -834,6 +835,7 @@ boost::mysql::error_code boost::mysql::detail::deserialize_server_hello_impl(
return to_error_code(err);

// Compose output
output.connection_id = pack.connection_id.value;
output.server = parse_db_version(pack.server_version.value);
output.server_capabilities = cap;
output.auth_plugin_name = pack.auth_plugin_name.value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ struct connection_state_data
// What are the connection's capabilities?
capabilities current_capabilities;

// The current connection ID. Supplied by handshake, can be used in KILL statements
std::uint32_t connection_id{};

// Used by async ops without output diagnostics params, to avoid allocations
diagnostics shared_diag;

Expand Down
3 changes: 2 additions & 1 deletion include/boost/mysql/impl/internal/sansio/handshake.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,10 @@ class handshake_algo
if (err)
return err;

// Set capabilities & db flavor
// Set capabilities, db flavor and connection ID
st.current_capabilities = negotiated_caps;
st.flavor = hello.server;
st.connection_id = hello.connection_id;

// If we're using SSL, mark the channel as secure
secure_channel_ = secure_channel_ || use_ssl(st);
Expand Down
95 changes: 95 additions & 0 deletions test/integration/test/any_connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <boost/mysql/results.hpp>
#include <boost/mysql/ssl_mode.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/with_params.hpp>

#include <boost/mysql/detail/access.hpp>
#include <boost/mysql/detail/engine_impl.hpp>
Expand All @@ -24,14 +25,19 @@

#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/local/basic_endpoint.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/optional/optional.hpp>
#include <boost/test/data/test_case.hpp>

#include <chrono>
#include <cstdint>
#include <string>

#include "test_common/create_basic.hpp"
Expand Down Expand Up @@ -381,4 +387,93 @@ BOOST_FIXTURE_TEST_CASE(immediate_completions, any_connection_fixture)

#endif

// connection_id
std::uint32_t call_connection_id(any_connection& conn)
{
results r;
conn.async_execute("SELECT CONNECTION_ID()", r, as_netresult).validate_no_error();
return static_cast<std::uint32_t>(r.rows().at(0).at(0).as_uint64());
}

BOOST_FIXTURE_TEST_CASE(connection_id, any_connection_fixture)
{
// Before connection, connection_id returns an empty optional
BOOST_TEST(conn.connection_id() == boost::optional<std::uint32_t>());

// Connect
connect();

// The returned id matches CONNECTION_ID()
auto expected_id = call_connection_id(conn);
BOOST_TEST(conn.connection_id() == boost::optional<std::uint32_t>(expected_id));

// Calling reset connection doesn't change the ID
conn.async_reset_connection(as_netresult).validate_no_error();
expected_id = call_connection_id(conn);
BOOST_TEST(conn.connection_id() == boost::optional<std::uint32_t>(expected_id));

// Close the connection
conn.async_close(as_netresult).validate_no_error();

// After session termination, connection_id returns an empty optional
BOOST_TEST(conn.connection_id() == boost::optional<std::uint32_t>());

// If we re-establish the session, we get another connection id
connect();
auto expected_id_2 = call_connection_id(conn);
BOOST_TEST(expected_id_2 != expected_id);
BOOST_TEST(conn.connection_id() == boost::optional<std::uint32_t>(expected_id_2));
}

// After a fatal error (where we didn't call async_close), re-establishing the session
// updates the connection id
// TODO: this test requires fixing https://github.com/boostorg/mysql/issues/199
// BOOST_FIXTURE_TEST_CASE(connection_id_after_error, any_connection_fixture)
// {
// // Connect
// connect();
// auto id1 = call_connection_id(conn);

// // Force a fatal error
// results r;
// asio::cancellation_signal sig;
// auto execute_result = conn.async_execute(
// "DO SLEEP(60)",
// r,
// asio::bind_cancellation_slot(sig.slot(), as_netresult)
// );
// sig.emit(asio::cancellation_type_t::terminal);
// std::move(execute_result).validate_error(asio::error::operation_aborted);

// // The id can be obtained even after the fatal error
// BOOST_TEST(conn.connection_id() == boost::optional<std::uint32_t>(id1));

// // Reconnect
// connect();
// auto id2 = call_connection_id(conn);

// // The new id can be obtained
// BOOST_TEST(conn.connection_id() == boost::optional<std::uint32_t>(id2));
// }

// It's safe to obtain the connection id while an operation is in progress
BOOST_FIXTURE_TEST_CASE(connection_id_op_in_progress, any_connection_fixture)
{
// Setup
connect();
const auto expected_id = call_connection_id(conn);

// Issue a query
results r;
auto execute_result = conn.async_execute("SELECT * FROM three_rows_table", r, as_netresult);

// While in progress, obtain the connection id. We would usually do this to
// open a new connection and run a KILL statement. We don't do it here because
// it's unreliable as a test due to race conditions between sessions in the server.
BOOST_TEST(conn.connection_id() == boost::optional<std::uint32_t>(expected_id));

// Finish
std::move(execute_result).validate_no_error();
}

BOOST_AUTO_TEST_SUITE_END()
1 change: 1 addition & 0 deletions test/unit/include/test_unit/algo_test.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class BOOST_ATTRIBUTE_NODISCARD algo_test
boost::optional<bool> is_connected;
boost::optional<detail::db_flavor> flavor;
boost::optional<detail::capabilities> current_capabilities;
boost::optional<std::uint32_t> connection_id;
boost::optional<detail::ssl_state> ssl;
boost::optional<bool> backslash_escapes;
boost::optional<character_set> current_charset;
Expand Down
3 changes: 3 additions & 0 deletions test/unit/src/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class boost::mysql::test::algo_test::state_checker
bool expected_is_connected;
detail::db_flavor expected_flavor;
detail::capabilities expected_capabilities;
std::uint32_t expected_connection_id;
detail::ssl_state expected_ssl;
bool expected_backslash_escapes;
character_set expected_charset;
Expand All @@ -150,6 +151,7 @@ class boost::mysql::test::algo_test::state_checker
expected_is_connected(changes.is_connected.value_or(st.is_connected)),
expected_flavor(changes.flavor.value_or(st.flavor)),
expected_capabilities(changes.current_capabilities.value_or(st.current_capabilities)),
expected_connection_id(changes.connection_id.value_or(st.connection_id)),
expected_ssl(changes.ssl.value_or(st.ssl)),
expected_backslash_escapes(changes.backslash_escapes.value_or(st.backslash_escapes)),
expected_charset(changes.current_charset.value_or(st.current_charset))
Expand All @@ -161,6 +163,7 @@ class boost::mysql::test::algo_test::state_checker
BOOST_TEST(st_.is_connected == expected_is_connected);
BOOST_TEST(st_.flavor == expected_flavor);
BOOST_TEST(st_.current_capabilities == expected_capabilities);
BOOST_TEST(st_.connection_id == expected_connection_id);
BOOST_TEST(st_.ssl == expected_ssl);
BOOST_TEST(st_.backslash_escapes == expected_backslash_escapes);
BOOST_TEST(st_.current_charset == expected_charset);
Expand Down
5 changes: 3 additions & 2 deletions test/unit/test/protocol/deserialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -930,8 +930,7 @@ BOOST_AUTO_TEST_CASE(deserialize_row_message_error)
{},
client_errc::incomplete_message,
""
}
// clang-format on
} // clang-format on
};

for (const auto& tc : test_cases)
Expand Down Expand Up @@ -1278,6 +1277,7 @@ BOOST_AUTO_TEST_CASE(deserialize_server_hello_impl_success)
BOOST_TEST(actual.server == db_flavor::mysql);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(actual.auth_plugin_data.to_span(), auth_plugin_data);
BOOST_TEST(actual.server_capabilities == capabilities(caps));
BOOST_TEST(actual.connection_id == 2u);
BOOST_TEST(actual.auth_plugin_name == "mysql_native_password");

// TODO: mysql8, mariadb, edge case where auth plugin length is < 13
Expand Down Expand Up @@ -1491,6 +1491,7 @@ BOOST_AUTO_TEST_CASE(deserialize_server_hello_success)
BOOST_TEST(actual.server == db_flavor::mysql);
BOOST_MYSQL_ASSERT_BUFFER_EQUALS(actual.auth_plugin_data.to_span(), auth_plugin_data);
BOOST_TEST(actual.server_capabilities == capabilities(caps));
BOOST_TEST(actual.connection_id == 2u);
BOOST_TEST(actual.auth_plugin_name == "mysql_native_password");
}

Expand Down