Home | History | Annotate | Line # | Download | only in cachedb
      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