diff --git a/include/ichor/Common.h b/include/ichor/Common.h index 5e6ca7e1..0d9cda24 100644 --- a/include/ichor/Common.h +++ b/include/ichor/Common.h @@ -152,29 +152,6 @@ namespace Ichor { inline constexpr bool PreventOthersHandling = false; inline constexpr bool AllowOthersHandling = true; - /// Code modified from https://stackoverflow.com/a/73078442/1460998 - /// converts a string to an integer with little error checking. Only use if you're very sure that the string is actually a number. - static constexpr inline int64_t FastAtoi(const char* str) noexcept { - int64_t val = 0; - uint8_t x; - bool neg{}; - if(str[0] == '-') { - str++; - neg = true; - } - while ((x = uint8_t(*str++ - '0')) <= 9) val = val * 10 + x; - return neg ? -val : val; - } - - /// Code from https://stackoverflow.com/a/73078442/1460998 - /// converts a string to an unsigned integer with little error checking. Only use if you're very sure that the string is actually a number. - static constexpr inline uint64_t FastAtoiu(const char* str) noexcept { - uint64_t val = 0; - uint8_t x; - while ((x = uint8_t(*str++ - '0')) <= 9) val = val * 10 + x; - return val; - } - // Code from https://artificial-mind.net/blog/2020/10/31/constexpr-for template constexpr void constexpr_for(F&& f) { diff --git a/include/ichor/services/redis/HiredisService.h b/include/ichor/services/redis/HiredisService.h index d0ed9b41..a41b1ea5 100644 --- a/include/ichor/services/redis/HiredisService.h +++ b/include/ichor/services/redis/HiredisService.h @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace Ichor { @@ -12,11 +13,12 @@ namespace Ichor { * Service for the redis protocol. * * Properties: - * - "Address" - What address to connect to (required) - * - "Port" - What port to connect to (required) - * - "PollIntervalMs" - with which interval in milliseconds to poll hiredis for responses. Lower values reduce latency at the cost of more CPU usage. (default: 10 ms) - * - "TryConnectIntervalMs" - with which interval in milliseconds to try (re)connecting (default: 10 ms) - * - "TimeoutMs" - with which interval in milliseconds to timeout for (re)connecting, after which the service stops itself (default: 10'000 ms) + * - "Address" std::string - What address to connect to (required) + * - "Port" uint16_t - What port to connect to (required) + * - "PollIntervalMs" uint64_t - with which interval in milliseconds to poll hiredis for responses. Lower values reduce latency at the cost of more CPU usage. (default: 10 ms) + * - "TryConnectIntervalMs" uint64_t - with which interval in milliseconds to try (re)connecting (default: 100 ms) + * - "TimeoutMs" uint64_t - with which interval in milliseconds to timeout for (re)connecting, after which the service stops itself (default: 10'000 ms) + * - "Debug" bool - Enable verbose logging of redis requests and responses (default: false) */ class HiredisService final : public IRedis, public AdvancedService { public: @@ -26,17 +28,27 @@ namespace Ichor { void onRedisConnect(int status); void onRedisDisconnect(int status); + void setDebug(bool debug) noexcept; + [[nodiscard]] bool getDebug() const noexcept; + // see IRedis for function descriptions Task> auth(std::string_view user, std::string_view password) final; Task> set(std::string_view key, std::string_view value) final; Task> set(std::string_view key, std::string_view value, RedisSetOptions const &opts) final; Task> get(std::string_view key) final; + Task> getdel(std::string_view key) final; Task> del(std::string_view keys) final; Task> incr(std::string_view keys) final; Task> incrBy(std::string_view keys, int64_t incr) final; Task> incrByFloat(std::string_view keys, double incr) final; Task> decr(std::string_view keys) final; Task> decrBy(std::string_view keys, int64_t decr) final; + Task> strlen(std::string_view key) final; + Task> multi() final; + Task>, RedisError>> exec() final; + Task> discard() final; + Task, RedisError>> info() final; + Task> getServerVersion() final; private: Task> start() final; @@ -51,18 +63,23 @@ namespace Ichor { void addDependencyInstance(IEventQueue &queue, IService&); void removeDependencyInstance(IEventQueue &queue, IService&); - tl::expected connect(); + tl::expected connect(std::string const &addr, uint16_t port); friend DependencyRegister; ILogger *_logger{}; redisAsyncContext *_redisContext{}; + AsyncManualResetEvent _startEvt{}; AsyncManualResetEvent _disconnectEvt{}; ITimerFactory *_timerFactory{}; IEventQueue *_queue{}; ITimer *_timeoutTimer{}; + std::vector _queuedResponseTypes{}; + bool _timeoutTimerRunning{}; + bool _debug{}; + tl::optional _redisVersion{}; uint64_t _pollIntervalMs{10}; - uint64_t _tryConnectIntervalMs{10}; + uint64_t _tryConnectIntervalMs{100}; uint64_t _timeoutMs{10'000}; uint64_t _timeWhenDisconnected{}; }; diff --git a/include/ichor/services/redis/IRedis.h b/include/ichor/services/redis/IRedis.h index cb59e9b3..c5e36db3 100644 --- a/include/ichor/services/redis/IRedis.h +++ b/include/ichor/services/redis/IRedis.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -37,7 +38,11 @@ namespace Ichor { }; enum class RedisError : uint_fast16_t { - DISCONNECTED + UNKNOWN, + DISCONNECTED, + FUNCTION_NOT_AVAILABLE_IN_SERVER, // probably redis-server version too low + TRANSACTION_NOT_STARTED, // probably forgot to execute a multi command + QUEUED, // Queued for transaction, use exec() to get the result }; class IRedis { @@ -66,6 +71,11 @@ namespace Ichor { /// \return coroutine with the reply from Redis virtual Task> get(std::string_view key) = 0; + /// Get key and delete the key + /// \param key + /// \return coroutine with the reply from Redis + virtual Task> getdel(std::string_view key) = 0; + /// Deletes a key in redis /// \param keys space-seperated list of keys to delete /// \return coroutine with the number of deleted values @@ -102,12 +112,26 @@ namespace Ichor { /// Returns the length of the string /// \param key /// \return coroutine with the length of the value stored for the key -// virtual Task> strlen(std::string_view key, int64_t decr) = 0; + virtual Task> strlen(std::string_view key) = 0; + + /// Start a transaction + /// \return coroutine with a possible error + virtual Task> multi() = 0; + + /// Execute all commands in a transaction + /// \return coroutine with a possible error + virtual Task>, RedisError>> exec() = 0; + + /// Abort a transaction + /// \return coroutine with a possible error + virtual Task> discard() = 0; + + /// Returns information and statistics about the server + /// \return coroutine with the length of the value stored for the key + virtual Task, RedisError>> info() = 0; + + virtual Task> getServerVersion() = 0; -// strlen -// multi -// exec -// discard // getrange // setrange // append (multi?) diff --git a/include/ichor/stl/StringUtils.h b/include/ichor/stl/StringUtils.h new file mode 100644 index 00000000..3f5d0344 --- /dev/null +++ b/include/ichor/stl/StringUtils.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Ichor { + + /// Code modified from https://stackoverflow.com/a/73078442/1460998 + /// converts a string to an integer with little error checking. Only use if you're very sure that the string is actually a number. + static constexpr inline int64_t FastAtoi(const char* str) noexcept { + int64_t val = 0; + uint8_t x; + bool neg{}; + if(str[0] == '-') { + str++; + neg = true; + } + while ((x = uint8_t(*str++ - '0')) <= 9) val = val * 10 + x; + return neg ? -val : val; + } + + /// Code from https://stackoverflow.com/a/73078442/1460998 + /// converts a string to an unsigned integer with little error checking. Only use if you're very sure that the string is actually a number. + static constexpr inline uint64_t FastAtoiu(const char* str) noexcept { + uint64_t val = 0; + uint8_t x; + while ((x = uint8_t(*str++ - '0')) <= 9) val = val * 10 + x; + return val; + } + + struct Version { + uint64_t major; + uint64_t minor; + uint64_t patch; + + auto operator<=>(Version const &v) const = default; + }; + + // Taken from https://www.cppstories.com/2018/07/string-view-perf-followup/ + static inline std::vector split(std::string_view str, std::string_view delims) { + std::vector output; + if(delims.size() == 1) { + auto count = std::count(str.cbegin(), str.cend(), delims[0]); + output.reserve(static_cast(count)); + } + + for(auto first = str.data(), second = str.data(), last = first + str.size(); second != last && first != last; first = second + 1) { + second = std::find_first_of(first, last, std::cbegin(delims), std::cend(delims)); + + if(first != second) { + output.emplace_back(first, second - first); + } + } + + return output; + } + + static inline tl::optional parseStringAsVersion(std::string_view str) { + if(str.empty()) { + return {}; + } + + auto wrongLetterCount = std::count_if(str.cbegin(), str.cend(), [](char const c) { + return c != '0' && c != '1' && c != '2' && c != '3' && c != '4' && c != '5' && c != '6' && c != '7' && c != '8' && c != '9' && c != '.'; + }); + + if(wrongLetterCount != 0) { + return {}; + } + + if(*str.crbegin() == '.') { + return {}; + } + + auto splitStr = split(str, "."); + + if(splitStr.size() != 3) { + return {}; + } + + return Version{FastAtoiu(splitStr[0].data()), FastAtoiu(splitStr[1].data()), FastAtoiu(splitStr[2].data())}; + } +} + + + +template <> +struct fmt::formatter { + constexpr auto parse(format_parse_context& ctx) { + return ctx.end(); + } + + template + auto format(const Ichor::Version& v, FormatContext& ctx) { + return fmt::format_to(ctx.out(), "{}.{}.{}", v.major, v.minor, v.patch); + } +}; diff --git a/src/services/etcd/EtcdV2Service.cpp b/src/services/etcd/EtcdV2Service.cpp index bca71f0b..3405efac 100644 --- a/src/services/etcd/EtcdV2Service.cpp +++ b/src/services/etcd/EtcdV2Service.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include diff --git a/src/services/redis/HiRedisService.cpp b/src/services/redis/HiRedisService.cpp index 95172b81..ff319100 100644 --- a/src/services/redis/HiRedisService.cpp +++ b/src/services/redis/HiRedisService.cpp @@ -1,18 +1,20 @@ #include #include +#include #include #include #include -#include + +using namespace std::literals; #define FMT_INLINE_BUFFER_SIZE 1024 #define ICHOR_WAIT_IF_NOT_CONNECTED \ if(_redisContext == nullptr) { \ co_await _disconnectEvt; \ - fmt::print("post-await\n"); \ + ICHOR_LOG_TRACE(_logger, "post-await\n"); \ if(_redisContext == nullptr) [[unlikely]] { \ - fmt::print("rediscontext null\n"); \ + ICHOR_LOG_TRACE(_logger, "rediscontext null\n"); \ co_return tl::unexpected(Ichor::RedisError::DISCONNECTED); \ } \ } \ @@ -25,28 +27,73 @@ namespace Ichor { freeReplyObject(reply); } } + std::string origCommand; redisReply *reply; AsyncManualResetEvent evt{}; }; + // this function assumes it is being called from the correct ichor thread. + // the poll timer should take care of that. static void _onRedisConnect(const struct redisAsyncContext *c, int status) { auto *svc = reinterpret_cast(c->data); svc->onRedisConnect(status); } + // this function assumes it is being called from the correct ichor thread. + // the poll timer should take care of that. static void _onRedisDisconnect(const struct redisAsyncContext *c, int status) { auto *svc = reinterpret_cast(c->data); svc->onRedisDisconnect(status); } + static void _printReply(redisReply const * const r, char const * const indent) { + switch(r->type) { + case REDIS_REPLY_ERROR: + case REDIS_REPLY_STRING: + case REDIS_REPLY_DOUBLE: + fmt::print("{}String-ish reply from redis {} \"{}\"\n", indent, r->dval, r->str); + break; + case REDIS_REPLY_BIGNUM: + fmt::print("{}Bignum reply from redis \"{}\"\n", indent, r->str); + break; + case REDIS_REPLY_INTEGER: + fmt::print("{}Integer reply from redis {}\n", indent, r->integer); + break; + case REDIS_REPLY_VERB: + fmt::print("{}Verb reply from redis \"{}\" \"{}\"\n", indent, r->vtype, r->str); + break; + case REDIS_REPLY_STATUS: + fmt::print("{}Status reply from redis {}\n", indent, r->str); + break; + case REDIS_REPLY_ARRAY: + fmt::print("{}Array reply from redis\n", indent); + for(size_t i = 0; i < r->elements; i++) { + _printReply(r->element[i], "\t"); + } + break; + default: + fmt::print("{}Unknown reply from redis type {}\n", indent, r->type); + } + } + + // this function assumes it is being called from the correct ichor thread. + // the poll timer should take care of that. static void _onAsyncReply(redisAsyncContext *c, void *reply, void *privdata) { auto *ichorReply = static_cast(privdata); auto *svc = static_cast(c->data); ichorReply->reply = static_cast(reply); + if(svc->getDebug()) { + fmt::print("hiredis command \"{}\" got reply from redis type {}\n", ichorReply->origCommand, reply == nullptr ? -1 : ichorReply->reply->type); + } + if(reply != nullptr && svc->getDebug()) { + _printReply(ichorReply->reply, ""); + } - GetThreadLocalManager().getEventQueue().pushEvent(svc->getServiceId(), [ichorReply]() { - ichorReply->evt.set(); - }); + if(reply != nullptr && ichorReply->reply->type == REDIS_REPLY_ERROR) { + fmt::print("hiredis command \"{}\" got error from redis: {}\n", ichorReply->origCommand, ichorReply->reply->str); + } + + ichorReply->evt.set(); } static fmt::basic_memory_buffer _formatSet(std::string_view const &key, std::string_view const &value, RedisSetOptions const &opts) { @@ -99,6 +146,16 @@ Ichor::HiredisService::HiredisService(DependencyRegister ®, Properties props) } Ichor::Task> Ichor::HiredisService::start() { + auto addrIt = getProperties().find("Address"); + auto portIt = getProperties().find("Port"); + + if(addrIt == getProperties().end()) [[unlikely]] { + throw std::runtime_error("Missing address when starting HiredisService"); + } + if(portIt == getProperties().end()) [[unlikely]] { + throw std::runtime_error("Missing port when starting HiredisService"); + } + if(getProperties().contains("PollIntervalMs")) { _pollIntervalMs = Ichor::any_cast(getProperties()["PollIntervalMs"]); } @@ -108,12 +165,79 @@ Ichor::Task> Ichor::HiredisService::start( if(getProperties().contains("TryConnectIntervalMs")) { _tryConnectIntervalMs = Ichor::any_cast(getProperties()["TryConnectIntervalMs"]); } - - auto outcome = connect(); - if(!outcome) { - co_return tl::unexpected(StartError::FAILED); + if(getProperties().contains("Debug")) { + _debug = Ichor::any_cast(getProperties()["Debug"]); } + _timeWhenDisconnected = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + _timeoutTimer = &_timerFactory->createTimer(); + _timeoutTimer->setCallbackAsync([this]() -> AsyncGenerator { + ICHOR_LOG_INFO(_logger, "Trying to (re)connect"); + + if(_timeoutTimerRunning) { + co_return {}; + } + + bool error{}; + _timeoutTimerRunning = true; + ScopeGuard sg([this, &error = error]() { + uint64_t now = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + if(_redisContext != nullptr) { + ICHOR_LOG_INFO(_logger, "Connected"); + _timeoutTimer->stopTimer(); + _startEvt.set(); + } else if(error || now > _timeWhenDisconnected + _timeoutMs) { + ICHOR_LOG_INFO(_logger, "Could not connect within {:L} ms", _timeoutMs); + _timeoutTimer->stopTimer(); + _startEvt.set(); + _queue->pushPrioritisedEvent(getServiceId(), INTERNAL_DEPENDENCY_EVENT_PRIORITY, getServiceId()); + } + _timeoutTimerRunning = false; + }); + + auto addrIt = getProperties().find("Address"); + auto portIt = getProperties().find("Port"); + auto &addr = Ichor::any_cast(addrIt->second); + auto port = Ichor::any_cast(portIt->second); + + if(!connect(addr, port)) { + ICHOR_LOG_ERROR(_logger, "Couldn't setup hiredis"); + error = true; + co_return {}; + } + + auto i = co_await info(); + if(!i) { + if(i.error() != RedisError::DISCONNECTED) { + ICHOR_LOG_ERROR(_logger, "Couldn't get info from redis"); + error = true; + } + co_return {}; + } + + auto versionStr = i.value().find("redis_version"); + + if(versionStr == i.value().end()) { + ICHOR_LOG_ERROR(_logger, "Couldn't get proper info from redis:"); + for(auto const &[k, v] : i.value()) { + ICHOR_LOG_ERROR(_logger, "\t{} : {}", k, v); + } + error = true; + co_return {}; + } + + _redisVersion = parseStringAsVersion(versionStr->second); + + if(!_redisVersion) { + ICHOR_LOG_ERROR(_logger, "Couldn't parse version from redis: \"{}\"", versionStr->second); + error = true; + co_return {}; + } + + co_return {}; + }); + _timeoutTimer->setChronoInterval(std::chrono::milliseconds(_tryConnectIntervalMs)); + auto &pollTimer = _timerFactory->createTimer(); pollTimer.setCallback([this]() { if(_redisContext != nullptr) { @@ -122,23 +246,13 @@ Ichor::Task> Ichor::HiredisService::start( }); pollTimer.setChronoInterval(std::chrono::milliseconds(_pollIntervalMs)); pollTimer.startTimer(); + _timeoutTimer->startTimer(); - _timeoutTimer = &_timerFactory->createTimer(); - _timeoutTimer->setCallback([this]() { - fmt::print("Trying to reconnect\n"); - if(!connect()) { - _queue->pushPrioritisedEvent(getServiceId(), INTERNAL_DEPENDENCY_EVENT_PRIORITY, getServiceId()); - return; - } + co_await _startEvt; - uint64_t now = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); - if(now > _timeWhenDisconnected + _timeoutMs) { - _disconnectEvt.set(); - _queue->pushPrioritisedEvent(getServiceId(), INTERNAL_DEPENDENCY_EVENT_PRIORITY, getServiceId()); - _timeoutTimer->stopTimer(); - } - }); - _timeoutTimer->setChronoInterval(std::chrono::milliseconds(_tryConnectIntervalMs)); + if(_redisContext == nullptr) { + co_return tl::unexpected(StartError::FAILED); + } INTERNAL_DEBUG("HiredisService::start() co_return"); @@ -181,14 +295,22 @@ Ichor::Task> Ichor::Hired ICHOR_WAIT_IF_NOT_CONNECTED; IchorRedisReply evt{}; + evt.origCommand = fmt::format("AUTH {} password of size {}", user, password.size()); auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "AUTH %b %b", user.data(), user.size(), password.data(), password.size()); if(ret == REDIS_ERR) [[unlikely]] { - throw std::runtime_error("couldn't run async command"); + throw std::runtime_error("couldn't run async command auth"); } co_await evt.evt; if(evt.reply == nullptr) [[unlikely]] { - co_return RedisAuthReply{}; + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); } co_return RedisAuthReply{true}; @@ -198,14 +320,22 @@ Ichor::Task> Ichor::Hiredi ICHOR_WAIT_IF_NOT_CONNECTED; IchorRedisReply evt{}; + evt.origCommand = fmt::format("SET {} {}", key, value); auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "SET %b %b", key.data(), key.size(), value.data(), value.size()); if(ret == REDIS_ERR) [[unlikely]] { - throw std::runtime_error("couldn't run async command"); + throw std::runtime_error("couldn't run async command set"); } co_await evt.evt; if(evt.reply == nullptr) [[unlikely]] { - co_return RedisSetReply{}; + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); } co_return RedisSetReply{true, evt.reply->str}; @@ -217,14 +347,22 @@ Ichor::Task> Ichor::Hiredi auto buf = _formatSet(key, value, opts); IchorRedisReply evt{}; + evt.origCommand = fmt::format("{}", buf.data()); auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, buf.data()); if(ret == REDIS_ERR) [[unlikely]] { - throw std::runtime_error("couldn't run async command"); + throw std::runtime_error("couldn't run async command set opts"); } co_await evt.evt; if(evt.reply == nullptr) [[unlikely]] { - co_return RedisSetReply{}; + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); } co_return RedisSetReply{true, evt.reply->str}; @@ -234,31 +372,80 @@ Ichor::Task> Ichor::Hiredi ICHOR_WAIT_IF_NOT_CONNECTED; IchorRedisReply evt{}; + evt.origCommand = fmt::format("GET {}", key); auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "GET %b", key.data(), key.size()); if(ret == REDIS_ERR) [[unlikely]] { - throw std::runtime_error("couldn't run async command"); + throw std::runtime_error("couldn't run async command get"); } co_await evt.evt; if(evt.reply == nullptr) [[unlikely]] { + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); + } + + if(evt.reply->type == REDIS_REPLY_NIL) { co_return RedisGetReply{}; } co_return RedisGetReply{evt.reply->str}; } +Ichor::Task> Ichor::HiredisService::getdel(std::string_view key) { + ICHOR_WAIT_IF_NOT_CONNECTED; + + if(*_redisVersion < Version{6, 2, 0}) { + co_return tl::unexpected(RedisError::FUNCTION_NOT_AVAILABLE_IN_SERVER); + } + + IchorRedisReply evt{}; + evt.origCommand = fmt::format("GETDEL {}", key); + auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "GETDEL %b", key.data(), key.size()); + if(ret == REDIS_ERR) [[unlikely]] { + throw std::runtime_error("couldn't run async command getdel"); + } + co_await evt.evt; + + if(evt.reply == nullptr) [[unlikely]] { + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); + } + + co_return RedisGetReply{evt.reply->str}; +} + Ichor::Task> Ichor::HiredisService::del(std::string_view keys) { ICHOR_WAIT_IF_NOT_CONNECTED; IchorRedisReply evt{}; + evt.origCommand = fmt::format("DEL {}", keys); auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "DEL %b", keys.data(), keys.size()); if(ret == REDIS_ERR) [[unlikely]] { - throw std::runtime_error("couldn't run async command"); + throw std::runtime_error("couldn't run async command del"); } co_await evt.evt; if(evt.reply == nullptr) [[unlikely]] { - co_return RedisIntegerReply{}; + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); } co_return RedisIntegerReply{evt.reply->integer}; @@ -268,14 +455,22 @@ Ichor::Task> Ichor::Hi ICHOR_WAIT_IF_NOT_CONNECTED; IchorRedisReply evt{}; + evt.origCommand = fmt::format("INCR {}", keys); auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "INCR %b", keys.data(), keys.size()); if(ret == REDIS_ERR) [[unlikely]] { - throw std::runtime_error("couldn't run async command"); + throw std::runtime_error("couldn't run async command incr"); } co_await evt.evt; if(evt.reply == nullptr) [[unlikely]] { - co_return RedisIntegerReply{}; + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); } co_return RedisIntegerReply{evt.reply->integer}; @@ -285,14 +480,22 @@ Ichor::Task> Ichor::Hi ICHOR_WAIT_IF_NOT_CONNECTED; IchorRedisReply evt{}; + evt.origCommand = fmt::format("INCRBY {} {}", keys, incr); auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "INCRBY %b %i", keys.data(), keys.size(), incr); if(ret == REDIS_ERR) [[unlikely]] { - throw std::runtime_error("couldn't run async command"); + throw std::runtime_error("couldn't run async command incrBy"); } co_await evt.evt; if(evt.reply == nullptr) [[unlikely]] { - co_return RedisIntegerReply{}; + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); } co_return RedisIntegerReply{evt.reply->integer}; @@ -301,15 +504,27 @@ Ichor::Task> Ichor::Hi Ichor::Task> Ichor::HiredisService::incrByFloat(std::string_view keys, double incr) { ICHOR_WAIT_IF_NOT_CONNECTED; + if(*_redisVersion < Version{2, 6, 0}) { + co_return tl::unexpected(RedisError::FUNCTION_NOT_AVAILABLE_IN_SERVER); + } + IchorRedisReply evt{}; + evt.origCommand = fmt::format("INCRBYFLOAT {} {}", keys, incr); auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "INCRBYFLOAT %b %f", keys.data(), keys.size(), incr); if(ret == REDIS_ERR) [[unlikely]] { - throw std::runtime_error("couldn't run async command"); + throw std::runtime_error("couldn't run async command incrByFloat"); } co_await evt.evt; if(evt.reply == nullptr) [[unlikely]] { - co_return RedisIntegerReply{}; + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); } co_return RedisIntegerReply{evt.reply->integer}; @@ -319,14 +534,22 @@ Ichor::Task> Ichor::Hi ICHOR_WAIT_IF_NOT_CONNECTED; IchorRedisReply evt{}; + evt.origCommand = fmt::format("DECR {}", keys); auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "DECR %b", keys.data(), keys.size()); if(ret == REDIS_ERR) [[unlikely]] { - throw std::runtime_error("couldn't run async command"); + throw std::runtime_error("couldn't run async command decr"); } co_await evt.evt; if(evt.reply == nullptr) [[unlikely]] { - co_return RedisIntegerReply{}; + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); } co_return RedisIntegerReply{evt.reply->integer}; @@ -336,23 +559,237 @@ Ichor::Task> Ichor::Hi ICHOR_WAIT_IF_NOT_CONNECTED; IchorRedisReply evt{}; + evt.origCommand = fmt::format("DECRBY {} {}", keys, decr); auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "DECRBY %b %i", keys.data(), keys.size(), decr); if(ret == REDIS_ERR) [[unlikely]] { - throw std::runtime_error("couldn't run async command"); + throw std::runtime_error("couldn't run async command decrBy"); + } + co_await evt.evt; + + if(evt.reply == nullptr) [[unlikely]] { + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); + } + + co_return RedisIntegerReply{evt.reply->integer}; +} + +Ichor::Task> Ichor::HiredisService::strlen(std::string_view key) { + ICHOR_WAIT_IF_NOT_CONNECTED; + + if(*_redisVersion < Version{2, 2, 0}) { + co_return tl::unexpected(RedisError::FUNCTION_NOT_AVAILABLE_IN_SERVER); + } + + IchorRedisReply evt{}; + evt.origCommand = fmt::format("STRLEN {}", key); + auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "STRLEN %b", key.data(), key.size()); + if(ret == REDIS_ERR) [[unlikely]] { + throw std::runtime_error("couldn't run async command strlen"); } co_await evt.evt; if(evt.reply == nullptr) [[unlikely]] { - co_return RedisIntegerReply{}; + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->type == REDIS_REPLY_STATUS && evt.reply->str == "QUEUED"sv) { + _queuedResponseTypes.emplace_back(typeNameHash()); + co_return tl::unexpected(RedisError::QUEUED); } co_return RedisIntegerReply{evt.reply->integer}; } +Ichor::Task> Ichor::HiredisService::multi() { + ICHOR_WAIT_IF_NOT_CONNECTED; + + if(*_redisVersion < Version{1, 2, 0}) { + co_return tl::unexpected(RedisError::FUNCTION_NOT_AVAILABLE_IN_SERVER); + } + + IchorRedisReply evt{}; + evt.origCommand = fmt::format("MULTI"); + auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "MULTI"); + if(ret == REDIS_ERR) [[unlikely]] { + throw std::runtime_error("couldn't run async command strlen"); + } + co_await evt.evt; + + if(evt.reply == nullptr) [[unlikely]] { + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + + co_return {}; +} + +Ichor::Task>, Ichor::RedisError>> Ichor::HiredisService::exec() { + ScopeGuard sg{[this]() { + _queuedResponseTypes.clear(); + }}; + + ICHOR_WAIT_IF_NOT_CONNECTED; + + if(*_redisVersion < Version{1, 2, 0}) { + co_return tl::unexpected(RedisError::FUNCTION_NOT_AVAILABLE_IN_SERVER); + } + + IchorRedisReply evt{}; + evt.origCommand = fmt::format("EXEC"); + auto redisRet = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "EXEC"); + if(redisRet == REDIS_ERR) [[unlikely]] { + throw std::runtime_error("couldn't run async command strlen"); + } + co_await evt.evt; + + if(evt.reply == nullptr) [[unlikely]] { + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type != REDIS_REPLY_ARRAY) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + if(evt.reply->elements != _queuedResponseTypes.size()) { + ICHOR_LOG_ERROR(_logger, "Please open a bug report, number of queued responses does not match"); + std::terminate(); + } + + std::vector> ret; + for(size_t i = 0; i < evt.reply->elements; i++) { + auto *element = evt.reply->element[i]; + switch (_queuedResponseTypes[i]) { + case typeNameHash(): + if(element->type != REDIS_REPLY_STRING && element->type != REDIS_REPLY_NIL) [[unlikely]] { + ICHOR_LOG_ERROR(_logger, "Please open a bug report, queued responses does not match"); + std::terminate(); + } + if(element->type == REDIS_REPLY_STRING) { + ret.emplace_back(RedisGetReply{element->str}); + } else { + ret.emplace_back(RedisGetReply{}); + } + break; + case typeNameHash(): + if(element->type != REDIS_REPLY_STATUS) [[unlikely]] { + ICHOR_LOG_ERROR(_logger, "Please open a bug report, queued responses does not match"); + std::terminate(); + } + ret.emplace_back(RedisSetReply{true, element->str}); + break; + case typeNameHash(): + if(element->type != REDIS_REPLY_STRING) [[unlikely]] { + ICHOR_LOG_ERROR(_logger, "Please open a bug report, queued responses does not match"); + std::terminate(); + } + ret.emplace_back(RedisAuthReply{true}); + break; + case typeNameHash(): + if(element->type != REDIS_REPLY_INTEGER) [[unlikely]] { + ICHOR_LOG_ERROR(_logger, "Please open a bug report, queued responses does not match"); + std::terminate(); + } + ret.emplace_back(RedisIntegerReply{element->integer}); + break; + default: + ICHOR_LOG_ERROR(_logger, "Please open a bug report, queued responses does not match"); + std::terminate(); + } + } + + co_return ret; +} + +Ichor::Task> Ichor::HiredisService::discard() { + ScopeGuard sg{[this]() { + _queuedResponseTypes.clear(); + }}; + + ICHOR_WAIT_IF_NOT_CONNECTED; + + if(*_redisVersion < Version{2, 0, 0}) { + co_return tl::unexpected(RedisError::FUNCTION_NOT_AVAILABLE_IN_SERVER); + } + + IchorRedisReply evt{}; + evt.origCommand = fmt::format("DISCARD"); + auto ret = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "DISCARD"); + if(ret == REDIS_ERR) [[unlikely]] { + throw std::runtime_error("couldn't run async command strlen"); + } + co_await evt.evt; + + if(evt.reply == nullptr) [[unlikely]] { + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + + co_return {}; +} + +Ichor::Task, Ichor::RedisError>> Ichor::HiredisService::info() { + ICHOR_WAIT_IF_NOT_CONNECTED; + + IchorRedisReply evt{}; + evt.origCommand = fmt::format("INFO"); + auto commandRet = redisAsyncCommand(_redisContext, _onAsyncReply, &evt, "INFO"); + if(commandRet == REDIS_ERR) [[unlikely]] { + throw std::runtime_error("couldn't run async command info"); + } + co_await evt.evt; + + if(evt.reply == nullptr) [[unlikely]] { + co_return tl::unexpected(RedisError::DISCONNECTED); + } + if(evt.reply->type == REDIS_REPLY_ERROR) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + + std::string_view infoView{evt.reply->str}; + auto infoSplit = split(infoView, "\r\n"); + std::unordered_map ret; + ret.reserve(infoSplit.size()); + for(auto const &i : infoSplit) { + if(i.find(":") != std::string_view::npos) { + auto lineSplit = split(i, ":"); + if(lineSplit.size() != 2) { + continue; + } + ret.emplace(lineSplit[0], lineSplit[1]); + } + } + + co_return ret; +} +Ichor::Task> Ichor::HiredisService::getServerVersion() { + ICHOR_WAIT_IF_NOT_CONNECTED; + + if(!_redisVersion) { + co_return tl::unexpected(RedisError::UNKNOWN); + } + + co_return _redisVersion.value(); +} + void Ichor::HiredisService::onRedisConnect(int status) { if(status != REDIS_OK) { if(!_timeoutTimer->running()) { - ICHOR_LOG_ERROR(_logger, "connect error {}", _redisContext->err); + if(_redisContext != nullptr) { + ICHOR_LOG_ERROR(_logger, "Couldn't connect because {} \"{}\"", _redisContext->err, _redisContext->errstr); + } else { + ICHOR_LOG_ERROR(_logger, "Couldn't connect reason unknown (allocation error?)"); + } _redisContext = nullptr; _timeWhenDisconnected = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); _timeoutTimer->startTimer(); @@ -367,11 +804,18 @@ void Ichor::HiredisService::onRedisConnect(int status) { void Ichor::HiredisService::onRedisDisconnect(int status) { if(status == REDIS_OK) { + // user requested disconnect + ICHOR_LOG_INFO(_logger, "Disconnected"); _redisContext = nullptr; } else { + if(_redisContext != nullptr) { + ICHOR_LOG_ERROR(_logger, "Disconnected because {} \"{}\"", _redisContext->err, _redisContext->errstr); + } else { + ICHOR_LOG_ERROR(_logger, "Disconnected"); + } if(!_timeoutTimer->running()) { + _disconnectEvt.reset(); _redisContext = nullptr; - ICHOR_LOG_ERROR(_logger, "connect error {}", _redisContext->err); _timeWhenDisconnected = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); _timeoutTimer->startTimer(); } else { @@ -380,20 +824,18 @@ void Ichor::HiredisService::onRedisDisconnect(int status) { } } +void Ichor::HiredisService::setDebug(bool debug) noexcept { + _debug = debug; +} -tl::expected Ichor::HiredisService::connect() { - auto addrIt = getProperties().find("Address"); - auto portIt = getProperties().find("Port"); +bool Ichor::HiredisService::getDebug() const noexcept { + return _debug; +} - if(addrIt == getProperties().end()) [[unlikely]] { - throw std::runtime_error("Missing address when starting HiredisService"); - } - if(portIt == getProperties().end()) [[unlikely]] { - throw std::runtime_error("Missing port when starting HiredisService"); - } +tl::expected Ichor::HiredisService::connect(std::string const &addr, uint16_t port) { redisOptions opts{}; - REDIS_OPTIONS_SET_TCP(&opts, Ichor::any_cast(addrIt->second).c_str(), Ichor::any_cast(portIt->second)); + REDIS_OPTIONS_SET_TCP(&opts, addr.c_str(), port); opts.options |= REDIS_OPT_REUSEADDR; opts.options |= REDIS_OPT_NOAUTOFREEREPLIES; _redisContext = redisAsyncConnectWithOptions(&opts); diff --git a/test/RedisTests.cpp b/test/RedisTests.cpp index b099e38f..9c8e211f 100644 --- a/test/RedisTests.cpp +++ b/test/RedisTests.cpp @@ -3,12 +3,19 @@ #include #include #include -#include #include #include #include "TestServices/RedisUsingService.h" #include "Common.h" +#ifdef ICHOR_USE_SPDLOG +#include +#define LOGGER_TYPE SpdlogLogger +#else +#include +#define LOGGER_TYPE CoutLogger +#endif + using namespace Ichor; TEST_CASE("RedisTests") { @@ -17,9 +24,12 @@ TEST_CASE("RedisTests") { auto &dm = queue->createManager(); std::thread t([&]() { - dm.createServiceManager({}, 10); - dm.createServiceManager, ILoggerFactory>(); - dm.createServiceManager(Properties{{"Address", Ichor::make_any("127.0.0.1")}, {"Port", Ichor::make_any(static_cast(6379))}, {"PollIntervalMs", Ichor::make_any(1ul)}, {"TimeoutMs", Ichor::make_any(1'000ul)}}); + dm.createServiceManager(Properties{{"LogLevel", Ichor::make_any(LogLevel::LOG_TRACE)}}, 10); +#ifdef ICHOR_USE_SPDLOG + dm.createServiceManager(); +#endif + dm.createServiceManager, ILoggerFactory>(Properties{{"DefaultLogLevel", Ichor::make_any(LogLevel::LOG_TRACE)}}); + dm.createServiceManager(Properties{{"Address", Ichor::make_any("127.0.0.1")}, {"Port", Ichor::make_any(static_cast(6379))}, {"PollIntervalMs", Ichor::make_any(1ul)}, {"TimeoutMs", Ichor::make_any(1'000ul)}, {"Debug", Ichor::make_any(true)}}); dm.createServiceManager(); dm.createServiceManager(); diff --git a/test/StlTests.cpp b/test/StlTests.cpp index 2bf0bae6..3e7bdd7d 100644 --- a/test/StlTests.cpp +++ b/test/StlTests.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "TestServices/UselessService.h" @@ -362,4 +363,73 @@ TEST_CASE("STL Tests") { REQUIRE(*i2 == 7); } } + + SECTION("FastAtoi(u) tests") { + REQUIRE(Ichor::FastAtoiu("10") == 10); + REQUIRE(Ichor::FastAtoiu("0") == 0); + REQUIRE(Ichor::FastAtoiu("u10") == 0); + REQUIRE(Ichor::FastAtoiu("10u") == 10); + REQUIRE(Ichor::FastAtoiu(std::to_string(std::numeric_limits::max()).c_str()) == std::numeric_limits::max()); + REQUIRE(Ichor::FastAtoi("10") == 10); + REQUIRE(Ichor::FastAtoi("0") == 0); + REQUIRE(Ichor::FastAtoi("u10") == 0); + REQUIRE(Ichor::FastAtoi("10u") == 10); + REQUIRE(Ichor::FastAtoi("-10") == -10); + REQUIRE(Ichor::FastAtoi(std::to_string(std::numeric_limits::max()).c_str()) == std::numeric_limits::max()); + REQUIRE(Ichor::FastAtoi(std::to_string(std::numeric_limits::min()).c_str()) == std::numeric_limits::min()); + } + + SECTION("string_view split tests") { + std::string_view s{"this\nis\na\nstring"}; + auto ret = split(s, "\n"); + REQUIRE(ret.size() == 4); + ret = split(s, " "); + REQUIRE(ret.size() == 1); + } + + SECTION("Version parse tests") { + REQUIRE(!parseStringAsVersion("").has_value()); + REQUIRE(!parseStringAsVersion("0").has_value()); + REQUIRE(!parseStringAsVersion("0.0").has_value()); + REQUIRE(!parseStringAsVersion("-1.2.3").has_value()); + REQUIRE(!parseStringAsVersion("a.b.c").has_value()); + REQUIRE(!parseStringAsVersion("1.2.3.4").has_value()); + REQUIRE(!parseStringAsVersion("1.2.3.").has_value()); + + auto v = parseStringAsVersion("0.0.0"); + REQUIRE(v.has_value()); + REQUIRE((*v).major == 0); + REQUIRE((*v).minor == 0); + REQUIRE((*v).patch == 0); + + v = parseStringAsVersion("27398.128937618973.123123"); + REQUIRE(v.has_value()); + REQUIRE((*v).major == 27398); + REQUIRE((*v).minor == 128937618973); + REQUIRE((*v).patch == 123123); + } + + SECTION("Version compare tests") { + Version v1{1, 2, 3}; + REQUIRE(Version{1, 2, 4} > v1); + REQUIRE(Version{1, 3, 3} > v1); + REQUIRE(Version{2, 2, 3} > v1); + REQUIRE(Version{2, 0, 0} > v1); + REQUIRE(Version{1, 3, 0} > v1); + REQUIRE(!(Version{1, 2, 3} > v1)); + REQUIRE(Version{1, 2, 3} >= v1); + REQUIRE(!(Version{1, 2, 2} >= v1)); + REQUIRE(v1 > Version{1, 2, 2}); + REQUIRE(!(v1 > Version{1, 2, 3})); + REQUIRE(v1 >= Version{1, 2, 3}); + REQUIRE(!(v1 >= Version{1, 2, 4})); + REQUIRE(v1 < Version{1, 2, 4}); + REQUIRE(!(v1 < Version{1, 2, 3})); + REQUIRE(v1 <= Version{1, 2, 3}); + REQUIRE(!(v1 <= Version{1, 2, 2})); + REQUIRE(v1 == Version{1, 2, 3}); + REQUIRE(v1 != Version{2, 2, 3}); + REQUIRE(v1 != Version{1, 3, 3}); + REQUIRE(v1 != Version{1, 2, 4}); + } } diff --git a/test/TestServices/RedisUsingService.h b/test/TestServices/RedisUsingService.h index 31da9d6e..d68f9922 100644 --- a/test/TestServices/RedisUsingService.h +++ b/test/TestServices/RedisUsingService.h @@ -7,107 +7,228 @@ namespace Ichor { struct RedisUsingService final : public AdvancedService { RedisUsingService(DependencyRegister ®, Properties props) : AdvancedService(std::move(props)) { reg.registerDependency(this, DependencyFlags::REQUIRED); + reg.registerDependency(this, DependencyFlags::REQUIRED); } Task> start() final { - { - auto setReply = co_await _redis->set("test_key", "test_value"); - if(!setReply) { - throw std::runtime_error(""); - } - auto getReply = co_await _redis->get("test_key"); - if(!getReply) { - throw std::runtime_error(""); - } + co_await version_test(); + co_await set_test(); + co_await strlen_test(); + co_await delete_test(); + co_await get_delete_test(); + co_await incr_test(); + co_await incrBy_test(); + co_await decr_test(); + co_await decrBy_test(); + co_await transaction_test(); - if (!getReply.value().value || *getReply.value().value != "test_value") { - throw std::runtime_error("Incorrect value"); - } + GetThreadLocalEventQueue().pushEvent(getServiceId()); + + co_return {}; + } + + void addDependencyInstance(IRedis &redis, IService&) { + _redis = &redis; + } + + void removeDependencyInstance(IRedis&, IService&) { + _redis = nullptr; + } + + void addDependencyInstance(ILogger &logger, IService&) { + _logger = &logger; + } + + void removeDependencyInstance(ILogger &, IService&) { + _logger = nullptr; + } + + Task version_test() { + ICHOR_LOG_INFO(_logger, "running test"); + auto versionReply = co_await _redis->getServerVersion(); + if(!versionReply) { + throw std::runtime_error("version"); } - { - auto setReply = co_await _redis->set("delete_key", "delete"); + _v = versionReply.value(); + } + + Task set_test() const { + ICHOR_LOG_INFO(_logger, "running test"); + auto setReply = co_await _redis->set("test_key", "test_value"); + if(!setReply) { + throw std::runtime_error("set"); + } + auto getReply = co_await _redis->get("test_key"); + if(!getReply) { + throw std::runtime_error("get"); + } + + if (!getReply.value().value || *getReply.value().value != "test_value") { + throw std::runtime_error("Incorrect value"); + } + } + + Task strlen_test() const { + ICHOR_LOG_INFO(_logger, "running test"); + auto strlenReply = co_await _redis->strlen("test_key"); + if(!strlenReply) { + throw std::runtime_error("strlen"); + } + + if (strlenReply.value().value != 10) { + throw std::runtime_error("Incorrect value"); + } + } + + Task delete_test() const { + ICHOR_LOG_INFO(_logger, "running test"); + auto setReply = co_await _redis->set("delete_key", "delete"); + if(!setReply) { + throw std::runtime_error("set"); + } + auto delReply= co_await _redis->del("delete_key"); + if(!delReply) { + throw std::runtime_error("del"); + } + + if (delReply.value().value != 1) { + throw std::runtime_error("Incorrect value delReply"); + } + } + + Task get_delete_test() const { + ICHOR_LOG_INFO(_logger, "running test"); + if(_v >= Version{6, 2, 0}) { + auto setReply = co_await _redis->set("get_delete_key", "get_delete"); if(!setReply) { - throw std::runtime_error(""); + throw std::runtime_error("set"); } - auto delReply= co_await _redis->del("delete_key"); + auto delReply= co_await _redis->getdel("get_delete_key"); if(!delReply) { - throw std::runtime_error(""); + throw std::runtime_error("getdel"); } - if (delReply.value().value != 1) { - throw std::runtime_error("Incorrect value"); + if (!delReply.value().value || *delReply.value().value != "get_delete") { + throw std::runtime_error("Incorrect value get_delete"); } - } - { - auto setReply = co_await _redis->set("integer_key", "10"); - if(!setReply) { - throw std::runtime_error(""); + auto getReply = co_await _redis->get("test_key"); + if(getReply) { + throw std::runtime_error("get"); } - auto incrReply= co_await _redis->incr("integer_key"); - if(!incrReply) { - throw std::runtime_error(""); + } else { + auto delReply = co_await _redis->getdel("get_delete_key"); + if(delReply || delReply.error() != RedisError::FUNCTION_NOT_AVAILABLE_IN_SERVER) { + throw std::runtime_error("getdel"); } + } + } - if (incrReply.value().value != 11) { - throw std::runtime_error("Incorrect value"); - } + Task incr_test() const { + ICHOR_LOG_INFO(_logger, "running test"); + auto setReply = co_await _redis->set("integer_key", "10"); + if(!setReply) { + throw std::runtime_error("set"); + } + auto incrReply= co_await _redis->incr("integer_key"); + if(!incrReply) { + throw std::runtime_error("incr"); } - { - auto setReply = co_await _redis->set("integer_key", "10"); - if(!setReply) { - throw std::runtime_error(""); - } - auto incrByReply= co_await _redis->incrBy("integer_key", 10); - if(!incrByReply) { - throw std::runtime_error(""); - } - if (incrByReply.value().value != 20) { - throw std::runtime_error("Incorrect value"); - } + if (incrReply.value().value != 11) { + throw std::runtime_error("Incorrect value"); } - { - auto setReply = co_await _redis->set("integer_key", "10"); - if(!setReply) { - throw std::runtime_error(""); - } - auto decrReply = co_await _redis->decr("integer_key"); - if(!decrReply) { - throw std::runtime_error(""); - } + } - if (decrReply.value().value != 9) { - throw std::runtime_error("Incorrect value"); - } + Task incrBy_test() const { + ICHOR_LOG_INFO(_logger, "running test"); + auto setReply = co_await _redis->set("integer_key", "10"); + if(!setReply) { + throw std::runtime_error("set"); + } + auto incrByReply= co_await _redis->incrBy("integer_key", 10); + if(!incrByReply) { + throw std::runtime_error("incrBy"); } - { - auto setReply = co_await _redis->set("integer_key", "10"); - if(!setReply) { - throw std::runtime_error(""); - } - auto decrByReply= co_await _redis->decrBy("integer_key", 10); - if(!decrByReply) { - throw std::runtime_error(""); - } - if (decrByReply.value().value != 0) { - throw std::runtime_error("Incorrect value"); - } + if (incrByReply.value().value != 20) { + throw std::runtime_error("Incorrect value"); } + } - GetThreadLocalEventQueue().pushEvent(getServiceId()); + Task decr_test() const { + ICHOR_LOG_INFO(_logger, "running test"); + auto setReply = co_await _redis->set("integer_key", "10"); + if(!setReply) { + throw std::runtime_error("set"); + } + auto decrReply = co_await _redis->decr("integer_key"); + if(!decrReply) { + throw std::runtime_error("decr"); + } - co_return {}; + if (decrReply.value().value != 9) { + throw std::runtime_error("Incorrect value"); + } } - void addDependencyInstance(IRedis &redis, IService&) { - _redis = &redis; + Task decrBy_test() const { + ICHOR_LOG_INFO(_logger, "running test"); + auto setReply = co_await _redis->set("integer_key", "10"); + if(!setReply) { + throw std::runtime_error("set"); + } + auto decrByReply= co_await _redis->decrBy("integer_key", 10); + if(!decrByReply) { + throw std::runtime_error("decrBy"); + } + + if (decrByReply.value().value != 0) { + throw std::runtime_error("Incorrect value"); + } } - void removeDependencyInstance(IRedis&, IService&) { - _redis = nullptr; + Task transaction_test() const { + ICHOR_LOG_INFO(_logger, "running test"); + auto multiReply = co_await _redis->multi(); + if(!multiReply) { + throw std::runtime_error("multi"); + } + auto multi2Reply = co_await _redis->multi(); + if(multi2Reply) { + throw std::runtime_error("multi2"); + } + + auto setReply = co_await _redis->set("multi_key", "10"); + if(setReply || setReply.error() != RedisError::QUEUED) { + throw std::runtime_error("set"); + } + auto getReply = co_await _redis->get("multi_key"); + if(getReply || getReply.error() != RedisError::QUEUED) { + throw std::runtime_error("get"); + } + + auto execReply = co_await _redis->exec(); + if(!execReply) { + throw std::runtime_error("exec"); + } + + if(execReply.value().size() != 2) { + throw std::runtime_error("exec size"); + } + + if(std::get(execReply.value()[1]).value != "10") { + throw std::runtime_error("exec value"); + } + + auto discardReply = co_await _redis->discard(); + if(discardReply) { + throw std::runtime_error("discard"); + } } - IRedis *_redis; + + IRedis *_redis{}; + ILogger *_logger{}; + Version _v{}; }; } diff --git a/test/UtilTests.cpp b/test/UtilTests.cpp index 229d67df..5b25ab49 100644 --- a/test/UtilTests.cpp +++ b/test/UtilTests.cpp @@ -15,21 +15,6 @@ namespace Ichor { TEST_CASE("Util Tests") { - SECTION("FastAtoi(u) tests") { - REQUIRE(Ichor::FastAtoiu("10") == 10); - REQUIRE(Ichor::FastAtoiu("0") == 0); - REQUIRE(Ichor::FastAtoiu("u10") == 0); - REQUIRE(Ichor::FastAtoiu("10u") == 10); - REQUIRE(Ichor::FastAtoiu(std::to_string(std::numeric_limits::max()).c_str()) == std::numeric_limits::max()); - REQUIRE(Ichor::FastAtoi("10") == 10); - REQUIRE(Ichor::FastAtoi("0") == 0); - REQUIRE(Ichor::FastAtoi("u10") == 0); - REQUIRE(Ichor::FastAtoi("10u") == 10); - REQUIRE(Ichor::FastAtoi("-10") == -10); - REQUIRE(Ichor::FastAtoi(std::to_string(std::numeric_limits::max()).c_str()) == std::numeric_limits::max()); - REQUIRE(Ichor::FastAtoi(std::to_string(std::numeric_limits::min()).c_str()) == std::numeric_limits::min()); - } - SECTION("CTRE tests") { std::string_view input = "/some/http/10/11/12/test"; auto result = ctre::match<"\\/some\\/http\\/(\\d{1,2})\\/(\\d{1,2})\\/(\\d{1,2})\\/test">(input);