修复: 升级框架并完善报告导出

- 升级 Drogon 和 Trantor,修复畸形请求导致的连接计数泄漏\n- 增加第三方框架版本校验与自动重建\n- 完善完整报告导出和接口文档
This commit is contained in:
cloud
2026-08-10 09:50:09 +08:00
parent 99ed321d24
commit 0e28826073
82 changed files with 3095 additions and 566 deletions
@@ -67,6 +67,69 @@ struct Filter
std::string value;
};
/**
* @brief Represents a SQL JOIN clause.
*/
enum class JoinType
{
InnerJoin,
LeftJoin,
RightJoin,
FullJoin
};
inline std::string to_join_string(JoinType type)
{
switch (type)
{
case JoinType::InnerJoin:
return "INNER JOIN";
case JoinType::LeftJoin:
return "LEFT JOIN";
case JoinType::RightJoin:
return "RIGHT JOIN";
case JoinType::FullJoin:
return "FULL JOIN";
}
// Should never reach here
return "INNER JOIN";
}
struct JoinClause
{
JoinType type;
std::string table;
std::string onLeft; // e.g. "users.id"
std::string onRight; // e.g. "posts.user_id"
};
/**
* @brief Validate that a string is a safe SQL identifier.
*
* Only allows alphanumeric characters, underscores, and dots
* (for table.column notation). This prevents SQL injection when
* building JOIN clauses from user-provided identifiers.
*
* @param identifier The identifier to validate.
* @return true if the identifier is safe to use in SQL.
*/
inline bool isValidSqlIdentifier(const std::string &identifier)
{
if (identifier.empty())
{
return false;
}
for (auto c : identifier)
{
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_' &&
c != '.')
{
return false;
}
}
return true;
}
// Forward declaration to be a friend
template <typename T, bool SelectAll, bool Single = false>
class TransformBuilder;
@@ -87,6 +150,7 @@ class BaseBuilder
std::string from_;
std::string columns_;
std::vector<Filter> filters_;
std::vector<JoinClause> joins_;
std::optional<std::uint64_t> limit_;
std::optional<std::uint64_t> offset_;
// The order is important; use vector<pair> instead of unordered_map and
@@ -122,6 +186,11 @@ class BaseBuilder
};
std::string sql = "select " + columns_ + " from " + from_;
for (const auto &join : joins_)
{
sql += " " + to_join_string(join.type) + " " + join.table + " ON " +
join.onLeft + " = " + join.onRight;
}
if (!filters_.empty())
{
sql += " where " + filters_[0].column + " " +
@@ -211,6 +211,54 @@ class CoroMapper : public Mapper<T>
return *this;
}
/**
* @brief Add an INNER JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return CoroMapper<T>& The CoroMapper itself.
*/
CoroMapper<T> &innerJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
Mapper<T>::innerJoin(table, onLeft, onRight);
return *this;
}
/**
* @brief Add a LEFT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return CoroMapper<T>& The CoroMapper itself.
*/
CoroMapper<T> &leftJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
Mapper<T>::leftJoin(table, onLeft, onRight);
return *this;
}
/**
* @brief Add a RIGHT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return CoroMapper<T>& The CoroMapper itself.
*/
CoroMapper<T> &rightJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
Mapper<T>::rightJoin(table, onLeft, onRight);
return *this;
}
// Read api for coroutines
inline internal::MapperAwaiter<std::vector<T>> findAll()
@@ -225,6 +273,7 @@ class CoroMapper : public Mapper<T>
ExceptPtrCallback &&errCallback) {
std::string sql = "select count(*) from ";
sql += T::tableName;
sql += this->joinString_;
if (criteria)
{
sql += " where ";
@@ -250,6 +299,7 @@ class CoroMapper : public Mapper<T>
ExceptPtrCallback &&errCallback) {
std::string sql = "select * from ";
sql += T::tableName;
sql += this->joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -311,6 +361,7 @@ class CoroMapper : public Mapper<T>
ExceptPtrCallback &&errCallback) {
std::string sql = "select * from ";
sql += T::tableName;
sql += this->joinString_;
bool hasParameters = false;
if (criteria)
{
+41 -6
View File
@@ -43,6 +43,15 @@ using ExceptionCallback = std::function<void(const DrogonDbException &)>;
class Transaction;
class DbClient;
/// Transaction locking mode.
enum class TransactionType
{
Deferred, ///< BEGIN — lock acquired on first write (default)
Immediate, ///< BEGIN IMMEDIATE — write lock acquired upfront (SQLite only)
Exclusive, ///< BEGIN EXCLUSIVE — exclusive lock acquired upfront (SQLite
///< only)
};
namespace internal
{
#ifdef __cpp_impl_coroutine
@@ -73,7 +82,10 @@ struct [[nodiscard]] SqlAwaiter : public CallbackAwaiter<Result>
struct [[nodiscard]] TransactionAwaiter
: public CallbackAwaiter<std::shared_ptr<Transaction> >
{
explicit TransactionAwaiter(DbClient *client) : client_(client)
explicit TransactionAwaiter(
DbClient *client,
TransactionType transType = TransactionType::Deferred)
: client_(client), transType_(transType)
{
}
@@ -81,6 +93,7 @@ struct [[nodiscard]] TransactionAwaiter
private:
DbClient *client_;
TransactionType transType_;
};
#endif
@@ -269,7 +282,16 @@ class DROGON_EXPORT DbClient : public trantor::NonCopyable
*/
virtual std::shared_ptr<Transaction> newTransaction(
const std::function<void(bool)> &commitCallback =
std::function<void(bool)>()) noexcept(false) = 0;
std::function<void(bool)>(),
TransactionType transType =
TransactionType::Deferred) noexcept(false) = 0;
/// Convenience overload: create a transaction with a specific locking mode.
std::shared_ptr<Transaction> newTransaction(
TransactionType transType) noexcept(false)
{
return newTransaction(std::function<void(bool)>(), transType);
}
/// Create a transaction object in asynchronous mode.
/**
@@ -278,12 +300,24 @@ class DROGON_EXPORT DbClient : public trantor::NonCopyable
*/
virtual void newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)>
&callback) = 0;
&callback,
TransactionType transType = TransactionType::Deferred) = 0;
/// Convenience overload: create an async transaction with a specific
/// locking mode, with transType as the first argument.
void newTransactionAsync(
TransactionType transType,
const std::function<void(const std::shared_ptr<Transaction> &)>
&callback)
{
newTransactionAsync(callback, transType);
}
#ifdef __cpp_impl_coroutine
orm::internal::TransactionAwaiter newTransactionCoro()
orm::internal::TransactionAwaiter newTransactionCoro(
TransactionType transType = TransactionType::Deferred)
{
return orm::internal::TransactionAwaiter(this);
return orm::internal::TransactionAwaiter(this, transType);
}
#endif
@@ -408,7 +442,8 @@ inline void internal::TransactionAwaiter::await_suspend(
else
setValue(transaction);
handle.resume();
});
},
transType_);
}
#endif
@@ -157,6 +157,90 @@ class FilterBuilder : public TransformBuilder<T, SelectAll, false>
this->filters_.push_back({column, CompareOperator::Like, pattern});
return *this;
}
/**
* @brief Add an INNER JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of the ON condition (e.g. "users.id").
* @param onRight The right side of the ON condition (e.g.
* "posts.user_id").
*
* @return FilterBuilder& The FilterBuilder itself.
*/
inline FilterBuilder &innerJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
this->joins_.push_back({JoinType::InnerJoin, table, onLeft, onRight});
return *this;
}
/**
* @brief Add a LEFT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of the ON condition (e.g. "users.id").
* @param onRight The right side of the ON condition (e.g.
* "posts.user_id").
*
* @return FilterBuilder& The FilterBuilder itself.
*/
inline FilterBuilder &leftJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
this->joins_.push_back({JoinType::LeftJoin, table, onLeft, onRight});
return *this;
}
/**
* @brief Add a RIGHT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of the ON condition (e.g. "users.id").
* @param onRight The right side of the ON condition (e.g.
* "posts.user_id").
*
* @return FilterBuilder& The FilterBuilder itself.
*/
inline FilterBuilder &rightJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
this->joins_.push_back({JoinType::RightJoin, table, onLeft, onRight});
return *this;
}
/**
* @brief Add a FULL JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of the ON condition (e.g. "users.id").
* @param onRight The right side of the ON condition (e.g.
* "posts.user_id").
*
* @return FilterBuilder& The FilterBuilder itself.
*/
inline FilterBuilder &fullJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
this->joins_.push_back({JoinType::FullJoin, table, onLeft, onRight});
return *this;
}
};
} // namespace orm
} // namespace drogon
+90 -3
View File
@@ -14,6 +14,7 @@
#pragma once
#include <drogon/orm/Criteria.h>
#include <drogon/orm/BaseBuilder.h>
#include <drogon/orm/DbClient.h>
#include <drogon/utils/Utilities.h>
#include <string>
@@ -178,6 +179,78 @@ class Mapper
*/
Mapper<T> &forUpdate();
/**
* @brief Add an INNER JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return Mapper<T>& The Mapper itself.
*/
Mapper<T> &innerJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
joinString_ += " INNER JOIN ";
joinString_ += table;
joinString_ += " ON ";
joinString_ += onLeft;
joinString_ += " = ";
joinString_ += onRight;
return *this;
}
/**
* @brief Add a LEFT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return Mapper<T>& The Mapper itself.
*/
Mapper<T> &leftJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
joinString_ += " LEFT JOIN ";
joinString_ += table;
joinString_ += " ON ";
joinString_ += onLeft;
joinString_ += " = ";
joinString_ += onRight;
return *this;
}
/**
* @brief Add a RIGHT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return Mapper<T>& The Mapper itself.
*/
Mapper<T> &rightJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
assert(isValidSqlIdentifier(table));
assert(isValidSqlIdentifier(onLeft));
assert(isValidSqlIdentifier(onRight));
joinString_ += " RIGHT JOIN ";
joinString_ += table;
joinString_ += " ON ";
joinString_ += onLeft;
joinString_ += " = ";
joinString_ += onRight;
return *this;
}
using SingleRowCallback = std::function<void(T)>;
using MultipleRowsCallback = std::function<void(std::vector<T>)>;
using CountCallback = std::function<void(const size_t)>;
@@ -719,6 +792,7 @@ class Mapper
size_t limit_{0};
size_t offset_{0};
std::string orderByString_;
std::string joinString_;
bool forUpdate_{false};
void clear()
@@ -726,6 +800,7 @@ class Mapper
limit_ = 0;
offset_ = 0;
orderByString_.clear();
joinString_.clear();
forUpdate_ = false;
}
@@ -792,6 +867,7 @@ inline T Mapper<T>::findOne(const Criteria &criteria) noexcept(false)
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -849,6 +925,7 @@ inline void Mapper<T>::findOne(const Criteria &criteria,
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -904,6 +981,7 @@ inline std::future<T> Mapper<T>::findFutureOne(
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -964,6 +1042,7 @@ inline std::vector<T> Mapper<T>::findBy(const Criteria &criteria) noexcept(
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -1003,9 +1082,10 @@ inline std::vector<T> Mapper<T>::findBy(const Criteria &criteria) noexcept(
binder.exec(); // exec may be throw exception;
}
std::vector<T> ret;
ret.reserve(r.size());
for (auto const &row : r)
{
ret.push_back(T(row));
ret.emplace_back(row);
}
return ret;
}
@@ -1017,6 +1097,7 @@ inline void Mapper<T>::findBy(const Criteria &criteria,
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -1051,6 +1132,7 @@ inline void Mapper<T>::findBy(const Criteria &criteria,
clear();
binder >> [rcb](const Result &r) {
std::vector<T> ret;
ret.reserve(r.size());
for (auto const &row : r)
{
ret.emplace_back(row);
@@ -1066,6 +1148,7 @@ inline std::future<std::vector<T>> Mapper<T>::findFutureBy(
{
std::string sql = "select * from ";
sql += T::tableName;
sql += joinString_;
bool hasParameters = false;
if (criteria)
{
@@ -1102,11 +1185,12 @@ inline std::future<std::vector<T>> Mapper<T>::findFutureBy(
std::make_shared<std::promise<std::vector<T>>>();
binder >> [prom](const Result &r) {
std::vector<T> ret;
ret.reserve(r.size());
for (auto const &row : r)
{
ret.push_back(T(row));
ret.emplace_back(row);
}
prom->set_value(ret);
prom->set_value(std::move(ret));
};
binder >> [prom](const std::exception_ptr &e) { prom->set_exception(e); };
binder.exec();
@@ -1137,6 +1221,7 @@ inline size_t Mapper<T>::count(const Criteria &criteria) noexcept(false)
{
std::string sql = "select count(*) from ";
sql += T::tableName;
sql += joinString_;
if (criteria)
{
sql += " where ";
@@ -1164,6 +1249,7 @@ inline void Mapper<T>::count(const Criteria &criteria,
{
std::string sql = "select count(*) from ";
sql += T::tableName;
sql += joinString_;
if (criteria)
{
sql += " where ";
@@ -1187,6 +1273,7 @@ inline std::future<size_t> Mapper<T>::countFuture(
{
std::string sql = "select count(*) from ";
sql += T::tableName;
sql += joinString_;
if (criteria)
{
sql += " where ";
+26 -13
View File
@@ -197,7 +197,8 @@ void DbClientImpl::execSql(
}
void DbClientImpl::newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)> &callback)
const std::function<void(const std::shared_ptr<Transaction> &)> &callback,
TransactionType transType)
{
DbConnectionPtr conn;
{
@@ -231,7 +232,7 @@ void DbClientImpl::newTransactionAsync(
iter != transCallbacks_.end();
++iter)
{
if (cbPtr == *iter)
if (cbPtr == iter->first)
{
transCallbacks_.erase(iter);
break;
@@ -251,24 +252,29 @@ void DbClientImpl::newTransactionAsync(
(*newCallbackPtr) = callbackPtr;
timeoutFlagPtr->runTimer();
}
transCallbacks_.push_back(callbackPtr);
transCallbacks_.push_back({callbackPtr, transType});
}
}
if (conn)
{
makeTrans(conn,
std::function<void(const std::shared_ptr<Transaction> &)>(
callback));
callback),
transType);
}
}
void DbClientImpl::makeTrans(
const DbConnectionPtr &conn,
std::function<void(const std::shared_ptr<Transaction> &)> &&callback)
std::function<void(const std::shared_ptr<Transaction> &)> &&callback,
TransactionType transType)
{
std::weak_ptr<DbClientImpl> weakThis = shared_from_this();
auto trans = std::make_shared<TransactionImpl>(
type_, conn, std::function<void(bool)>(), [weakThis, conn]() {
type_,
conn,
std::function<void(bool)>(),
[weakThis, conn]() {
auto thisPtr = weakThis.lock();
if (!thisPtr)
return;
@@ -306,7 +312,8 @@ void DbClientImpl::makeTrans(
});
thisPtr->handleNewTask(conn);
});
});
},
transType);
trans->doBegin();
if (timeout_ > 0.0)
{
@@ -317,13 +324,16 @@ void DbClientImpl::makeTrans(
}
std::shared_ptr<Transaction> DbClientImpl::newTransaction(
const std::function<void(bool)> &commitCallback) noexcept(false)
const std::function<void(bool)> &commitCallback,
TransactionType transType) noexcept(false)
{
std::promise<std::shared_ptr<Transaction>> pro;
auto f = pro.get_future();
newTransactionAsync([&pro](const std::shared_ptr<Transaction> &trans) {
pro.set_value(trans);
});
newTransactionAsync(
[&pro](const std::shared_ptr<Transaction> &trans) {
pro.set_value(trans);
},
transType);
auto trans = f.get();
if (!trans)
{
@@ -336,12 +346,15 @@ std::shared_ptr<Transaction> DbClientImpl::newTransaction(
void DbClientImpl::handleNewTask(const DbConnectionPtr &connPtr)
{
std::function<void(const std::shared_ptr<Transaction> &)> transCallback;
TransactionType transType{TransactionType::Deferred};
std::shared_ptr<SqlCmd> cmd;
{
std::lock_guard<std::mutex> guard(connectionsMutex_);
if (!transCallbacks_.empty())
{
transCallback = std::move(*(transCallbacks_.front()));
auto &entry = transCallbacks_.front();
transCallback = std::move(*entry.first);
transType = entry.second;
transCallbacks_.pop_front();
}
else if (!sqlCmdBuffer_.empty())
@@ -358,7 +371,7 @@ void DbClientImpl::handleNewTask(const DbConnectionPtr &connPtr)
}
if (transCallback)
{
makeTrans(connPtr, std::move(transCallback));
makeTrans(connPtr, std::move(transCallback), transType);
return;
}
if (cmd)
+12 -6
View File
@@ -52,10 +52,13 @@ class DbClientImpl : public DbClient,
&&exceptCallback) override;
std::shared_ptr<Transaction> newTransaction(
const std::function<void(bool)> &commitCallback =
std::function<void(bool)>()) noexcept(false) override;
std::function<void(bool)>(),
TransactionType transType =
TransactionType::Deferred) noexcept(false) override;
void newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)>
&callback) override;
&callback,
TransactionType transType = TransactionType::Deferred) override;
bool hasAvailableConnections() const noexcept override;
void setTimeout(double timeout) override
@@ -78,16 +81,19 @@ class DbClientImpl : public DbClient,
void makeTrans(
const DbConnectionPtr &conn,
std::function<void(const std::shared_ptr<Transaction> &)> &&callback);
std::function<void(const std::shared_ptr<Transaction> &)> &&callback,
TransactionType transType = TransactionType::Deferred);
mutable std::mutex connectionsMutex_;
std::unordered_set<DbConnectionPtr> connections_;
std::unordered_set<DbConnectionPtr> readyConnections_;
std::unordered_set<DbConnectionPtr> busyConnections_;
std::list<std::shared_ptr<
std::function<void(const std::shared_ptr<Transaction> &)>>>
transCallbacks_;
using TransCallbackEntry =
std::pair<std::shared_ptr<std::function<void(
const std::shared_ptr<Transaction> &)>>,
TransactionType>;
std::list<TransCallbackEntry> transCallbacks_;
std::deque<std::shared_ptr<SqlCmd>> sqlCmdBuffer_;
+24 -12
View File
@@ -230,7 +230,8 @@ void DbClientLockFree::execSql(
}
std::shared_ptr<Transaction> DbClientLockFree::newTransaction(
const std::function<void(bool)> &) noexcept(false)
const std::function<void(bool)> &,
TransactionType) noexcept(false)
{
// Don't support transaction;
LOG_ERROR
@@ -241,7 +242,8 @@ std::shared_ptr<Transaction> DbClientLockFree::newTransaction(
}
void DbClientLockFree::newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)> &callback)
const std::function<void(const std::shared_ptr<Transaction> &)> &callback,
TransactionType transType)
{
loop_->assertInLoopThread();
for (auto &conn : connections_)
@@ -250,7 +252,8 @@ void DbClientLockFree::newTransactionAsync(
{
makeTrans(conn,
std::function<void(const std::shared_ptr<Transaction> &)>(
callback));
callback),
transType);
return;
}
}
@@ -272,7 +275,7 @@ void DbClientLockFree::newTransactionAsync(
iter != transCallbacks_.end();
++iter)
{
if (cbPtr == *iter)
if (cbPtr == iter->first)
{
transCallbacks_.erase(iter);
break;
@@ -292,16 +295,20 @@ void DbClientLockFree::newTransactionAsync(
*newCallbackPtr = callbackPtr;
timeoutFlagPtr->runTimer();
}
transCallbacks_.push_back(callbackPtr);
transCallbacks_.push_back({callbackPtr, transType});
}
void DbClientLockFree::makeTrans(
const DbConnectionPtr &conn,
std::function<void(const std::shared_ptr<Transaction> &)> &&callback)
std::function<void(const std::shared_ptr<Transaction> &)> &&callback,
TransactionType transType)
{
std::weak_ptr<DbClientLockFree> weakThis = shared_from_this();
auto trans = std::make_shared<TransactionImpl>(
type_, conn, std::function<void(bool)>(), [weakThis, conn]() {
type_,
conn,
std::function<void(bool)>(),
[weakThis, conn]() {
auto thisPtr = weakThis.lock();
if (!thisPtr)
return;
@@ -312,9 +319,11 @@ void DbClientLockFree::makeTrans(
}
if (!thisPtr->transCallbacks_.empty())
{
auto callback = std::move(thisPtr->transCallbacks_.front());
auto &entry = thisPtr->transCallbacks_.front();
auto nextCallback = std::move(*entry.first);
auto nextType = entry.second;
thisPtr->transCallbacks_.pop_front();
thisPtr->makeTrans(conn, std::move(*callback));
thisPtr->makeTrans(conn, std::move(nextCallback), nextType);
return;
}
@@ -342,7 +351,8 @@ void DbClientLockFree::makeTrans(
break;
}
}
});
},
transType);
transSet_.insert(conn);
trans->doBegin();
if (timeout_ > 0.0)
@@ -360,9 +370,11 @@ void DbClientLockFree::handleNewTask(const DbConnectionPtr &conn)
if (!transCallbacks_.empty())
{
auto callback = std::move(transCallbacks_.front());
auto &entry = transCallbacks_.front();
auto callback = std::move(*entry.first);
auto transType = entry.second;
transCallbacks_.pop_front();
makeTrans(conn, std::move(*callback));
makeTrans(conn, std::move(callback), transType);
return;
}
+12 -6
View File
@@ -55,10 +55,13 @@ class DbClientLockFree : public DbClient,
&&exceptCallback) override;
std::shared_ptr<Transaction> newTransaction(
const std::function<void(bool)> &commitCallback =
std::function<void(bool)>()) noexcept(false) override;
std::function<void(bool)>(),
TransactionType transType =
TransactionType::Deferred) noexcept(false) override;
void newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)>
&callback) override;
&callback,
TransactionType transType = TransactionType::Deferred) override;
bool hasAvailableConnections() const noexcept override;
void setTimeout(double timeout) override
@@ -78,15 +81,18 @@ class DbClientLockFree : public DbClient,
std::unordered_set<DbConnectionPtr> transSet_;
std::deque<std::shared_ptr<SqlCmd>> sqlCmdBuffer_;
std::list<std::shared_ptr<
std::function<void(const std::shared_ptr<Transaction> &)>>>
transCallbacks_;
using TransCallbackEntry =
std::pair<std::shared_ptr<std::function<void(
const std::shared_ptr<Transaction> &)>>,
TransactionType>;
std::list<TransCallbackEntry> transCallbacks_;
double timeout_{-1.0};
void makeTrans(
const DbConnectionPtr &conn,
std::function<void(const std::shared_ptr<Transaction> &)> &&callback);
std::function<void(const std::shared_ptr<Transaction> &)> &&callback,
TransactionType transType = TransactionType::Deferred);
void execSqlWithTimeout(
const char *sql,
size_t sqlLength,
+53 -19
View File
@@ -23,11 +23,13 @@ using namespace drogon;
TransactionImpl::TransactionImpl(ClientType type,
const DbConnectionPtr &connPtr,
std::function<void(bool)> commitCallback,
std::function<void()> usedUpCallback)
std::function<void()> usedUpCallback,
TransactionType transType)
: connectionPtr_(connPtr),
usedUpCallback_(std::move(usedUpCallback)),
loop_(connPtr->loop()),
commitCallback_(std::move(commitCallback))
commitCallback_(std::move(commitCallback)),
transactionType_(transType)
{
type_ = type;
}
@@ -203,6 +205,8 @@ void TransactionImpl::execNewTask()
{
loop_->assertInLoopThread();
thisPtr_.reset();
if (!isWorking_)
return;
assert(isWorking_);
if (!isCommitedOrRolledback_)
{
@@ -244,27 +248,51 @@ void TransactionImpl::execNewTask()
else
{
isWorking_ = false;
if (!sqlCmdBuffer_.empty())
failBufferedCommands(std::make_exception_ptr(
TransactionRollback("The transaction has been rolled back")));
releaseConnection();
}
}
void TransactionImpl::releaseConnection()
{
if (usedUpCallback_)
{
usedUpCallback_();
usedUpCallback_ = std::function<void()>();
}
}
void TransactionImpl::failBufferedCommands(const std::exception_ptr &ePtr)
{
std::list<SqlCmdPtr> pendingCmds;
pendingCmds.swap(sqlCmdBuffer_);
for (auto &cmd : pendingCmds)
{
cmd->thisPtr_.reset();
if (cmd->exceptionCallback_)
{
auto exceptPtr = std::make_exception_ptr(
TransactionRollback("The transaction has been rolled back"));
for (auto const &cmd : sqlCmdBuffer_)
{
if (cmd->exceptionCallback_)
{
cmd->exceptionCallback_(exceptPtr);
}
}
sqlCmdBuffer_.clear();
}
if (usedUpCallback_)
{
usedUpCallback_();
usedUpCallback_ = std::function<void()>();
cmd->exceptionCallback_(ePtr);
}
}
}
const char *TransactionImpl::beginSql() const noexcept
{
if (type_ != ClientType::Sqlite3)
return "begin";
switch (transactionType_)
{
case TransactionType::Immediate:
return "begin immediate";
case TransactionType::Exclusive:
return "begin exclusive";
default:
return "begin";
}
}
void TransactionImpl::doBegin()
{
loop_->queueInLoop([thisPtr = shared_from_this()]() {
@@ -280,7 +308,7 @@ void TransactionImpl::doBegin()
thisPtr->isWorking_ = true;
thisPtr->thisPtr_ = thisPtr;
thisPtr->connectionPtr_->execSql(
"begin",
thisPtr->beginSql(),
0,
{},
{},
@@ -289,6 +317,12 @@ void TransactionImpl::doBegin()
[thisPtr](const std::exception_ptr &) {
LOG_ERROR << "Error occurred in transaction begin";
thisPtr->isCommitedOrRolledback_ = true;
thisPtr->isWorking_ = false;
thisPtr->thisPtr_.reset();
thisPtr->failBufferedCommands(std::make_exception_ptr(
TransactionRollback("Transaction begin failed, cannot "
"execute queued SQL")));
thisPtr->releaseConnection();
});
});
}
+10 -3
View File
@@ -30,7 +30,8 @@ class TransactionImpl : public Transaction,
TransactionImpl(ClientType type,
const DbConnectionPtr &connPtr,
std::function<void(bool)> commitCallback,
std::function<void()> usedUpCallback);
std::function<void()> usedUpCallback,
TransactionType transType = TransactionType::Deferred);
~TransactionImpl() override;
void rollback() override;
@@ -113,14 +114,16 @@ class TransactionImpl : public Transaction,
std::function<void(const std::exception_ptr &)> &&exceptCallback);
std::shared_ptr<Transaction> newTransaction(
const std::function<void(bool)> &) noexcept(false) override
const std::function<void(bool)> &,
TransactionType) noexcept(false) override
{
return shared_from_this();
}
void newTransactionAsync(
const std::function<void(const std::shared_ptr<Transaction> &)>
&callback) override
&callback,
TransactionType) override
{
callback(shared_from_this());
}
@@ -129,6 +132,8 @@ class TransactionImpl : public Transaction,
bool isCommitedOrRolledback_{false};
bool isWorking_{false};
void execNewTask();
void releaseConnection();
void failBufferedCommands(const std::exception_ptr &ePtr);
struct SqlCmd
{
@@ -149,10 +154,12 @@ class TransactionImpl : public Transaction,
friend class DbClientImpl;
friend class DbClientLockFree;
void doBegin();
const char *beginSql() const noexcept;
trantor::EventLoop *loop_;
std::function<void(bool)> commitCallback_;
std::shared_ptr<TransactionImpl> thisPtr_;
double timeout_{-1.0};
TransactionType transactionType_{TransactionType::Deferred};
};
} // namespace orm
} // namespace drogon
+185 -6
View File
@@ -2741,11 +2741,9 @@ DROGON_TEST(MySQLTest)
#endif
#if USE_SQLITE3
DbClientPtr sqlite3Client;
DROGON_TEST(SQLite3Test)
{
auto &clientPtr = sqlite3Client;
auto clientPtr = DbClient::newSqlite3Client("filename=:memory:", 1);
REQUIRE(clientPtr != nullptr);
// Prepare the test environment
@@ -4063,6 +4061,190 @@ DROGON_TEST(SQLite3Test)
}
#endif
#if USE_SQLITE3
DROGON_TEST(SQLite3TransactionTypeTest)
{
auto clientPtr = DbClient::newSqlite3Client("filename=:memory:", 1);
REQUIRE(clientPtr != nullptr);
// Ensure the test table exists
try
{
clientPtr->execSqlSync(
"CREATE TABLE IF NOT EXISTS trans_type_test "
"(id INTEGER PRIMARY KEY, val INTEGER NOT NULL)");
clientPtr->execSqlSync("DELETE FROM trans_type_test");
}
catch (const DrogonDbException &e)
{
FAULT("sqlite3 - TransactionType setup what():", e.base().what());
return;
}
// --- Deferred (default) ---
{
try
{
auto trans = clientPtr->newTransaction(TransactionType::Deferred);
trans->execSqlSync(
"INSERT INTO trans_type_test(id, val) VALUES(1, 10)");
// trans commits on destruction
}
catch (const DrogonDbException &e)
{
FAULT("sqlite3 - TransactionType::Deferred what():",
e.base().what());
return;
}
auto r = clientPtr->execSqlSync(
"SELECT val FROM trans_type_test WHERE id=1");
MANDATE(r.size() == 1);
MANDATE(r[0][0].as<int>() == 10);
SUCCESS();
}
// --- Immediate ---
{
try
{
auto trans = clientPtr->newTransaction(TransactionType::Immediate);
trans->execSqlSync(
"INSERT INTO trans_type_test(id, val) VALUES(2, 20)");
}
catch (const DrogonDbException &e)
{
FAULT("sqlite3 - TransactionType::Immediate what():",
e.base().what());
return;
}
auto r = clientPtr->execSqlSync(
"SELECT val FROM trans_type_test WHERE id=2");
MANDATE(r.size() == 1);
MANDATE(r[0][0].as<int>() == 20);
SUCCESS();
}
// --- Exclusive ---
{
try
{
auto trans = clientPtr->newTransaction(TransactionType::Exclusive);
trans->execSqlSync(
"INSERT INTO trans_type_test(id, val) VALUES(3, 30)");
}
catch (const DrogonDbException &e)
{
FAULT("sqlite3 - TransactionType::Exclusive what():",
e.base().what());
return;
}
auto r = clientPtr->execSqlSync(
"SELECT val FROM trans_type_test WHERE id=3");
MANDATE(r.size() == 1);
MANDATE(r[0][0].as<int>() == 30);
SUCCESS();
}
// --- Rollback works correctly with Immediate ---
{
try
{
auto trans = clientPtr->newTransaction(TransactionType::Immediate);
trans->execSqlSync(
"INSERT INTO trans_type_test(id, val) VALUES(99, 99)");
trans->rollback();
}
catch (const DrogonDbException &e)
{
FAULT("sqlite3 - TransactionType::Immediate rollback what():",
e.base().what());
return;
}
auto r = clientPtr->execSqlSync(
"SELECT val FROM trans_type_test WHERE id=99");
MANDATE(r.size() == 0);
SUCCESS();
}
}
// Verify the locking mode is actually used by testing observable SQLite
// locking behaviour. BEGIN IMMEDIATE acquires a RESERVED lock upfront, so a
// second concurrent BEGIN IMMEDIATE on another connection to the same
// database must fail with SQLITE_BUSY. If plain BEGIN were used instead, the
// second connection would succeed (only a SHARED lock is held until the first
// write).
DROGON_TEST(SQLite3TransactionTypeLockingTest)
{
// A pool of 2 connections to a shared file-based database gives us two
// independent SQLite connections that observe each other's locks.
const auto nonce =
std::chrono::steady_clock::now().time_since_epoch().count();
const auto dbPath =
"drogon_trans_type_lock_test_" + std::to_string(nonce) + ".db";
std::remove(dbPath.c_str());
auto pool = DbClient::newSqlite3Client("filename=" + dbPath, 2);
// WAL mode is required: it changes BEGIN IMMEDIATE from acquiring a
// RESERVED lock to acquiring the WAL write lock. This matches production
// usage and makes the busy semantics more predictable — only one writer
// is ever permitted and SQLITE_BUSY is returned immediately (no timeout
// retry) when a second BEGIN IMMEDIATE is attempted.
pool->execSqlSync("PRAGMA journal_mode=WAL");
// No retry delay: SQLITE_BUSY must surface as an exception immediately.
pool->execSqlSync("PRAGMA busy_timeout=0");
pool->execSqlSync(
"CREATE TABLE IF NOT EXISTS lock_test (id INTEGER PRIMARY KEY)");
std::shared_ptr<Transaction> transA;
// Hold an IMMEDIATE transaction on connection A.
try
{
transA = pool->newTransaction(TransactionType::Immediate);
// doBegin() is asynchronous — the BEGIN IMMEDIATE is queued to the
// connection's event loop. Run a synchronous query through the
// transaction to flush the queue; once execSqlSync returns, the
// RESERVED lock is definitely held.
transA->execSqlSync("SELECT 1");
}
catch (const DrogonDbException &e)
{
std::remove(dbPath.c_str());
FAULT("sqlite3 - TransactionType::Immediate locking setup what():",
e.base().what());
return;
}
// Connection B attempting BEGIN IMMEDIATE must fail because A already
// holds the RESERVED lock. SQLite's default busy_timeout is 0.
bool gotBusy = false;
try
{
auto transB = pool->newTransaction(TransactionType::Immediate);
transB->execSqlSync("SELECT 1");
transB->rollback();
}
catch (const DrogonDbException &)
{
gotBusy = true;
}
transA->rollback();
std::remove(dbPath.c_str());
if (gotBusy)
{
SUCCESS();
}
else
{
FAULT(
"sqlite3 - TransactionType::Immediate locking: second BEGIN "
"IMMEDIATE should have failed while the first was held, but it "
"succeeded. This means BEGIN IMMEDIATE is not being sent.");
}
}
#endif
using namespace drogon;
int main(int argc, char **argv)
@@ -4079,9 +4261,6 @@ int main(int argc, char **argv)
"client_encoding=utf8",
1,
true);
#endif
#if USE_SQLITE3
sqlite3Client = DbClient::newSqlite3Client("filename=:memory:", 1);
#endif
const int testStatus = test::run(argc, argv);
return testStatus;