eb837ae05ac988ee944c6d94bf03a190d768e99d
[ric-plt/sdl.git] / src / redis / redisgeneral.cpp
1 /*
2    Copyright (c) 2018-2019 Nokia.
3
4    Licensed under the Apache License, Version 2.0 (the "License");
5    you may not use this file except in compliance with the License.
6    You may obtain a copy of the License at
7
8        http://www.apache.org/licenses/LICENSE-2.0
9
10    Unless required by applicable law or agreed to in writing, software
11    distributed under the License is distributed on an "AS IS" BASIS,
12    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    See the License for the specific language governing permissions and
14    limitations under the License.
15 */
16
17 #include "config.h"
18 #include "private/createlogger.hpp"
19 #include "private/error.hpp"
20 #include "private/redis/redisgeneral.hpp"
21 #include <arpa/inet.h>
22 #include <cstring>
23 #if HAVE_HIREDIS_VIP
24 #include <hircluster.h>
25 #endif
26 #include <hiredis/hiredis.h>
27 #include <sstream>
28
29 using namespace shareddatalayer;
30 using namespace shareddatalayer::redis;
31
32 namespace
33 {
34     bool equals(const std::string& s1, const char* s2, size_t s2Len)
35     {
36         if (s2 == nullptr)
37         {
38             logErrorOnce("redisGeneral: null pointer passed to equals function");
39             return false;
40         }
41
42         return ((s1.size() == s2Len) && (std::memcmp(s1.data(), s2, s2Len) == 0));
43     }
44
45     bool startsWith(const std::string& s1, const char* s2, size_t s2Len)
46     {
47         if (s2 == nullptr)
48         {
49             logErrorOnce("redisGeneral: null pointer passed to startsWith function");
50             return false;
51         }
52
53         return ((s1.size() <= s2Len) && (std::memcmp(s1.data(), s2, s1.size()) == 0));
54     }
55
56     AsyncRedisCommandDispatcherErrorCode mapRedisReplyErrorToSdlError(const redisReply* rr)
57     {
58         if (equals("LOADING Redis is loading the dataset in memory", rr->str, static_cast<size_t>(rr->len)))
59             return AsyncRedisCommandDispatcherErrorCode::DATASET_LOADING;
60
61         /* This error reply comes when some cluster node(s) is down and rest of the cluster
62          * nodes cannot operate due to that. This error reply typically comes from nodes
63          * which are working but cannot handle requests because other node(s) are down.
64          * Nodes which are actually down (under failover handling) typically return
65          * CLUSTER_ERROR_NOT_CONNECTED or CLUSTER_ERROR_CONNECTION_LOST.
66          */
67         if (startsWith("CLUSTERDOWN", rr->str, static_cast<size_t>(rr->len)))
68             return AsyncRedisCommandDispatcherErrorCode::NOT_CONNECTED;
69
70         if (startsWith("ERR Protocol error", rr->str, static_cast<size_t>(rr->len)))
71             return AsyncRedisCommandDispatcherErrorCode::PROTOCOL_ERROR;
72
73         std::ostringstream oss;
74         oss << "redis reply error: " << std::string(rr->str, static_cast<size_t>(rr->len));
75         logErrorOnce(oss.str());
76         return AsyncRedisCommandDispatcherErrorCode::UNKNOWN_ERROR;
77     }
78
79     AsyncRedisCommandDispatcherErrorCode mapRedisContextErrorToSdlError(int redisContextErr, const char* redisContextErrstr)
80     {
81         switch (redisContextErr)
82         {
83             /* From hiredis/read.h:
84              * When an error occurs, the err flag in a context is set to hold the type of
85              * error that occurred. REDIS_ERR_IO means there was an I/O error and you
86              * should use the "errno" variable to find out what is wrong.
87              * For other values, the "errstr" field will hold a description. */
88             case REDIS_ERR_IO:
89                 if (errno == ECONNRESET)
90                     return AsyncRedisCommandDispatcherErrorCode::CONNECTION_LOST;
91                 logErrorOnce("redis io error. Errno: " + std::to_string(errno));
92                 return AsyncRedisCommandDispatcherErrorCode::IO_ERROR;
93             case REDIS_ERR_EOF:
94                 return AsyncRedisCommandDispatcherErrorCode::CONNECTION_LOST;
95             case REDIS_ERR_PROTOCOL:
96                 return AsyncRedisCommandDispatcherErrorCode::PROTOCOL_ERROR;
97             case REDIS_ERR_OOM:
98                 return AsyncRedisCommandDispatcherErrorCode::OUT_OF_MEMORY;
99 #if HAVE_HIREDIS_VIP
100             /* hiredis_vip returns CLUSTER_ERROR_NOT_CONNECTED when cluster node is disconnected
101              * but failover handling has not started yet (node_timeout not elapsed yet). In
102              * this situation hiredis_vip does not send request to redis (as it is clear that
103              * request cannot succeed), therefore we can map this error to NOT_CONNECTED error
104              * as it best descripes this situation.
105              */
106             case CLUSTER_ERROR_NOT_CONNECTED:
107                 return AsyncRedisCommandDispatcherErrorCode::NOT_CONNECTED;
108              /* hiredis_vip returns CLUSTER_ERROR_CONNECTION_LOST when connection is lost while
109               * hiredis is waiting for reply from redis.
110               */
111             case CLUSTER_ERROR_CONNECTION_LOST:
112                 return AsyncRedisCommandDispatcherErrorCode::CONNECTION_LOST;
113 #endif
114             default:
115                 std::ostringstream oss;
116                 oss << "redis error: "
117                     << redisContextErrstr
118                     << " (" << redisContextErr << ")";
119                 logErrorOnce(oss.str());
120                 return AsyncRedisCommandDispatcherErrorCode::UNKNOWN_ERROR;
121         }
122     }
123 }
124
125 namespace shareddatalayer
126 {
127     namespace redis
128     {
129         std::string formatToClusterSyntax(const DatabaseConfiguration::Addresses& addresses)
130         {
131             std::ostringstream oss;
132             for (auto i(addresses.begin()); i != addresses.end(); ++i)
133             {
134                 oss << i->getHost() << ':' << ntohs(i->getPort());
135                 if (i == --addresses.end())
136                     break;
137                 else
138                     oss << ',';
139             }
140             return oss.str();
141         }
142
143         const std::set<std::string>& getRequiredRedisModuleCommands()
144         {
145             static const std::set<std::string> requiredRedisModuleCommands({"msetpub","setie","setiepub","setnxpub","delpub","delie","deliepub"});
146             return requiredRedisModuleCommands;
147         }
148
149         std::error_code getRedisError(int redisContextErr, const char* redisContextErrstr, const redisReply* rr)
150         {
151             if (rr != nullptr)
152             {
153                 if (rr->type != REDIS_REPLY_ERROR)
154                     return std::error_code();
155
156                 return std::error_code(mapRedisReplyErrorToSdlError(rr));
157             }
158
159             return std::error_code(mapRedisContextErrorToSdlError(redisContextErr, redisContextErrstr));
160         }
161
162         std::set<std::string> parseCommandListReply(const redis::Reply& reply)
163         {
164             std::set<std::string> availableCommands;
165             auto replyArray(reply.getArray());
166             for (const auto& j : *replyArray)
167             {
168                 auto element = j->getArray();
169                 auto command = element->front()->getString();
170                 availableCommands.insert(command->str);
171             }
172             return availableCommands;
173         }
174
175         bool checkRedisModuleCommands(const std::set<std::string>& availableCommands)
176         {
177             std::set<std::string> missingCommands;
178
179             for (const auto& i : getRequiredRedisModuleCommands())
180             {
181                 const auto it = availableCommands.find(i);
182                 if (it == availableCommands.end())
183                     missingCommands.insert(i);
184             }
185             if (!missingCommands.empty())
186             {
187                 logErrorOnce("Missing Redis module extension commands:");
188                 for (const auto& i : missingCommands)
189                     logErrorOnce(i);
190             }
191             return missingCommands.empty();
192         }
193     }
194 }