1 /* 2 * cachedb/redis.c - cachedb redis module 3 * 4 * Copyright (c) 2018, NLnet Labs. All rights reserved. 5 * 6 * This software is open source. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 12 * Redistributions of source code must retain the above copyright notice, 13 * this list of conditions and the following disclaimer. 14 * 15 * Redistributions in binary form must reproduce the above copyright notice, 16 * this list of conditions and the following disclaimer in the documentation 17 * and/or other materials provided with the distribution. 18 * 19 * Neither the name of the NLNET LABS nor the names of its contributors may 20 * be used to endorse or promote products derived from this software without 21 * specific prior written permission. 22 * 23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 */ 35 36 /** 37 * \file 38 * 39 * This file contains a module that uses the redis database to cache 40 * dns responses. 41 */ 42 43 #include "config.h" 44 #ifdef USE_CACHEDB 45 #include "cachedb/redis.h" 46 #include "cachedb/cachedb.h" 47 #include "util/alloc.h" 48 #include "util/config_file.h" 49 #include "util/locks.h" 50 #include "util/timeval_func.h" 51 #include "sldns/sbuffer.h" 52 53 #ifdef USE_REDIS 54 #include "hiredis/hiredis.h" 55 56 struct redis_moddata { 57 /* thread-specific redis contexts */ 58 redisContext** ctxs; 59 redisContext** replica_ctxs; 60 /* number of ctx entries */ 61 int numctxs; 62 /* server's IP address or host name */ 63 const char* server_host; 64 const char* replica_server_host; 65 /* server's TCP port */ 66 int server_port; 67 int replica_server_port; 68 /* server's unix path, or "", NULL if unused */ 69 const char* server_path; 70 const char* replica_server_path; 71 /* server's AUTH password, or "", NULL if unused */ 72 const char* server_password; 73 const char* replica_server_password; 74 /* timeout for commands */ 75 struct timeval command_timeout; 76 struct timeval replica_command_timeout; 77 /* timeout for connection setup */ 78 struct timeval connect_timeout; 79 struct timeval replica_connect_timeout; 80 /* the reconnect interval time. */ 81 struct timeval reconnect_interval; 82 struct timeval replica_reconnect_interval; 83 /* reconnect attempts, 0 if connected, counts up failed reconnects. */ 84 int reconnect_attempts; 85 int replica_reconnect_attempts; 86 /* Lock on reconnect_wait time. */ 87 lock_basic_type wait_lock; 88 lock_basic_type replica_wait_lock; 89 /* reconnect wait time, wait until it has passed before reconnect. */ 90 struct timeval reconnect_wait; 91 struct timeval replica_reconnect_wait; 92 /* the redis logical database to use */ 93 int logical_db; 94 int replica_logical_db; 95 /* if the SET with EX command is supported */ 96 int set_with_ex_available; 97 }; 98 99 /** The limit on the number of redis connect attempts. After failure if 100 * the number is exceeded, the reconnects are throttled by the wait time. */ 101 #define REDIS_RECONNECT_ATTEMPT_LIMIT 3 102 103 static redisReply* redis_command(struct module_env*, struct cachedb_env*, 104 const char*, const uint8_t*, size_t, int); 105 106 static void 107 moddata_clean(struct redis_moddata** moddata) { 108 if(!moddata || !*moddata) 109 return; 110 if((*moddata)->ctxs) { 111 int i; 112 for(i = 0; i < (*moddata)->numctxs; i++) { 113 if((*moddata)->ctxs[i]) 114 redisFree((*moddata)->ctxs[i]); 115 } 116 free((*moddata)->ctxs); 117 } 118 if((*moddata)->replica_ctxs) { 119 int i; 120 for(i = 0; i < (*moddata)->numctxs; i++) { 121 if((*moddata)->replica_ctxs[i]) 122 redisFree((*moddata)->replica_ctxs[i]); 123 } 124 free((*moddata)->replica_ctxs); 125 } 126 lock_basic_destroy(&(*moddata)->wait_lock); 127 lock_basic_destroy(&(*moddata)->replica_wait_lock); 128 free(*moddata); 129 *moddata = NULL; 130 } 131 132 static redisContext* 133 redis_connect(const char* host, int port, const char* path, 134 const char* password, int logical_db, 135 const struct timeval connect_timeout, 136 const struct timeval command_timeout, 137 const struct timeval* reconnect_interval, 138 int* reconnect_attempts, 139 struct timeval* reconnect_wait, 140 lock_basic_type* wait_lock, 141 struct timeval* now_tv, 142 const char* infostr) 143 { 144 struct timeval now_val; 145 redisContext* ctx; 146 #ifdef THREADS_DISABLED 147 /* Fix attribute unused warning. 148 * wait_lock is only used with lock_basic_* functions that are nop'ed 149 * when compiled without thread support. */ 150 (void)wait_lock; 151 #endif /* THREADS_DISABLED */ 152 153 /* See if the redis server is down, and reconnect has to wait. */ 154 if(*reconnect_attempts > REDIS_RECONNECT_ATTEMPT_LIMIT) { 155 /* Acquire lock to look at timeval, the integer has atomic 156 * integrity. */ 157 struct timeval wait_tv; 158 if(now_tv) { 159 now_val = *now_tv; 160 } else { 161 if(gettimeofday(&now_val, NULL) < 0) 162 log_err("redis: gettimeofday: %s", 163 strerror(errno)); 164 } 165 lock_basic_lock(wait_lock); 166 wait_tv = *reconnect_wait; 167 lock_basic_unlock(wait_lock); 168 if(timeval_smaller(&now_val, &wait_tv)) { 169 verbose(VERB_ALGO, "redis %sdown, reconnect wait", 170 infostr); 171 return NULL; 172 } 173 } 174 175 if(path && path[0]!=0) { 176 ctx = redisConnectUnixWithTimeout(path, connect_timeout); 177 } else { 178 ctx = redisConnectWithTimeout(host, port, connect_timeout); 179 } 180 if(!ctx || ctx->err) { 181 const char *errstr = "out of memory"; 182 if(ctx) 183 errstr = ctx->errstr; 184 log_err("failed to connect to redis %sserver: %s", infostr, errstr); 185 goto fail; 186 } 187 if(redisSetTimeout(ctx, command_timeout) != REDIS_OK) { 188 log_err("failed to set redis %stimeout, %s", infostr, ctx->errstr); 189 goto fail; 190 } 191 if(password && password[0]!=0) { 192 redisReply* rep; 193 rep = redisCommand(ctx, "AUTH %s", password); 194 if(!rep || rep->type == REDIS_REPLY_ERROR) { 195 log_err("failed to authenticate %swith password", infostr); 196 freeReplyObject(rep); 197 goto fail; 198 } 199 freeReplyObject(rep); 200 } 201 if(logical_db > 0) { 202 redisReply* rep; 203 rep = redisCommand(ctx, "SELECT %d", logical_db); 204 if(!rep || rep->type == REDIS_REPLY_ERROR) { 205 log_err("failed %sto set logical database (%d)", 206 infostr, logical_db); 207 freeReplyObject(rep); 208 goto fail; 209 } 210 freeReplyObject(rep); 211 } 212 *reconnect_attempts = 0; 213 if(verbosity >= VERB_OPS) { 214 char port_str[6+1]; 215 port_str[0] = ' '; 216 (void)snprintf(port_str+1, sizeof(port_str)-1, "%d", port); 217 verbose(VERB_OPS, "Connection to Redis %sestablished (%s%s)", 218 infostr, 219 path&&path[0]!=0?path:host, 220 path&&path[0]!=0?"":port_str); 221 } 222 return ctx; 223 224 fail: 225 if(ctx) 226 redisFree(ctx); 227 (*reconnect_attempts)++; 228 if(*reconnect_attempts > REDIS_RECONNECT_ATTEMPT_LIMIT) { 229 /* Wait for the reconnect interval before trying again. */ 230 struct timeval tv; 231 if(now_tv) { 232 now_val = *now_tv; 233 } else { 234 if(gettimeofday(&now_val, NULL) < 0) 235 log_err("redis: gettimeofday: %s", 236 strerror(errno)); 237 } 238 tv = now_val; 239 timeval_add(&tv, reconnect_interval); 240 lock_basic_lock(wait_lock); 241 *reconnect_wait = tv; 242 lock_basic_unlock(wait_lock); 243 verbose(VERB_ALGO, "redis %sreconnect wait until %d.%6.6d", 244 infostr, (int)tv.tv_sec, (int)tv.tv_usec); 245 } 246 return NULL; 247 } 248 249 static void 250 set_timeout(struct timeval* timeout, int value, int explicit_value) 251 { 252 int v = explicit_value != 0 ? explicit_value : value; 253 timeout->tv_sec = v / 1000; 254 timeout->tv_usec = (v % 1000) * 1000; 255 } 256 257 static int 258 redis_init(struct module_env* env, struct cachedb_env* cachedb_env) 259 { 260 int i; 261 struct redis_moddata* moddata = NULL; 262 263 verbose(VERB_OPS, "Redis initialization"); 264 265 moddata = calloc(1, sizeof(struct redis_moddata)); 266 if(!moddata) { 267 log_err("out of memory"); 268 goto fail; 269 } 270 lock_basic_init(&moddata->wait_lock); 271 lock_protect(&moddata->wait_lock, &moddata->reconnect_wait, 272 sizeof(moddata->reconnect_wait)); 273 lock_basic_init(&moddata->replica_wait_lock); 274 lock_protect(&moddata->replica_wait_lock, 275 &moddata->replica_reconnect_wait, 276 sizeof(moddata->replica_reconnect_wait)); 277 moddata->numctxs = env->cfg->num_threads; 278 /* note: server_host and similar string configuration options are 279 * shallow references to configured strings; we don't have to free them 280 * in this module. */ 281 moddata->server_host = env->cfg->redis_server_host; 282 moddata->replica_server_host = env->cfg->redis_replica_server_host; 283 284 moddata->server_port = env->cfg->redis_server_port; 285 moddata->replica_server_port = env->cfg->redis_replica_server_port; 286 287 moddata->server_path = env->cfg->redis_server_path; 288 moddata->replica_server_path = env->cfg->redis_replica_server_path; 289 290 moddata->server_password = env->cfg->redis_server_password; 291 moddata->replica_server_password = env->cfg->redis_replica_server_password; 292 293 set_timeout(&moddata->command_timeout, 294 env->cfg->redis_timeout, 295 env->cfg->redis_command_timeout); 296 set_timeout(&moddata->replica_command_timeout, 297 env->cfg->redis_replica_timeout, 298 env->cfg->redis_replica_command_timeout); 299 set_timeout(&moddata->connect_timeout, 300 env->cfg->redis_timeout, 301 env->cfg->redis_connect_timeout); 302 set_timeout(&moddata->replica_connect_timeout, 303 env->cfg->redis_replica_timeout, 304 env->cfg->redis_replica_connect_timeout); 305 set_timeout(&moddata->reconnect_interval, 1000, 0); 306 set_timeout(&moddata->replica_reconnect_interval, 1000, 0); 307 308 moddata->logical_db = env->cfg->redis_logical_db; 309 moddata->replica_logical_db = env->cfg->redis_replica_logical_db; 310 311 moddata->ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*)); 312 if(!moddata->ctxs) { 313 log_err("out of memory"); 314 goto fail; 315 } 316 if((moddata->replica_server_host && moddata->replica_server_host[0]!=0) 317 || (moddata->replica_server_path && moddata->replica_server_path[0]!=0)) { 318 /* There is a replica configured, allocate ctxs */ 319 moddata->replica_ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*)); 320 if(!moddata->replica_ctxs) { 321 log_err("out of memory"); 322 goto fail; 323 } 324 } 325 for(i = 0; i < moddata->numctxs; i++) { 326 redisContext* ctx = redis_connect( 327 moddata->server_host, 328 moddata->server_port, 329 moddata->server_path, 330 moddata->server_password, 331 moddata->logical_db, 332 moddata->connect_timeout, 333 moddata->command_timeout, 334 &moddata->reconnect_interval, 335 &moddata->reconnect_attempts, 336 &moddata->reconnect_wait, 337 &moddata->wait_lock, 338 env->now_tv, 339 ""); 340 if(!ctx) { 341 log_err("redis_init: failed to init redis " 342 "(for thread %d)", i); 343 /* And continue, the context can be established 344 * later, just like after a disconnect. */ 345 } 346 moddata->ctxs[i] = ctx; 347 } 348 if(moddata->replica_ctxs) { 349 for(i = 0; i < moddata->numctxs; i++) { 350 redisContext* ctx = redis_connect( 351 moddata->replica_server_host, 352 moddata->replica_server_port, 353 moddata->replica_server_path, 354 moddata->replica_server_password, 355 moddata->replica_logical_db, 356 moddata->replica_connect_timeout, 357 moddata->replica_command_timeout, 358 &moddata->replica_reconnect_interval, 359 &moddata->replica_reconnect_attempts, 360 &moddata->replica_reconnect_wait, 361 &moddata->replica_wait_lock, 362 env->now_tv, 363 "replica "); 364 if(!ctx) { 365 log_err("redis_init: failed to init redis " 366 "replica (for thread %d)", i); 367 /* And continue, the context can be established 368 * later, just like after a disconnect. */ 369 } 370 moddata->replica_ctxs[i] = ctx; 371 } 372 } 373 cachedb_env->backend_data = moddata; 374 if(env->cfg->redis_expire_records && 375 moddata->ctxs[env->alloc->thread_num] != NULL) { 376 redisReply* rep = NULL; 377 int redis_reply_type = 0; 378 /** check if set with ex command is supported */ 379 rep = redis_command(env, cachedb_env, 380 "SET __UNBOUND_REDIS_CHECK__ none EX 1", NULL, 0, 1); 381 if(!rep) { 382 /** init failed, no response from redis server*/ 383 goto set_with_ex_fail; 384 } 385 redis_reply_type = rep->type; 386 freeReplyObject(rep); 387 switch(redis_reply_type) { 388 case REDIS_REPLY_STATUS: 389 break; 390 default: 391 /** init failed, set_with_ex command not supported */ 392 goto set_with_ex_fail; 393 } 394 moddata->set_with_ex_available = 1; 395 } 396 return 1; 397 398 set_with_ex_fail: 399 log_err("redis_init: failure during redis_init, the " 400 "redis-expire-records option requires the SET with EX command " 401 "(redis >= 2.6.12)"); 402 return 1; 403 fail: 404 moddata_clean(&moddata); 405 return 0; 406 } 407 408 static void 409 redis_deinit(struct module_env* env, struct cachedb_env* cachedb_env) 410 { 411 struct redis_moddata* moddata = (struct redis_moddata*) 412 cachedb_env->backend_data; 413 (void)env; 414 415 verbose(VERB_OPS, "Redis deinitialization"); 416 moddata_clean(&moddata); 417 } 418 419 /* 420 * Send a redis command and get a reply. Unified so that it can be used for 421 * both SET and GET. If 'data' is non-NULL the command is supposed to be 422 * SET and GET otherwise, but the implementation of this function is agnostic 423 * about the semantics (except for logging): 'command', 'data', and 'data_len' 424 * are opaquely passed to redisCommand(). 425 * This function first checks whether a connection with a redis server has 426 * been established; if not it tries to set up a new one. 427 * It returns redisReply returned from redisCommand() or NULL if some low 428 * level error happens. The caller is responsible to check the return value, 429 * if it's non-NULL, it has to free it with freeReplyObject(). 430 */ 431 static redisReply* 432 redis_command(struct module_env* env, struct cachedb_env* cachedb_env, 433 const char* command, const uint8_t* data, size_t data_len, int write) 434 { 435 redisContext* ctx, **ctx_selector; 436 redisReply* rep; 437 struct redis_moddata* d = (struct redis_moddata*) 438 cachedb_env->backend_data; 439 440 /* We assume env->alloc->thread_num is a unique ID for each thread 441 * in [0, num-of-threads). We could treat it as an error condition 442 * if the assumption didn't hold, but it seems to be a fundamental 443 * assumption throughout the unbound architecture, so we simply assert 444 * it. */ 445 log_assert(env->alloc->thread_num < d->numctxs); 446 447 ctx_selector = !write && d->replica_ctxs 448 ?d->replica_ctxs 449 :d->ctxs; 450 ctx = ctx_selector[env->alloc->thread_num]; 451 452 /* If we've not established a connection to the server or we've closed 453 * it on a failure, try to re-establish a new one. Failures will be 454 * logged in redis_connect(). */ 455 if(!ctx) { 456 if(!write && d->replica_ctxs) { 457 ctx = redis_connect( 458 d->replica_server_host, 459 d->replica_server_port, 460 d->replica_server_path, 461 d->replica_server_password, 462 d->replica_logical_db, 463 d->replica_connect_timeout, 464 d->replica_command_timeout, 465 &d->replica_reconnect_interval, 466 &d->replica_reconnect_attempts, 467 &d->replica_reconnect_wait, 468 &d->replica_wait_lock, 469 env->now_tv, 470 "replica "); 471 } else { 472 ctx = redis_connect( 473 d->server_host, 474 d->server_port, 475 d->server_path, 476 d->server_password, 477 d->logical_db, 478 d->connect_timeout, 479 d->command_timeout, 480 &d->reconnect_interval, 481 &d->reconnect_attempts, 482 &d->reconnect_wait, 483 &d->wait_lock, 484 env->now_tv, 485 ""); 486 } 487 ctx_selector[env->alloc->thread_num] = ctx; 488 } 489 if(!ctx) return NULL; 490 491 /* Send the command and get a reply, synchronously. */ 492 rep = (redisReply*)redisCommand(ctx, command, data, data_len); 493 if(!rep) { 494 /* Once an error as a NULL-reply is returned the context cannot 495 * be reused and we'll need to set up a new connection. */ 496 log_err("redis_command: failed to receive a reply, " 497 "closing connection: %s", ctx->errstr); 498 redisFree(ctx); 499 ctx_selector[env->alloc->thread_num] = NULL; 500 return NULL; 501 } 502 503 /* Check error in reply to unify logging in that case. 504 * The caller may perform context-dependent checks and logging. */ 505 if(rep->type == REDIS_REPLY_ERROR) 506 log_err("redis: %s resulted in an error: %s", 507 data ? "set" : "get", rep->str); 508 509 return rep; 510 } 511 512 static int 513 redis_lookup(struct module_env* env, struct cachedb_env* cachedb_env, 514 char* key, struct sldns_buffer* result_buffer) 515 { 516 redisReply* rep; 517 /* Supported commands: 518 * - "GET " + key 519 */ 520 #define REDIS_LOOKUP_MAX_BUF_LEN \ 521 4 /* "GET " */ \ 522 +(CACHEDB_HASHSIZE/8)*2 /* key hash */ \ 523 + 1 /* \0 */ 524 char cmdbuf[REDIS_LOOKUP_MAX_BUF_LEN]; 525 int n; 526 int ret = 0; 527 528 verbose(VERB_ALGO, "redis_lookup of %s", key); 529 530 n = snprintf(cmdbuf, sizeof(cmdbuf), "GET %s", key); 531 if(n < 0 || n >= (int)sizeof(cmdbuf)) { 532 log_err("redis_lookup: unexpected failure to build command"); 533 return 0; 534 } 535 536 rep = redis_command(env, cachedb_env, cmdbuf, NULL, 0, 0); 537 if(!rep) 538 return 0; 539 switch(rep->type) { 540 case REDIS_REPLY_NIL: 541 verbose(VERB_ALGO, "redis_lookup: no data cached"); 542 break; 543 case REDIS_REPLY_STRING: 544 verbose(VERB_ALGO, "redis_lookup found %d bytes", 545 (int)rep->len); 546 if((size_t)rep->len > sldns_buffer_capacity(result_buffer)) { 547 log_err("redis_lookup: replied data too long: %lu", 548 (size_t)rep->len); 549 break; 550 } 551 sldns_buffer_clear(result_buffer); 552 sldns_buffer_write(result_buffer, rep->str, rep->len); 553 sldns_buffer_flip(result_buffer); 554 ret = 1; 555 break; 556 case REDIS_REPLY_ERROR: 557 break; /* already logged */ 558 default: 559 log_err("redis_lookup: unexpected type of reply for (%d)", 560 rep->type); 561 break; 562 } 563 freeReplyObject(rep); 564 return ret; 565 } 566 567 static void 568 redis_store(struct module_env* env, struct cachedb_env* cachedb_env, 569 char* key, uint8_t* data, size_t data_len, time_t ttl) 570 { 571 redisReply* rep; 572 int n; 573 struct redis_moddata* moddata = (struct redis_moddata*) 574 cachedb_env->backend_data; 575 int set_ttl = (moddata->set_with_ex_available && 576 env->cfg->redis_expire_records && 577 (!env->cfg->serve_expired || env->cfg->serve_expired_ttl > 0)); 578 /* Supported commands: 579 * - "SET " + key + " %b" 580 * - "SET " + key + " %b EX " + ttl 581 * older redis 2.0.0 was "SETEX " + key + " " + ttl + " %b" 582 * - "EXPIRE " + key + " 0" 583 */ 584 #define REDIS_STORE_MAX_BUF_LEN \ 585 7 /* "EXPIRE " */ \ 586 +(CACHEDB_HASHSIZE/8)*2 /* key hash */ \ 587 + 7 /* " %b EX " */ \ 588 + 20 /* ttl (uint64_t) */ \ 589 + 1 /* \0 */ 590 char cmdbuf[REDIS_STORE_MAX_BUF_LEN]; 591 592 if (!set_ttl) { 593 verbose(VERB_ALGO, "redis_store %s (%d bytes)", key, (int)data_len); 594 /* build command to set to a binary safe string */ 595 n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b", key); 596 } else if(ttl == 0) { 597 /* use the EXPIRE command, SET with EX 0 is an invalid time. */ 598 /* Replies with REDIS_REPLY_INTEGER of 1. */ 599 verbose(VERB_ALGO, "redis_store expire %s (%d bytes)", 600 key, (int)data_len); 601 n = snprintf(cmdbuf, sizeof(cmdbuf), "EXPIRE %s 0", key); 602 data = NULL; 603 data_len = 0; 604 } else { 605 /* add expired ttl time to redis ttl to avoid premature eviction of key */ 606 ttl += env->cfg->serve_expired_ttl; 607 verbose(VERB_ALGO, "redis_store %s (%d bytes) with ttl %u", 608 key, (int)data_len, (unsigned)(uint32_t)ttl); 609 /* build command to set to a binary safe string */ 610 n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b EX %u", key, 611 (unsigned)(uint32_t)ttl); 612 } 613 614 615 if(n < 0 || n >= (int)sizeof(cmdbuf)) { 616 log_err("redis_store: unexpected failure to build command"); 617 return; 618 } 619 620 rep = redis_command(env, cachedb_env, cmdbuf, data, data_len, 1); 621 if(rep) { 622 verbose(VERB_ALGO, "redis_store set completed"); 623 if(rep->type != REDIS_REPLY_STATUS && 624 rep->type != REDIS_REPLY_ERROR && 625 rep->type != REDIS_REPLY_INTEGER) { 626 log_err("redis_store: unexpected type of reply (%d)", 627 rep->type); 628 } 629 freeReplyObject(rep); 630 } 631 } 632 633 struct cachedb_backend redis_backend = { "redis", 634 redis_init, redis_deinit, redis_lookup, redis_store 635 }; 636 #endif /* USE_REDIS */ 637 #endif /* USE_CACHEDB */ 638