diff --git a/docs/recipes.md b/docs/recipes.md index a942ef4a2..6e239bffd 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -7,8 +7,9 @@ sqlpp23 can be extended to make it more powerful or easy to use in a given conte This section contains some recipes for doing so. * [Add a custom SQL function](/docs/recipes/custom_function.md) -* [Optimistic concurrency control](/docs/recipes/optimistic_concurrency_control.md) * [Mapping to/from key-value store](/docs/recipes/key_value_store.md) +* [Optimistic concurrency control](/docs/recipes/optimistic_concurrency_control.md) +* [Thread-local connection](/docs/recipes/thread_local_connection.md) Additional ideas are welcome, of course. Please file issues or pull requests. diff --git a/docs/recipes/thread_local_connection.md b/docs/recipes/thread_local_connection.md new file mode 100644 index 000000000..f2efbebc0 --- /dev/null +++ b/docs/recipes/thread_local_connection.md @@ -0,0 +1,43 @@ +[**\< Recipes**](/docs/recipes.md) + +# Thread safety in the database connectors + +As of the time of this writing, database connections created by the three main databases supported by sqlpp23 (MySQL, PostgreSQL, and SQLite3) are not thread-safe. The sqlpp23 library is thread-agnostic, which means that it does not add any requirements or guarantees to the thread safety of the underlying database objects and operations. So it is up to the library user to ensure the thread safety of the database operations performed through these database connections. + +## Making thread-safe queries using thread-unsafe connectors + +In this document we provide a simple pattern that allows us to make thread-safe database queries using thread-unsafe database connections. The pattern is based on the idea that each user thread is given its own database connection. When a thread wants to execute a database query, it uses its own database connection to execute the query, thus avoiding the need to implement complex and potentially expensive thread synchronization. + +We define a database connection class called `lazy_connection`, which mimics the regular database connections provided by sqlpp23 and lets the user execute database queries, pretty much like a regular sqlpp connection does. In fact, our lazy connection creates an underlying sqlpp database connection and forwards all database queries to the sqlpp connection, but that sqlpp connection is not +created immediately in the constructor of the lazy connection. Instead, its creation is postponed until the moment when the user tries to execute their first query through our lazy connection object, which is why our connection class is called "lazy". + +There is only one, global instance of our `lazy_connection` class, called `g_dbc`, which is defined as +``` +thread_local lazy_connection g_dbc{g_pool}; +``` + +As you can see from the definition of g_dbc, it is defined as `thread_local`, which means that each user thread gets its own copy of `g_dbc`, stored in the thread's TLS (Thread Local Storage). By using the `thread_local` keyword, we offload all the thread-related chores to the C++ compiler and runtime. When a user thread tries to execute a query through `g_dbc`, the C++ runtime automatically gets a thread-local lazy connection, creating it if necessary. The lazy connection in turn gets a new sqlpp connection from the thread pool and uses it to execute the query. The thread pool is merely an implementation detail; strictly speaking, we could skip the thread pool and create a new connection inside `lazy_connection`, but we use the connection pool for performance reasons. + +## Why not make the connection object local? + +One might be tempted to make our instance of `lazy_connection` local; after all, a local variable can also be declared as `thread_local`. So why did we make `g_dbc` local? It is because making it local does not work the way one might expect. Let's say that we try to define and use the thread-local lazy connection in block scope: +``` +sqlpp::postgresql::connection_pool g_pool{...}; + +int main() +{ + thread_local dbc{&g_pool}; + std::thread t{[&] { + dbc(...); + }}; + t.join (); +} +``` + +Attempting to use our lazy connection in this fashion will cause a runtime error, because the newly spawned thread uses an uninitialized copy of the lazy connection. While global thread-local variables are guaranteed to be initialized the moment when a thread tries to use them, the local thread-local variables are only initialized when execution passes through their definition. The newly spawned thread never actually entered the `main()` function, so its thread-local copy of the database connection was never initialized, and the attempt to use the uninitialized lazy connection caused the runtime error. + +## Sample code + +The sample source code, implementing this pattern, is available [here](/tests/postgresql/recipes/thread_local_connection.cpp). + +[**\< Recipes**](/docs/recipes.md) diff --git a/examples/connection_pool/CMakeLists.txt b/examples/connection_pool/CMakeLists.txt deleted file mode 100644 index 2c9547672..000000000 --- a/examples/connection_pool/CMakeLists.txt +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) 2023, Vesselin Atanasov -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -cmake_minimum_required(VERSION 3.14) -set (APP_NAME "connection_pool") -project ("${APP_NAME}" CXX) -set (CMAKE_CXX_STANDARD 11) -set (CMAKE_CXX_STANDARD_REQUIRED true) -set (CMAKE_CXX_EXTENSIONS false) - -# Executable file and its build settings -add_executable ("${APP_NAME}") -target_include_directories ("${APP_NAME}" PRIVATE "${GEN_HEADERS_DIR}" "${PROJECT_SOURCE_DIR}/src") - -# Linked libraries -find_package (Sqlpp23 REQUIRED COMPONENTS PostgreSQL) -target_link_libraries ("${APP_NAME}" PRIVATE sqlpp23::core sqlpp23::postgresql) - -# Project sources -target_sources ( - "${APP_NAME}" PRIVATE - "src/db_connection.h" - "src/db_connection.cpp" - "src/db_global.h" - "src/db_global.cpp" - "src/main.cpp" -) diff --git a/examples/connection_pool/src/db_connection.cpp b/examples/connection_pool/src/db_connection.cpp deleted file mode 100644 index 0fab1e53f..000000000 --- a/examples/connection_pool/src/db_connection.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include -#include - -db_connection::pq_conn& db_connection::fetch() -{ - if (m_conn_ptr == nullptr) - { - m_conn_ptr = std::make_unique(m_pool.get()); - } - return *m_conn_ptr; -} - -db_connection::db_connection(sqlpp::postgresql::connection_pool& pool) : m_pool{pool}, m_conn_ptr{nullptr} -{ -} diff --git a/examples/connection_pool/src/db_connection.h b/examples/connection_pool/src/db_connection.h deleted file mode 100644 index 7bd03e5fc..000000000 --- a/examples/connection_pool/src/db_connection.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include - -class db_connection -{ -private: - using pq_conn = sqlpp::postgresql::pooled_connection; - - sqlpp::postgresql::connection_pool& m_pool; - // For C++17 or newer just use std::optional m_conn; - std::unique_ptr m_conn_ptr; - - pq_conn& fetch(); - -public: - db_connection(sqlpp::postgresql::connection_pool& pool); - db_connection(const db_connection&) = delete; - db_connection(db_connection&&) = delete; - - db_connection& operator=(const db_connection&) = delete; - db_connection& operator=(db_connection&&) = delete; - - // Delegate any methods of sqlpp::postgresql::connection that you may need - - template - auto operator()(const T& t) -> decltype(fetch()(t)) - { - return fetch()(t); - } -}; diff --git a/examples/connection_pool/src/db_global.cpp b/examples/connection_pool/src/db_global.cpp deleted file mode 100644 index 3d84f7758..000000000 --- a/examples/connection_pool/src/db_global.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include - -#include - -static sqlpp::postgresql::connection_pool g_db_pool{}; - -thread_local db_connection g_dbc{g_db_pool}; - -void db_global_init(std::shared_ptr config) -{ - g_db_pool.initialize(config, 5); -} diff --git a/examples/connection_pool/src/db_global.h b/examples/connection_pool/src/db_global.h deleted file mode 100644 index e9edebd92..000000000 --- a/examples/connection_pool/src/db_global.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include - -extern thread_local db_connection g_dbc; - -void db_global_init(std::shared_ptr config); diff --git a/examples/connection_pool/src/main.cpp b/examples/connection_pool/src/main.cpp deleted file mode 100644 index 4efd8cdd8..000000000 --- a/examples/connection_pool/src/main.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include - -#include -#include - -#include -#include - -int main() -{ - // Initialize the global connection variable - auto config = std::make_shared(); - config->dbname = "my_database"; - config->user = "my_username"; - config->password = "my_password"; - db_global_init(config); - - // Spawn 10 threads and make them send SQL queries in parallel - int num_threads = 10; - int num_queries = 5; - std::vector threads {}; - for (int i = 0; i < num_threads; ++i) - { - threads.push_back(std::thread([&] () { - for (int j = 0; j < num_queries; ++j) - { - g_dbc(select (sqlpp::value (1).as(sqlpp::alias::a))); - } - })); - } - for (auto&& t : threads) - { - t.join(); - } - - return 0; -} diff --git a/tests/postgresql/recipes/CMakeLists.txt b/tests/postgresql/recipes/CMakeLists.txt index 6c9c1fc27..db2baa9dd 100644 --- a/tests/postgresql/recipes/CMakeLists.txt +++ b/tests/postgresql/recipes/CMakeLists.txt @@ -30,3 +30,4 @@ function(create_test name) endfunction() create_test(optimistic_concurrency_control) +create_test(thread_local_connection) diff --git a/tests/postgresql/recipes/thread_local_connection.cpp b/tests/postgresql/recipes/thread_local_connection.cpp new file mode 100644 index 000000000..4a93f6333 --- /dev/null +++ b/tests/postgresql/recipes/thread_local_connection.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023-2026, Vesselin Atanasov + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +// A sample program demonstrating how to create and use a thread-safe connection +// using a lazy database connection, connection pool and thread-local storage. +// +// For details on the actual pattern see +// /docs/recipes/thread_local_connection.md + +#include +#include +#include + +#include + +namespace sql = ::sqlpp::postgresql; + +// This is our main database connection class. It mimics a regular database +// connection, while delegating the execution of SQL queries to an underlying +// sqlpp23 database connection. This underlying database connections is not +// created immediately upon construction of our lazy connection. Instead the +// underlying database connection is created the first time when the user +// tries to execute a database query through operator(). +// +// The class constructor received a reference to a connection pool, which later +// is used to get the underlying database connection. When the constructor is +// called, the connection pool does not have to be fully initialized, because +// the constructor does not use the connection pool and just stores the +// reference to it for later use. +// +class lazy_connection { + private: + sql::connection_pool& _pool; + std::optional _dbc; + + public: + lazy_connection(sql::connection_pool& pool) : _pool{pool}, _dbc{} {} + lazy_connection(const lazy_connection&) = delete; + lazy_connection(lazy_connection&&) = delete; + + lazy_connection& operator=(const lazy_connection&) = delete; + lazy_connection& operator=(lazy_connection&&) = delete; + + // Delegate to _dbc any methods of sql::connection that you may need + // In our example the only delegated method is operator() + + template + auto operator()(const T& t) { + if (!_dbc) { + _dbc = _pool.get(); + } + return (*_dbc)(t); + } +}; + +sql::connection_pool g_pool{}; + +// This is our lazy connection object, which we use to execute SQL queries. +// It is marked with the thread_local storage class specifier, which means +// that the C++ runtime creates one instance of the object per thread, each +// instance having its own underlying database connection. +// +// We don't really care about the order in which the connection pool and +// the global connection object are initialized because, as described above, +// the constructor of the lazy connection only stores a reference to the pool +// without actually using it. +// +thread_local lazy_connection g_dbc{g_pool}; + +int main() { + const int num_threads = 5; + const int num_queries = 10; + + // Initialize the global connection pool + g_pool.initialize(sql::make_test_config(), num_threads); + + test::createTabBar(g_dbc); + test::TabBar tb{}; + // Spawn the threads and make each thread execute multiple SQL queries + std::vector threads{}; + for (int i = 0; i < num_threads; ++i) { + threads.push_back(std::thread([&]() { + for (int j = 0; j < num_queries; ++j) { + // For simplicity we don't begin/commit a transaction explicitly. + // Instead we use the database autocommit mode in which the database + // engine wraps each quety in its own transaction. + // + g_dbc(insert_into(tb).set(tb.intN = i * j)); + } + })); + } + for (auto&& t : threads) { + t.join(); + } + + return 0; +}