Annotation of parser3/src/classes/redis.C, revision 1.4

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.4     ! moko       17: volatile const char * IDENT_REDIS_C="$Id: redis.C,v 1.3 2026/10/06 23:57:02 moko Exp $" IDENT_PA_VREDIS_H;
1.1       moko       18: 
                     19: // defines
                     20: 
                     21: #define DEFAULT_TIMEOUT 2 // seconds
1.4     ! moko       22: // ^r.file-get[key], ^r.file-call[GET;key]: the strings of the reply are files
        !            23: #define FILE_PREFIX "file-"
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*>(&param)) {
                    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*>(&param)) {
                    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: // ^r.call[command;arguments...]
                    240: static void _call(Request& r, MethodParams& params) {
                    241:        redis_command(r, params, params.as_string(0, "command must be string"), 1, false);
                    242: }
                    243: 
1.4     ! moko      244: // ^r.file-call[command;arguments...]: the strings of the reply are files
        !           245: static void _file_call(Request& r, MethodParams& params) {
1.1       moko      246:        redis_command(r, params, params.as_string(0, "command must be string"), 1, true);
                    247: }
                    248: 
                    249: // ^r.command_name[arguments...]: the command is the name of the method called, see VRedis::get_element4call
                    250: void redis_command_method(Request& r, MethodParams& params) {
1.4     ! moko      251:        const String& name=*r.get_method_frame()->method.name;
        !           252:        if(name.starts_with(FILE_PREFIX)) {
        !           253:                size_t prefix_length=strlen(FILE_PREFIX);
        !           254:                if(name.length()==prefix_length)
        !           255:                        throw Exception(PARSER_RUNTIME, &name, "command name expected after " FILE_PREFIX);
        !           256:                redis_command(r, params, name.mid(prefix_length, name.length()), 0, true);
        !           257:        } else
        !           258:                redis_command(r, params, name, 0, false);
1.1       moko      259: }
                    260: 
                    261: #else
                    262: 
                    263: static void _open(Request&, MethodParams&) {
                    264:        throw Exception("redis", 0, "compiled without redis support");
                    265: }
                    266: 
                    267: #endif // WITH_REDIS
                    268: 
                    269: MRedis::MRedis(): Methoded("redis") {
                    270:        // ^redis::open[ $.host[localhost] $.port(6379) $.unix_socket[path] $.user[] $.password[] $.db(0) $.timeout(seconds, 0 is none) $.protocol(3) $.auto_reconnect(seconds) ]
                    271:        add_native_method("open", Method::CT_DYNAMIC, _open, 0, 1);
                    272: 
                    273: #ifdef WITH_REDIS
                    274:        // ^r.call[command;arguments...]
                    275:        add_native_method("call", Method::CT_DYNAMIC, _call, 1, MAX_REDIS_COMMAND_PARAMS);
                    276: 
1.4     ! moko      277:        // ^r.file-call[command;arguments...]
        !           278:        add_native_method(FILE_PREFIX "call", Method::CT_DYNAMIC, _file_call, 1, MAX_REDIS_COMMAND_PARAMS);
1.1       moko      279: 
                    280:        // ^r.release[]
                    281:        add_native_method("release", Method::CT_DYNAMIC, _release, 0, 0);
                    282: #endif // WITH_REDIS
                    283: }

E-mail: