xref: /freebsd/contrib/unbound/cachedb/redis.c (revision be771a7b7f4580a30d99e41a5bb1b93a385a119d)
10fb34990SDag-Erling Smørgrav /*
20fb34990SDag-Erling Smørgrav  * cachedb/redis.c - cachedb redis module
30fb34990SDag-Erling Smørgrav  *
40fb34990SDag-Erling Smørgrav  * Copyright (c) 2018, NLnet Labs. All rights reserved.
50fb34990SDag-Erling Smørgrav  *
60fb34990SDag-Erling Smørgrav  * This software is open source.
70fb34990SDag-Erling Smørgrav  *
80fb34990SDag-Erling Smørgrav  * Redistribution and use in source and binary forms, with or without
90fb34990SDag-Erling Smørgrav  * modification, are permitted provided that the following conditions
100fb34990SDag-Erling Smørgrav  * are met:
110fb34990SDag-Erling Smørgrav  *
120fb34990SDag-Erling Smørgrav  * Redistributions of source code must retain the above copyright notice,
130fb34990SDag-Erling Smørgrav  * this list of conditions and the following disclaimer.
140fb34990SDag-Erling Smørgrav  *
150fb34990SDag-Erling Smørgrav  * Redistributions in binary form must reproduce the above copyright notice,
160fb34990SDag-Erling Smørgrav  * this list of conditions and the following disclaimer in the documentation
170fb34990SDag-Erling Smørgrav  * and/or other materials provided with the distribution.
180fb34990SDag-Erling Smørgrav  *
190fb34990SDag-Erling Smørgrav  * Neither the name of the NLNET LABS nor the names of its contributors may
200fb34990SDag-Erling Smørgrav  * be used to endorse or promote products derived from this software without
210fb34990SDag-Erling Smørgrav  * specific prior written permission.
220fb34990SDag-Erling Smørgrav  *
230fb34990SDag-Erling Smørgrav  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
240fb34990SDag-Erling Smørgrav  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
250fb34990SDag-Erling Smørgrav  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
260fb34990SDag-Erling Smørgrav  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
270fb34990SDag-Erling Smørgrav  * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
280fb34990SDag-Erling Smørgrav  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
290fb34990SDag-Erling Smørgrav  * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
300fb34990SDag-Erling Smørgrav  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
310fb34990SDag-Erling Smørgrav  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
320fb34990SDag-Erling Smørgrav  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
330fb34990SDag-Erling Smørgrav  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
340fb34990SDag-Erling Smørgrav  */
350fb34990SDag-Erling Smørgrav 
360fb34990SDag-Erling Smørgrav /**
370fb34990SDag-Erling Smørgrav  * \file
380fb34990SDag-Erling Smørgrav  *
390fb34990SDag-Erling Smørgrav  * This file contains a module that uses the redis database to cache
400fb34990SDag-Erling Smørgrav  * dns responses.
410fb34990SDag-Erling Smørgrav  */
420fb34990SDag-Erling Smørgrav 
430fb34990SDag-Erling Smørgrav #include "config.h"
440fb34990SDag-Erling Smørgrav #ifdef USE_CACHEDB
450fb34990SDag-Erling Smørgrav #include "cachedb/redis.h"
460fb34990SDag-Erling Smørgrav #include "cachedb/cachedb.h"
470fb34990SDag-Erling Smørgrav #include "util/alloc.h"
480fb34990SDag-Erling Smørgrav #include "util/config_file.h"
490fb34990SDag-Erling Smørgrav #include "sldns/sbuffer.h"
500fb34990SDag-Erling Smørgrav 
510fb34990SDag-Erling Smørgrav #ifdef USE_REDIS
520fb34990SDag-Erling Smørgrav #include "hiredis/hiredis.h"
530fb34990SDag-Erling Smørgrav 
540fb34990SDag-Erling Smørgrav struct redis_moddata {
55*be771a7bSCy Schubert 	/* thread-specific redis contexts */
56*be771a7bSCy Schubert 	redisContext** ctxs;
57*be771a7bSCy Schubert 	redisContext** replica_ctxs;
58*be771a7bSCy Schubert 	/* number of ctx entries */
59*be771a7bSCy Schubert 	int numctxs;
60*be771a7bSCy Schubert 	/* server's IP address or host name */
61*be771a7bSCy Schubert 	const char* server_host;
62*be771a7bSCy Schubert 	const char* replica_server_host;
63*be771a7bSCy Schubert 	/* server's TCP port */
64*be771a7bSCy Schubert 	int server_port;
65*be771a7bSCy Schubert 	int replica_server_port;
66*be771a7bSCy Schubert 	/* server's unix path, or "", NULL if unused */
67*be771a7bSCy Schubert 	const char* server_path;
68*be771a7bSCy Schubert 	const char* replica_server_path;
69*be771a7bSCy Schubert 	/* server's AUTH password, or "", NULL if unused */
70*be771a7bSCy Schubert 	const char* server_password;
71*be771a7bSCy Schubert 	const char* replica_server_password;
72*be771a7bSCy Schubert 	/* timeout for commands */
73*be771a7bSCy Schubert 	struct timeval command_timeout;
74*be771a7bSCy Schubert 	struct timeval replica_command_timeout;
75*be771a7bSCy Schubert 	/* timeout for connection setup */
76*be771a7bSCy Schubert 	struct timeval connect_timeout;
77*be771a7bSCy Schubert 	struct timeval replica_connect_timeout;
78*be771a7bSCy Schubert 	/* the redis logical database to use */
79*be771a7bSCy Schubert 	int logical_db;
80*be771a7bSCy Schubert 	int replica_logical_db;
81*be771a7bSCy Schubert 	/* if the SET with EX command is supported */
82*be771a7bSCy Schubert 	int set_with_ex_available;
830fb34990SDag-Erling Smørgrav };
840fb34990SDag-Erling Smørgrav 
8525039b37SCy Schubert static redisReply* redis_command(struct module_env*, struct cachedb_env*,
86*be771a7bSCy Schubert 	const char*, const uint8_t*, size_t, int);
8725039b37SCy Schubert 
88103ba509SCy Schubert static void
89103ba509SCy Schubert moddata_clean(struct redis_moddata** moddata) {
90103ba509SCy Schubert 	if(!moddata || !*moddata)
91103ba509SCy Schubert 		return;
92103ba509SCy Schubert 	if((*moddata)->ctxs) {
93103ba509SCy Schubert 		int i;
94103ba509SCy Schubert 		for(i = 0; i < (*moddata)->numctxs; i++) {
95103ba509SCy Schubert 			if((*moddata)->ctxs[i])
96103ba509SCy Schubert 				redisFree((*moddata)->ctxs[i]);
97103ba509SCy Schubert 		}
98103ba509SCy Schubert 		free((*moddata)->ctxs);
99103ba509SCy Schubert 	}
100*be771a7bSCy Schubert 	if((*moddata)->replica_ctxs) {
101*be771a7bSCy Schubert 		int i;
102*be771a7bSCy Schubert 		for(i = 0; i < (*moddata)->numctxs; i++) {
103*be771a7bSCy Schubert 			if((*moddata)->replica_ctxs[i])
104*be771a7bSCy Schubert 				redisFree((*moddata)->replica_ctxs[i]);
105*be771a7bSCy Schubert 		}
106*be771a7bSCy Schubert 		free((*moddata)->replica_ctxs);
107*be771a7bSCy Schubert 	}
108103ba509SCy Schubert 	free(*moddata);
109103ba509SCy Schubert 	*moddata = NULL;
110103ba509SCy Schubert }
111103ba509SCy Schubert 
1120fb34990SDag-Erling Smørgrav static redisContext*
113*be771a7bSCy Schubert redis_connect(const char* host, int port, const char* path,
114*be771a7bSCy Schubert 	const char* password, int logical_db,
115*be771a7bSCy Schubert 	const struct timeval connect_timeout,
116*be771a7bSCy Schubert 	const struct timeval command_timeout)
1170fb34990SDag-Erling Smørgrav {
1180fb34990SDag-Erling Smørgrav 	redisContext* ctx;
1190fb34990SDag-Erling Smørgrav 
120*be771a7bSCy Schubert 	if(path && path[0]!=0) {
121*be771a7bSCy Schubert 		ctx = redisConnectUnixWithTimeout(path, connect_timeout);
1228f76bb7dSCy Schubert 	} else {
123*be771a7bSCy Schubert 		ctx = redisConnectWithTimeout(host, port, connect_timeout);
1248f76bb7dSCy Schubert 	}
1250fb34990SDag-Erling Smørgrav 	if(!ctx || ctx->err) {
1260fb34990SDag-Erling Smørgrav 		const char *errstr = "out of memory";
1270fb34990SDag-Erling Smørgrav 		if(ctx)
1280fb34990SDag-Erling Smørgrav 			errstr = ctx->errstr;
1290fb34990SDag-Erling Smørgrav 		log_err("failed to connect to redis server: %s", errstr);
1300fb34990SDag-Erling Smørgrav 		goto fail;
1310fb34990SDag-Erling Smørgrav 	}
132*be771a7bSCy Schubert 	if(redisSetTimeout(ctx, command_timeout) != REDIS_OK) {
133*be771a7bSCy Schubert 		log_err("failed to set redis timeout, %s", ctx->errstr);
1340fb34990SDag-Erling Smørgrav 		goto fail;
1350fb34990SDag-Erling Smørgrav 	}
136*be771a7bSCy Schubert 	if(password && password[0]!=0) {
1378f76bb7dSCy Schubert 		redisReply* rep;
138*be771a7bSCy Schubert 		rep = redisCommand(ctx, "AUTH %s", password);
1398f76bb7dSCy Schubert 		if(!rep || rep->type == REDIS_REPLY_ERROR) {
1408f76bb7dSCy Schubert 			log_err("failed to authenticate with password");
1418f76bb7dSCy Schubert 			freeReplyObject(rep);
1428f76bb7dSCy Schubert 			goto fail;
1438f76bb7dSCy Schubert 		}
1448f76bb7dSCy Schubert 		freeReplyObject(rep);
1458f76bb7dSCy Schubert 	}
146*be771a7bSCy Schubert 	if(logical_db > 0) {
147103ba509SCy Schubert 		redisReply* rep;
148*be771a7bSCy Schubert 		rep = redisCommand(ctx, "SELECT %d", logical_db);
149103ba509SCy Schubert 		if(!rep || rep->type == REDIS_REPLY_ERROR) {
150103ba509SCy Schubert 			log_err("failed to set logical database (%d)",
151*be771a7bSCy Schubert 				logical_db);
152103ba509SCy Schubert 			freeReplyObject(rep);
153103ba509SCy Schubert 			goto fail;
154103ba509SCy Schubert 		}
155103ba509SCy Schubert 		freeReplyObject(rep);
156103ba509SCy Schubert 	}
157*be771a7bSCy Schubert 	if(verbosity >= VERB_OPS) {
158*be771a7bSCy Schubert 		char port_str[6+1];
159*be771a7bSCy Schubert 		port_str[0] = ' ';
160*be771a7bSCy Schubert 		(void)snprintf(port_str+1, sizeof(port_str)-1, "%d", port);
161*be771a7bSCy Schubert 		verbose(VERB_OPS, "Connection to Redis established (%s%s)",
162*be771a7bSCy Schubert 			path&&path[0]!=0?path:host,
163*be771a7bSCy Schubert 			path&&path[0]!=0?"":port_str);
164*be771a7bSCy Schubert 	}
1650fb34990SDag-Erling Smørgrav 	return ctx;
1660fb34990SDag-Erling Smørgrav 
1670fb34990SDag-Erling Smørgrav fail:
1680fb34990SDag-Erling Smørgrav 	if(ctx)
1690fb34990SDag-Erling Smørgrav 		redisFree(ctx);
1700fb34990SDag-Erling Smørgrav 	return NULL;
1710fb34990SDag-Erling Smørgrav }
1720fb34990SDag-Erling Smørgrav 
173*be771a7bSCy Schubert static void
174*be771a7bSCy Schubert set_timeout(struct timeval* timeout, int value, int explicit_value)
175*be771a7bSCy Schubert {
176*be771a7bSCy Schubert 	int v = explicit_value != 0 ? explicit_value : value;
177*be771a7bSCy Schubert 	timeout->tv_sec = v / 1000;
178*be771a7bSCy Schubert 	timeout->tv_usec = (v % 1000) * 1000;
179*be771a7bSCy Schubert }
180*be771a7bSCy Schubert 
1810fb34990SDag-Erling Smørgrav static int
1820fb34990SDag-Erling Smørgrav redis_init(struct module_env* env, struct cachedb_env* cachedb_env)
1830fb34990SDag-Erling Smørgrav {
1840fb34990SDag-Erling Smørgrav 	int i;
1850fb34990SDag-Erling Smørgrav 	struct redis_moddata* moddata = NULL;
1860fb34990SDag-Erling Smørgrav 
1878f76bb7dSCy Schubert 	verbose(VERB_OPS, "Redis initialization");
1880fb34990SDag-Erling Smørgrav 
1890fb34990SDag-Erling Smørgrav 	moddata = calloc(1, sizeof(struct redis_moddata));
1900fb34990SDag-Erling Smørgrav 	if(!moddata) {
1910fb34990SDag-Erling Smørgrav 		log_err("out of memory");
192103ba509SCy Schubert 		goto fail;
1930fb34990SDag-Erling Smørgrav 	}
1940fb34990SDag-Erling Smørgrav 	moddata->numctxs = env->cfg->num_threads;
195*be771a7bSCy Schubert 	/* note: server_host and similar string configuration options are
196*be771a7bSCy Schubert 	 * shallow references to configured strings; we don't have to free them
197*be771a7bSCy Schubert 	 * in this module. */
198*be771a7bSCy Schubert 	moddata->server_host = env->cfg->redis_server_host;
199*be771a7bSCy Schubert 	moddata->replica_server_host = env->cfg->redis_replica_server_host;
200*be771a7bSCy Schubert 
201*be771a7bSCy Schubert 	moddata->server_port = env->cfg->redis_server_port;
202*be771a7bSCy Schubert 	moddata->replica_server_port = env->cfg->redis_replica_server_port;
203*be771a7bSCy Schubert 
204*be771a7bSCy Schubert 	moddata->server_path = env->cfg->redis_server_path;
205*be771a7bSCy Schubert 	moddata->replica_server_path = env->cfg->redis_replica_server_path;
206*be771a7bSCy Schubert 
207*be771a7bSCy Schubert 	moddata->server_password = env->cfg->redis_server_password;
208*be771a7bSCy Schubert 	moddata->replica_server_password = env->cfg->redis_replica_server_password;
209*be771a7bSCy Schubert 
210*be771a7bSCy Schubert 	set_timeout(&moddata->command_timeout,
211*be771a7bSCy Schubert 		env->cfg->redis_timeout,
212*be771a7bSCy Schubert 		env->cfg->redis_command_timeout);
213*be771a7bSCy Schubert 	set_timeout(&moddata->replica_command_timeout,
214*be771a7bSCy Schubert 		env->cfg->redis_replica_timeout,
215*be771a7bSCy Schubert 		env->cfg->redis_replica_command_timeout);
216*be771a7bSCy Schubert 	set_timeout(&moddata->connect_timeout,
217*be771a7bSCy Schubert 		env->cfg->redis_timeout,
218*be771a7bSCy Schubert 		env->cfg->redis_connect_timeout);
219*be771a7bSCy Schubert 	set_timeout(&moddata->replica_connect_timeout,
220*be771a7bSCy Schubert 		env->cfg->redis_replica_timeout,
221*be771a7bSCy Schubert 		env->cfg->redis_replica_connect_timeout);
222*be771a7bSCy Schubert 
223*be771a7bSCy Schubert 	moddata->logical_db = env->cfg->redis_logical_db;
224*be771a7bSCy Schubert 	moddata->replica_logical_db = env->cfg->redis_replica_logical_db;
225*be771a7bSCy Schubert 
2260fb34990SDag-Erling Smørgrav 	moddata->ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*));
2270fb34990SDag-Erling Smørgrav 	if(!moddata->ctxs) {
2280fb34990SDag-Erling Smørgrav 		log_err("out of memory");
229103ba509SCy Schubert 		goto fail;
2300fb34990SDag-Erling Smørgrav 	}
231*be771a7bSCy Schubert 	if((moddata->replica_server_host && moddata->replica_server_host[0]!=0)
232*be771a7bSCy Schubert 		|| (moddata->replica_server_path && moddata->replica_server_path[0]!=0)) {
233*be771a7bSCy Schubert 		/* There is a replica configured, allocate ctxs */
234*be771a7bSCy Schubert 		moddata->replica_ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*));
235*be771a7bSCy Schubert 		if(!moddata->replica_ctxs) {
236*be771a7bSCy Schubert 			log_err("out of memory");
237103ba509SCy Schubert 			goto fail;
238103ba509SCy Schubert 		}
239*be771a7bSCy Schubert 	}
240*be771a7bSCy Schubert 	for(i = 0; i < moddata->numctxs; i++) {
241*be771a7bSCy Schubert 		redisContext* ctx = redis_connect(
242*be771a7bSCy Schubert 			moddata->server_host,
243*be771a7bSCy Schubert 			moddata->server_port,
244*be771a7bSCy Schubert 			moddata->server_path,
245*be771a7bSCy Schubert 			moddata->server_password,
246*be771a7bSCy Schubert 			moddata->logical_db,
247*be771a7bSCy Schubert 			moddata->connect_timeout,
248*be771a7bSCy Schubert 			moddata->command_timeout);
249*be771a7bSCy Schubert 		if(!ctx) {
250*be771a7bSCy Schubert 			log_err("redis_init: failed to init redis "
251*be771a7bSCy Schubert 				"(for thread %d)", i);
252*be771a7bSCy Schubert 			/* And continue, the context can be established
253*be771a7bSCy Schubert 			 * later, just like after a disconnect. */
254*be771a7bSCy Schubert 		}
255103ba509SCy Schubert 		moddata->ctxs[i] = ctx;
256103ba509SCy Schubert 	}
257*be771a7bSCy Schubert 	if(moddata->replica_ctxs) {
258*be771a7bSCy Schubert 		for(i = 0; i < moddata->numctxs; i++) {
259*be771a7bSCy Schubert 			redisContext* ctx = redis_connect(
260*be771a7bSCy Schubert 				moddata->replica_server_host,
261*be771a7bSCy Schubert 				moddata->replica_server_port,
262*be771a7bSCy Schubert 				moddata->replica_server_path,
263*be771a7bSCy Schubert 				moddata->replica_server_password,
264*be771a7bSCy Schubert 				moddata->replica_logical_db,
265*be771a7bSCy Schubert 				moddata->replica_connect_timeout,
266*be771a7bSCy Schubert 				moddata->replica_command_timeout);
267*be771a7bSCy Schubert 			if(!ctx) {
268*be771a7bSCy Schubert 				log_err("redis_init: failed to init redis "
269*be771a7bSCy Schubert 					"replica (for thread %d)", i);
270*be771a7bSCy Schubert 				/* And continue, the context can be established
271*be771a7bSCy Schubert 				* later, just like after a disconnect. */
272*be771a7bSCy Schubert 			}
273*be771a7bSCy Schubert 			moddata->replica_ctxs[i] = ctx;
274*be771a7bSCy Schubert 		}
275*be771a7bSCy Schubert 	}
2760fb34990SDag-Erling Smørgrav 	cachedb_env->backend_data = moddata;
277*be771a7bSCy Schubert 	if(env->cfg->redis_expire_records &&
278*be771a7bSCy Schubert 		moddata->ctxs[env->alloc->thread_num] != NULL) {
27925039b37SCy Schubert 		redisReply* rep = NULL;
28025039b37SCy Schubert 		int redis_reply_type = 0;
281*be771a7bSCy Schubert 		/** check if set with ex command is supported */
28225039b37SCy Schubert 		rep = redis_command(env, cachedb_env,
283*be771a7bSCy Schubert 			"SET __UNBOUND_REDIS_CHECK__ none EX 1", NULL, 0, 1);
28425039b37SCy Schubert 		if(!rep) {
28525039b37SCy Schubert 			/** init failed, no response from redis server*/
286*be771a7bSCy Schubert 			goto set_with_ex_fail;
28725039b37SCy Schubert 		}
28825039b37SCy Schubert 		redis_reply_type = rep->type;
28925039b37SCy Schubert 		freeReplyObject(rep);
29025039b37SCy Schubert 		switch(redis_reply_type) {
29125039b37SCy Schubert 		case REDIS_REPLY_STATUS:
29225039b37SCy Schubert 			break;
29325039b37SCy Schubert 		default:
294*be771a7bSCy Schubert 			/** init failed, set_with_ex command not supported */
295*be771a7bSCy Schubert 			goto set_with_ex_fail;
29625039b37SCy Schubert 		}
297*be771a7bSCy Schubert 		moddata->set_with_ex_available = 1;
29825039b37SCy Schubert 	}
2990fb34990SDag-Erling Smørgrav 	return 1;
300103ba509SCy Schubert 
301*be771a7bSCy Schubert set_with_ex_fail:
302*be771a7bSCy Schubert 	log_err("redis_init: failure during redis_init, the "
303*be771a7bSCy Schubert 		"redis-expire-records option requires the SET with EX command "
304*be771a7bSCy Schubert 		"(redis >= 2.6.2)");
305*be771a7bSCy Schubert 	return 1;
306103ba509SCy Schubert fail:
307103ba509SCy Schubert 	moddata_clean(&moddata);
308103ba509SCy Schubert 	return 0;
3090fb34990SDag-Erling Smørgrav }
3100fb34990SDag-Erling Smørgrav 
3110fb34990SDag-Erling Smørgrav static void
3120fb34990SDag-Erling Smørgrav redis_deinit(struct module_env* env, struct cachedb_env* cachedb_env)
3130fb34990SDag-Erling Smørgrav {
3140fb34990SDag-Erling Smørgrav 	struct redis_moddata* moddata = (struct redis_moddata*)
3150fb34990SDag-Erling Smørgrav 		cachedb_env->backend_data;
3160fb34990SDag-Erling Smørgrav 	(void)env;
3170fb34990SDag-Erling Smørgrav 
3188f76bb7dSCy Schubert 	verbose(VERB_OPS, "Redis deinitialization");
319103ba509SCy Schubert 	moddata_clean(&moddata);
3200fb34990SDag-Erling Smørgrav }
3210fb34990SDag-Erling Smørgrav 
3220fb34990SDag-Erling Smørgrav /*
3230fb34990SDag-Erling Smørgrav  * Send a redis command and get a reply.  Unified so that it can be used for
3240fb34990SDag-Erling Smørgrav  * both SET and GET.  If 'data' is non-NULL the command is supposed to be
3250fb34990SDag-Erling Smørgrav  * SET and GET otherwise, but the implementation of this function is agnostic
3260fb34990SDag-Erling Smørgrav  * about the semantics (except for logging): 'command', 'data', and 'data_len'
3270fb34990SDag-Erling Smørgrav  * are opaquely passed to redisCommand().
3280fb34990SDag-Erling Smørgrav  * This function first checks whether a connection with a redis server has
3290fb34990SDag-Erling Smørgrav  * been established; if not it tries to set up a new one.
3300fb34990SDag-Erling Smørgrav  * It returns redisReply returned from redisCommand() or NULL if some low
3310fb34990SDag-Erling Smørgrav  * level error happens.  The caller is responsible to check the return value,
3320fb34990SDag-Erling Smørgrav  * if it's non-NULL, it has to free it with freeReplyObject().
3330fb34990SDag-Erling Smørgrav  */
3340fb34990SDag-Erling Smørgrav static redisReply*
3350fb34990SDag-Erling Smørgrav redis_command(struct module_env* env, struct cachedb_env* cachedb_env,
336*be771a7bSCy Schubert 	const char* command, const uint8_t* data, size_t data_len, int write)
3370fb34990SDag-Erling Smørgrav {
338*be771a7bSCy Schubert 	redisContext* ctx, **ctx_selector;
3390fb34990SDag-Erling Smørgrav 	redisReply* rep;
3400fb34990SDag-Erling Smørgrav 	struct redis_moddata* d = (struct redis_moddata*)
3410fb34990SDag-Erling Smørgrav 		cachedb_env->backend_data;
3420fb34990SDag-Erling Smørgrav 
3430fb34990SDag-Erling Smørgrav 	/* We assume env->alloc->thread_num is a unique ID for each thread
3440fb34990SDag-Erling Smørgrav 	 * in [0, num-of-threads).  We could treat it as an error condition
3450fb34990SDag-Erling Smørgrav 	 * if the assumption didn't hold, but it seems to be a fundamental
3460fb34990SDag-Erling Smørgrav 	 * assumption throughout the unbound architecture, so we simply assert
3470fb34990SDag-Erling Smørgrav 	 * it. */
3480fb34990SDag-Erling Smørgrav 	log_assert(env->alloc->thread_num < d->numctxs);
349*be771a7bSCy Schubert 
350*be771a7bSCy Schubert 	ctx_selector = !write && d->replica_ctxs
351*be771a7bSCy Schubert 		?d->replica_ctxs
352*be771a7bSCy Schubert 		:d->ctxs;
353*be771a7bSCy Schubert 	ctx = ctx_selector[env->alloc->thread_num];
3540fb34990SDag-Erling Smørgrav 
3550fb34990SDag-Erling Smørgrav 	/* If we've not established a connection to the server or we've closed
3560fb34990SDag-Erling Smørgrav 	 * it on a failure, try to re-establish a new one.   Failures will be
3570fb34990SDag-Erling Smørgrav 	 * logged in redis_connect(). */
3580fb34990SDag-Erling Smørgrav 	if(!ctx) {
359*be771a7bSCy Schubert 		if(!write && d->replica_ctxs) {
360*be771a7bSCy Schubert 			ctx = redis_connect(
361*be771a7bSCy Schubert 				d->replica_server_host,
362*be771a7bSCy Schubert 				d->replica_server_port,
363*be771a7bSCy Schubert 				d->replica_server_path,
364*be771a7bSCy Schubert 				d->replica_server_password,
365*be771a7bSCy Schubert 				d->replica_logical_db,
366*be771a7bSCy Schubert 				d->replica_connect_timeout,
367*be771a7bSCy Schubert 				d->replica_command_timeout);
368*be771a7bSCy Schubert 		} else {
369*be771a7bSCy Schubert 			ctx = redis_connect(
370*be771a7bSCy Schubert 				d->server_host,
371*be771a7bSCy Schubert 				d->server_port,
372*be771a7bSCy Schubert 				d->server_path,
373*be771a7bSCy Schubert 				d->server_password,
374*be771a7bSCy Schubert 				d->logical_db,
375*be771a7bSCy Schubert 				d->connect_timeout,
376*be771a7bSCy Schubert 				d->command_timeout);
3770fb34990SDag-Erling Smørgrav 		}
378*be771a7bSCy Schubert 		ctx_selector[env->alloc->thread_num] = ctx;
379*be771a7bSCy Schubert 	}
380*be771a7bSCy Schubert 	if(!ctx) return NULL;
3810fb34990SDag-Erling Smørgrav 
3820fb34990SDag-Erling Smørgrav 	/* Send the command and get a reply, synchronously. */
3830fb34990SDag-Erling Smørgrav 	rep = (redisReply*)redisCommand(ctx, command, data, data_len);
3840fb34990SDag-Erling Smørgrav 	if(!rep) {
3850fb34990SDag-Erling Smørgrav 		/* Once an error as a NULL-reply is returned the context cannot
3860fb34990SDag-Erling Smørgrav 		 * be reused and we'll need to set up a new connection. */
3870fb34990SDag-Erling Smørgrav 		log_err("redis_command: failed to receive a reply, "
3880fb34990SDag-Erling Smørgrav 			"closing connection: %s", ctx->errstr);
3890fb34990SDag-Erling Smørgrav 		redisFree(ctx);
390*be771a7bSCy Schubert 		ctx_selector[env->alloc->thread_num] = NULL;
3910fb34990SDag-Erling Smørgrav 		return NULL;
3920fb34990SDag-Erling Smørgrav 	}
3930fb34990SDag-Erling Smørgrav 
3940fb34990SDag-Erling Smørgrav 	/* Check error in reply to unify logging in that case.
3950fb34990SDag-Erling Smørgrav 	 * The caller may perform context-dependent checks and logging. */
3960fb34990SDag-Erling Smørgrav 	if(rep->type == REDIS_REPLY_ERROR)
3970fb34990SDag-Erling Smørgrav 		log_err("redis: %s resulted in an error: %s",
3980fb34990SDag-Erling Smørgrav 			data ? "set" : "get", rep->str);
3990fb34990SDag-Erling Smørgrav 
4000fb34990SDag-Erling Smørgrav 	return rep;
4010fb34990SDag-Erling Smørgrav }
4020fb34990SDag-Erling Smørgrav 
4030fb34990SDag-Erling Smørgrav static int
4040fb34990SDag-Erling Smørgrav redis_lookup(struct module_env* env, struct cachedb_env* cachedb_env,
4050fb34990SDag-Erling Smørgrav 	char* key, struct sldns_buffer* result_buffer)
4060fb34990SDag-Erling Smørgrav {
4070fb34990SDag-Erling Smørgrav 	redisReply* rep;
4080fb34990SDag-Erling Smørgrav 	char cmdbuf[4+(CACHEDB_HASHSIZE/8)*2+1]; /* "GET " + key */
4090fb34990SDag-Erling Smørgrav 	int n;
4100fb34990SDag-Erling Smørgrav 	int ret = 0;
4110fb34990SDag-Erling Smørgrav 
4120fb34990SDag-Erling Smørgrav 	verbose(VERB_ALGO, "redis_lookup of %s", key);
4130fb34990SDag-Erling Smørgrav 
4140fb34990SDag-Erling Smørgrav 	n = snprintf(cmdbuf, sizeof(cmdbuf), "GET %s", key);
4150fb34990SDag-Erling Smørgrav 	if(n < 0 || n >= (int)sizeof(cmdbuf)) {
4160fb34990SDag-Erling Smørgrav 		log_err("redis_lookup: unexpected failure to build command");
4170fb34990SDag-Erling Smørgrav 		return 0;
4180fb34990SDag-Erling Smørgrav 	}
4190fb34990SDag-Erling Smørgrav 
420*be771a7bSCy Schubert 	rep = redis_command(env, cachedb_env, cmdbuf, NULL, 0, 0);
4210fb34990SDag-Erling Smørgrav 	if(!rep)
4220fb34990SDag-Erling Smørgrav 		return 0;
4230fb34990SDag-Erling Smørgrav 	switch(rep->type) {
4240fb34990SDag-Erling Smørgrav 	case REDIS_REPLY_NIL:
4250fb34990SDag-Erling Smørgrav 		verbose(VERB_ALGO, "redis_lookup: no data cached");
4260fb34990SDag-Erling Smørgrav 		break;
4270fb34990SDag-Erling Smørgrav 	case REDIS_REPLY_STRING:
4280fb34990SDag-Erling Smørgrav 		verbose(VERB_ALGO, "redis_lookup found %d bytes",
4290fb34990SDag-Erling Smørgrav 			(int)rep->len);
4300fb34990SDag-Erling Smørgrav 		if((size_t)rep->len > sldns_buffer_capacity(result_buffer)) {
4310fb34990SDag-Erling Smørgrav 			log_err("redis_lookup: replied data too long: %lu",
4320fb34990SDag-Erling Smørgrav 				(size_t)rep->len);
4330fb34990SDag-Erling Smørgrav 			break;
4340fb34990SDag-Erling Smørgrav 		}
4350fb34990SDag-Erling Smørgrav 		sldns_buffer_clear(result_buffer);
4360fb34990SDag-Erling Smørgrav 		sldns_buffer_write(result_buffer, rep->str, rep->len);
4370fb34990SDag-Erling Smørgrav 		sldns_buffer_flip(result_buffer);
4380fb34990SDag-Erling Smørgrav 		ret = 1;
4390fb34990SDag-Erling Smørgrav 		break;
4400fb34990SDag-Erling Smørgrav 	case REDIS_REPLY_ERROR:
4410fb34990SDag-Erling Smørgrav 		break;		/* already logged */
4420fb34990SDag-Erling Smørgrav 	default:
4430fb34990SDag-Erling Smørgrav 		log_err("redis_lookup: unexpected type of reply for (%d)",
4440fb34990SDag-Erling Smørgrav 			rep->type);
4450fb34990SDag-Erling Smørgrav 		break;
4460fb34990SDag-Erling Smørgrav 	}
4470fb34990SDag-Erling Smørgrav 	freeReplyObject(rep);
4480fb34990SDag-Erling Smørgrav 	return ret;
4490fb34990SDag-Erling Smørgrav }
4500fb34990SDag-Erling Smørgrav 
4510fb34990SDag-Erling Smørgrav static void
4520fb34990SDag-Erling Smørgrav redis_store(struct module_env* env, struct cachedb_env* cachedb_env,
45325039b37SCy Schubert 	char* key, uint8_t* data, size_t data_len, time_t ttl)
4540fb34990SDag-Erling Smørgrav {
4550fb34990SDag-Erling Smørgrav 	redisReply* rep;
4560fb34990SDag-Erling Smørgrav 	int n;
457*be771a7bSCy Schubert 	struct redis_moddata* moddata = (struct redis_moddata*)
458*be771a7bSCy Schubert 		cachedb_env->backend_data;
459*be771a7bSCy Schubert 	int set_ttl = (moddata->set_with_ex_available &&
460*be771a7bSCy Schubert 		env->cfg->redis_expire_records &&
46125039b37SCy Schubert 		(!env->cfg->serve_expired || env->cfg->serve_expired_ttl > 0));
46225039b37SCy Schubert 	/* Supported commands:
46325039b37SCy Schubert 	 * - "SET " + key + " %b"
464*be771a7bSCy Schubert 	 * - "SET " + key + " %b EX " + ttl
465*be771a7bSCy Schubert 	 *   older redis 2.0.0 was "SETEX " + key + " " + ttl + " %b"
466*be771a7bSCy Schubert 	 * - "EXPIRE " + key + " 0"
46725039b37SCy Schubert 	 */
46825039b37SCy Schubert 	char cmdbuf[6+(CACHEDB_HASHSIZE/8)*2+11+3+1];
4690fb34990SDag-Erling Smørgrav 
47025039b37SCy Schubert 	if (!set_ttl) {
4710fb34990SDag-Erling Smørgrav 		verbose(VERB_ALGO, "redis_store %s (%d bytes)", key, (int)data_len);
4720fb34990SDag-Erling Smørgrav 		/* build command to set to a binary safe string */
4730fb34990SDag-Erling Smørgrav 		n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b", key);
474*be771a7bSCy Schubert 	} else if(ttl == 0) {
475*be771a7bSCy Schubert 		/* use the EXPIRE command, SET with EX 0 is an invalid time. */
476*be771a7bSCy Schubert 		/* Replies with REDIS_REPLY_INTEGER of 1. */
477*be771a7bSCy Schubert 		verbose(VERB_ALGO, "redis_store expire %s (%d bytes)",
478*be771a7bSCy Schubert 			key, (int)data_len);
479*be771a7bSCy Schubert 		n = snprintf(cmdbuf, sizeof(cmdbuf), "EXPIRE %s 0", key);
480*be771a7bSCy Schubert 		data = NULL;
481*be771a7bSCy Schubert 		data_len = 0;
48225039b37SCy Schubert 	} else {
48325039b37SCy Schubert 		/* add expired ttl time to redis ttl to avoid premature eviction of key */
48425039b37SCy Schubert 		ttl += env->cfg->serve_expired_ttl;
48525039b37SCy Schubert 		verbose(VERB_ALGO, "redis_store %s (%d bytes) with ttl %u",
486*be771a7bSCy Schubert 			key, (int)data_len, (unsigned)(uint32_t)ttl);
48725039b37SCy Schubert 		/* build command to set to a binary safe string */
488*be771a7bSCy Schubert 		n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b EX %u", key,
489*be771a7bSCy Schubert 			(unsigned)(uint32_t)ttl);
49025039b37SCy Schubert 	}
49125039b37SCy Schubert 
49225039b37SCy Schubert 
4930fb34990SDag-Erling Smørgrav 	if(n < 0 || n >= (int)sizeof(cmdbuf)) {
4940fb34990SDag-Erling Smørgrav 		log_err("redis_store: unexpected failure to build command");
4950fb34990SDag-Erling Smørgrav 		return;
4960fb34990SDag-Erling Smørgrav 	}
4970fb34990SDag-Erling Smørgrav 
498*be771a7bSCy Schubert 	rep = redis_command(env, cachedb_env, cmdbuf, data, data_len, 1);
4990fb34990SDag-Erling Smørgrav 	if(rep) {
5000fb34990SDag-Erling Smørgrav 		verbose(VERB_ALGO, "redis_store set completed");
5010fb34990SDag-Erling Smørgrav 		if(rep->type != REDIS_REPLY_STATUS &&
502*be771a7bSCy Schubert 			rep->type != REDIS_REPLY_ERROR &&
503*be771a7bSCy Schubert 			rep->type != REDIS_REPLY_INTEGER) {
5040fb34990SDag-Erling Smørgrav 			log_err("redis_store: unexpected type of reply (%d)",
5050fb34990SDag-Erling Smørgrav 				rep->type);
5060fb34990SDag-Erling Smørgrav 		}
5070fb34990SDag-Erling Smørgrav 		freeReplyObject(rep);
5080fb34990SDag-Erling Smørgrav 	}
5090fb34990SDag-Erling Smørgrav }
5100fb34990SDag-Erling Smørgrav 
5110fb34990SDag-Erling Smørgrav struct cachedb_backend redis_backend = { "redis",
5120fb34990SDag-Erling Smørgrav 	redis_init, redis_deinit, redis_lookup, redis_store
5130fb34990SDag-Erling Smørgrav };
5140fb34990SDag-Erling Smørgrav #endif	/* USE_REDIS */
5150fb34990SDag-Erling Smørgrav #endif /* USE_CACHEDB */
516