修复: 升级框架并完善报告导出
- 升级 Drogon 和 Trantor,修复畸形请求导致的连接计数泄漏\n- 增加第三方框架版本校验与自动重建\n- 完善完整报告导出和接口文档
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user