Annotation of parser3/src/classes/redis.C, revision 1.5
1.1 moko 1: /** @file
2: Parser: @b redis parser class.
3:
4: Copyright (c) 2001-2026 Art. Lebedev Studio (https://www.artlebedev.com)
5: Authors: Konstantin Morshnev <moko@design.ru>
6: */
7:
1.3 moko 8: #include "classes.h"
9: #include "pa_vmethod_frame.h"
10:
1.2 moko 11: #include "pa_vredis.h"
12: #include "pa_request.h"
1.1 moko 13: #include "pa_varray.h"
14: #include "pa_vbool.h"
15: #include "pa_vfile.h"
16:
1.5 ! moko 17: volatile const char * IDENT_REDIS_C="$Id: redis.C,v 1.4 2026/10/07 12:07:48 moko Exp $" IDENT_PA_VREDIS_H;
1.1 moko 18:
19: // defines
20:
21: #define DEFAULT_TIMEOUT 2 // seconds
1.5 ! moko 22: // ^r.f-get[key], ^r.f-call[GET;key]: the strings of the reply are files
! 23: #define FILE_PREFIX "f-"
1.1 moko 24:
25: class MRedis: public Methoded {
26: public: // VStateless_class
27: Value* create_new_value(Pool&) { return new VRedis(); }
28: public:
29: MRedis();
30: };
31:
32: DECLARE_CLASS_VAR(redis, new MRedis);
33:
34: #ifdef WITH_REDIS
35:
36: static void _open(Request& r, MethodParams& params) {
37: VRedis& self=GET_SELF(r, VRedis);
38:
39: const char* host="localhost";
40: int port=6379;
41: bool tcp_specified=false;
42: const char* unix_socket=0;
43: double timeout=DEFAULT_TIMEOUT;
44: const char* user=0;
45: const char* password=0;
46: int db=0;
47: int protocol=3;
48: int auto_reconnect=0;
49:
50: if(params.count()>0)
51: if(HashStringValue* options=params.as_hash(0)) {
52: for(HashStringValue::Iterator i(*options); i; i.next()) {
53: String::Body key=i.key();
54: Value* value=i.value();
55: if(key=="host") {
56: host=value->as_string().cstr();
57: tcp_specified=true;
58: } else if(key=="port") {
59: port=r.process(*value).as_int();
60: tcp_specified=true;
61: } else if(key=="unix_socket") {
62: unix_socket=value->as_string().cstr();
63: } else if(key=="timeout") {
64: timeout=r.process(*value).as_double();
65: } else if(key=="user") {
66: user=value->as_string().cstr();
67: } else if(key=="password") {
68: password=value->as_string().cstr();
69: } else if(key=="db") {
70: db=r.process(*value).as_int();
71: } else if(key=="protocol") {
72: protocol=r.process(*value).as_int();
73: if(protocol!=2 && protocol!=3)
74: throw Exception(PARSER_RUNTIME, 0, "protocol must be 2 or 3");
75: } else if(key=="auto_reconnect") {
76: auto_reconnect=r.process(*value).as_int();
77: } else
78: throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION);
79: }
80: }
81:
82: if(unix_socket && tcp_specified)
83: throw Exception(PARSER_RUNTIME, 0, "unix_socket can not be used with host or port");
84:
85: redisOptions options;
86: memset(&options, 0, sizeof(options));
87: if(unix_socket)
88: REDIS_OPTIONS_SET_UNIX(&options, unix_socket);
89: else
90: REDIS_OPTIONS_SET_TCP(&options, host, port);
91:
92: struct timeval tv;
93: if(timeout>0) { // 0 is none
94: tv.tv_sec=(long)timeout;
95: tv.tv_usec=(long)((timeout-tv.tv_sec)*1000000);
96: options.connect_timeout=&tv;
97: options.command_timeout=&tv;
98: }
99:
100: // ^r.open[] of an opened object: close the old connection first
101: if(self.fcontext) {
102: redisFree(self.fcontext);
103: self.fcontext=0;
104: }
105:
106: redisContext* context=redisConnectWithOptions(&options);
107: if(!context)
108: throw Exception("redis", 0, "connect failed: out of memory");
109: if(context->err) {
110: const char* error=pa_strdup(context->errstr);
111: redisFree(context);
112: throw Exception("redis", 0, "connect failed: %s", error);
113: }
114:
115: self.fcontext=context;
116: self.fuser=user;
117: self.fpassword=password;
118: self.fdb=db;
119: self.fprotocol=protocol;
120: self.fauto_reconnect=auto_reconnect;
121:
122: try {
123: self.handshake();
124: } catch(...) {
125: redisFree(context);
126: self.fcontext=0;
127: throw;
128: }
129: }
130:
131: static void _release(Request& r, MethodParams&) {
132: VRedis& self=GET_SELF(r, VRedis);
133:
134: if(self.fcontext) {
135: redisFree(self.fcontext);
136: self.fcontext=0;
137: }
138: }
139:
1.4 moko 140: static Value* reply_value(redisReply& reply, bool as_file, const String& command) {
1.1 moko 141: switch(reply.type) {
142: case REDIS_REPLY_STRING:
143: case REDIS_REPLY_STATUS:
144: case REDIS_REPLY_VERB:
145: case REDIS_REPLY_BIGNUM: {
146: char* data=pa_strdup(reply.str, reply.len);
1.4 moko 147: if(as_file) {
1.1 moko 148: VFile& file=*new VFile;
149: file.set_binary(true/*tainted*/, data, reply.len);
150: return &file;
151: }
152: return new VString(*new String(String::C(data, reply.len), String::L_TAINTED));
153: }
154: case REDIS_REPLY_INTEGER:
155: if(reply.integer>=PA_WINT_MIN && reply.integer<=PA_WINT_MAX)
156: return new VInt(reply.integer);
157: // beyond the precise int range
158: return new VString(*new String(pa_itoa(reply.integer)));
159: case REDIS_REPLY_DOUBLE:
160: return new VDouble(reply.dval);
161: case REDIS_REPLY_BOOL:
162: return &VBool::get(reply.integer!=0);
163: case REDIS_REPLY_NIL:
164: return VVoid::get();
165: case REDIS_REPLY_ARRAY:
166: case REDIS_REPLY_SET:
167: case REDIS_REPLY_PUSH: {
168: VArray& result=*new VArray(reply.elements);
169: ArrayValue& array=result.array();
170: for(size_t i=0; i<reply.elements; i++)
1.4 moko 171: array+=reply_value(*reply.element[i], as_file, command);
1.1 moko 172: return &result;
173: }
174: case REDIS_REPLY_MAP: {
175: VHash& result=*new VHash;
176: for(size_t i=0; i+1<reply.elements; i+=2)
1.4 moko 177: result.hash().put(reply_value(*reply.element[i], false, command)->as_string(), reply_value(*reply.element[i+1], as_file, command));
1.1 moko 178: return &result;
179: }
180: case REDIS_REPLY_ERROR:
181: throw Exception("redis", &command, "%s", reply.str);
182: case REDIS_REPLY_ATTR:
183: return VVoid::get();
184: }
185: throw Exception("redis", &command, "unknown reply type %d", reply.type);
186: }
187:
188: // a string, a number, a bool or a file
189: static void add_argument(Array<const char*>& argv, Array<size_t>& lengths, Value& value) {
190: if(VFile* file=dynamic_cast<VFile*>(&value)) {
191: argv+=file->value_ptr();
192: lengths+=file->value_size();
193: } else if(dynamic_cast<VBool*>(&value)) {
194: // as redis itself encodes booleans in integer replies
195: argv+=value.as_bool() ? "1" : "0";
196: lengths+=1;
197: } else if(dynamic_cast<VArray*>(&value) || dynamic_cast<VHash*>(&value)) {
198: throw Exception(PARSER_RUNTIME, 0, "redis command argument can not be a nested array or a hash");
199: } else {
200: const char* string=value.as_string().cstr();
201: argv+=string;
202: lengths+=strlen(string);
203: }
204: }
205:
206: // arguments start at params[first]: strings, numbers, files, not nested arrays and hashes as key value pairs
1.4 moko 207: static void redis_command(Request& r, MethodParams& params, const String& command, size_t first, bool as_file) {
1.1 moko 208: VRedis& self=GET_SELF(r, VRedis);
209:
210: Array<const char*> argv;
211: Array<size_t> lengths;
212: const char* command_cstr=command.cstr();
213: argv+=command_cstr;
214: lengths+=strlen(command_cstr);
215:
216: for(size_t i=first; i<params.count(); i++) {
217: Value& param=params[i];
218: if(VArray* array=dynamic_cast<VArray*>(¶m)) {
219: for(ArrayValue::Iterator e(array->array()); e; e.next())
220: if(e.value())
221: add_argument(argv, lengths, *e.value());
222: } else if(VHash* hash=dynamic_cast<VHash*>(¶m)) {
223: for(HashStringValue::Iterator e(hash->hash()); e; e.next()) {
224: const char* key=e.key().cstr();
225: argv+=key;
226: lengths+=strlen(key);
227: add_argument(argv, lengths, *e.value());
228: }
229: } else
230: add_argument(argv, lengths, param);
231: }
232:
233: redisReply* reply=self.command((int)argv.count(), argv.ptr(0), lengths.ptr(0));
234: Temp_redis_reply temp_reply(reply);
235:
1.4 moko 236: r.write(*reply_value(*reply, as_file, command));
1.1 moko 237: }
238:
239: static void _call(Request& r, MethodParams& params) {
240: redis_command(r, params, params.as_string(0, "command must be string"), 1, false);
241: }
242:
1.4 moko 243: static void _file_call(Request& r, MethodParams& params) {
1.1 moko 244: redis_command(r, params, params.as_string(0, "command must be string"), 1, true);
245: }
246:
247: // ^r.command_name[arguments...]: the command is the name of the method called, see VRedis::get_element4call
248: void redis_command_method(Request& r, MethodParams& params) {
1.4 moko 249: const String& name=*r.get_method_frame()->method.name;
250: if(name.starts_with(FILE_PREFIX)) {
251: size_t prefix_length=strlen(FILE_PREFIX);
252: if(name.length()==prefix_length)
253: throw Exception(PARSER_RUNTIME, &name, "command name expected after " FILE_PREFIX);
254: redis_command(r, params, name.mid(prefix_length, name.length()), 0, true);
255: } else
256: redis_command(r, params, name, 0, false);
1.1 moko 257: }
258:
259: #else
260:
261: static void _open(Request&, MethodParams&) {
262: throw Exception("redis", 0, "compiled without redis support");
263: }
264:
265: #endif // WITH_REDIS
266:
267: MRedis::MRedis(): Methoded("redis") {
268: // ^redis::open[ $.host[localhost] $.port(6379) $.unix_socket[path] $.user[] $.password[] $.db(0) $.timeout(seconds, 0 is none) $.protocol(3) $.auto_reconnect(seconds) ]
269: add_native_method("open", Method::CT_DYNAMIC, _open, 0, 1);
270:
271: #ifdef WITH_REDIS
272: // ^r.call[command;arguments...]
273: add_native_method("call", Method::CT_DYNAMIC, _call, 1, MAX_REDIS_COMMAND_PARAMS);
274:
1.5 ! moko 275: // ^r.f-call[command;arguments...]
1.4 moko 276: add_native_method(FILE_PREFIX "call", Method::CT_DYNAMIC, _file_call, 1, MAX_REDIS_COMMAND_PARAMS);
1.1 moko 277:
278: // ^r.release[]
279: add_native_method("release", Method::CT_DYNAMIC, _release, 0, 0);
280: #endif // WITH_REDIS
281: }
E-mail: