Add Sentinel change notification handling logic
[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         if (startsWith("READONLY", rr->str, static_cast<size_t>(rr->len)))
74             return AsyncRedisCommandDispatcherErrorCode::WRITING_TO_SLAVE;
75
76         std::ostringstream oss;
77         oss << "redis reply error: " << std::string(rr->str, static_cast<size_t>(rr->len));
78         logErrorOnce(oss.str());
79         return AsyncRedisCommandDispatcherErrorCode::UNKNOWN_ERROR;
80     }
81
82     AsyncRedisCommandDispatcherErrorCode mapRedisContextErrorToSdlError(int redisContextErr, const char* redisContextErrstr)
83     {
84         switch (redisContextErr)
85         {
86             /* From hiredis/read.h:
87              * When an error occurs, the err flag in a context is set to hold the type of
88              * error that occurred. REDIS_ERR_IO means there was an I/O error and you
89              * should use the "errno" variable to find out what is wrong.
90              * For other values, the "errstr" field will hold a description. */
91             case REDIS_ERR_IO:
92                 if (errno == ECONNRESET)
93                     return AsyncRedisCommandDispatcherErrorCode::CONNECTION_LOST;
94                 logErrorOnce("redis io error. Errno: " + std::to_string(errno));
95                 return AsyncRedisCommandDispatcherErrorCode::IO_ERROR;
96             case REDIS_ERR_EOF:
97                 return AsyncRedisCommandDispatcherErrorCode::CONNECTION_LOST;
98             case REDIS_ERR_PROTOCOL:
99                 return AsyncRedisCommandDispatcherErrorCode::PROTOCOL_ERROR;
100             case REDIS_ERR_OOM:
101                 return AsyncRedisCommandDispatcherErrorCode::OUT_OF_MEMORY;
102 #if HAVE_HIREDIS_VIP
103             /* hiredis_vip returns CLUSTER_ERROR_NOT_CONNECTED when cluster node is disconnected
104              * but failover handling has not started yet (node_timeout not elapsed yet). In
105              * this situation hiredis_vip does not send request to redis (as it is clear that
106              * request cannot succeed), therefore we can map this error to NOT_CONNECTED error
107              * as it best descripes this situation.
108              */
109             case CLUSTER_ERROR_NOT_CONNECTED:
110                 return AsyncRedisCommandDispatcherErrorCode::NOT_CONNECTED;
111              /* hiredis_vip returns CLUSTER_ERROR_CONNECTION_LOST when connection is lost while
112               * hiredis is waiting for reply from redis.
113               */
114             case CLUSTER_ERROR_CONNECTION_LOST:
115                 return AsyncRedisCommandDispatcherErrorCode::CONNECTION_LOST;
116 #endif
117             default:
118                 std::ostringstream oss;
119                 oss << "redis error: "
120                     << redisContextErrstr
121                     << " (" << redisContextErr << ")";
122                 logErrorOnce(oss.str());
123                 return AsyncRedisCommandDispatcherErrorCode::UNKNOWN_ERROR;
124         }
125     }
126 }
127
128 namespace shareddatalayer
129 {
130     namespace redis
131     {
132         std::string formatToClusterSyntax(const DatabaseConfiguration::Addresses& addresses)
133         {
134             std::ostringstream oss;
135             for (auto i(addresses.begin()); i != addresses.end(); ++i)
136             {
137                 oss << i->getHost() << ':' << ntohs(i->getPort());
138                 if (i == --addresses.end())
139                     break;
140                 else
141                     oss << ',';
142             }
143             return oss.str();
144         }
145
146         const std::set<std::string>& getRequiredRedisModuleCommands()
147         {
148             static const std::set<std::string> requiredRedisModuleCommands({"msetpub","setie","setiepub","setnxpub","delpub","delie","deliepub"});
149             return requiredRedisModuleCommands;
150         }
151
152         std::error_code getRedisError(int redisContextErr, const char* redisContextErrstr, const redisReply* rr)
153         {
154             if (rr != nullptr)
155             {
156                 if (rr->type != REDIS_REPLY_ERROR)
157                     return std::error_code();
158
159                 return std::error_code(mapRedisReplyErrorToSdlError(rr));
160             }
161
162             return std::error_code(mapRedisContextErrorToSdlError(redisContextErr, redisContextErrstr));
163         }
164
165         std::set<std::string> parseCommandListReply(const redis::Reply& reply)
166         {
167             std::set<std::string> availableCommands;
168             auto replyArray(reply.getArray());
169             for (const auto& j : *replyArray)
170             {
171                 auto element = j->getArray();
172                 auto command = element->front()->getString();
173                 availableCommands.insert(command->str);
174             }
175             return availableCommands;
176         }
177
178         bool checkRedisModuleCommands(const std::set<std::string>& availableCommands)
179         {
180             std::set<std::string> missingCommands;
181
182             for (const auto& i : getRequiredRedisModuleCommands())
183             {
184                 const auto it = availableCommands.find(i);
185                 if (it == availableCommands.end())
186                     missingCommands.insert(i);
187             }
188             if (!missingCommands.empty())
189             {
190                 logErrorOnce("Missing Redis module extension commands:");
191                 for (const auto& i : missingCommands)
192                     logErrorOnce(i);
193             }
194             return missingCommands.empty();
195         }
196     }
197 }