From 7f04435576036fc3f06d929760b5c7623485f069 Mon Sep 17 00:00:00 2001 From: MeanSquaredError <35379301+MeanSquaredError@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:45:21 +0300 Subject: [PATCH] Connection pools documentation and example (#533) * Add an example of using a connection pool. * Add documentation about the connection pools. Expand the documentation about multi-threading issues. --- docs/Connection-Pools.md | 111 ++++++++++++++++++ docs/Home.md | 3 +- docs/Threads.md | 17 ++- examples/connection_pool/CMakeLists.txt | 48 ++++++++ .../connection_pool/src/db_connection.cpp | 15 +++ examples/connection_pool/src/db_connection.h | 31 +++++ examples/connection_pool/src/db_global.cpp | 12 ++ examples/connection_pool/src/db_global.h | 7 ++ examples/connection_pool/src/main.cpp | 38 ++++++ 9 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 docs/Connection-Pools.md create mode 100644 examples/connection_pool/CMakeLists.txt create mode 100644 examples/connection_pool/src/db_connection.cpp create mode 100644 examples/connection_pool/src/db_connection.h create mode 100644 examples/connection_pool/src/db_global.cpp create mode 100644 examples/connection_pool/src/db_global.h create mode 100644 examples/connection_pool/src/main.cpp diff --git a/docs/Connection-Pools.md b/docs/Connection-Pools.md new file mode 100644 index 00000000..dcc98c94 --- /dev/null +++ b/docs/Connection-Pools.md @@ -0,0 +1,111 @@ +# Introduction + +SQLPP11 has support for connection pools which are centralized caches of database connections. When you need a database connection, you can fetch one from the connection pool, use the connection to make SQL +queries and when you no longer need the connection object, destroy it, usually by letting it go out of scope. When a connection object is destroyed, the actual connection to the database server is not closed, +but put in a cache instead and next time when you need a database connection object, you will be handed one that reuses a cached connection. If there are no connections is the cache, then a new connection +will be created, wrapped in a connection object and handed to you. + +## Creating connection pools + +Each connector has its own connection pool class. Currently we have + +* sqlpp::mysql::connection_pool +* sqlpp::postgresql::connection_pool +* sqlpp::sqlite3::connection_pool + +The connection pool constructors accept two parameters + +* Shared pointer to a configuration object. This is the same configuration as the one that you use when creating regular (non-pooled) database connections. +* An integer specifying the initial size of the connection cache. This cache size grows automatically when necessary, so this value is not very important as pretty much any small positive value will do. In our example below we use 5. + +In this example we create a PostgreSQL connection pool: + +``` +auto config = std::make_shared(); +config->dbname = "my_database"; +config->user = "my_user"; +config->password = "my_password"; +config->debug = true; +auto pool = sqlpp::postgresql::connection_pool{config, 5}; +``` +You can also create a pool object without a configuration and initialize it later. + +``` +auto pool = sqlpp::postgresql::connection_pool{} +.... +.... +.... +auto config = std::make_shared(); +config->dbname = "my_database"; +config->user = "my_user"; +config->password = "my_password"; +config->debug = true; +pool.initialize(config, 5); +``` +## Getting connections from the connection pool + +Once the connection pool object is established we can use the _get()_ method to fetch connections + +``` +auto db = pool.get(); +for (row : db(select(....))) { + .... +} +``` + +## Returning connections to the connection pool + +We don't really need to do anything to return the connection to the pool. Once the connection object's destructor is called the connection is not really destroyed but instead is returned automatically to the connection +pool's cache. This means that we can use the connection pool in the following way + +``` +for (row : pool.get()(select(....))) { + .... +} +``` +In the above example we fetch a connection from the pool, use it to make an SQL query and then return the connection to the pool. + +## Ensuring that connections handed out by the connection pool are valid + +Connection pools handle out connections that are either newly created or fetched from the connection cache. For connections that are fetched from the cache an optional check can be made to ensure that the connection is still active. +If the cached connection is no longer active, then it is discarded and the user is handed a newly created connection. + +The check type is specified as an optional parameter of the _get()_ method. Currently the following check types are supported: + +* **sqlpp::connection_check::none** Don't check the connection +* **sqlpp::connection_check::passive** A passive check which does not send anything to the server but just checks if the server side has already closed their side of the connection. This check type is supported only for PostgreSQL, for the other connector types it is treated as _none_. +* **sqlpp::connection_check::ping** Send a dummy request to the server to check if the connection is still alive. For MySQL connections this check uses the `mysql_ping` library function. For the other connector types +this check sends `SELECT 1` to the server. + +For example: + +``` +auto db = pool.get(sqlpp::connection_check::ping); +for (row : db(select(....))) { + .... +} +``` + +## Working around connection thread-safety issues + +Connection pools can be used to work around [thread-safety issues](Threads.md) by ensuring that no connection is used simultaneously by multiple threads. + +### Getting a new connection for each request + +One possible usage pattern is getting a new connection handle for each request. For example: + +``` +for (row : pool.get()(select(....))) { + .... +} +pool.get()(insert_into(mytable)....) +pool.get()(remove_from(mytable)....) +``` + +This usage pattern works well provided that you don't use transactions. If you use transactions then you must make sure that the transaction object and all queries inside the transaction use the same database connection. + +### Using one connection per thread + +Another usage pattern that works around the multi-threading problems is keeping a connection handle in a global thread_local object. This global object is not a real connection, but a wrapper which lazily fetches a connection from the +thread pool the first time when it is used to execute a database query. The wrapper will expose all the relevant methods and will forward them to the real connection. This way each thread can use the global wrapper as a database +connection and the thread_local storage class specifier will make sure that each thread accesses its own database connection. You can see examples/connection_pool as an example of this usage pattern. diff --git a/docs/Home.md b/docs/Home.md index 769ecfef..6fc18dc0 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -1,5 +1,5 @@ # Introduction -Let's see: +Let's see: * You know C++? * You know some SQL? * You want to use SQL in your C++ program? @@ -23,4 +23,5 @@ The following pages will tell you how to use it: * [Transactions](Transactions.md) * [Thread Safety](Threads.md) * [NULL](NULL.md) +* [Connection Pools](Connection-Pools.md) * [New Features](New-Features.md) diff --git a/docs/Threads.md b/docs/Threads.md index ca2d5e59..3ab526a8 100644 --- a/docs/Threads.md +++ b/docs/Threads.md @@ -1,10 +1,19 @@ # Thread Safety sqlpp11 aspires to have no influence on thread safety itself, but offers -no particular guarantees (PRs welcome). +no particular guarantees (PRs welcome). This means that in the general case +your program may have problems if it does one of the following + +* Creates a connection in one thread and then uses it in another thread.. +* Uses the same connection simultaneously in multiple threads. + +The exact level of thread-safety depends on the underlying client library, for +example with MySQL, PostgreSQL and SQLite3 it is generally safe to create a +connection in one thread and then use it in another thread, but attempting to +use the same connection simultaneously in multiple threads causes crashes, +lock-ups and SQL errors. The recommendation therefore is to **not share** connections between threads and to read about thread safety of the underlying database for more -information. - - +information. You may also look into [Connection Pools](Connection-Pools.md) +as a way to make SQL queries simultaneously in multiple threads. diff --git a/examples/connection_pool/CMakeLists.txt b/examples/connection_pool/CMakeLists.txt new file mode 100644 index 00000000..e65e093d --- /dev/null +++ b/examples/connection_pool/CMakeLists.txt @@ -0,0 +1,48 @@ +# 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 (Sqlpp11 REQUIRED COMPONENTS PostgreSQL) +target_link_libraries ("${APP_NAME}" PRIVATE sqlpp11::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 new file mode 100644 index 00000000..66ac8577 --- /dev/null +++ b/examples/connection_pool/src/db_connection.cpp @@ -0,0 +1,15 @@ +#include +#include + +db_connection::pq_conn& db_connection::fetch() +{ + if (m_conn_ptr == nullptr) + { + m_conn_ptr = sqlpp::compat::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 new file mode 100644 index 00000000..1306a217 --- /dev/null +++ b/examples/connection_pool/src/db_connection.h @@ -0,0 +1,31 @@ +#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 new file mode 100644 index 00000000..b5c78401 --- /dev/null +++ b/examples/connection_pool/src/db_global.cpp @@ -0,0 +1,12 @@ +#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 new file mode 100644 index 00000000..e9edebd9 --- /dev/null +++ b/examples/connection_pool/src/db_global.h @@ -0,0 +1,7 @@ +#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 new file mode 100644 index 00000000..dea21916 --- /dev/null +++ b/examples/connection_pool/src/main.cpp @@ -0,0 +1,38 @@ +#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"; + config->debug = false; + 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; +}