复现已有算法

This commit is contained in:
cloud
2026-07-14 15:43:18 +08:00
parent abebd2a683
commit 50b8111fd9
860 changed files with 182250 additions and 18 deletions
@@ -0,0 +1,378 @@
/**
*
* @file RedisClient.h
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include <drogon/exports.h>
#include <drogon/nosql/RedisResult.h>
#include <drogon/nosql/RedisException.h>
#include <drogon/nosql/RedisSubscriber.h>
#include <string_view>
#include <trantor/net/InetAddress.h>
#include <trantor/utils/Logger.h>
#include <memory>
#include <functional>
#include <future>
#ifdef __cpp_impl_coroutine
#include <drogon/utils/coroutine.h>
#endif
namespace drogon
{
namespace nosql
{
#ifdef __cpp_impl_coroutine
class RedisClient;
class RedisTransaction;
namespace internal
{
struct [[nodiscard]] RedisAwaiter : public CallbackAwaiter<RedisResult>
{
using RedisFunction =
std::function<void(RedisResultCallback &&, RedisExceptionCallback &&)>;
explicit RedisAwaiter(RedisFunction &&function)
: function_(std::move(function))
{
}
void await_suspend(std::coroutine_handle<> handle)
{
function_(
[handle, this](const RedisResult &result) {
this->setValue(result);
handle.resume();
},
[handle, this](const RedisException &e) {
LOG_ERROR << e.what();
this->setException(std::make_exception_ptr(e));
handle.resume();
});
}
private:
RedisFunction function_;
};
struct [[nodiscard]] RedisTransactionAwaiter
: public CallbackAwaiter<std::shared_ptr<RedisTransaction>>
{
RedisTransactionAwaiter(RedisClient *client) : client_(client)
{
}
void await_suspend(std::coroutine_handle<> handle);
private:
RedisClient *client_;
};
} // namespace internal
#endif
class RedisTransaction;
/**
* @brief This class represents a redis client that contains several connections
* to a redis server.
*
*/
class DROGON_EXPORT RedisClient
{
public:
/**
* @brief Create a new redis client with multiple connections;
*
* @param serverAddress The server address.
* @param numberOfConnections The number of connections. 1 by default.
* @param username The username to authenticate if necessary.
* @param password The password to authenticate if necessary.
* @return std::shared_ptr<RedisClient>
*/
static std::shared_ptr<RedisClient> newRedisClient(
const trantor::InetAddress &serverAddress,
size_t numberOfConnections = 1,
const std::string &password = "",
unsigned int db = 0,
const std::string &username = "");
/**
* @brief Execute a redis command
*
* @param resultCallback The callback is called when a redis reply is
* received successfully.
* @param exceptionCallback The callback is called when an error occurs.
* @note When a redis reply with REDIS_REPLY_ERROR code is received, this
* callback is called.
* @param command The command to be executed. the command string can contain
* some placeholders for parameters, such as '%s', '%d', etc.
* @param ... The command parameters.
* For example:
* @code
redisClientPtr->execCommandAsync([](const RedisResult &r){
std::cout << r.getStringForDisplaying() << std::endl;
},[](const std::exception &err){
std::cerr << err.what() << std::endl;
}, "get %s", key.data());
@endcode
*/
virtual void execCommandAsync(RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
std::string_view command,
...) noexcept = 0;
/**
* @brief Execute a redis command synchronously
*
* @param processFunc Function to extract data from redis result.
* received successfully.
* @param command The command to be executed. the command string can contain
* some placeholders for parameters, such as '%s', '%d', etc.
* @param ... The command parameters.
* @return Returns the same value as process callback.
* For example:
* @code
try
{
std::string res = redisClientPtr->execCommandSync<std::string>(
[](const RedisResult &r){
return r.asString();
},
"get %s",
key.data()
);
}
catch (const RedisException & err)
{
}
catch (const std::exception & err)
{
}
@endcode
*/
template <typename T, typename... Args>
T execCommandSync(std::function<T(const RedisResult &)> &&processFunc,
std::string_view command,
Args &&...args)
{
return execCommandSync<std::decay_t<decltype(processFunc)>>(
std::move(processFunc), command, std::forward<Args>(args)...);
}
/**
* @brief Execute a redis command synchronously
* Return type can be deduced automatically in this version.
*/
template <typename F, typename... Args>
std::invoke_result_t<F, const RedisResult &> execCommandSync(
F &&processFunc,
std::string_view command,
Args &&...args)
{
using Ret = std::invoke_result_t<F, const RedisResult &>;
std::promise<Ret> prom;
execCommandAsync(
[&processFunc, &prom](const RedisResult &result) {
try
{
prom.set_value(processFunc(result));
}
catch (...)
{
prom.set_exception(std::current_exception());
}
},
[&prom](const RedisException &err) {
prom.set_exception(std::make_exception_ptr(err));
},
command,
std::forward<Args>(args)...);
return prom.get_future().get();
}
/**
* @brief Create a subscriber for redis subscribe commands.
*
* @return std::shared_ptr<RedisSubscriber>
* @note This subscriber creates a new redis connection dedicated to
* subscribe commands. This connection is managed by RedisClient.
*/
virtual std::shared_ptr<RedisSubscriber> newSubscriber() noexcept = 0;
/**
* @brief Create a redis transaction object.
*
* @return std::shared_ptr<RedisTransaction>
* @note An exception with kTimeout code is thrown if the operation is
* timed out. see RedisException.h
*/
virtual std::shared_ptr<RedisTransaction> newTransaction() noexcept(
false) = 0;
/**
* @brief Create a transaction object in asynchronous mode.
*
* @return std::shared_ptr<RedisTransaction>
* @note An empty shared_ptr object is returned via the callback if the
* operation is timed out.
*/
virtual void newTransactionAsync(
const std::function<void(const std::shared_ptr<RedisTransaction> &)>
&callback) = 0;
/**
* @brief Set the Timeout value of execution of a command.
*
* @param timeout in seconds, if the result is not returned from the
* server within the timeout, a RedisException with "Command execution
* timeout" string is generated and returned to the caller.
* @note set the timeout value to zero or negative for no limit on time.
* The default value is -1.0, this means there is no time limit if this
* method is not called.
*/
virtual void setTimeout(double timeout) = 0;
virtual ~RedisClient() = default;
/**
* @brief Close all connections in the client. usually used by Drogon in the
* quit() method.
* */
virtual void closeAll() = 0;
#ifdef __cpp_impl_coroutine
/**
* @brief Send a Redis command and await the RedisResult in a coroutine.
*
* @tparam Arguments
* @param command
* @param args
* @return internal::RedisAwaiter that can be awaited in a coroutine.
* For example:
* @code
try
{
auto result = co_await redisClient->execCommandCoro("get %s",
"keyname");
std::cout << result.getStringForDisplaying() << "\n";
}
catch(const RedisException &err)
{
std::cout << err.what() << "\n";
}
@endcode
*/
template <typename... Arguments>
internal::RedisAwaiter execCommandCoro(std::string_view command,
Arguments... args)
{
return internal::RedisAwaiter(
[command,
this,
args...](RedisResultCallback &&commandCallback,
RedisExceptionCallback &&exceptionCallback) {
execCommandAsync(std::move(commandCallback),
std::move(exceptionCallback),
command,
args...);
});
}
/**
* @brief await a RedisTransactionPtr in a coroutine.
*
* @return internal::RedisTransactionAwaiter that can be awaited in a
* coroutine.
* For example:
* @code
try
{
auto transPtr = co_await redisClient->newTransactionCoro();
...
}
catch(const RedisException &err)
{
std::cout << err.what() << "\n";
}
@endcode
*/
internal::RedisTransactionAwaiter newTransactionCoro()
{
return internal::RedisTransactionAwaiter(this);
}
#endif
};
class DROGON_EXPORT RedisTransaction : public RedisClient
{
public:
// virtual void cancel() = 0;
virtual void execute(RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback) = 0;
#ifdef __cpp_impl_coroutine
/**
* @brief Send a "exec" command to execute the transaction and await a
* RedisResult in a coroutine.
*
* @return internal::RedisAwaiter that can be awaited in a coroutine.
* For example:
* @code
try
{
auto transPtr = co_await redisClient->newTransactionCoro();
...
auto result = co_await transPtr->executeCoro();
std::cout << result.getStringForDisplaying() << "\n";
}
catch(const RedisException &err)
{
std::cout << err.what() << "\n";
}
@endcode
*/
internal::RedisAwaiter executeCoro()
{
return internal::RedisAwaiter(
[this](RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback) {
execute(std::move(resultCallback),
std::move(exceptionCallback));
});
}
#endif
void closeAll() override
{
}
};
using RedisClientPtr = std::shared_ptr<RedisClient>;
using RedisTransactionPtr = std::shared_ptr<RedisTransaction>;
#ifdef __cpp_impl_coroutine
inline void internal::RedisTransactionAwaiter::await_suspend(
std::coroutine_handle<> handle)
{
assert(client_ != nullptr);
client_->newTransactionAsync(
[this, handle](const std::shared_ptr<RedisTransaction> &transaction) {
if (transaction == nullptr)
setException(std::make_exception_ptr(RedisException(
RedisErrorCode::kTimeout,
"Timeout, no connection available for transaction")));
else
setValue(transaction);
handle.resume();
});
}
#endif
} // namespace nosql
} // namespace drogon
@@ -0,0 +1,69 @@
/**
*
* @file RedisException.h
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include <exception>
#include <functional>
#include <string>
namespace drogon
{
namespace nosql
{
enum class RedisErrorCode
{
kNone = 0,
kUnknown,
kConnectionBroken,
kNoConnectionAvailable,
kRedisError,
kInternalError,
kTransactionCancelled,
kBadType,
kTimeout
};
class RedisException final : public std::exception
{
public:
const char *what() const noexcept override
{
return message_.data();
}
RedisErrorCode code() const
{
return code_;
}
RedisException(RedisErrorCode code, const std::string &message)
: message_(message), code_(code)
{
}
RedisException(RedisErrorCode code, std::string &&message)
: message_(std::move(message)), code_(code)
{
}
RedisException() = delete;
private:
std::string message_;
RedisErrorCode code_{RedisErrorCode::kNone};
};
using RedisExceptionCallback = std::function<void(const RedisException &)>;
} // namespace nosql
} // namespace drogon
@@ -0,0 +1,132 @@
/**
*
* @file RedisResult.h
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include <drogon/exports.h>
#include <vector>
#include <string>
#include <memory>
#include <functional>
struct redisReply;
namespace drogon
{
namespace nosql
{
enum class RedisResultType
{
kInteger = 0,
kString,
kArray,
kStatus,
kNil,
kError
};
/**
* @brief This class represents a redis reply with no error.
* @note Limited by the hiredis library, the RedisResult object is only
* available in the context of the result callback, one can't hold or copy or
* move a RedisResult object for later use after the callback is returned.
*/
class DROGON_EXPORT RedisResult
{
public:
explicit RedisResult(redisReply *result) : result_(result)
{
}
~RedisResult() = default;
/**
* @brief Return the type of the result_
* @return RedisResultType
* @note The kError type is never obtained here.
*/
RedisResultType type() const noexcept;
/**
* @brief Get the string value of the result.
*
* @return std::string
* @note Calling the method of a result object which is the kArray type
* throws a runtime exception.
*/
std::string asString() const noexcept(false);
/**
* @brief Get the array value of the result.
*
* @return std::vector<RedisResult>
* @note Calling the method of a result object whose type is not kArray type
* throws a runtime exception.
*/
std::vector<RedisResult> asArray() const noexcept(false);
/**
* @brief Get the integer value of the result.
*
* @return long long
* @note Calling the method of a result object whose type is not kInteger
* type throws a runtime exception.
*/
long long asInteger() const noexcept(false);
/**
* @brief Get the string for displaying the result.
*
* @return std::string
*/
std::string getStringForDisplaying() const noexcept;
/**
* @brief Get the string for displaying with indent.
*
* @param indent The indent value.
* @return std::string
*/
std::string getStringForDisplayingWithIndent(
size_t indent = 0) const noexcept;
/**
* @brief return true if the result object is nil.
*
* @return true
* @return false
*/
bool isNil() const noexcept;
/**
* @brief Check if the result object is not nil.
*
* @return true
* @return false
*/
explicit operator bool() const
{
return !isNil();
}
private:
redisReply *result_;
};
using RedisResultCallback = std::function<void(const RedisResult &)>;
using RedisMessageCallback =
std::function<void(const std::string &channel, const std::string &message)>;
} // namespace nosql
} // namespace drogon
@@ -0,0 +1,61 @@
/**
*
* @file RedisSubscriber.h
* @author Nitromelon
*
* Copyright 2022, Nitromelon. All rights reserved.
* https://github.com/drogonframework/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include <string>
#include <drogon/nosql/RedisResult.h>
namespace drogon::nosql
{
class RedisSubscriber
{
public:
/**
* @brief Subscribe to a channel. The subscriber will keep the channel
* subscribed, until unsubscribe() is called on this channel, or the
* subscriber or RedisClient who creates it no longer exists.
* This method will not block.
*
* @param messageCallback The callback is called when a message is received
* from the channel.
* @param channel The channel to subscribe to.
*
* @note: Subscribing to same channel multiple times is supported. All
* message callbacks will be called in subscription order.
* One unsubscribe() call will remove all callbacks.
*/
virtual void subscribe(const std::string &channel,
RedisMessageCallback &&messageCallback) noexcept = 0;
// Subscribe to channel pattern
virtual void psubscribe(
const std::string &pattern,
RedisMessageCallback &&messageCallback) noexcept = 0;
/**
* @brief Unsubscribe from a channel. Once this function returns, the
* messageCallback registered through subscribe() will no longer be called.
* This method will not block.
*
* @param channel The channel to subscribe to.
*/
virtual void unsubscribe(const std::string &channel) noexcept = 0;
// Unsubscribe from channel pattern
virtual void punsubscribe(const std::string &pattern) noexcept = 0;
virtual ~RedisSubscriber() = default;
};
} // namespace drogon::nosql
@@ -0,0 +1,490 @@
/**
*
* @file RedisClientImpl.cc
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#include "RedisConnection.h"
#include "RedisClientImpl.h"
#include "RedisSubscriberImpl.h"
#include "RedisTransactionImpl.h"
#include "../../lib/src/TaskTimeoutFlag.h"
using namespace drogon::nosql;
std::shared_ptr<RedisClient> RedisClient::newRedisClient(
const trantor::InetAddress &serverAddress,
size_t connectionNumber,
const std::string &password,
unsigned int db,
const std::string &username)
{
auto client = std::make_shared<RedisClientImpl>(
serverAddress, connectionNumber, username, password, db);
client->init();
return client;
}
RedisClientImpl::RedisClientImpl(const trantor::InetAddress &serverAddress,
size_t numberOfConnections,
std::string username,
std::string password,
unsigned int db)
: loops_(numberOfConnections < std::thread::hardware_concurrency()
? numberOfConnections
: std::thread::hardware_concurrency(),
"RedisLoop"),
serverAddr_(serverAddress),
username_(std::move(username)),
password_(std::move(password)),
db_(db),
numberOfConnections_(numberOfConnections)
{
}
void RedisClientImpl::init()
{
loops_.start();
for (size_t i = 0; i < numberOfConnections_; ++i)
{
auto loop = loops_.getNextLoop();
loop->queueInLoop([this, loop]() {
std::lock_guard<std::mutex> lock(connectionsMutex_);
connections_.insert(newConnection(loop));
});
}
}
RedisConnectionPtr RedisClientImpl::newConnection(trantor::EventLoop *loop)
{
auto conn = std::make_shared<RedisConnection>(
serverAddr_, username_, password_, db_, loop);
std::weak_ptr<RedisClientImpl> thisWeakPtr = shared_from_this();
conn->setConnectCallback([thisWeakPtr](RedisConnectionPtr &&conn) {
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
{
std::lock_guard<std::mutex> lock(thisPtr->connectionsMutex_);
thisPtr->readyConnections_.push_back(conn);
}
thisPtr->handleNextTask(conn);
}
});
conn->setDisconnectCallback([thisWeakPtr](RedisConnectionPtr &&conn) {
// assert(status == REDIS_CONNECTED);
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
std::lock_guard<std::mutex> lock(thisPtr->connectionsMutex_);
thisPtr->connections_.erase(conn);
for (auto iter = thisPtr->readyConnections_.begin();
iter != thisPtr->readyConnections_.end();
++iter)
{
if (*iter == conn)
{
thisPtr->readyConnections_.erase(iter);
break;
}
}
auto loop = trantor::EventLoop::getEventLoopOfCurrentThread();
assert(loop);
loop->runAfter(2.0, [thisPtr, loop, conn]() {
std::lock_guard<std::mutex> lock(thisPtr->connectionsMutex_);
thisPtr->connections_.insert(thisPtr->newConnection(loop));
});
}
});
conn->setIdleCallback([thisWeakPtr](const RedisConnectionPtr &connPtr) {
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
thisPtr->handleNextTask(connPtr);
}
});
return conn;
}
RedisConnectionPtr RedisClientImpl::newSubscribeConnection(
trantor::EventLoop *loop,
const std::shared_ptr<RedisSubscriberImpl> &subscriber)
{
auto conn = std::make_shared<RedisConnection>(
serverAddr_, username_, password_, db_, loop);
std::weak_ptr<RedisClientImpl> weakThis = shared_from_this();
std::weak_ptr<RedisSubscriberImpl> weakSub(subscriber);
conn->setConnectCallback([weakThis, weakSub](RedisConnectionPtr &&conn) {
auto thisPtr = weakThis.lock();
if (!thisPtr)
return;
auto subPtr = weakSub.lock();
std::lock_guard<std::mutex> lock(thisPtr->connectionsMutex_);
if (subPtr)
{
subPtr->setConnection(conn);
subPtr->subscribeAll();
}
else
{
thisPtr->connections_.erase(conn);
}
});
conn->setDisconnectCallback([weakThis, weakSub](RedisConnectionPtr &&conn) {
// assert(status == REDIS_CONNECTED);
auto thisPtr = weakThis.lock();
if (!thisPtr)
return;
std::lock_guard<std::mutex> lock(thisPtr->connectionsMutex_);
thisPtr->connections_.erase(conn);
auto subPtr = weakSub.lock();
if (!subPtr)
return;
subPtr->clearConnection();
auto loop = trantor::EventLoop::getEventLoopOfCurrentThread();
assert(loop);
loop->runAfter(2.0, [thisPtr, loop, subPtr]() {
std::lock_guard<std::mutex> lock(thisPtr->connectionsMutex_);
thisPtr->connections_.insert(
thisPtr->newSubscribeConnection(loop, subPtr));
});
});
conn->setIdleCallback([weakThis, weakSub](const RedisConnectionPtr &) {
auto thisPtr = weakThis.lock();
if (!thisPtr)
return;
auto subPtr = weakSub.lock();
if (!subPtr)
return;
subPtr->subscribeNext();
});
return conn;
}
void RedisClientImpl::execCommandAsync(
RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
std::string_view command,
...) noexcept
{
if (timeout_ > 0.0)
{
va_list args;
va_start(args, command);
execCommandAsyncWithTimeout(command,
std::move(resultCallback),
std::move(exceptionCallback),
args);
va_end(args);
return;
}
RedisConnectionPtr connPtr;
{
std::lock_guard<std::mutex> lock(connectionsMutex_);
if (!readyConnections_.empty())
{
if (connectionPos_ >= readyConnections_.size())
{
connPtr = readyConnections_[0];
connectionPos_ = 1;
}
else
{
connPtr = readyConnections_[connectionPos_++];
}
}
}
if (connPtr)
{
va_list args;
va_start(args, command);
connPtr->sendvCommand(command,
std::move(resultCallback),
std::move(exceptionCallback),
args);
va_end(args);
}
else
{
LOG_TRACE << "no connection available, push command to buffer";
va_list args;
va_start(args, command);
auto formattedCmd = RedisConnection::getFormattedCommand(command, args);
va_end(args);
std::lock_guard<std::mutex> lock(connectionsMutex_);
tasks_.emplace_back(
std::make_shared<std::function<void(const RedisConnectionPtr &)>>(
[resultCallback = std::move(resultCallback),
exceptionCallback = std::move(exceptionCallback),
formattedCmd = std::move(formattedCmd)](
const RedisConnectionPtr &connPtr) mutable {
connPtr->sendFormattedCommand(std::move(formattedCmd),
std::move(resultCallback),
std::move(exceptionCallback));
}));
}
}
RedisClientImpl::~RedisClientImpl()
{
closeAll();
}
void RedisClientImpl::closeAll()
{
std::lock_guard<std::mutex> lock(connectionsMutex_);
for (auto &conn : connections_)
{
conn->disconnect();
}
readyConnections_.clear();
connections_.clear();
}
void RedisClientImpl::newTransactionAsync(
const std::function<void(const std::shared_ptr<RedisTransaction> &)>
&callback)
{
RedisConnectionPtr connPtr;
{
std::lock_guard<std::mutex> lock(connectionsMutex_);
if (!readyConnections_.empty())
{
connPtr = readyConnections_[readyConnections_.size() - 1];
readyConnections_.resize(readyConnections_.size() - 1);
}
}
if (connPtr)
{
callback(makeTransaction(connPtr));
}
else
{
if (timeout_ <= 0.0)
{
std::weak_ptr<RedisClientImpl> thisWeakPtr = shared_from_this();
std::lock_guard<std::mutex> lock(connectionsMutex_);
tasks_.emplace_back(
std::make_shared<
std::function<void(const RedisConnectionPtr &)>>(
[callback,
thisWeakPtr](const RedisConnectionPtr & /*connPtr*/) {
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
thisPtr->newTransactionAsync(callback);
}
}));
}
else
{
auto callbackPtr = std::make_shared<
std::function<void(const std::shared_ptr<RedisTransaction> &)>>(
callback);
auto transCbPtr = std::make_shared<std::weak_ptr<
std::function<void(const RedisConnectionPtr &)>>>();
auto timeoutFlagPtr = std::make_shared<TaskTimeoutFlag>(
loops_.getNextLoop(),
std::chrono::duration<double>(timeout_),
[callbackPtr, transCbPtr, this]() {
auto cbPtr = (*transCbPtr).lock();
if (cbPtr)
{
std::lock_guard<std::mutex> lock(connectionsMutex_);
for (auto iter = tasks_.begin(); iter != tasks_.end();
++iter)
{
if (cbPtr == *iter)
{
tasks_.erase(iter);
break;
}
}
}
(*callbackPtr)(nullptr);
});
std::weak_ptr<RedisClientImpl> thisWeakPtr = shared_from_this();
auto bufferCbPtr = std::make_shared<
std::function<void(const RedisConnectionPtr &)>>(
[callbackPtr, timeoutFlagPtr, thisWeakPtr](
const RedisConnectionPtr & /*connPtr*/) {
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
if (timeoutFlagPtr->done())
{
return;
}
thisPtr->newTransactionAsync(*callbackPtr);
}
});
{
std::lock_guard<std::mutex> lock(connectionsMutex_);
tasks_.emplace_back(bufferCbPtr);
}
(*transCbPtr) = bufferCbPtr;
timeoutFlagPtr->runTimer();
}
}
}
std::shared_ptr<RedisTransaction> RedisClientImpl::makeTransaction(
const RedisConnectionPtr &connPtr)
{
std::weak_ptr<RedisClientImpl> thisWeakPtr = shared_from_this();
auto trans = std::shared_ptr<RedisTransactionImpl>(
new RedisTransactionImpl(connPtr),
[thisWeakPtr, connPtr](RedisTransactionImpl *p) {
delete p;
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
{
std::lock_guard<std::mutex> lock(
thisPtr->connectionsMutex_);
thisPtr->readyConnections_.push_back(connPtr);
}
thisPtr->handleNextTask(connPtr);
}
});
trans->doBegin();
return trans;
}
void RedisClientImpl::handleNextTask(const RedisConnectionPtr &connPtr)
{
std::shared_ptr<std::function<void(const RedisConnectionPtr &)>> taskPtr;
{
std::lock_guard<std::mutex> lock(connectionsMutex_);
if (!tasks_.empty())
{
taskPtr = std::move(tasks_.front());
tasks_.pop_front();
}
}
if (taskPtr && (*taskPtr))
{
(*taskPtr)(connPtr);
}
}
void RedisClientImpl::execCommandAsyncWithTimeout(
std::string_view command,
RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
va_list ap)
{
auto expCbPtr =
std::make_shared<RedisExceptionCallback>(std::move(exceptionCallback));
auto bufferCbPtr = std::make_shared<
std::weak_ptr<std::function<void(const RedisConnectionPtr &)>>>();
auto timeoutFlagPtr = std::make_shared<TaskTimeoutFlag>(
loops_.getNextLoop(),
std::chrono::duration<double>(timeout_),
[expCbPtr, bufferCbPtr, this]() {
auto bfCbPtr = (*bufferCbPtr).lock();
if (bfCbPtr)
{
std::lock_guard<std::mutex> lock(connectionsMutex_);
for (auto iter = tasks_.begin(); iter != tasks_.end(); ++iter)
{
if (bfCbPtr == *iter)
{
tasks_.erase(iter);
break;
}
}
}
if (*expCbPtr)
{
(*expCbPtr)(RedisException(RedisErrorCode::kTimeout,
"Command execution timeout"));
}
});
auto newResultCallback = [resultCallback = std::move(resultCallback),
timeoutFlagPtr](const RedisResult &result) {
if (timeoutFlagPtr->done())
{
return;
}
if (resultCallback)
{
resultCallback(result);
}
};
auto newExceptionCallback = [expCbPtr,
timeoutFlagPtr](const RedisException &err) {
if (timeoutFlagPtr->done())
{
return;
}
if (*expCbPtr)
{
(*expCbPtr)(err);
}
};
RedisConnectionPtr connPtr;
{
std::lock_guard<std::mutex> lock(connectionsMutex_);
if (!readyConnections_.empty())
{
if (connectionPos_ >= readyConnections_.size())
{
connPtr = readyConnections_[0];
connectionPos_ = 1;
}
else
{
connPtr = readyConnections_[connectionPos_++];
}
}
}
if (connPtr)
{
connPtr->sendvCommand(command,
std::move(newResultCallback),
std::move(newExceptionCallback),
ap);
}
else
{
LOG_TRACE << "no connection available, push command to buffer";
auto formattedCmd = RedisConnection::getFormattedCommand(command, ap);
auto bfCbPtr =
std::make_shared<std::function<void(const RedisConnectionPtr &)>>(
[resultCallback = std::move(newResultCallback),
exceptionCallback = std::move(newExceptionCallback),
formattedCmd = std::move(formattedCmd)](
const RedisConnectionPtr &connPtr) mutable {
connPtr->sendFormattedCommand(std::move(formattedCmd),
std::move(resultCallback),
std::move(exceptionCallback));
});
(*bufferCbPtr) = bfCbPtr;
std::lock_guard<std::mutex> lock(connectionsMutex_);
tasks_.emplace_back(bfCbPtr);
}
timeoutFlagPtr->runTimer();
}
std::shared_ptr<RedisSubscriber> RedisClientImpl::newSubscriber() noexcept
{
auto subscriber = std::make_shared<RedisSubscriberImpl>();
auto loop = loops_.getNextLoop();
loop->queueInLoop([this, loop, subscriber]() {
std::lock_guard<std::mutex> lock(connectionsMutex_);
connections_.insert(newSubscribeConnection(loop, subscriber));
});
return subscriber;
}
@@ -0,0 +1,110 @@
/**
*
* @file RedisClientImpl.h
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include "RedisConnection.h"
#include "RedisSubscriberImpl.h"
#include "SubscribeContext.h"
#include <drogon/nosql/RedisClient.h>
#include <trantor/utils/NonCopyable.h>
#include <trantor/net/EventLoopThreadPool.h>
#include <vector>
#include <unordered_set>
#include <list>
#include <future>
namespace drogon
{
namespace nosql
{
class RedisConnection;
using RedisConnectionPtr = std::shared_ptr<RedisConnection>;
class RedisClientImpl final
: public RedisClient,
public trantor::NonCopyable,
public std::enable_shared_from_this<RedisClientImpl>
{
public:
RedisClientImpl(const trantor::InetAddress &serverAddress,
size_t numberOfConnections,
std::string username = "",
std::string password = "",
unsigned int db = 0);
void execCommandAsync(RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
std::string_view command,
...) noexcept override;
~RedisClientImpl() override;
std::shared_ptr<RedisSubscriber> newSubscriber() noexcept override;
RedisTransactionPtr newTransaction() noexcept(false) override
{
std::promise<RedisTransactionPtr> prom;
auto f = prom.get_future();
newTransactionAsync([&prom](const RedisTransactionPtr &transPtr) {
prom.set_value(transPtr);
});
auto trans = f.get();
if (!trans)
{
throw RedisException(
RedisErrorCode::kTimeout,
"Timeout, no connection available for transaction");
}
return trans;
}
void newTransactionAsync(
const std::function<void(const RedisTransactionPtr &)> &callback)
override;
void setTimeout(double timeout) override
{
timeout_ = timeout;
}
void init();
void closeAll() override;
private:
trantor::EventLoopThreadPool loops_;
std::mutex connectionsMutex_;
std::unordered_set<RedisConnectionPtr> connections_;
std::vector<RedisConnectionPtr> readyConnections_;
size_t connectionPos_{0};
const trantor::InetAddress serverAddr_;
const std::string username_;
const std::string password_;
const unsigned int db_;
const size_t numberOfConnections_;
double timeout_{-1.0};
std::list<std::shared_ptr<std::function<void(const RedisConnectionPtr &)>>>
tasks_;
RedisConnectionPtr newConnection(trantor::EventLoop *loop);
RedisConnectionPtr newSubscribeConnection(
trantor::EventLoop *loop,
const std::shared_ptr<RedisSubscriberImpl> &subscriber);
std::shared_ptr<RedisTransaction> makeTransaction(
const RedisConnectionPtr &connPtr);
void handleNextTask(const RedisConnectionPtr &connPtr);
void execCommandAsyncWithTimeout(std::string_view command,
RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
va_list ap);
};
} // namespace nosql
} // namespace drogon
@@ -0,0 +1,445 @@
/**
*
* @file RedisClientLockFree.cc
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#include "RedisConnection.h"
#include "RedisClientLockFree.h"
#include "RedisSubscriberImpl.h"
#include "RedisTransactionImpl.h"
#include "../../lib/src/TaskTimeoutFlag.h"
using namespace drogon::nosql;
RedisClientLockFree::RedisClientLockFree(
const trantor::InetAddress &serverAddress,
size_t numberOfConnections,
trantor::EventLoop *loop,
std::string username,
std::string password,
unsigned int db)
: loop_(loop),
serverAddr_(serverAddress),
username_(std::move(username)),
password_(std::move(password)),
db_(db),
numberOfConnections_(numberOfConnections)
{
assert(loop_);
for (size_t i = 0; i < numberOfConnections_; ++i)
{
loop_->queueInLoop([this]() { connections_.insert(newConnection()); });
}
}
RedisConnectionPtr RedisClientLockFree::newConnection()
{
loop_->assertInLoopThread();
auto conn = std::make_shared<RedisConnection>(
serverAddr_, username_, password_, db_, loop_);
std::weak_ptr<RedisClientLockFree> thisWeakPtr = shared_from_this();
conn->setConnectCallback([thisWeakPtr](RedisConnectionPtr &&conn) {
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
thisPtr->readyConnections_.push_back(conn);
thisPtr->handleNextTask(conn);
}
});
conn->setDisconnectCallback([thisWeakPtr](RedisConnectionPtr &&conn) {
// assert(status == REDIS_CONNECTED);
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
thisPtr->connections_.erase(conn);
for (auto iter = thisPtr->readyConnections_.begin();
iter != thisPtr->readyConnections_.end();
++iter)
{
if (*iter == conn)
{
thisPtr->readyConnections_.erase(iter);
break;
}
}
thisPtr->loop_->runAfter(2.0, [thisPtr, conn]() {
thisPtr->connections_.insert(thisPtr->newConnection());
});
}
});
conn->setIdleCallback([thisWeakPtr](const RedisConnectionPtr &connPtr) {
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
thisPtr->handleNextTask(connPtr);
}
});
return conn;
}
RedisConnectionPtr RedisClientLockFree::newSubscribeConnection(
const std::shared_ptr<RedisSubscriberImpl> &subscriber)
{
loop_->assertInLoopThread();
auto conn = std::make_shared<RedisConnection>(
serverAddr_, username_, password_, db_, loop_);
std::weak_ptr<RedisClientLockFree> weakThis = shared_from_this();
std::weak_ptr<RedisSubscriberImpl> weakSub(subscriber);
conn->setConnectCallback([weakThis, weakSub](RedisConnectionPtr &&conn) {
conn->getLoop()->assertInLoopThread(); // TODO: remove
auto thisPtr = weakThis.lock();
if (!thisPtr)
return;
thisPtr->loop_->assertInLoopThread(); // TODO: remove
auto subPtr = weakSub.lock();
if (subPtr)
{
subPtr->setConnection(conn);
subPtr->subscribeAll();
}
else
{
thisPtr->connections_.erase(conn);
}
});
conn->setDisconnectCallback([weakThis, weakSub](RedisConnectionPtr &&conn) {
// assert(status == REDIS_CONNECTED);
auto thisPtr = weakThis.lock();
if (!thisPtr)
return;
thisPtr->connections_.erase(conn);
auto subPtr = weakSub.lock();
if (!subPtr)
return;
subPtr->clearConnection();
thisPtr->loop_->runAfter(2.0, [thisPtr, subPtr]() {
thisPtr->connections_.insert(
thisPtr->newSubscribeConnection(subPtr));
});
});
conn->setIdleCallback(
[weakThis, weakSub](const RedisConnectionPtr &connPtr) {
auto thisPtr = weakThis.lock();
if (!thisPtr)
return;
auto subPtr = weakSub.lock();
if (!subPtr)
return;
subPtr->subscribeNext();
});
return conn;
}
void RedisClientLockFree::execCommandAsync(
RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
std::string_view command,
...) noexcept
{
loop_->assertInLoopThread();
if (timeout_ > 0.0)
{
va_list args;
va_start(args, command);
execCommandAsyncWithTimeout(command,
std::move(resultCallback),
std::move(exceptionCallback),
args);
va_end(args);
return;
}
RedisConnectionPtr connPtr;
{
if (!readyConnections_.empty())
{
if (connectionPos_ >= readyConnections_.size())
{
connPtr = readyConnections_[0];
connectionPos_ = 1;
}
else
{
connPtr = readyConnections_[connectionPos_++];
}
}
}
if (connPtr)
{
va_list args;
va_start(args, command);
connPtr->sendvCommand(command,
std::move(resultCallback),
std::move(exceptionCallback),
args);
va_end(args);
}
else
{
LOG_TRACE << "no connection available, push command to buffer";
std::weak_ptr<RedisClientLockFree> thisWeakPtr = shared_from_this();
va_list args;
va_start(args, command);
auto formattedCmd = RedisConnection::getFormattedCommand(command, args);
va_end(args);
tasks_.emplace_back(
std::make_shared<std::function<void(const RedisConnectionPtr &)>>(
[thisWeakPtr,
resultCallback = std::move(resultCallback),
exceptionCallback = std::move(exceptionCallback),
formattedCmd = std::move(formattedCmd)](
const RedisConnectionPtr &connPtr) mutable {
connPtr->sendFormattedCommand(std::move(formattedCmd),
std::move(resultCallback),
std::move(exceptionCallback));
}));
}
}
RedisClientLockFree::~RedisClientLockFree()
{
closeAll();
}
void RedisClientLockFree::closeAll()
{
for (auto &conn : connections_)
{
conn->disconnect();
}
readyConnections_.clear();
connections_.clear();
}
void RedisClientLockFree::newTransactionAsync(
const std::function<void(const std::shared_ptr<RedisTransaction> &)>
&callback)
{
loop_->assertInLoopThread();
RedisConnectionPtr connPtr;
if (!readyConnections_.empty())
{
connPtr = readyConnections_[readyConnections_.size() - 1];
readyConnections_.resize(readyConnections_.size() - 1);
}
if (connPtr)
{
callback(makeTransaction(connPtr));
}
else
{
if (timeout_ <= 0.0)
{
std::weak_ptr<RedisClientLockFree> thisWeakPtr = shared_from_this();
tasks_.emplace_back(
std::make_shared<
std::function<void(const RedisConnectionPtr &)>>(
[callback,
thisWeakPtr](const RedisConnectionPtr & /*connPtr*/) {
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
thisPtr->newTransactionAsync(callback);
}
}));
}
else
{
auto callbackPtr = std::make_shared<
std::function<void(const std::shared_ptr<RedisTransaction> &)>>(
callback);
auto transCbPtr = std::make_shared<std::weak_ptr<
std::function<void(const RedisConnectionPtr &)>>>();
auto timeoutFlagPtr = std::make_shared<TaskTimeoutFlag>(
loop_,
std::chrono::duration<double>(timeout_),
[callbackPtr, transCbPtr, this]() {
auto cbPtr = (*transCbPtr).lock();
if (cbPtr)
{
for (auto iter = tasks_.begin(); iter != tasks_.end();
++iter)
{
if (cbPtr == *iter)
{
tasks_.erase(iter);
break;
}
}
}
(*callbackPtr)(nullptr);
});
std::weak_ptr<RedisClientLockFree> thisWeakPtr = shared_from_this();
auto bufferCbPtr = std::make_shared<
std::function<void(const RedisConnectionPtr &)>>(
[callbackPtr, timeoutFlagPtr, thisWeakPtr](
const RedisConnectionPtr & /*connPtr*/) {
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
if (timeoutFlagPtr->done())
{
return;
}
thisPtr->newTransactionAsync(*callbackPtr);
}
});
tasks_.emplace_back(bufferCbPtr);
(*transCbPtr) = bufferCbPtr;
timeoutFlagPtr->runTimer();
}
}
}
std::shared_ptr<RedisTransaction> RedisClientLockFree::makeTransaction(
const RedisConnectionPtr &connPtr)
{
std::weak_ptr<RedisClientLockFree> thisWeakPtr = shared_from_this();
auto trans = std::shared_ptr<RedisTransactionImpl>(
new RedisTransactionImpl(connPtr),
[thisWeakPtr, connPtr](RedisTransactionImpl *p) {
delete p;
auto thisPtr = thisWeakPtr.lock();
if (thisPtr)
{
thisPtr->readyConnections_.push_back(connPtr);
thisPtr->handleNextTask(connPtr);
}
});
trans->doBegin();
return trans;
}
void RedisClientLockFree::handleNextTask(const RedisConnectionPtr &connPtr)
{
loop_->assertInLoopThread();
std::shared_ptr<std::function<void(const RedisConnectionPtr &)>> taskPtr;
if (!tasks_.empty())
{
taskPtr = std::move(tasks_.front());
tasks_.pop_front();
}
if (taskPtr && (*taskPtr))
{
(*taskPtr)(connPtr);
}
}
void RedisClientLockFree::execCommandAsyncWithTimeout(
std::string_view command,
RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
va_list ap)
{
auto expCbPtr =
std::make_shared<RedisExceptionCallback>(std::move(exceptionCallback));
auto bufferCbPtr = std::make_shared<
std::weak_ptr<std::function<void(const RedisConnectionPtr &)>>>();
auto timeoutFlagPtr = std::make_shared<TaskTimeoutFlag>(
loop_,
std::chrono::duration<double>(timeout_),
[expCbPtr, bufferCbPtr, this]() {
auto bfCbPtr = (*bufferCbPtr).lock();
if (bfCbPtr)
{
for (auto iter = tasks_.begin(); iter != tasks_.end(); ++iter)
{
if (bfCbPtr == *iter)
{
tasks_.erase(iter);
break;
}
}
}
if (*expCbPtr)
{
(*expCbPtr)(RedisException(RedisErrorCode::kTimeout,
"Command execution timeout"));
}
});
auto newResultCallback = [resultCallback = std::move(resultCallback),
timeoutFlagPtr](const RedisResult &result) {
if (timeoutFlagPtr->done())
{
return;
}
if (resultCallback)
{
resultCallback(result);
}
};
auto newExceptionCallback = [expCbPtr,
timeoutFlagPtr](const RedisException &err) {
if (timeoutFlagPtr->done())
{
return;
}
if (*expCbPtr)
{
(*expCbPtr)(err);
}
};
RedisConnectionPtr connPtr;
{
if (!readyConnections_.empty())
{
if (connectionPos_ >= readyConnections_.size())
{
connPtr = readyConnections_[0];
connectionPos_ = 1;
}
else
{
connPtr = readyConnections_[connectionPos_++];
}
}
}
if (connPtr)
{
connPtr->sendvCommand(command,
std::move(newResultCallback),
std::move(newExceptionCallback),
ap);
}
else
{
LOG_TRACE << "no connection available, push command to buffer";
auto formattedCmd = RedisConnection::getFormattedCommand(command, ap);
auto bfCbPtr =
std::make_shared<std::function<void(const RedisConnectionPtr &)>>(
[resultCallback = std::move(newResultCallback),
exceptionCallback = std::move(newExceptionCallback),
formattedCmd = std::move(formattedCmd)](
const RedisConnectionPtr &connPtr) mutable {
connPtr->sendFormattedCommand(std::move(formattedCmd),
std::move(resultCallback),
std::move(exceptionCallback));
});
(*bufferCbPtr) = bfCbPtr;
tasks_.emplace_back(bfCbPtr);
}
timeoutFlagPtr->runTimer();
}
std::shared_ptr<RedisSubscriber> RedisClientLockFree::newSubscriber() noexcept
{
auto subscriber = std::make_shared<RedisSubscriberImpl>();
loop_->runInLoop([this, subscriber]() {
connections_.insert(newSubscribeConnection(subscriber));
});
return subscriber;
}
@@ -0,0 +1,99 @@
/**
*
* @file RedisClientLockFree.h
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include "RedisConnection.h"
#include "RedisSubscriberImpl.h"
#include <drogon/nosql/RedisClient.h>
#include <trantor/utils/NonCopyable.h>
#include <trantor/net/EventLoopThreadPool.h>
#include <vector>
#include <unordered_set>
#include <list>
#include <future>
namespace drogon
{
namespace nosql
{
class RedisConnection;
using RedisConnectionPtr = std::shared_ptr<RedisConnection>;
class RedisClientLockFree final
: public RedisClient,
public trantor::NonCopyable,
public std::enable_shared_from_this<RedisClientLockFree>
{
public:
RedisClientLockFree(const trantor::InetAddress &serverAddress,
size_t numberOfConnections,
trantor::EventLoop *loop,
std::string username = "",
std::string password = "",
unsigned int db = 0);
void execCommandAsync(RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
std::string_view command,
...) noexcept override;
~RedisClientLockFree() override;
std::shared_ptr<RedisSubscriber> newSubscriber() noexcept override;
RedisTransactionPtr newTransaction() override
{
LOG_ERROR
<< "You can't use the synchronous interface in the fast redis "
"client, please use the asynchronous version "
"(newTransactionAsync)";
assert(0);
return nullptr;
}
void newTransactionAsync(
const std::function<void(const RedisTransactionPtr &)> &callback)
override;
void setTimeout(double timeout) override
{
timeout_ = timeout;
}
void closeAll() override;
private:
trantor::EventLoop *loop_;
std::unordered_set<RedisConnectionPtr> connections_;
std::vector<RedisConnectionPtr> readyConnections_;
size_t connectionPos_{0};
const trantor::InetAddress serverAddr_;
const std::string username_;
const std::string password_;
const unsigned int db_;
const size_t numberOfConnections_;
std::list<std::shared_ptr<std::function<void(const RedisConnectionPtr &)>>>
tasks_;
double timeout_{-1.0};
RedisConnectionPtr newConnection();
RedisConnectionPtr newSubscribeConnection(
const std::shared_ptr<RedisSubscriberImpl> &);
std::shared_ptr<RedisTransaction> makeTransaction(
const RedisConnectionPtr &connPtr);
void handleNextTask(const RedisConnectionPtr &connPtr);
void execCommandAsyncWithTimeout(std::string_view command,
RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
va_list ap);
};
} // namespace nosql
} // namespace drogon
@@ -0,0 +1,133 @@
/**
*
* @file RedisClientManager.cc
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#include "../../lib/src/RedisClientManager.h"
#include "RedisClientLockFree.h"
#include "RedisClientImpl.h"
#include <algorithm>
using namespace drogon::nosql;
using namespace drogon;
void RedisClientManager::createRedisClients(
const std::vector<trantor::EventLoop *> &ioLoops)
{
assert(redisClientsMap_.empty());
assert(redisFastClientsMap_.empty());
for (auto &redisInfo : redisInfos_)
{
if (redisInfo.isFast_)
{
redisFastClientsMap_[redisInfo.name_] =
IOThreadStorage<RedisClientPtr>();
redisFastClientsMap_[redisInfo.name_].init([&](RedisClientPtr &c,
size_t idx) {
assert(idx == ioLoops[idx]->index());
LOG_TRACE << "create fast redis client for the thread " << idx;
c = std::make_shared<RedisClientLockFree>(
trantor::InetAddress(redisInfo.addr_, redisInfo.port_),
redisInfo.connectionNumber_,
ioLoops[idx],
redisInfo.username_,
redisInfo.password_,
redisInfo.db_);
if (redisInfo.timeout_ > 0.0)
{
c->setTimeout(redisInfo.timeout_);
}
});
}
else
{
auto clientPtr = std::make_shared<RedisClientImpl>(
trantor::InetAddress(redisInfo.addr_, redisInfo.port_),
redisInfo.connectionNumber_,
redisInfo.username_,
redisInfo.password_,
redisInfo.db_);
if (redisInfo.timeout_ > 0.0)
{
clientPtr->setTimeout(redisInfo.timeout_);
}
clientPtr->init();
redisClientsMap_[redisInfo.name_] = std::move(clientPtr);
}
}
}
void RedisClientManager::createRedisClient(const std::string &name,
const std::string &addr,
unsigned short port,
const std::string &username,
const std::string &password,
const size_t connectionNum,
const bool isFast,
double timeout,
unsigned int db)
{
RedisInfo info;
info.name_ = name;
info.addr_ = addr;
info.port_ = port;
info.username_ = username;
info.password_ = password;
info.connectionNumber_ = connectionNum;
info.isFast_ = isFast;
info.timeout_ = timeout;
info.db_ = db;
redisInfos_.emplace_back(std::move(info));
}
// bool RedisClientManager::areAllRedisClientsAvailable() const noexcept
//{
// for (auto const &pair : redisClientsMap_)
// {
// if (!(pair.second)->hasAvailableConnections())
// return false;
// }
// auto loop = trantor::EventLoop::getEventLoopOfCurrentThread();
// if (loop && loop->index() < app().getThreadNum())
// {
// for (auto const &pair : redisFastClientsMap_)
// {
// if (!(*(pair.second))->hasAvailableConnections())
// return false;
// }
// }
// return true;
//}
RedisClientManager::~RedisClientManager()
{
for (auto &pair : redisClientsMap_)
{
pair.second->closeAll();
}
for (auto &pair : redisFastClientsMap_)
{
pair.second.init([](RedisClientPtr &clientPtr, size_t index) {
// the main loop;
std::promise<void> p;
auto f = p.get_future();
drogon::getIOThreadStorageLoop(index)->runInLoop(
[&clientPtr, &p]() {
clientPtr->closeAll();
p.set_value();
});
f.get();
});
}
}
@@ -0,0 +1,522 @@
/**
*
* @file RedisConnection.cc
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#include "RedisConnection.h"
#include <drogon/nosql/RedisResult.h>
#include <future>
#include <string.h>
#ifdef _MSC_VER
#define strcasecmp _stricmp
#endif
using namespace drogon::nosql;
RedisConnection::RedisConnection(const trantor::InetAddress &serverAddress,
const std::string &username,
const std::string &password,
unsigned int db,
trantor::EventLoop *loop)
: serverAddr_(serverAddress),
username_(username),
password_(password),
db_(db),
loop_(loop)
{
assert(loop_);
loop_->queueInLoop([this]() { startConnectionInLoop(); });
}
void RedisConnection::startConnectionInLoop()
{
loop_->assertInLoopThread();
assert(!redisContext_);
redisContext_ =
::redisAsyncConnect(serverAddr_.toIp().c_str(), serverAddr_.toPort());
status_ = ConnectStatus::kConnecting;
if (redisContext_->err)
{
LOG_ERROR << "Error: " << redisContext_->errstr;
if (disconnectCallback_)
{
disconnectCallback_(shared_from_this());
}
// Strange things have happened. In some kinds of connection errors,
// such as setsockopt errors, hiredis already set redisContext_->c.fd to
// -1, but the tcp connection stays in ESTABLISHED status. And there is
// no way for us to obtain the fd of that socket nor close it. This
// probably is a bug of hiredis.
return;
}
redisContext_->ev.addWrite = addWrite;
redisContext_->ev.delWrite = delWrite;
redisContext_->ev.addRead = addRead;
redisContext_->ev.delRead = delRead;
redisContext_->ev.cleanup = cleanup;
redisContext_->ev.data = this;
channel_ = std::make_unique<trantor::Channel>(loop_, redisContext_->c.fd);
channel_->setReadCallback([this]() { handleRedisRead(); });
channel_->setWriteCallback([this]() { handleRedisWrite(); });
redisAsyncSetConnectCallback(
redisContext_, [](const redisAsyncContext *context, int status) {
auto thisPtr = static_cast<RedisConnection *>(context->ev.data);
if (status != REDIS_OK)
{
LOG_ERROR << "Failed to connect to "
<< thisPtr->serverAddr_.toIpPort() << "! "
<< context->errstr;
thisPtr->handleDisconnect();
if (thisPtr->disconnectCallback_)
{
thisPtr->disconnectCallback_(thisPtr->shared_from_this());
}
}
else
{
LOG_TRACE << "Connected successfully to "
<< thisPtr->serverAddr_.toIpPort();
if (thisPtr->password_.empty())
{
if (thisPtr->db_ == 0)
{
thisPtr->status_ = ConnectStatus::kConnected;
if (thisPtr->connectCallback_)
{
thisPtr->connectCallback_(
thisPtr->shared_from_this());
}
}
}
else
{
if (thisPtr->username_.empty())
{
std::weak_ptr<RedisConnection> weakThisPtr =
thisPtr->shared_from_this();
thisPtr->sendCommand(
[weakThisPtr](const RedisResult &r) {
auto thisPtr = weakThisPtr.lock();
if (!thisPtr)
return;
if (r.asString() == "OK")
{
if (thisPtr->db_ == 0)
{
thisPtr->status_ =
ConnectStatus::kConnected;
if (thisPtr->connectCallback_)
thisPtr->connectCallback_(
thisPtr->shared_from_this());
}
}
else
{
LOG_ERROR << r.asString();
thisPtr->disconnect();
thisPtr->status_ = ConnectStatus::kEnd;
}
},
[weakThisPtr](const std::exception &err) {
LOG_ERROR << err.what();
auto thisPtr = weakThisPtr.lock();
if (!thisPtr)
return;
thisPtr->disconnect();
thisPtr->status_ = ConnectStatus::kEnd;
},
"auth %s",
thisPtr->password_.c_str());
}
else
{
std::weak_ptr<RedisConnection> weakThisPtr =
thisPtr->shared_from_this();
thisPtr->sendCommand(
[weakThisPtr](const RedisResult &r) {
auto thisPtr = weakThisPtr.lock();
if (!thisPtr)
return;
if (r.asString() == "OK")
{
if (thisPtr->db_ == 0)
{
thisPtr->status_ =
ConnectStatus::kConnected;
if (thisPtr->connectCallback_)
thisPtr->connectCallback_(
thisPtr->shared_from_this());
}
}
else
{
LOG_ERROR << r.asString();
thisPtr->disconnect();
thisPtr->status_ = ConnectStatus::kEnd;
}
},
[weakThisPtr](const std::exception &err) {
LOG_ERROR << err.what();
auto thisPtr = weakThisPtr.lock();
if (!thisPtr)
return;
thisPtr->disconnect();
thisPtr->status_ = ConnectStatus::kEnd;
},
"auth %s %s",
thisPtr->username_.c_str(),
thisPtr->password_.c_str());
}
}
if (thisPtr->db_ != 0)
{
LOG_TRACE << "redis db:" << thisPtr->db_;
std::weak_ptr<RedisConnection> weakThisPtr =
thisPtr->shared_from_this();
thisPtr->sendCommand(
[weakThisPtr](const RedisResult &r) {
auto thisPtr = weakThisPtr.lock();
if (!thisPtr)
return;
if (r.asString() == "OK")
{
thisPtr->status_ = ConnectStatus::kConnected;
if (thisPtr->connectCallback_)
{
thisPtr->connectCallback_(
thisPtr->shared_from_this());
}
}
else
{
LOG_ERROR << r.asString();
thisPtr->disconnect();
thisPtr->status_ = ConnectStatus::kEnd;
}
},
[weakThisPtr](const std::exception &err) {
LOG_ERROR << err.what();
auto thisPtr = weakThisPtr.lock();
if (!thisPtr)
return;
thisPtr->disconnect();
thisPtr->status_ = ConnectStatus::kEnd;
},
"select %u",
thisPtr->db_);
}
}
});
redisAsyncSetDisconnectCallback(
redisContext_, [](const redisAsyncContext *context, int /*status*/) {
auto thisPtr = static_cast<RedisConnection *>(context->ev.data);
thisPtr->handleDisconnect();
if (thisPtr->disconnectCallback_)
{
thisPtr->disconnectCallback_(thisPtr->shared_from_this());
}
LOG_TRACE << "Disconnected from "
<< thisPtr->serverAddr_.toIpPort();
});
}
void RedisConnection::handleDisconnect()
{
LOG_TRACE << "handleDisconnect";
loop_->assertInLoopThread();
while ((!resultCallbacks_.empty()) && (!exceptionCallbacks_.empty()))
{
if (exceptionCallbacks_.front())
{
exceptionCallbacks_.front()(
RedisException(RedisErrorCode::kConnectionBroken,
"Connection is broken"));
}
resultCallbacks_.pop();
exceptionCallbacks_.pop();
}
status_ = ConnectStatus::kEnd;
channel_->disableAll();
channel_->remove();
redisContext_->ev.addWrite = nullptr;
redisContext_->ev.delWrite = nullptr;
redisContext_->ev.addRead = nullptr;
redisContext_->ev.delRead = nullptr;
redisContext_->ev.cleanup = nullptr;
redisContext_->ev.data = nullptr;
}
void RedisConnection::addWrite(void *userData)
{
auto thisPtr = static_cast<RedisConnection *>(userData);
assert(thisPtr->channel_);
thisPtr->channel_->enableWriting();
}
void RedisConnection::delWrite(void *userData)
{
auto thisPtr = static_cast<RedisConnection *>(userData);
assert(thisPtr->channel_);
thisPtr->channel_->disableWriting();
}
void RedisConnection::addRead(void *userData)
{
auto thisPtr = static_cast<RedisConnection *>(userData);
assert(thisPtr->channel_);
thisPtr->channel_->enableReading();
}
void RedisConnection::delRead(void *userData)
{
auto thisPtr = static_cast<RedisConnection *>(userData);
assert(thisPtr->channel_);
thisPtr->channel_->disableReading();
}
void RedisConnection::cleanup(void * /*userData*/)
{
LOG_TRACE << "cleanup";
}
void RedisConnection::handleRedisRead()
{
if (status_ != ConnectStatus::kEnd)
{
redisAsyncHandleRead(redisContext_);
}
}
void RedisConnection::handleRedisWrite()
{
if (status_ != ConnectStatus::kEnd)
{
redisAsyncHandleWrite(redisContext_);
}
}
void RedisConnection::sendCommandInLoop(
const std::string &command,
RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback)
{
resultCallbacks_.emplace(std::move(resultCallback));
exceptionCallbacks_.emplace(std::move(exceptionCallback));
redisAsyncFormattedCommand(
redisContext_,
[](redisAsyncContext *context, void *r, void * /*userData*/) {
auto thisPtr = static_cast<RedisConnection *>(context->ev.data);
thisPtr->handleResult(static_cast<redisReply *>(r));
},
nullptr,
command.c_str(),
command.length());
}
void RedisConnection::handleResult(redisReply *result)
{
auto commandCallback = std::move(resultCallbacks_.front());
resultCallbacks_.pop();
auto exceptionCallback = std::move(exceptionCallbacks_.front());
exceptionCallbacks_.pop();
if (result && result->type != REDIS_REPLY_ERROR)
{
commandCallback(RedisResult(result));
}
else
{
if (result)
{
exceptionCallback(
RedisException(RedisErrorCode::kRedisError,
std::string{result->str, result->len}));
}
else
{
exceptionCallback(RedisException(RedisErrorCode::kConnectionBroken,
"Network failure"));
}
}
if (resultCallbacks_.empty())
{
assert(exceptionCallbacks_.empty());
if (idleCallback_)
{
idleCallback_(shared_from_this());
}
}
}
void RedisConnection::disconnect()
{
auto thisPtr = shared_from_this();
loop_->queueInLoop(
[thisPtr]() { redisAsyncDisconnect(thisPtr->redisContext_); });
}
void RedisConnection::sendSubscribe(
const std::shared_ptr<SubscribeContext> &subCtx)
{
if (loop_->isInLoopThread())
{
sendSubscribeInLoop(subCtx);
}
else
{
loop_->queueInLoop([this, subCtx]() { sendSubscribeInLoop(subCtx); });
}
}
void RedisConnection::sendUnsubscribe(
const std::shared_ptr<SubscribeContext> &subCtx)
{
if (loop_->isInLoopThread())
{
sendUnsubscribeInLoop(subCtx);
}
else
{
loop_->queueInLoop([this, subCtx]() { sendUnsubscribeInLoop(subCtx); });
}
}
void RedisConnection::sendSubscribeInLoop(
const std::shared_ptr<SubscribeContext> &subCtx)
{
if (!subCtx->alive())
{
// Unsub-ed by somewhere else
return;
}
subContexts_.emplace(subCtx->contextId(), subCtx);
redisAsyncFormattedCommand(
redisContext_,
[](redisAsyncContext *context, void *r, void *subCtx) {
auto thisPtr = static_cast<RedisConnection *>(context->ev.data);
thisPtr->handleSubscribeResult(static_cast<redisReply *>(r),
static_cast<SubscribeContext *>(
subCtx));
},
subCtx.get(),
subCtx->subscribeCommand().c_str(),
subCtx->subscribeCommand().size());
}
void RedisConnection::sendUnsubscribeInLoop(
const std::shared_ptr<SubscribeContext> &subCtx)
{
// There is a Hiredis issue here
// The un-sub callback will not be called, sub callback will be called
// instead, with first element in result as "unsubscribe".
// This problem is fixed in 2021-12-02 commit da5a4ff, but
// have not been released as a tag.
// Here we just register a same function to deal with both situation.
redisAsyncFormattedCommand(
redisContext_,
[](redisAsyncContext *context, void *r, void *subCtx) {
auto thisPtr = static_cast<RedisConnection *>(context->ev.data);
thisPtr->handleSubscribeResult(static_cast<redisReply *>(r),
static_cast<SubscribeContext *>(
subCtx));
},
subCtx.get(),
subCtx->unsubscribeCommand().c_str(),
subCtx->unsubscribeCommand().size());
}
void RedisConnection::handleSubscribeResult(redisReply *result,
SubscribeContext *subCtx)
{
if (result && result->type == REDIS_REPLY_ARRAY && result->elements >= 3 &&
result->element[0]->type == REDIS_REPLY_STRING)
{
const char *type = result->element[0]->str;
int isPattern = (type[0] == 'p' || type[0] == 'P') ? 1 : 0;
if (isPattern)
{
type += 1;
}
if (strcasecmp(type, "message") == 0)
{
std::string channel(result->element[1 + isPattern]->str,
result->element[1 + isPattern]->len);
std::string message(result->element[2 + isPattern]->str,
result->element[2 + isPattern]->len);
if (!subCtx->alive())
{
LOG_DEBUG << "Subscribe callback receive message, but "
"context is no "
"longer alive"
<< ", channel: " << channel
<< ", message: " << message;
}
else
{
subCtx->onMessage(channel, message);
}
// Message callback, no need to call idleCallback_
return;
}
std::string channel(result->element[1]->str, result->element[1]->len);
long long number = result->element[2]->integer;
// On channel subscribed
if (strcasecmp(type, "subscribe") == 0)
{
subCtx->onSubscribe(channel, number);
}
// On channel unsubscribed
else if (strcasecmp(type, "unsubscribe") == 0)
{
subCtx->onUnsubscribe(channel, number);
subContexts_.erase(subCtx->contextId());
}
// Should not happen
else
{
LOG_ERROR << "Unknown redis response: " << result->element[0]->str;
// Shouldn't let message from another endpoint to abort this
// program. So no assert(false) here.
}
}
else if (!result)
{
// When connection close, if a channel has been subscribed,
// this callback will be called with empty result.
LOG_DEBUG << "Empty result (connection lost)";
}
else if (result->type == REDIS_REPLY_ERROR)
{
LOG_ERROR << "Subscribe callback receive error result: " << result->str;
}
else
{
LOG_ERROR << "Subscribe callback receive error result type: "
<< result->type;
}
if (idleCallback_)
{
idleCallback_(shared_from_this());
}
}
@@ -0,0 +1,229 @@
/**
*
* @file RedisConnection.h
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include <string_view>
#include <drogon/nosql/RedisException.h>
#include <drogon/nosql/RedisResult.h>
#include <drogon/utils/Utilities.h>
#include <trantor/utils/NonCopyable.h>
#include <trantor/net/InetAddress.h>
#include <trantor/net/EventLoop.h>
#include <trantor/net/Channel.h>
#include <hiredis/async.h>
#include <hiredis/hiredis.h>
#include <memory>
#include <queue>
#include "SubscribeContext.h"
namespace drogon
{
namespace nosql
{
enum class ConnectStatus
{
kNone = 0,
kConnecting,
kConnected,
kEnd
};
class RedisConnection : public trantor::NonCopyable,
public std::enable_shared_from_this<RedisConnection>
{
public:
RedisConnection(const trantor::InetAddress &serverAddress,
const std::string &username,
const std::string &password,
unsigned int db,
trantor::EventLoop *loop);
void setConnectCallback(
const std::function<void(std::shared_ptr<RedisConnection> &&)>
&callback)
{
connectCallback_ = callback;
}
void setDisconnectCallback(
const std::function<void(std::shared_ptr<RedisConnection> &&)>
&callback)
{
disconnectCallback_ = callback;
}
void setIdleCallback(
const std::function<void(const std::shared_ptr<RedisConnection> &)>
&callback)
{
idleCallback_ = callback;
}
static std::string getFormattedCommand(const std::string_view &command,
va_list ap) noexcept(false)
{
char *cmd{nullptr};
auto len = redisvFormatCommand(&cmd, command.data(), ap);
if (len == -1)
{
throw RedisException(RedisErrorCode::kInternalError,
"Out of memory");
}
else if (len == -2)
{
throw RedisException(RedisErrorCode::kInternalError,
"Invalid format string");
}
else if (len <= 0)
{
throw RedisException(RedisErrorCode::kInternalError,
"Unknown format error");
}
std::string fullCommand{cmd, static_cast<size_t>(len)};
redisFreeCommand(cmd);
return fullCommand;
}
void sendFormattedCommand(std::string &&command,
RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback)
{
if (loop_->isInLoopThread())
{
sendCommandInLoop(command,
std::move(resultCallback),
std::move(exceptionCallback));
}
else
{
loop_->queueInLoop(
[this,
callback = std::move(resultCallback),
exceptionCallback = std::move(exceptionCallback),
command = std::move(command)]() mutable {
sendCommandInLoop(command,
std::move(callback),
std::move(exceptionCallback));
});
}
}
void sendvCommand(std::string_view command,
RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
va_list ap)
{
LOG_TRACE << "redis command: " << command;
try
{
auto fullCommand = getFormattedCommand(command, ap);
if (loop_->isInLoopThread())
{
sendCommandInLoop(fullCommand,
std::move(resultCallback),
std::move(exceptionCallback));
}
else
{
loop_->queueInLoop(
[this,
callback = std::move(resultCallback),
exceptionCallback = std::move(exceptionCallback),
fullCommand = std::move(fullCommand)]() mutable {
sendCommandInLoop(fullCommand,
std::move(callback),
std::move(exceptionCallback));
});
}
}
catch (const RedisException &err)
{
exceptionCallback(err);
}
}
void sendSubscribe(const std::shared_ptr<SubscribeContext> &subCtx);
void sendUnsubscribe(const std::shared_ptr<SubscribeContext> &subCtx);
~RedisConnection()
{
LOG_TRACE << (int)status_;
if (redisContext_ && status_ != ConnectStatus::kEnd)
redisAsyncDisconnect(redisContext_);
}
void disconnect();
void sendCommand(RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
std::string_view command,
...)
{
va_list args;
va_start(args, command);
sendvCommand(command,
std::move(resultCallback),
std::move(exceptionCallback),
args);
va_end(args);
}
trantor::EventLoop *getLoop() const
{
return loop_;
}
private:
redisAsyncContext *redisContext_{nullptr};
const trantor::InetAddress serverAddr_;
const std::string username_;
const std::string password_;
const unsigned int db_;
trantor::EventLoop *loop_{nullptr};
std::unique_ptr<trantor::Channel> channel_{nullptr};
std::function<void(std::shared_ptr<RedisConnection> &&)> connectCallback_;
std::function<void(std::shared_ptr<RedisConnection> &&)>
disconnectCallback_;
std::function<void(const std::shared_ptr<RedisConnection> &)> idleCallback_;
std::queue<RedisResultCallback> resultCallbacks_;
std::queue<RedisExceptionCallback> exceptionCallbacks_;
ConnectStatus status_{ConnectStatus::kNone};
// used to keep the lifetime of context object
std::unordered_map<unsigned long long, std::shared_ptr<SubscribeContext>>
subContexts_;
void startConnectionInLoop();
static void addWrite(void *userData);
static void delWrite(void *userData);
static void addRead(void *userData);
static void delRead(void *userData);
static void cleanup(void *userData);
void handleRedisRead();
void handleRedisWrite();
void handleResult(redisReply *result);
void sendCommandInLoop(const std::string &command,
RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback);
void sendSubscribeInLoop(const std::shared_ptr<SubscribeContext> &subCtx);
void sendUnsubscribeInLoop(const std::shared_ptr<SubscribeContext> &subCtx);
void handleSubscribeResult(redisReply *result, SubscribeContext *subCtx);
void handleDisconnect();
};
using RedisConnectionPtr = std::shared_ptr<RedisConnection>;
} // namespace nosql
} // namespace drogon
@@ -0,0 +1,129 @@
/**
*
* @file RedisResult.cc
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#include <drogon/nosql/RedisResult.h>
#include <drogon/nosql/RedisClient.h>
#include <hiredis/hiredis.h>
using namespace drogon::nosql;
std::string RedisResult::getStringForDisplaying() const noexcept
{
return getStringForDisplayingWithIndent(0);
}
std::string RedisResult::getStringForDisplayingWithIndent(
size_t indent) const noexcept
{
switch (result_->type)
{
case REDIS_REPLY_STRING:
return "\"" + std::string{result_->str, result_->len} + "\"";
case REDIS_REPLY_STATUS:
return std::string{result_->str, result_->len};
case REDIS_REPLY_ERROR:
return "'ERROR:" + std::string{result_->str, result_->len} + "'";
case REDIS_REPLY_NIL:
return "(nil)";
case REDIS_REPLY_INTEGER:
return std::to_string(result_->integer);
case REDIS_REPLY_ARRAY:
{
std::string ret;
for (size_t i = 0; i < result_->elements; ++i)
{
std::string lineNum = std::to_string(i + 1) + ") ";
if (i > 0)
{
ret += std::string(indent, ' ');
}
ret += lineNum;
ret += RedisResult(result_->element[i])
.getStringForDisplayingWithIndent(lineNum.length());
if (i != result_->elements - 1)
{
ret += '\n';
}
}
return ret;
}
default:
return "*";
}
}
std::string RedisResult::asString() const noexcept(false)
{
auto rtype = type();
if (rtype == RedisResultType::kString ||
rtype == RedisResultType::kStatus || rtype == RedisResultType::kError)
{
return std::string(result_->str, result_->len);
}
else if (rtype == RedisResultType::kInteger)
{
return std::to_string(result_->integer);
}
else
{
throw RedisException(RedisErrorCode::kBadType, "bad type");
}
}
RedisResultType RedisResult::type() const noexcept
{
switch (result_->type)
{
case REDIS_REPLY_STRING:
return RedisResultType::kString;
case REDIS_REPLY_ARRAY:
return RedisResultType::kArray;
case REDIS_REPLY_INTEGER:
return RedisResultType::kInteger;
case REDIS_REPLY_NIL:
return RedisResultType::kNil;
case REDIS_REPLY_STATUS:
return RedisResultType::kStatus;
case REDIS_REPLY_ERROR:
default:
return RedisResultType::kError;
}
}
std::vector<RedisResult> RedisResult::asArray() const noexcept(false)
{
auto rtype = type();
if (rtype == RedisResultType::kArray)
{
std::vector<RedisResult> array;
for (size_t i = 0; i < result_->elements; ++i)
{
array.emplace_back(result_->element[i]);
}
return array;
}
throw RedisException(RedisErrorCode::kBadType, "bad type");
}
long long RedisResult::asInteger() const noexcept(false)
{
if (type() == RedisResultType::kInteger)
return result_->integer;
throw RedisException(RedisErrorCode::kBadType, "bad type");
}
bool RedisResult::isNil() const noexcept
{
return type() == RedisResultType::kNil;
}
@@ -0,0 +1,237 @@
/**
*
* @file RedisSubscriberImpl.cpp
* @author Nitromelon
*
* Copyright 2022, Nitromelon. All rights reserved.
* https://github.com/drogonframework/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#include "RedisSubscriberImpl.h"
using namespace drogon::nosql;
RedisSubscriberImpl::~RedisSubscriberImpl()
{
RedisConnectionPtr conn;
std::lock_guard<std::mutex> lock(mutex_);
if (conn_)
{
conn.swap(conn_);
conn->getLoop()->runInLoop([conn]() {
// Run in self loop to avoid blocking
conn->disconnect();
});
}
}
void RedisSubscriberImpl::subscribe(
const std::string &channel,
RedisMessageCallback &&messageCallback) noexcept
{
LOG_TRACE << "Subscribe " << channel;
std::shared_ptr<SubscribeContext> subCtx;
{
std::lock_guard<std::mutex> lock(mutex_);
if (subContexts_.find(channel) != subContexts_.end())
{
subCtx = subContexts_.at(channel);
}
else
{
subCtx = SubscribeContext::newContext(shared_from_this(), channel);
subContexts_.emplace(channel, subCtx);
}
subCtx->addMessageCallback(std::move(messageCallback));
}
RedisConnectionPtr connPtr;
{
std::lock_guard<std::mutex> lock(mutex_);
connPtr = conn_;
}
if (connPtr)
{
connPtr->sendSubscribe(subCtx);
}
else
{
LOG_TRACE << "no subscribe connection available, wait for connection";
// Just wait for connection, all channels will be re-sub
}
}
void RedisSubscriberImpl::psubscribe(
const std::string &pattern,
RedisMessageCallback &&messageCallback) noexcept
{
LOG_TRACE << "Psubscribe " << pattern;
std::shared_ptr<SubscribeContext> subCtx;
{
std::lock_guard<std::mutex> lock(mutex_);
if (psubContexts_.find(pattern) != psubContexts_.end())
{
subCtx = psubContexts_.at(pattern);
}
else
{
subCtx =
SubscribeContext::newContext(shared_from_this(), pattern, true);
psubContexts_.emplace(pattern, subCtx);
}
subCtx->addMessageCallback(std::move(messageCallback));
}
RedisConnectionPtr connPtr;
{
std::lock_guard<std::mutex> lock(mutex_);
connPtr = conn_;
}
if (connPtr)
{
connPtr->sendSubscribe(subCtx);
}
else
{
LOG_TRACE << "no subscribe connection available, wait for connection";
// Just wait for connection, all channels will be re-sub
}
}
void RedisSubscriberImpl::unsubscribe(const std::string &channel) noexcept
{
LOG_TRACE << "Unsubscribe " << channel;
std::shared_ptr<SubscribeContext> subCtx;
{
std::lock_guard<std::mutex> lock(mutex_);
auto iter = subContexts_.find(channel);
if (iter == subContexts_.end())
{
LOG_DEBUG << "Attempt to unsubscribe from unknown channel "
<< channel;
return;
}
subCtx = std::move(iter->second);
subContexts_.erase(iter);
}
subCtx->disable();
RedisConnectionPtr connPtr;
{
std::lock_guard<std::mutex> lock(mutex_);
connPtr = conn_;
}
if (!connPtr)
{
LOG_TRACE << "Connection unavailable, no need to send unsub command";
return;
}
connPtr->sendUnsubscribe(subCtx);
}
void RedisSubscriberImpl::punsubscribe(const std::string &pattern) noexcept
{
LOG_TRACE << "Punsubscribe " << pattern;
std::shared_ptr<SubscribeContext> subCtx;
{
std::lock_guard<std::mutex> lock(mutex_);
auto iter = psubContexts_.find(pattern);
if (iter == psubContexts_.end())
{
LOG_DEBUG << "Attempt to punsubscribe from unknown pattern "
<< pattern;
return;
}
subCtx = std::move(iter->second);
psubContexts_.erase(iter);
}
subCtx->disable();
RedisConnectionPtr connPtr;
{
std::lock_guard<std::mutex> lock(mutex_);
connPtr = conn_;
}
if (!connPtr)
{
LOG_TRACE << "Connection unavailable, no need to send unsub command";
return;
}
connPtr->sendUnsubscribe(subCtx);
}
void RedisSubscriberImpl::setConnection(const RedisConnectionPtr &conn)
{
assert(conn);
std::lock_guard<std::mutex> lock(mutex_);
assert(!conn_);
conn_ = conn;
}
void RedisSubscriberImpl::clearConnection()
{
std::lock_guard<std::mutex> lock(mutex_);
if (conn_)
{
conn_ = nullptr;
tasks_.clear();
}
}
void RedisSubscriberImpl::subscribeNext()
{
RedisConnectionPtr connPtr;
std::shared_ptr<std::function<void(const RedisConnectionPtr &)>> taskPtr;
{
std::lock_guard<std::mutex> lock(mutex_);
if (!conn_ || tasks_.empty())
{
return;
}
connPtr = conn_;
taskPtr = std::move(tasks_.front());
tasks_.pop_front();
}
(*taskPtr)(connPtr);
}
void RedisSubscriberImpl::subscribeAll()
{
{
std::lock_guard<std::mutex> lock(mutex_);
for (auto &item : subContexts_)
{
auto subCtx = item.second;
tasks_.emplace_back(
std::make_shared<
std::function<void(const RedisConnectionPtr &)>>(
[subCtx](const RedisConnectionPtr &connPtr) mutable {
connPtr->sendSubscribe(subCtx);
}));
}
for (auto &item : psubContexts_)
{
auto subCtx = item.second;
tasks_.emplace_back(
std::make_shared<
std::function<void(const RedisConnectionPtr &)>>(
[subCtx](const RedisConnectionPtr &connPtr) mutable {
connPtr->sendSubscribe(subCtx);
}));
}
}
subscribeNext();
}
@@ -0,0 +1,62 @@
/**
*
* @file RedisSubscriberImpl.h
* @author Nitromelon
*
* Copyright 2022, Nitromelon. All rights reserved.
* https://github.com/drogonframework/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include <drogon/nosql/RedisSubscriber.h>
#include "RedisConnection.h"
#include "SubscribeContext.h"
#include <mutex>
#include <unordered_map>
#include <memory>
#include <list>
namespace drogon::nosql
{
class RedisSubscriberImpl
: public RedisSubscriber,
public std::enable_shared_from_this<RedisSubscriberImpl>
{
public:
~RedisSubscriberImpl() override;
void subscribe(const std::string &channel,
RedisMessageCallback &&messageCallback) noexcept override;
void psubscribe(const std::string &pattern,
RedisMessageCallback &&messageCallback) noexcept override;
void unsubscribe(const std::string &channel) noexcept override;
void punsubscribe(const std::string &pattern) noexcept override;
// Set a connected connection to subscriber.
void setConnection(const RedisConnectionPtr &conn);
// Clear connection and task queue.
void clearConnection();
// Subscribe next channel in task queue.
void subscribeNext();
// Subscribe all channels.
void subscribeAll();
private:
RedisConnectionPtr conn_;
std::unordered_map<std::string, std::shared_ptr<SubscribeContext>>
subContexts_;
std::unordered_map<std::string, std::shared_ptr<SubscribeContext>>
psubContexts_;
std::list<std::shared_ptr<std::function<void(const RedisConnectionPtr &)>>>
tasks_;
std::mutex mutex_;
};
} // namespace drogon::nosql
@@ -0,0 +1,128 @@
/**
*
* @file RedisConnection.cc
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#include "RedisTransactionImpl.h"
#include "../../lib/src/TaskTimeoutFlag.h"
using namespace drogon::nosql;
RedisTransactionImpl::RedisTransactionImpl(RedisConnectionPtr connPtr) noexcept
: connPtr_(std::move(connPtr))
{
}
void RedisTransactionImpl::execute(RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback)
{
execCommandAsync(
[thisPtr = shared_from_this(),
resultCallback =
std::move(resultCallback)](const RedisResult &result) {
thisPtr->isExecutedOrCancelled_ = true;
resultCallback(result);
},
std::move(exceptionCallback),
"EXEC");
}
void RedisTransactionImpl::execCommandAsync(
RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
std::string_view command,
...) noexcept
{
if (isExecutedOrCancelled_)
{
exceptionCallback(RedisException(RedisErrorCode::kTransactionCancelled,
"Transaction was cancelled"));
return;
}
if (timeout_ <= 0.0)
{
va_list args;
va_start(args, command);
connPtr_->sendvCommand(
command,
std::move(resultCallback),
[thisPtr = shared_from_this(),
exceptionCallback =
std::move(exceptionCallback)](const RedisException &err) {
LOG_ERROR << err.what();
thisPtr->isExecutedOrCancelled_ = true;
exceptionCallback(err);
},
args);
va_end(args);
}
else
{
auto expCbPtr = std::make_shared<RedisExceptionCallback>(
std::move(exceptionCallback));
auto timeoutFlagPtr = std::make_shared<TaskTimeoutFlag>(
connPtr_->getLoop(),
std::chrono::duration<double>(timeout_),
[expCbPtr]() {
if (*expCbPtr)
{
(*expCbPtr)(RedisException(RedisErrorCode::kTimeout,
"Command execution timeout"));
}
});
va_list args;
va_start(args, command);
connPtr_->sendvCommand(
command,
[resultCallback = std::move(resultCallback),
timeoutFlagPtr](const RedisResult &result) {
if (timeoutFlagPtr->done())
{
return;
}
resultCallback(result);
},
[thisPtr = shared_from_this(), expCbPtr, timeoutFlagPtr](
const RedisException &err) {
if (timeoutFlagPtr->done())
{
return;
}
LOG_ERROR << err.what();
thisPtr->isExecutedOrCancelled_ = true;
if (*expCbPtr)
(*expCbPtr)(err);
},
args);
va_end(args);
timeoutFlagPtr->runTimer();
}
}
void RedisTransactionImpl::doBegin()
{
assert(!isExecutedOrCancelled_);
execCommandAsync([](const RedisResult & /*result*/) {},
[](const RedisException & /*err*/) {},
"MULTI");
}
RedisTransactionImpl::~RedisTransactionImpl()
{
if (!isExecutedOrCancelled_)
{
LOG_WARN << "The transaction is not executed before being destroyed";
connPtr_->sendCommand([](const RedisResult & /*result*/) {},
[](const RedisException & /*err*/) {},
"DISCARD");
}
LOG_TRACE << "transaction is destroyed";
}
@@ -0,0 +1,71 @@
/**
*
* @file RedisConnection.h
* @author An Tao
*
* Copyright 2018, An Tao. All rights reserved.
* https://github.com/an-tao/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include "RedisConnection.h"
#include <drogon/nosql/RedisClient.h>
#include <memory>
namespace drogon
{
namespace nosql
{
class RedisTransactionImpl final
: public RedisTransaction,
public std::enable_shared_from_this<RedisTransactionImpl>
{
public:
explicit RedisTransactionImpl(RedisConnectionPtr connection) noexcept;
// virtual void cancel() override;
void execute(RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback) override;
void execCommandAsync(RedisResultCallback &&resultCallback,
RedisExceptionCallback &&exceptionCallback,
std::string_view command,
...) noexcept override;
std::shared_ptr<RedisSubscriber> newSubscriber() noexcept override
{
LOG_ERROR << "You can't create subscriber from redis transaction";
assert(0);
return nullptr;
}
std::shared_ptr<RedisTransaction> newTransaction() override
{
return shared_from_this();
}
void newTransactionAsync(
const std::function<void(const std::shared_ptr<RedisTransaction> &)>
&callback) override
{
callback(shared_from_this());
}
void setTimeout(double timeout) override
{
timeout_ = timeout;
}
void doBegin();
~RedisTransactionImpl() override;
private:
bool isExecutedOrCancelled_{false};
RedisConnectionPtr connPtr_;
double timeout_{-1.0};
};
} // namespace nosql
} // namespace drogon
@@ -0,0 +1,136 @@
/**
*
* @file SubscribeContext.cc
* @author Nitromelon
*
* Copyright 2022, Nitromelon. All rights reserved.
* https://github.com/drogonframework/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#include "SubscribeContext.h"
#include <stdio.h>
#include <string>
#include <utility>
using namespace drogon::nosql;
std::atomic<unsigned long long> SubscribeContext::maxContextId_{0};
enum class SubCommandType
{
Subscribe,
Unsubscribe,
Psubscribe,
Punsubscribe
};
static std::string formatSubscribeCommand(const std::string &channel,
SubCommandType type)
{
// Avoid using redisvFormatCommand, we don't want to emit unknown error
static const char *redisSubFmt =
"*2\r\n$9\r\nsubscribe\r\n$%zu\r\n%.*s\r\n";
static const char *redisUnsubFmt =
"*2\r\n$11\r\nunsubscribe\r\n$%zu\r\n%.*s\r\n";
static const char *redisPsubFmt =
"*2\r\n$10\r\npsubscribe\r\n$%zu\r\n%.*s\r\n";
static const char *redisPunsubFmt =
"*2\r\n$12\r\npunsubscribe\r\n$%zu\r\n%.*s\r\n";
const char *fmt;
switch (type)
{
case SubCommandType::Subscribe:
fmt = redisSubFmt;
break;
case SubCommandType::Unsubscribe:
fmt = redisUnsubFmt;
break;
case SubCommandType::Psubscribe:
fmt = redisPsubFmt;
break;
case SubCommandType::Punsubscribe:
fmt = redisPunsubFmt;
break;
}
std::string command;
if (channel.size() < 32)
{
char buf[64];
size_t bufSize = sizeof(buf);
int len = snprintf(buf,
bufSize,
fmt,
channel.size(),
(int)channel.size(),
channel.c_str());
command = std::string(buf, len);
}
else
{
size_t bufSize = channel.size() + 64;
char *buf = static_cast<char *>(malloc(bufSize));
int len = snprintf(buf,
bufSize,
fmt,
channel.size(),
(int)channel.size(),
channel.c_str());
command = std::string(buf, len);
free(buf);
}
return command;
}
SubscribeContext::SubscribeContext(std::weak_ptr<RedisSubscriber> &&weakSub,
const std::string &channel,
bool isPattern)
: contextId_(++maxContextId_),
weakSub_(std::move(weakSub)),
channel_(channel),
isPattern_(isPattern)
{
if (isPattern)
{
subscribeCommand_ =
formatSubscribeCommand(channel, SubCommandType::Psubscribe);
unsubscribeCommand_ =
formatSubscribeCommand(channel, SubCommandType::Punsubscribe);
}
else
{
subscribeCommand_ =
formatSubscribeCommand(channel, SubCommandType::Subscribe);
unsubscribeCommand_ =
formatSubscribeCommand(channel, SubCommandType::Unsubscribe);
}
}
void SubscribeContext::onMessage(const std::string &channel,
const std::string &message)
{
std::lock_guard<std::mutex> lock(mutex_);
for (auto &callback : messageCallbacks_)
{
callback(channel, message);
}
}
void SubscribeContext::onSubscribe(const std::string &channel,
long long numChannels)
{
LOG_DEBUG << "Subscribe success to [" << channel << "], total "
<< numChannels;
}
void SubscribeContext::onUnsubscribe(const std::string &channel,
long long numChannels)
{
LOG_DEBUG << "Unsubscribe success from [" << channel << "], total "
<< numChannels;
}
@@ -0,0 +1,115 @@
/**
*
* @file SubscribeContext.h
* @author Nitromelon
*
* Copyright 2022, Nitromelon. All rights reserved.
* https://github.com/drogonframework/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include <drogon/nosql/RedisClient.h>
#include <list>
#include <atomic>
#include <mutex>
#include <utility>
namespace drogon::nosql
{
class SubscribeContext
{
public:
static std::shared_ptr<SubscribeContext> newContext(
std::weak_ptr<RedisSubscriber> &&weakSub,
const std::string &channel,
bool isPattern = false)
{
return std::shared_ptr<SubscribeContext>(
new SubscribeContext(std::move(weakSub), channel, isPattern));
}
unsigned long long contextId() const
{
return contextId_;
}
const std::string &channel() const
{
return channel_;
}
const std::string &subscribeCommand() const
{
return subscribeCommand_;
}
const std::string &unsubscribeCommand() const
{
return unsubscribeCommand_;
}
void addMessageCallback(RedisMessageCallback &&messageCallback)
{
std::lock_guard<std::mutex> lock(mutex_);
messageCallbacks_.emplace_back(std::move(messageCallback));
}
void disable()
{
std::lock_guard<std::mutex> lock(mutex_);
disabled_ = true;
messageCallbacks_.clear();
}
void clear()
{
std::lock_guard<std::mutex> lock(mutex_);
messageCallbacks_.clear();
}
/**
* Message callback called by RedisConnection, whenever a message is
* published in target channel
* @param channel : target channel name
* @param message : message from channel
*/
void onMessage(const std::string &channel, const std::string &message);
/**
* Callback called by RedisConnection, whenever a sub or re-sub is success
*/
void onSubscribe(const std::string &channel, long long numChannels);
/**
* Callback called by RedisConnection, when unsubscription success.
*/
void onUnsubscribe(const std::string &channel, long long numChannels);
bool alive() const
{
return !disabled_ && weakSub_.lock() != nullptr;
}
private:
SubscribeContext(std::weak_ptr<RedisSubscriber> &&weakSub,
const std::string &channel,
bool isPattern);
static std::atomic<unsigned long long> maxContextId_;
unsigned long long contextId_;
std::weak_ptr<RedisSubscriber> weakSub_;
std::string channel_;
bool isPattern_{false};
std::string subscribeCommand_;
std::string unsubscribeCommand_;
std::mutex mutex_;
std::list<RedisMessageCallback> messageCallbacks_;
bool disabled_{false};
};
} // namespace drogon::nosql
@@ -0,0 +1,20 @@
link_libraries(${PROJECT_NAME})
if(WIN32)
link_libraries(iphlpapi)
endif(WIN32)
add_executable(redis_test
redis_test.cc
)
set_property(TARGET redis_test PROPERTY CXX_STANDARD ${DROGON_CXX_STANDARD})
set_property(TARGET redis_test PROPERTY CXX_STANDARD_REQUIRED ON)
set_property(TARGET redis_test PROPERTY CXX_EXTENSIONS OFF)
add_executable(redis_subscriber_test
redis_subscriber_test.cc
)
set_property(TARGET redis_subscriber_test PROPERTY CXX_STANDARD ${DROGON_CXX_STANDARD})
set_property(TARGET redis_subscriber_test PROPERTY CXX_STANDARD_REQUIRED ON)
set_property(TARGET redis_subscriber_test PROPERTY CXX_EXTENSIONS OFF)
@@ -0,0 +1,118 @@
#define DROGON_TEST_MAIN
#include <drogon/nosql/RedisClient.h>
#include <drogon/drogon_test.h>
#include <drogon/drogon.h>
#include <iostream>
#include <thread>
using namespace std::chrono_literals;
using namespace drogon::nosql;
static std::atomic_int nMsgRecv{0};
static std::atomic_int nPmsgRecv{0};
static std::atomic_int nMsgSent{0};
RedisClientPtr redisClient;
DROGON_TEST(RedisSubscriberTest)
{
redisClient = drogon::nosql::RedisClient::newRedisClient(
trantor::InetAddress("127.0.0.1", 6379), 1);
REQUIRE(redisClient != nullptr);
auto subscriber = redisClient->newSubscriber();
subscriber->subscribe("test_sub",
[](const std::string &channel,
const std::string &message) {
++nMsgRecv;
LOG_INFO << "Channel test_sub receive "
<< nMsgRecv << " messages: " << message;
});
subscriber->psubscribe("test_*",
[](const std::string &channel,
const std::string &message) {
++nPmsgRecv;
LOG_INFO << "Channel " << channel << " receive "
<< nPmsgRecv
<< " pmessages: " << message;
});
std::this_thread::sleep_for(1s);
auto fnPublish = [TEST_CTX](const char *channel, int i) {
redisClient->execCommandAsync(
[TEST_CTX](const drogon::nosql::RedisResult &r) {
SUCCESS();
++nMsgSent;
},
[TEST_CTX](const std::exception &err) {
MANDATE(err.what());
LOG_ERROR << err.what();
++nMsgSent;
},
"publish %s %s%d",
channel,
"drogon",
i);
};
for (int i = 0; i < 5; ++i)
{
fnPublish("test_sub", i);
}
for (int i = 5; i < 10; ++i)
{
fnPublish("test_test", i);
}
while (nMsgSent < 10)
{
std::this_thread::sleep_for(100ms);
}
std::this_thread::sleep_for(1s);
MANDATE(nMsgRecv == 5);
MANDATE(nPmsgRecv == 10);
// Unsub from channel
subscriber->unsubscribe("test_sub");
fnPublish("test_sub", 11);
while (nMsgSent < 11)
{
std::this_thread::sleep_for(100ms);
}
std::this_thread::sleep_for(1s);
MANDATE(nMsgRecv == 5);
MANDATE(nPmsgRecv == 11);
// Unsub from pattern
subscriber->punsubscribe("test_*");
fnPublish("test_sub", 12);
while (nMsgSent < 12)
{
std::this_thread::sleep_for(100ms);
}
std::this_thread::sleep_for(1s);
MANDATE(nMsgRecv == 5);
MANDATE(nPmsgRecv == 11);
}
int main(int argc, char **argv)
{
#ifndef USE_REDIS
LOG_DEBUG << "Drogon is built without "
"Redis. No tests executed.";
return 0;
#endif
std::promise<void> p1;
std::future<void> f1 = p1.get_future();
std::thread thr([&]() {
p1.set_value();
drogon::app().run();
});
f1.get();
int testStatus = drogon::test::run(argc, argv);
drogon::app().getLoop()->queueInLoop([]() { drogon::app().quit(); });
thr.join();
return testStatus;
}
@@ -0,0 +1,205 @@
#define DROGON_TEST_MAIN
#include <drogon/nosql/RedisClient.h>
#include <drogon/drogon_test.h>
#include <drogon/drogon.h>
#include <iostream>
#include <thread>
using namespace std::chrono_literals;
using namespace drogon::nosql;
RedisClientPtr redisClient;
DROGON_TEST(RedisTest)
{
redisClient = drogon::nosql::RedisClient::newRedisClient(
trantor::InetAddress("127.0.0.1", 6379), 1);
REQUIRE(redisClient != nullptr);
// std::this_thread::sleep_for(1s);
redisClient->newTransactionAsync(
[TEST_CTX](const RedisTransactionPtr &transPtr) {
// 1
transPtr->execCommandAsync(
[TEST_CTX](const drogon::nosql::RedisResult &r) { SUCCESS(); },
[TEST_CTX](const std::exception &err) { MANDATE(err.what()); },
"ping");
// 2
transPtr->execute(
[TEST_CTX](const drogon::nosql::RedisResult &r) { SUCCESS(); },
[TEST_CTX](const std::exception &err) { MANDATE(err.what()); });
});
// 3
redisClient->execCommandAsync(
[TEST_CTX](const drogon::nosql::RedisResult &r) { SUCCESS(); },
[TEST_CTX](const std::exception &err) { MANDATE(err.what()); },
"set %s %s",
"id_123",
"drogon");
// 4
redisClient->execCommandAsync(
[TEST_CTX](const drogon::nosql::RedisResult &r) {
MANDATE(r.type() == RedisResultType::kArray);
MANDATE(r.asArray().size() == 1UL);
},
[TEST_CTX](const std::exception &err) { MANDATE(err.what()); },
"keys id_*");
// 5
redisClient->execCommandAsync(
[TEST_CTX](const drogon::nosql::RedisResult &r) {
MANDATE(r.asString() == "hello");
},
[TEST_CTX](const RedisException &err) { MANDATE(err.what()); },
"echo %s",
"hello");
// 6
redisClient->execCommandAsync(
[TEST_CTX](const drogon::nosql::RedisResult &r) { SUCCESS(); },
[TEST_CTX](const RedisException &err) { MANDATE(err.what()); },
"flushall");
// 7
redisClient->execCommandAsync(
[TEST_CTX](const drogon::nosql::RedisResult &r) {
MANDATE(r.type() == RedisResultType::kNil);
},
[TEST_CTX](const RedisException &err) { MANDATE(err.what()); },
"get %s",
"xxxxx");
#ifdef __cpp_impl_coroutine
auto coro_test = [TEST_CTX]() -> drogon::Task<> {
// 8
try
{
auto r = co_await redisClient->execCommandCoro("get %s", "haha");
MANDATE(r.type() == RedisResultType::kNil);
}
catch (const RedisException &err)
{
FAULT(err.what());
}
};
drogon::sync_wait(coro_test());
#endif
// 9. Test sync
try
{
auto res = redisClient->execCommandSync<std::string>(
[](const RedisResult &result) { return result.asString(); },
"set %s %s",
"sync_key",
"sync_value");
MANDATE(res == "OK");
}
catch (const RedisException &err)
{
MANDATE(err.what());
}
try
{
auto [isNull, str] =
redisClient->execCommandSync<std::pair<bool, std::string>>(
[](const RedisResult &result) -> std::pair<bool, std::string> {
if (result.isNil())
{
return {true, ""};
}
return {false, result.asString()};
},
"get %s",
"sync_key");
MANDATE(isNull == false);
MANDATE(str == "sync_value");
}
catch (const RedisException &err)
{
MANDATE(err.what());
}
// 10. Test sync redis exception
try
{
auto [isNull, str] =
redisClient->execCommandSync<std::pair<bool, std::string>>(
[](const RedisResult &result) -> std::pair<bool, std::string> {
if (result.isNil())
{
return {true, ""};
}
return {false, result.asString()};
},
"get %s %s",
"sync_key",
"sync_key");
MANDATE(false);
}
catch (const RedisException &err)
{
LOG_INFO << "Successfully catch sync error: " << err.what();
MANDATE(err.code() == RedisErrorCode::kRedisError);
SUCCESS();
}
// 11. Test sync process function exception
try
{
auto value = redisClient->execCommandSync<std::string>(
[](const RedisResult &result) {
if (result.isNil())
{
throw std::runtime_error("Key not exists");
}
return result.asString();
},
"get %s",
"not_exists");
MANDATE(false);
}
catch (const RedisException &err)
{
(void)err;
MANDATE(false);
}
catch (const std::runtime_error &err)
{
MANDATE(std::string("Key not exists") == err.what());
SUCCESS();
}
// 12. Test omit template parameter
try
{
auto i = redisClient->execCommandSync(
[](const RedisResult &r) { return r.asInteger(); },
"del %s",
"sync_key");
MANDATE(i == 1);
}
catch (const RedisException &err)
{
MANDATE(err.what());
}
}
int main(int argc, char **argv)
{
#ifndef USE_REDIS
LOG_DEBUG << "Drogon is built without Redis. No tests executed.";
return 0;
#endif
std::promise<void> p1;
std::future<void> f1 = p1.get_future();
std::thread thr([&]() {
p1.set_value();
drogon::app().run();
});
f1.get();
int testStatus = drogon::test::run(argc, argv);
drogon::app().getLoop()->queueInLoop([]() { drogon::app().quit(); });
thr.join();
return testStatus;
}