common/redisreply.cpp (396 lines of code) (raw):
#include <set>
#include <string.h>
#include <stdint.h>
#include <vector>
#include <iostream>
#include <sstream>
#include <system_error>
#include <functional>
#include <boost/algorithm/string.hpp>
#include "common/logger.h"
#include "common/redisreply.h"
#include "common/dbconnector.h"
#include "common/rediscommand.h"
#include "common/stringutility.h"
using namespace std;
using namespace boost;
namespace swss {
static set<string> g_intToBoolCommands = {
"COPY",
"EXPIRE",
"EXPIREAT",
"PEXPIRE",
"PEXPIREAT",
"HEXISTS",
"MOVE",
"MSETNX",
"PERSIST",
"RENAMENX",
"SISMEMBER",
"SMOVE",
"SETNX"
};
static set<string> g_strToBoolCommands = {
"AUTH",
"HMSET",
"PSETEX",
"SETEX",
"FLUSHALL",
"FLUSHDB",
"LSET",
"LTRIM",
"MSET",
"PFMERGE",
"ASKING",
"READONLY",
"READWRITE",
"RENAME",
"SAVE",
"SELECT",
"SHUTDOWN",
"SLAVEOF",
"SWAPDB",
"WATCH",
"UNWATCH",
"SET"
};
template <typename FUNC>
inline void guard(FUNC func, const char* command)
{
try
{
func();
}
catch (const system_error& ex)
{
// Combine more error message and throw again
string errmsg = "RedisReply catches system_error: command: " + string(command) + ", reason: " + ex.what();
SWSS_LOG_ERROR("%s", errmsg.c_str());
throw system_error(ex.code(), errmsg.c_str());
}
}
RedisReply::RedisReply(RedisContext *ctx, const RedisCommand& command)
{
int rc = command.appendTo(ctx->getContext());
if (rc != REDIS_OK)
{
// The only reason of error is REDIS_ERR_OOM (Out of memory)
// ref: https://github.com/redis/hiredis/blob/master/hiredis.c
throw bad_alloc();
}
rc = redisGetReply(ctx->getContext(), (void**)&m_reply);
if (rc != REDIS_OK)
{
throw RedisError("Failed to redisGetReply with " + command.toPrintableString(), ctx->getContext());
}
guard([&]{checkReply();}, command.toPrintableString().c_str());
}
RedisReply::RedisReply(RedisContext *ctx, const string& command)
{
int rc = redisAppendCommand(ctx->getContext(), command.c_str());
if (rc != REDIS_OK)
{
// The only reason of error is REDIS_ERR_OOM (Out of memory)
// ref: https://github.com/redis/hiredis/blob/master/hiredis.c
throw bad_alloc();
}
rc = redisGetReply(ctx->getContext(), (void**)&m_reply);
if (rc != REDIS_OK)
{
throw RedisError("Failed to redisGetReply with " + command, ctx->getContext());
}
guard([&]{checkReply();}, binary_to_printable(command.c_str(), command.length()).c_str());
}
RedisReply::RedisReply(RedisContext *ctx, const RedisCommand& command, int expectedType)
: RedisReply(ctx, command)
{
guard([&]{checkReplyType(expectedType);}, command.toPrintableString().c_str());
}
RedisReply::RedisReply(RedisContext *ctx, const string& command, int expectedType)
: RedisReply(ctx, command)
{
guard([&]{checkReplyType(expectedType);}, binary_to_printable(command.c_str(), command.length()).c_str());
}
RedisReply::RedisReply(redisReply *reply) :
m_reply(reply)
{
}
RedisReply::~RedisReply()
{
freeReplyObject(m_reply);
}
redisReply *RedisReply::release()
{
redisReply *ret = m_reply;
m_reply = NULL;
return ret;
}
redisReply *RedisReply::getContext()
{
return m_reply;
}
size_t RedisReply::getChildCount()
{
return m_reply->elements;
}
redisReply *RedisReply::getChild(size_t index)
{
if (index >= m_reply->elements)
{
throw out_of_range("Out of the range of redisReply elements");
}
return m_reply->element[index];
}
redisReply *RedisReply::releaseChild(size_t index)
{
auto ret = getChild(index);
m_reply->element[index] = NULL;
return ret;
}
void RedisReply::checkStatus(const char *status)
{
if (strcmp(m_reply->str, status) != 0)
{
SWSS_LOG_ERROR("Redis reply %s != %s", m_reply->str, status);
throw system_error(make_error_code(errc::io_error),
"Invalid return code");
}
}
void RedisReply::checkReply()
{
if (!m_reply)
{
throw system_error(make_error_code(errc::not_enough_memory),
"Memory exception, reply is null");
}
if (m_reply->type == REDIS_REPLY_ERROR)
{
system_error ex(make_error_code(errc::io_error),
m_reply->str);
freeReplyObject(m_reply);
m_reply = NULL;
throw ex;
}
}
void RedisReply::checkReplyType(int expectedType)
{
if (m_reply->type != expectedType)
{
const char *err = (m_reply->type == REDIS_REPLY_STRING || m_reply->type == REDIS_REPLY_ERROR) ?
m_reply->str : "NON-STRING-REPLY";
string errmsg = "Expected to get redis type " + std::to_string(expectedType) + " got type " + std::to_string(m_reply->type) + ", err: " + err;
SWSS_LOG_ERROR("%s", errmsg.c_str());
throw system_error(make_error_code(errc::io_error), errmsg);
}
}
void RedisReply::checkStatusOK()
{
checkStatus("OK");
}
void RedisReply::checkStatusQueued()
{
checkStatus("QUEUED");
}
template<> long long int RedisReply::getReply<long long int>()
{
return getContext()->integer;
}
template<> string RedisReply::getReply<string>()
{
char *s = getContext()->str;
if (s == NULL) return string();
return string(s);
}
template<> RedisMessage RedisReply::getReply<RedisMessage>()
{
RedisMessage ret;
/* if the Key-space notification is empty, try empty message. */
if (getContext()->type == REDIS_REPLY_NIL)
{
return ret;
}
if (getContext()->type != REDIS_REPLY_ARRAY)
{
SWSS_LOG_ERROR("invalid type %d for message", getContext()->type);
return ret;
}
size_t n = getContext()->elements;
/* Expecting 4 elements for each keyspace pmessage notification */
if (n != 4)
{
SWSS_LOG_ERROR("invalid number of elements %zu for message", n);
return ret;
}
auto ctx = getContext()->element[0];
ret.type = ctx->str;
/* The second element should be the original pattern matched */
ctx = getContext()->element[1];
ret.pattern = ctx->str;
ctx = getContext()->element[2];
ret.channel = ctx->str;
ctx = getContext()->element[3];
ret.data = ctx->str;
return ret;
}
string RedisReply::to_string()
{
return RedisReply::to_string(this->getContext());
}
string RedisReply::to_string(redisReply *reply, string command)
{
/*
Response format need keep as same as redis-py, redis-py using a command to result type mapping to convert result:
https://github.com/redis/redis-py/blob/bedf3c82a55b4b67eed93f686cb17e82f7ab19cd/redis/client.py#L682
Redis command result type can be found here:
https://redis.io/commands/
Only commands used by scripts in sonic repos are supported by these method.
For example: 'Info' command not used by any sonic scripts, so the output format will different with redis-py.
*/
switch(reply->type)
{
case REDIS_REPLY_INTEGER:
return formatReply(command, reply->integer);
case REDIS_REPLY_STRING:
case REDIS_REPLY_ERROR:
case REDIS_REPLY_STATUS:
case REDIS_REPLY_NIL:
return formatReply(command, reply->str, reply->len);
case REDIS_REPLY_ARRAY:
{
return formatReply(command, reply->element, reply->elements);
}
default:
SWSS_LOG_ERROR("invalid type %d for message", reply->type);
return string();
}
}
string RedisReply::formatReply(string command, long long integer)
{
if (g_intToBoolCommands.find(command) != g_intToBoolCommands.end())
{
if (integer == 1)
{
return string("True");
}
else if (integer == 0)
{
return string("False");
}
}
else if (command == "AUTH")
{
if (integer != 0)
{
return string("OK");
}
}
return std::to_string(integer);
}
string RedisReply::formatReply(string command, const char* str, size_t len)
{
string result = string(str, len);
if (g_strToBoolCommands.find(command) != g_strToBoolCommands.end()
&& result == "OK")
{
return string("True");
}
if (command == "PING")
{
// In redis-py, result for PING was hard coded: https://github.com/redis/redis-py/blob/bedf3c82a55b4b67eed93f686cb17e82f7ab19cd/redis/client.py#L796
if (result == "PONG")
{
return string("True");
}
else
{
return string("False");
}
}
return result;
}
string RedisReply::formatReply(string command, struct redisReply **element, size_t elements)
{
if (command == "HGETALL")
{
return formatDictReply(element, elements);
}
else if(command == "SCAN"
|| command == "SSCAN")
{
return formatSscanReply(element, elements);
}
else if(command == "HSCAN")
{
return formatHscanReply(element, elements);
}
else if(command == "BLPOP"
|| command == "BRPOP")
{
return formatTupleReply(element, elements);
}
else
{
return formatListReply(element, elements);
}
}
string RedisReply::formatSscanReply(struct redisReply **element, size_t elements)
{
if (elements != 2)
{
throw system_error(make_error_code(errc::io_error),
"Invalid result");
}
// format HSCAN result, here is a example:
// (0, {'test3': 'test3', 'test2': 'test2'})
ostringstream result;
result << "(" << element[0]->integer << ", ";
// format the field mapping part
result << formatArrayReply(element[1]->element, element[1]->elements);
result << ")";
return result.str();
}
string RedisReply::formatHscanReply(struct redisReply **element, size_t elements)
{
if (elements != 2)
{
throw system_error(make_error_code(errc::io_error),
"Invalid result");
}
// format HSCAN result, here is a example:
// (0, {'test3': 'test3', 'test2': 'test2'})
ostringstream result;
result << "(" << element[0]->integer << ", ";
// format the field mapping part
result << formatDictReply(element[1]->element, element[1]->elements);
result << ")";
return result.str();
}
string RedisReply::formatDictReply(struct redisReply **element, size_t elements)
{
if (elements%2 != 0)
{
throw system_error(make_error_code(errc::io_error),
"Invalid result");
}
// format dictionary, not using json.h because the output format are different, here is a example:
// {'test3': 'test3', 'test2': 'test2'}
vector<string> elementvector;
for (unsigned int i = 0; i < elements; i += 2)
{
string key = formatStringWithQuot(to_string(element[i]));
string value = formatStringWithQuot(to_string(element[i + 1]));
elementvector.push_back(key + ": " + value);
}
return swss::join(", ", '{', '}', elementvector.begin(), elementvector.end());
}
string RedisReply::formatArrayReply(struct redisReply **element, size_t elements)
{
vector<string> elementvector;
for (unsigned int i = 0; i < elements; i++)
{
elementvector.push_back(formatStringWithQuot(to_string(element[i])));
}
return swss::join(", ", '[', ']', elementvector.begin(), elementvector.end());
}
string RedisReply::formatListReply(struct redisReply **element, size_t elements)
{
vector<string> elementvector;
for (unsigned int i = 0; i < elements; i++)
{
elementvector.push_back(to_string(element[i]));
}
return swss::join("\n", elementvector.begin(), elementvector.end());
}
string RedisReply::formatTupleReply(struct redisReply **element, size_t elements)
{
vector<string> elementvector;
for (unsigned int i = 0; i < elements; i++)
{
elementvector.push_back(formatStringWithQuot(to_string(element[i])));
}
return swss::join(", ", '(', ')', elementvector.begin(), elementvector.end());
}
string RedisReply::formatStringWithQuot(string str)
{
if (str.find('\'') != std::string::npos)
{
return "\"" + str + "\"";
}
return "'" + str + "'";
}
}