Skip to content
Open
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
3 changes: 2 additions & 1 deletion docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
43 changes: 43 additions & 0 deletions docs/recipes/thread_local_connection.md
Original file line number Diff line number Diff line change
@@ -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)
48 changes: 0 additions & 48 deletions examples/connection_pool/CMakeLists.txt

This file was deleted.

15 changes: 0 additions & 15 deletions examples/connection_pool/src/db_connection.cpp

This file was deleted.

31 changes: 0 additions & 31 deletions examples/connection_pool/src/db_connection.h

This file was deleted.

12 changes: 0 additions & 12 deletions examples/connection_pool/src/db_global.cpp

This file was deleted.

7 changes: 0 additions & 7 deletions examples/connection_pool/src/db_global.h

This file was deleted.

37 changes: 0 additions & 37 deletions examples/connection_pool/src/main.cpp

This file was deleted.

1 change: 1 addition & 0 deletions tests/postgresql/recipes/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ function(create_test name)
endfunction()

create_test(optimistic_concurrency_control)
create_test(thread_local_connection)
120 changes: 120 additions & 0 deletions tests/postgresql/recipes/thread_local_connection.cpp
Original file line number Diff line number Diff line change
@@ -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 <optional>
#include <thread>
#include <vector>

#include <sqlpp23/tests/postgresql/all.h>

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<sql::pooled_connection> _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 <typename T>
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<std::thread> 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;
}