All Data Structures Files Functions Variables Typedefs Enumerations Enumerator Macros Groups Pages
rlm_cache_redis.c
Go to the documentation of this file.
1 /*
2  * This program is is free software; you can redistribute it and/or modify
3  * it under the terms of the GNU General Public License as published by
4  * the Free Software Foundation; either version 2 of the License, or (at
5  * your option) any later version.
6  *
7  * This program is distributed in the hope that it will be useful,
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10  * GNU General Public License for more details.
11  *
12  * You should have received a copy of the GNU General Public License
13  * along with this program; if not, write to the Free Software
14  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15  */
16 
17 /**
18  * $Id: fb0c94aea48ce8071bbb3775b4f66b4b2ee92ce6 $
19  * @file rlm_cache_redis.c
20  * @brief redis based cache.
21  *
22  * @copyright 2015 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
23  */
24 #include <freeradius-devel/radiusd.h>
25 #include <freeradius-devel/rad_assert.h>
26 
27 #include "../../rlm_cache.h"
28 #include "../../../rlm_redis/redis.h"
29 #include "../../../rlm_redis/cluster.h"
30 
34 };
35 
36 typedef struct rlm_cache_redis {
37  fr_redis_conf_t conf; //!< Connection parameters for the Redis server.
38  //!< Must be first field in this struct.
39 
40  vp_tmpl_t created_attr; //!< LHS of the Cache-Created map.
41  vp_tmpl_t expires_attr; //!< LHS of the Cache-Expires map.
42 
45 
46 /** Create a new rlm_cache_redis instance
47  *
48  * @copydetails cache_instantiate_t
49  */
50 static int mod_instantiate(CONF_SECTION *conf, rlm_cache_config_t const *config, void *driver_inst)
51 {
52  rlm_cache_redis_t *driver = driver_inst;
53  char buffer[256];
54 
55  buffer[0] = '\0';
56 
58 
59  if (cf_section_parse(conf, driver, driver_config) < 0) return -1;
60 
61  snprintf(buffer, sizeof(buffer), "rlm_cache (%s)", config->name);
62 
63  driver->cluster = fr_redis_cluster_alloc(driver, conf, &driver->conf);
64  if (!driver->cluster) {
65  ERROR("rlm_cache_redis: Cluster failure");
66  return -1;
67  }
68 
69  /*
70  * These never change, so do it once on instantiation
71  */
72  if (tmpl_from_attr_str(&driver->created_attr, "&Cache-Created",
73  REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false) < 0) {
74  ERROR("rlm_cache_redis: Cache-Created attribute not defined");
75  return -1;
76  }
77 
78  if (tmpl_from_attr_str(&driver->expires_attr, "&Cache-Expires",
79  REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false) < 0) {
80  ERROR("rlm_cache_redis: Cache-Expires attribute not defined");
81  return -1;
82  }
83 
84  return 0;
85 }
86 
88 {
89  talloc_free(c);
90 }
91 
92 /** Locate a cache entry in redis
93  *
94  * @copydetails cache_entry_find_t
95  */
97  UNUSED rlm_cache_config_t const *config, void *driver_inst,
98  REQUEST *request, UNUSED void *handle, uint8_t const *key, size_t key_len)
99 {
100  rlm_cache_redis_t *driver = driver_inst;
101  size_t i;
102 
104  fr_redis_conn_t *conn;
105  fr_redis_rcode_t status;
106  redisReply *reply = NULL;
107  int s_ret;
108 
109  vp_map_t *head = NULL, **last = &head;
110 #ifdef HAVE_TALLOC_POOLED_OBJECT
111  size_t pool_size = 0;
112 #endif
114 
115  for (s_ret = fr_redis_cluster_state_init(&state, &conn, driver->cluster, request, key, key_len, false);
116  s_ret == REDIS_RCODE_TRY_AGAIN; /* Continue */
117  s_ret = fr_redis_cluster_state_next(&state, &conn, driver->cluster, request, status, &reply)) {
118  /*
119  * Grab all the data for this hash, should return an array
120  * of alternating keys/values which we then convert into maps.
121  */
122  if (RDEBUG_ENABLED3) {
123  char *p;
124 
125  p = fr_asprint(NULL, (char const *)key, key_len, '"');
126  RDEBUG3("LRANGE %s 0 -1", key);
127  talloc_free(p);
128  }
129  reply = redisCommand(conn->handle, "LRANGE %b 0 -1", key, key_len);
130  status = fr_redis_command_status(conn, reply);
131  }
132  if (s_ret != REDIS_RCODE_SUCCESS) {
133  RERROR("Failed retrieving entry");
134  fr_redis_reply_free(reply);
135  return CACHE_ERROR;
136  }
137  rad_assert(reply); /* clang scan */
138 
139  if (reply->type != REDIS_REPLY_ARRAY) {
140  REDEBUG("Bad result type, expected array, got %s",
141  fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>"));
142  fr_redis_reply_free(reply);
143  return CACHE_ERROR;
144  }
145 
146  RDEBUG3("Entry contains %zu elements", reply->elements);
147 
148  if (reply->elements == 0) {
149  fr_redis_reply_free(reply);
150  return CACHE_MISS;
151  }
152 
153  if (reply->elements % 3) {
154  REDEBUG("Invalid number of reply elements (%zu). "
155  "Reply must contain triplets of keys operators and values",
156  reply->elements);
157  fr_redis_reply_free(reply);
158  return CACHE_ERROR;
159  }
160 
161 #ifdef HAVE_TALLOC_POOLED_OBJECT
162  /*
163  * We can get a pretty good idea of the required size of the pool
164  */
165  for (i = 0; i < reply->elements; i += 3) {
166  pool_size += sizeof(vp_map_t) + (sizeof(vp_tmpl_t) * 2);
167  if (reply->element[i]->type == REDIS_REPLY_STRING) pool_size += reply->element[i]->len + 1;
168  }
169 
170  /*
171  * reply->elements gives us the number of chunks, as the maps are triplets, and there
172  * are three chunks per map
173  */
174 
175  c = talloc_pooled_object(NULL, rlm_cache_entry_t, reply->elements, pool_size);
176  memset(&pool, 0, sizeof(rlm_cache_entry_t));
177 #else
178  c = talloc_zero(NULL, rlm_cache_entry_t);
179 #endif
180  /*
181  * Convert the key/value pairs back into maps
182  */
183  for (i = 0; i < reply->elements; i += 3) {
184  if (fr_redis_reply_to_map(c, last, request,
185  reply->element[i], reply->element[i + 1], reply->element[i + 2]) < 0) {
186  talloc_free(c);
187  fr_redis_reply_free(reply);
188  return CACHE_ERROR;
189  }
190  last = &(*last)->next;
191  }
192  fr_redis_reply_free(reply);
193 
194  /*
195  * Pull out the cache created date
196  */
197  if ((head->lhs->tmpl_da->vendor == 0) && (head->lhs->tmpl_da->attr == PW_CACHE_CREATED)) {
198  vp_map_t *map;
199 
200  c->created = head->rhs->tmpl_data_value.date;
201 
202  map = head;
203  head = head->next;
204  talloc_free(map);
205  }
206 
207  /*
208  * Pull out the cache expires date
209  */
210  if ((head->lhs->tmpl_da->vendor == 0) && (head->lhs->tmpl_da->attr == PW_CACHE_EXPIRES)) {
211  vp_map_t *map;
212 
213  c->expires = head->rhs->tmpl_data_value.date;
214 
215  map = head;
216  head = head->next;
217  talloc_free(map);
218  }
219 
220  c->key = talloc_memdup(c, key, key_len);
221  c->key_len = key_len;
222  c->maps = head;
223  *out = c;
224 
225  return CACHE_OK;
226 }
227 
228 
229 /** Insert a new entry into the data store
230  *
231  * @copydetails cache_entry_insert_t
232  */
233 static cache_status_t cache_entry_insert(UNUSED rlm_cache_config_t const *config, void *driver_inst,
234  REQUEST *request, UNUSED void *handle, const rlm_cache_entry_t *c)
235 {
236  rlm_cache_redis_t *driver = driver_inst;
237  TALLOC_CTX *pool;
238 
239  vp_map_t *map;
240 
241  fr_redis_conn_t *conn;
243  fr_redis_rcode_t status;
244  redisReply *reply = NULL;
245  int s_ret;
246 
247  static char const command[] = "RPUSH";
248  char const **argv;
249  size_t *argv_len;
250  char const **argv_p;
251  size_t *argv_len_p;
252 
253  int pipelined = 0; /* How many commands pending in the pipeline */
254  redisReply *replies[5]; /* Should have the same number of elements as pipelined commands */
255  size_t reply_num = 0, i;
256 
257  char *p;
258  int cnt;
259 
260  vp_tmpl_t expires_value;
261  vp_map_t expires = {
262  .op = T_OP_SET,
263  .lhs = &driver->expires_attr,
264  .rhs = &expires_value,
265  };
266 
267  vp_tmpl_t created_value;
268  vp_map_t created = {
269  .op = T_OP_SET,
270  .lhs = &driver->created_attr,
271  .rhs = &created_value,
272  .next = &expires
273  };
274 
275  /*
276  * Encode the entry created date
277  */
278  tmpl_init(&created_value, TMPL_TYPE_DATA, "<TEMP>", 6, T_BARE_WORD);
279  created_value.tmpl_data_type = PW_TYPE_DATE;
280  created_value.tmpl_data_length = sizeof(created_value.tmpl_data_value.date);
281  created_value.tmpl_data_value.date = c->created;
282 
283  /*
284  * Encode the entry expiry time
285  *
286  * Although Redis objects expire on their own, we still need this
287  * to ignore entries that were created before the last epoch.
288  */
289  tmpl_init(&expires_value, TMPL_TYPE_DATA, "<TEMP>", 6, T_BARE_WORD);
290  expires_value.tmpl_data_type = PW_TYPE_DATE;
291  expires_value.tmpl_data_length = sizeof(expires_value.tmpl_data_value.date);
292  expires_value.tmpl_data_value.date = c->expires;
293  expires.next = c->maps; /* Head of the list */
294 
295  for (cnt = 0, map = &created; map; cnt++, map = map->next);
296 
297  /*
298  * The majority of serialized entries should be under 1k.
299  *
300  * @todo We should really calculate this using some sort of moving average.
301  */
302  pool = talloc_pool(request, 1024);
303  if (!pool) return CACHE_ERROR;
304 
305  argv_p = argv = talloc_array(pool, char const *, (cnt * 3) + 2); /* pair = 3 + cmd + key */
306  argv_len_p = argv_len = talloc_array(pool, size_t, (cnt * 3) + 2); /* pair = 3 + cmd + key */
307 
308  *argv_p++ = command;
309  *argv_len_p++ = sizeof(command) - 1;
310 
311  *argv_p++ = (char const *)c->key;
312  *argv_len_p++ = c->key_len;
313 
314  /*
315  * Add the maps to the command string in reverse order
316  */
317  for (map = &created; map; map = map->next) {
318  if (fr_redis_tuple_from_map(pool, argv_p, argv_len_p, map) < 0) {
319  REDEBUG("Failed encoding map as Redis K/V pair");
320  talloc_free(pool);
321  return CACHE_ERROR;
322  }
323  argv_p += 3;
324  argv_len_p += 3;
325  }
326 
327  RDEBUG3("Pipelining commands");
328  RINDENT();
329 
330  for (s_ret = fr_redis_cluster_state_init(&state, &conn, driver->cluster, request, c->key, c->key_len, false);
331  s_ret == REDIS_RCODE_TRY_AGAIN; /* Continue */
332  s_ret = fr_redis_cluster_state_next(&state, &conn, driver->cluster, request, status, &reply)) {
333  /*
334  * Start the transaction, as we need to set an expiry time too.
335  */
336  if (c->expires > 0) {
337  RDEBUG3("MULTI");
338  if (redisAppendCommand(conn->handle, "MULTI") != REDIS_OK) {
339  append_error:
340  REXDENT();
341  RERROR("Failed appending Redis command to output buffer: %s", conn->handle->errstr);
342  talloc_free(pool);
343  return CACHE_ERROR;
344  }
345  pipelined++;
346  }
347 
348  if (RDEBUG_ENABLED3) {
349  p = fr_asprint(request, (char const *)c->key, c->key_len, '\0');
350  RDEBUG3("DEL \"%s\"", p);
351  talloc_free(p);
352 
353  }
354 
355  if (redisAppendCommand(conn->handle, "DEL %b", c->key, c->key_len) != REDIS_OK) goto append_error;
356  pipelined++;
357 
358  if (RDEBUG_ENABLED3) {
359  RDEBUG3("argv command");
360  RINDENT();
361  for (i = 0; i < talloc_array_length(argv); i++) {
362  p = fr_asprint(request, argv[i], argv_len[i], '\0');
363  RDEBUG3("%s", p);
364  talloc_free(p);
365  }
366  REXDENT();
367  }
368  redisAppendCommandArgv(conn->handle, talloc_array_length(argv), argv, argv_len);
369  pipelined++;
370 
371  /*
372  * Set the expiry time and close out the transaction.
373  */
374  if (c->expires > 0) {
375  if (RDEBUG_ENABLED3) {
376  p = fr_asprint(request, (char const *)c->key, c->key_len, '\"');
377  RDEBUG3("EXPIREAT \"%s\" %li", p, (long)c->expires);
378  talloc_free(p);
379  }
380  if (redisAppendCommand(conn->handle, "EXPIREAT %b %i", c->key,
381  c->key_len, c->expires) != REDIS_OK) goto append_error;
382  pipelined++;
383  RDEBUG3("EXEC");
384  if (redisAppendCommand(conn->handle, "EXEC") != REDIS_OK) goto append_error;
385  pipelined++;
386  }
387  REXDENT();
388 
389  reply_num = fr_redis_pipeline_result(&status, replies, sizeof(replies) / sizeof(*replies),
390  conn, pipelined);
391  reply = replies[0];
392  }
393  talloc_free(pool);
394 
395  if (s_ret != REDIS_RCODE_SUCCESS) {
396  RERROR("Failed inserting entry");
397  return CACHE_ERROR;
398  }
399 
400  RDEBUG3("Command results");
401  RINDENT();
402  for (i = 0; i < reply_num; i++) {
403  fr_redis_reply_print(L_DBG_LVL_3, replies[i], request, i);
404  fr_redis_reply_free(replies[i]);
405  }
406  REXDENT();
407 
408  return CACHE_OK;
409 }
410 
411 /** Call delete the cache entry from redis
412  *
413  * @copydetails cache_entry_expire_t
414  */
415 static cache_status_t cache_entry_expire(UNUSED rlm_cache_config_t const *config, void *driver_inst,
416  REQUEST *request, UNUSED void *handle, uint8_t const *key, size_t key_len)
417 {
418  rlm_cache_redis_t *driver = driver_inst;
420  fr_redis_conn_t *conn;
421  fr_redis_rcode_t status;
422  redisReply *reply = NULL;
423  int s_ret;
424 
425  for (s_ret = fr_redis_cluster_state_init(&state, &conn, driver->cluster, request, key, key_len, false);
426  s_ret == REDIS_RCODE_TRY_AGAIN; /* Continue */
427  s_ret = fr_redis_cluster_state_next(&state, &conn, driver->cluster, request, status, &reply)) {
428  reply = redisCommand(conn->handle, "DEL %b", key, key_len);
429  status = fr_redis_command_status(conn, reply);
430  }
431  if (s_ret != REDIS_RCODE_SUCCESS) {
432  RERROR("Failed expiring entry");
433  fr_redis_reply_free(reply);
434  return CACHE_ERROR;
435  }
436 
437  rad_assert(reply); /* clang scan */
438  if (reply->type == REDIS_REPLY_INTEGER) {
439  fr_redis_reply_free(reply);
440  if (reply->integer) return CACHE_OK; /* Affected */
441  return CACHE_MISS;
442  }
443 
444  REDEBUG("Bad result type, expected integer, got %s",
445  fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>"));
446  fr_redis_reply_free(reply);
447 
448  return CACHE_ERROR;
449 }
450 
452 cache_driver_t rlm_cache_redis = {
453  .name = "rlm_cache_redis",
454  .instantiate = mod_instantiate,
455  .inst_size = sizeof(rlm_cache_redis_t),
456  .free = cache_entry_free,
457 
458  .find = cache_entry_find,
459  .insert = cache_entry_insert,
460  .expire = cache_entry_expire,
461 };
Definition: rlm_cache.h:76
cache_driver_t rlm_cache_redis
fr_redis_conf_t conf
Connection parameters for the Redis server.
#define RINDENT()
Indent R* messages by one level.
Definition: log.h:265
#define RERROR(fmt,...)
Definition: log.h:207
#define RDEBUG_ENABLED3
True if request debug level 1-3 messages are enabled.
Definition: log.h:239
Fatal error.
Definition: rlm_cache.h:37
Cache entry notfound.
Definition: rlm_cache.h:39
struct vp_map vp_map_t
Value pair map.
Configuration parameters for a redis connection.
Definition: redis.h:88
vp_tmpl_t * lhs
Typically describes the attribute to add, modify or compare.
Definition: map.h:47
FR_NAME_NUMBER const redis_reply_types[]
Definition: redis.c:29
3rd highest priority debug messages (-xxx | -Xx).
Definition: log.h:53
vp_tmpl_t * rhs
Typically describes a literal value or a src attribute to copy or compare.
Definition: map.h:48
#define UNUSED
Definition: libradius.h:134
#define CONF_PARSER_TERMINATOR
Definition: conffile.h:289
PUBLIC int snprintf(char *string, size_t length, char *format, va_alist)
Definition: snprintf.c:686
#define fr_redis_reply_free(_p)
Wrap freeReplyObject so we consistently check for NULL pointers.
Definition: redis.h:56
static expr_map_t map[]
Definition: rlm_expr.c:169
A redis cluster.
Definition: cluster.c:254
cache_status_t
Definition: rlm_cache.h:35
void fr_redis_reply_print(log_lvl_t lvl, redisReply *reply, REQUEST *request, int idx)
Print the response data in a useful treelike form.
Definition: redis.c:139
Defines a CONF_PAIR to C data type mapping.
Definition: conffile.h:267
struct vp_map * next
The next valuepair map.
Definition: map.h:55
int fr_redis_reply_to_map(TALLOC_CTX *ctx, vp_map_t **out, REQUEST *request, redisReply *key, redisReply *op, redisReply *value)
Convert a pair of redis reply objects to a map.
Definition: redis.c:282
static cache_status_t cache_entry_insert(UNUSED rlm_cache_config_t const *config, void *driver_inst, REQUEST *request, UNUSED void *handle, const rlm_cache_entry_t *c)
Insert a new entry into the data store.
static cache_status_t cache_entry_find(rlm_cache_entry_t **out, UNUSED rlm_cache_config_t const *config, void *driver_inst, REQUEST *request, UNUSED void *handle, uint8_t const *key, size_t key_len)
Locate a cache entry in redis.
size_t key_len
Length of key data.
Definition: rlm_cache.h:78
#define rad_assert(expr)
Definition: rad_assert.h:38
redisContext * handle
Hiredis context used when issuing commands.
Definition: redis.h:81
time_t expires
When the entry expires.
Definition: rlm_cache.h:81
Value in native format.
Definition: tmpl.h:138
#define REDIS_COMMON_CONFIG
Definition: redis.h:106
ssize_t tmpl_from_attr_str(vp_tmpl_t *vpt, char const *name, request_refs_t request_def, pair_lists_t list_def, bool allow_unknown, bool allow_undefined)
Parse a string into a TMPL_TYPE_ATTR_* or TMPL_TYPE_LIST type vp_tmpl_t.
Definition: tmpl.c:877
void fr_redis_version_print(void)
Print the version of libhiredis the server was built against.
Definition: redis.c:52
int cf_section_parse(CONF_SECTION *, void *base, CONF_PARSER const *variables)
Parse a configuration section into user-supplied variables.
Definition: conffile.c:2234
Attributes in incoming or internally proxied request.
Definition: tmpl.h:82
fr_redis_rcode_t fr_redis_command_status(fr_redis_conn_t *conn, redisReply *reply)
Check the reply for errors.
Definition: redis.c:76
fr_redis_cluster_t * cluster
Redis connection sequence state.
Definition: cluster.h:43
static int mod_instantiate(CONF_SECTION *conf, rlm_cache_config_t const *config, void *driver_inst)
Create a new rlm_cache_redis instance.
#define REXDENT()
Exdent (unindent) R* messages by one level.
Definition: log.h:272
The current request.
Definition: tmpl.h:113
Definition: token.h:45
static rs_t * conf
Definition: radsniff.c:46
struct rlm_cache_redis rlm_cache_redis_t
time_t created
When the entry was created.
Definition: rlm_cache.h:80
static void cache_entry_free(rlm_cache_entry_t *c)
vp_tmpl_t * tmpl_init(vp_tmpl_t *vpt, tmpl_type_t type, char const *name, ssize_t len, FR_TOKEN quote)
Initialise stack allocated vp_tmpl_t.
Definition: tmpl.c:497
unsigned int state
Definition: proto_bfd.c:200
fr_redis_rcode_t fr_redis_cluster_state_next(fr_redis_cluster_state_t *state, fr_redis_conn_t **conn, fr_redis_cluster_t *cluster, REQUEST *request, fr_redis_rcode_t status, redisReply **reply)
Get the next connection to attempt a command against.
Definition: cluster.c:1714
32 Bit Unix timestamp.
Definition: radius.h:36
FR_TOKEN op
The operator that controls insertion of the dst attribute.
Definition: map.h:50
Cache entry found/updated.
Definition: rlm_cache.h:38
int fr_redis_tuple_from_map(TALLOC_CTX *pool, char const *out[], size_t out_len[], vp_map_t *map)
Add a single map pair to an existing command string as three elements.
Definition: redis.c:381
char const * name
Name of xlat function to register.
Definition: rlm_cache.h:48
char * fr_asprint(TALLOC_CTX *ctx, char const *in, ssize_t inlen, char quote)
Escape string that may contain binary data, and write it to a new buffer.
Definition: print.c:390
Try the operation again.
Definition: redis.h:70
vp_tmpl_t created_attr
LHS of the Cache-Created map.
fr_redis_cluster_t * fr_redis_cluster_alloc(TALLOC_CTX *ctx, CONF_SECTION *module, fr_redis_conf_t *conf)
Allocate and initialise a new cluster structure.
Definition: cluster.c:2059
#define REDEBUG(fmt,...)
Definition: log.h:254
fr_redis_rcode_t fr_redis_pipeline_result(fr_redis_rcode_t *rcode, redisReply *out[], size_t out_len, fr_redis_conn_t *conn, int pipelined)
Simplifies handling of pipelined commands with Redis cluster.
Definition: redis.c:460
fr_redis_rcode_t
Codes are ordered inversely by priority.
Definition: redis.h:67
static cache_status_t cache_entry_expire(UNUSED rlm_cache_config_t const *config, void *driver_inst, REQUEST *request, UNUSED void *handle, uint8_t const *key, size_t key_len)
Call delete the cache entry from redis.
fr_redis_rcode_t fr_redis_cluster_state_init(fr_redis_cluster_state_t *state, fr_redis_conn_t **conn, fr_redis_cluster_t *cluster, REQUEST *request, uint8_t const *key, size_t key_len, bool read_only)
Resolve a key to a pool, and reserve a connection in that pool.
Definition: cluster.c:1594
char const * fr_int2str(FR_NAME_NUMBER const *table, int number, char const *def)
Definition: token.c:506
static CONF_PARSER driver_config[]
vp_map_t * maps
Head of the maps list.
Definition: rlm_cache.h:83
char const * name
Driver name.
Definition: rlm_cache.h:279
Operation was successfull.
Definition: redis.h:68
uint8_t const * key
Key used to identify entry.
Definition: rlm_cache.h:77
Value pair map.
Definition: map.h:46
A source or sink of value data.
Definition: tmpl.h:187
#define ERROR(fmt,...)
Definition: log.h:145
Connection handle, holding a redis context.
Definition: redis.h:80
vp_tmpl_t expires_attr
LHS of the Cache-Expires map.
#define RDEBUG3(fmt,...)
Definition: log.h:245
Configuration for the rlm_cache module.
Definition: rlm_cache.h:47