├── LICENSE ├── Makefile.am ├── README.md ├── autogen.sh ├── configure.ac ├── examples ├── example.vcl └── session_affinity.vcl ├── m4 └── PLACEHOLDER └── src ├── Makefile.am ├── vmod_redis.c └── vmod_redis.vcc /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 ZephirWorks 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | ACLOCAL_AMFLAGS = -I m4 2 | 3 | SUBDIRS = src 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vmod-redis 2 | ========== 3 | 4 | A Varnish module that allows sending commands to redis from the VCL. 5 | 6 | At this stage it is mostly a proof-of-concept; it has only received minimal 7 | testing and we have never used it in production. At the very minimum, it will 8 | slow down Varnish a fair amount (at least a few milliseconds per request, 9 | depending on how fast your network and your redis server are). 10 | 11 | So far the module builds and runs on FreeBSD--on other platforms, you are on your own (pull requests welcome). 12 | 13 | 14 | Functions and procedures 15 | ------------------------ 16 | 17 | *redis.init_redis(host, port, timeout_ms)* 18 | 19 | Use the redis server at the given _host_ and _port_ with a timeout 20 | of _timeout__ms_ milliseconds. 21 | If _port_ is less than or equal to zero, the default port of 6379 is used. 22 | If _timeout__ms_ is less than or equal to zero, a default timeout of 200ms is used. 23 | 24 | This function is supposed to be called from the Varnish subroutine _vcl__init_. 25 | If the call is left out, the module will attempt to connect to the Redis server 26 | at 127.0.0.1:6379 with a connect timeout of 200ms. 27 | 28 | *redis.send(command)* 29 | 30 | Sends the given _command_ to redis; the response will be ignored. 31 | 32 | *redis.call(command)* 33 | 34 | Sends the given _command_ to redis; any response will be returned as a string. 35 | 36 | Dependencies 37 | ------------ 38 | 39 | * hiredis (https://github.com/antirez/hiredis) 40 | 41 | Building 42 | -------- 43 | 44 | * ./autogen.sh 45 | * make 46 | * sudo make install 47 | 48 | Configuration 49 | ------------- 50 | 51 | See the _examples_ folder. 52 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | warn() { 4 | echo "WARNING: $@" 1>&2 5 | } 6 | 7 | case `uname -s` in 8 | Darwin) 9 | LIBTOOLIZE=glibtoolize 10 | ;; 11 | FreeBSD) 12 | LIBTOOLIZE=libtoolize 13 | ;; 14 | Linux) 15 | LIBTOOLIZE=libtoolize 16 | ;; 17 | SunOS) 18 | LIBTOOLIZE=libtoolize 19 | ;; 20 | *) 21 | warn "unrecognized platform:" `uname -s` 22 | LIBTOOLIZE=libtoolize 23 | esac 24 | 25 | automake_version=`automake --version | tr ' ' '\n' | egrep '^[0-9]\.[0-9a-z.-]+'` 26 | if [ -z "$automake_version" ] ; then 27 | warn "unable to determine automake version" 28 | else 29 | case $automake_version in 30 | 0.*|1.[0-8]|1.[0-8][.-]*) 31 | warn "automake ($automake_version) detected; 1.9 or newer recommended" 32 | ;; 33 | *) 34 | ;; 35 | esac 36 | fi 37 | 38 | set -ex 39 | 40 | aclocal -I m4 41 | $LIBTOOLIZE --copy --force 42 | autoheader 43 | automake --add-missing --copy --foreign 44 | autoconf 45 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | AC_PREREQ(2.59) 2 | AC_COPYRIGHT([Copyright (c) 2011 ZephirWorks]) 3 | AC_INIT([libvmod-redis], [0.1]) 4 | AC_CONFIG_MACRO_DIR([m4]) 5 | AC_CONFIG_SRCDIR(src/vmod_redis.vcc) 6 | AM_CONFIG_HEADER(config.h) 7 | 8 | AC_CANONICAL_SYSTEM 9 | AC_LANG(C) 10 | 11 | AM_INIT_AUTOMAKE([foreign]) 12 | 13 | AC_GNU_SOURCE 14 | AC_PROG_CC 15 | AC_PROG_CC_STDC 16 | if test "x$ac_cv_prog_cc_c99" = xno; then 17 | AC_MSG_ERROR([Could not find a C99 compatible compiler]) 18 | fi 19 | AC_PROG_CPP 20 | 21 | AC_PROG_INSTALL 22 | AC_PROG_LIBTOOL 23 | AC_PROG_MAKE_SET 24 | 25 | # Check for pkg-config 26 | PKG_PROG_PKG_CONFIG 27 | 28 | # Checks for header files. 29 | AC_HEADER_STDC 30 | AC_CHECK_HEADERS([sys/stdlib.h]) 31 | 32 | # Check for python 33 | AC_CHECK_PROGS(PYTHON, [python3 python3.1 python3.2 python2.7 python2.6 python2.5 python2 python], [AC_MSG_ERROR([Python is needed to build this vmod, please install python.])]) 34 | 35 | # Varnish source tree 36 | AC_ARG_VAR([VARNISHSRC], [path to Varnish source tree (mandatory)]) 37 | if test "x$VARNISHSRC" = x; then 38 | AC_MSG_ERROR([No Varnish source tree specified]) 39 | fi 40 | VARNISHSRC=`cd $VARNISHSRC && pwd` 41 | AC_CHECK_FILE([$VARNISHSRC/include/varnishapi.h], 42 | [], 43 | [AC_MSG_FAILURE(["$VARNISHSRC" is not a Varnish source directory])] 44 | ) 45 | 46 | # vmod installation dir 47 | AC_ARG_VAR([VMODDIR], [vmod installation directory @<:@LIBDIR/varnish/vmods@:>@]) 48 | if test "x$VMODDIR" = x; then 49 | VMODDIR=`pkg-config --variable=vmoddir varnishapi` 50 | if test "x$VMODDIR" = x; then 51 | AC_MSG_FAILURE([Can't determine vmod installation directory]) 52 | fi 53 | fi 54 | 55 | AC_CONFIG_FILES([ 56 | Makefile 57 | src/Makefile 58 | ]) 59 | AC_OUTPUT 60 | -------------------------------------------------------------------------------- /examples/example.vcl: -------------------------------------------------------------------------------- 1 | # 2 | # A trivial example to demonstrate how to use this vmod 3 | # 4 | 5 | import redis; 6 | 7 | backend be1 { 8 | .host = "192.168.0.1"; 9 | .port = "80"; 10 | } 11 | 12 | #sub vcl_init { 13 | # 14 | # By default, the redis module will attempt to connect to a Redis server 15 | # at 127.0.0.1:6379 with a connect timeout of 200 milliseconds. 16 | # 17 | # The function redis.init_redis(host, port, timeout_ms) may be used to 18 | # connect to an alternate Redis server or use a different connect timeout. 19 | # 20 | # redis.init_redis("localhost", 6379, 200); /* default values */ 21 | #} 22 | 23 | sub vcl_recv { 24 | # 25 | # redis.send is a procedure, it will send the command to redis and ignore 26 | # the response. If the command errors out, it will be logged but the VCL 27 | # will not know. 28 | # 29 | redis.send("LPUSH client " + client.ip); 30 | 31 | # 32 | # redis.call is a function that sends the command to redis and return the 33 | # return value as a string. 34 | # 35 | set req.http.x-redis = redis.call("LTRIM client 0 99"); 36 | } 37 | -------------------------------------------------------------------------------- /examples/session_affinity.vcl: -------------------------------------------------------------------------------- 1 | # 2 | # Cookie-based session-affinity: non-cached replies will go to a backend per 3 | # the normal rules, but we'll keep track of the backend that served the 4 | # request and make sure any future request from the same client will go to 5 | # the same backend. 6 | # 7 | 8 | import redis; 9 | import std; 10 | 11 | backend be1 { 12 | .host = "192.168.0.1"; 13 | .port = "80"; 14 | } 15 | 16 | backend be2 { 17 | .host = "192.168.0.2"; 18 | .port = "80"; 19 | } 20 | 21 | director default random { 22 | .retries = 5; 23 | { 24 | .backend = be1; 25 | .weight = 7; 26 | } 27 | { 28 | .backend = be2; 29 | .weight = 3; 30 | } 31 | } 32 | 33 | # 34 | # Compute the redis key from a request. 35 | # 36 | # In this example a cookie is used, but it could be anything distinctive enough; 37 | # IP address + User Agent + Accept* headers should work nicely for most purposes. 38 | # 39 | sub redis_key_from_req { 40 | if (req.http.Cookie) { 41 | set req.http.redis_key = req.http.Cookie; 42 | } 43 | } 44 | 45 | # 46 | # Compute the redis key from a response. 47 | # 48 | # We use the reposnse cookie if present, as it might be different. We still 49 | # have the request headers in case we want to use a different schema (see the 50 | # comment for redis_key_from_req). 51 | # 52 | # Another option is to have the backend send us an explicit header. 53 | # 54 | sub redis_key_from_beresp { 55 | if (beresp.http.Set_Cookie) { 56 | set req.http.redis_key = beresp.http.Set_Cookie; 57 | } else if (req.http.Cookie) { 58 | set req.http.redis_key = req.http.Cookie; 59 | } 60 | } 61 | 62 | # 63 | # Compute the backend that will serve further requests for this user. 64 | # 65 | # We just use the backend name, but we may as well use an explicit header 66 | # from the backend. This would enable other scenarios, e.g. a "master" app 67 | # server that creates sessions and then hands them off to other servers. 68 | # 69 | sub backend_from_beresp { 70 | set beresp.http.x_backend = beresp.backend.name; 71 | } 72 | 73 | # 74 | # Set a backend given the name. 75 | # 76 | # This would be a very useful addition to the language, or it could be 77 | # implemented as a vmod. 78 | # 79 | sub set_backend_by_name { 80 | if (req.http.x_backend) { 81 | std.log("backend will be: " + req.http.x_backend); 82 | 83 | if (req.http.x_backend == "be1") { 84 | set req.backend = be1; 85 | } else if (req.http.x_backend == "be2") { 86 | set req.backend = be2; 87 | } else { 88 | error 503 "No such backend " + req.http.x_backend; 89 | } 90 | } 91 | } 92 | 93 | # 94 | # This is the main subroutine; it takes care of finding whether we have an 95 | # entry in redis, and sets the corresponding backed. 96 | # 97 | sub select_backend { 98 | call redis_key_from_req; 99 | 100 | if (req.http.redis_key) { 101 | set req.http.x_backend = redis.call("HGET sessions " + req.http.redis_key); 102 | if (req.http.x_backend) { 103 | std.log("Found in redis"); 104 | call set_backend_by_name; 105 | } else { 106 | # we will let the director choose 107 | } 108 | } else { 109 | std.log("Cookie not found"); 110 | } 111 | } 112 | 113 | sub vcl_miss { 114 | call select_backend; 115 | } 116 | 117 | sub vcl_pass { 118 | call select_backend; 119 | } 120 | 121 | sub vcl_pipe { 122 | call select_backend; 123 | } 124 | 125 | # 126 | # IF we don't have an entry in redis, we just let the director (or other 127 | # mechanism) choose. When we get the response we need to figure key and 128 | # value and store them. 129 | # 130 | sub vcl_fetch { 131 | if (!req.http.x_backend) { 132 | call redis_key_from_beresp; 133 | call backend_from_beresp; 134 | 135 | if (req.http.redis_key) { 136 | std.log("Fetched from: " + beresp.http.x_backend + ", key=" + req.http.redis_key); 137 | redis.send("HSET sessions " + req.http.redis_key + " " + beresp.http.x_backend); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /m4/PLACEHOLDER: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zephirworks/libvmod-redis/43976752eb225efd6417ad67389f5037aef9f7b8/m4/PLACEHOLDER -------------------------------------------------------------------------------- /src/Makefile.am: -------------------------------------------------------------------------------- 1 | INCLUDES = -I$(VARNISHSRC)/include -I$(VARNISHSRC) -I/usr/local/include 2 | 3 | vmoddir = $(VMODDIR) 4 | vmod_LTLIBRARIES = libvmod_redis.la 5 | 6 | libvmod_redis_la_LDFLAGS = -module -export-dynamic -version-info 1:0:0 7 | libvmod_redis_la_LIBADD = -lhiredis 8 | 9 | libvmod_redis_la_SOURCES = \ 10 | vcc_if.c \ 11 | vcc_if.h \ 12 | vmod_redis.c 13 | 14 | vcc_if.c vcc_if.h: $(VARNISHSRC)/lib/libvmod_std/vmod.py $(top_srcdir)/src/vmod_redis.vcc 15 | @PYTHON@ $(VARNISHSRC)/lib/libvmod_std/vmod.py $(top_srcdir)/src/vmod_redis.vcc 16 | 17 | EXTRA_DIST = vmod_redis.vcc 18 | 19 | CLEANFILES = $(builddir)/vcc_if.c $(builddir)/vcc_if.h 20 | -------------------------------------------------------------------------------- /src/vmod_redis.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "vrt.h" 6 | #include "bin/varnishd/cache.h" 7 | 8 | #include "vcc_if.h" 9 | 10 | #include 11 | #include 12 | 13 | 14 | #define REDIS_TIMEOUT_MS 200 /* 200 milliseconds */ 15 | 16 | 17 | #define LOG_E(...) fprintf(stderr, __VA_ARGS__); 18 | #ifdef DEBUG 19 | # define LOG_T(...) fprintf(stderr, __VA_ARGS__); 20 | #else 21 | # define LOG_T(...) do {} while(0); 22 | #endif 23 | 24 | typedef struct redisConfig { 25 | char *host; 26 | int port; 27 | struct timeval timeout; 28 | } config_t; 29 | 30 | static pthread_key_t redis_key; 31 | static pthread_once_t redis_key_once = PTHREAD_ONCE_INIT; 32 | 33 | 34 | static void __match_proto__() 35 | vmod_log(struct sess *sp, const char *fmt, ...) 36 | { 37 | char buf[8192], *p; 38 | va_list ap; 39 | 40 | va_start(ap, fmt); 41 | p = VRT_StringList(buf, sizeof buf, fmt, ap); 42 | va_end(ap); 43 | if (p != NULL) 44 | WSP(sp, SLT_VCL_Log, "%s", buf); 45 | } 46 | 47 | static void 48 | make_key() 49 | { 50 | (void)pthread_key_create(&redis_key, NULL); 51 | } 52 | 53 | static config_t * 54 | make_config(const char *host, int port, int timeout_ms) 55 | { 56 | config_t *cfg; 57 | 58 | LOG_T("make_config(%s,%d,%d)\n", host, port, timeout_ms); 59 | 60 | cfg = malloc(sizeof(config_t)); 61 | if(cfg == NULL) 62 | return NULL; 63 | 64 | if(port <= 0) 65 | port = 6379; 66 | 67 | if(timeout_ms <= 0) 68 | timeout_ms = REDIS_TIMEOUT_MS; 69 | 70 | cfg->host = strdup(host); 71 | cfg->port = port; 72 | 73 | cfg->timeout.tv_sec = timeout_ms / 1000; 74 | cfg->timeout.tv_usec = (timeout_ms % 1000) * 1000; 75 | 76 | return cfg; 77 | } 78 | 79 | int 80 | init_function(struct vmod_priv *priv, const struct VCL_conf *conf) 81 | { 82 | config_t *cfg; 83 | 84 | LOG_T("redis init called\n"); 85 | 86 | (void)pthread_once(&redis_key_once, make_key); 87 | 88 | if (priv->priv == NULL) { 89 | priv->priv = make_config("127.0.0.1", 6379, REDIS_TIMEOUT_MS); 90 | priv->free = free; 91 | } 92 | 93 | return (0); 94 | } 95 | 96 | void 97 | vmod_init_redis(struct sess *sp, struct vmod_priv *priv, const char *host, int port, int timeout_ms) 98 | { 99 | config_t *old_cfg = priv->priv; 100 | 101 | priv->priv = make_config(host, port, timeout_ms); 102 | if(priv->priv && old_cfg) { 103 | free(old_cfg->host); 104 | free(old_cfg); 105 | } 106 | } 107 | 108 | static redisReply * 109 | redis_common(struct sess *sp, struct vmod_priv *priv, const char *command) 110 | { 111 | config_t *cfg = priv->priv; 112 | redisContext *c; 113 | redisReply *reply = NULL; 114 | 115 | LOG_T("redis(%x): running %s %p\n", pthread_self(), command, priv->priv); 116 | 117 | if ((c = pthread_getspecific(redis_key)) == NULL) { 118 | c = redisConnectWithTimeout(cfg->host, cfg->port, cfg->timeout); 119 | if (c->err) { 120 | LOG_E("redis error (connect): %s\n", c->errstr); 121 | } 122 | (void)pthread_setspecific(redis_key, c); 123 | } 124 | 125 | reply = redisCommand(c, command); 126 | if (reply == NULL && c->err == REDIS_ERR_EOF) { 127 | c = redisConnectWithTimeout(cfg->host, cfg->port, cfg->timeout); 128 | if (c->err) { 129 | LOG_E("redis error (reconnect): %s\n", c->errstr); 130 | redisFree(c); 131 | } else { 132 | redisFree(pthread_getspecific(redis_key)); 133 | (void)pthread_setspecific(redis_key, c); 134 | 135 | reply = redisCommand(c, command); 136 | } 137 | } 138 | if (reply == NULL) { 139 | LOG_E("redis error (command): err=%d errstr=%s\n", c->err, c->errstr); 140 | } 141 | 142 | return reply; 143 | } 144 | 145 | void 146 | vmod_send(struct sess *sp, struct vmod_priv *priv, const char *command) 147 | { 148 | redisReply *reply = redis_common(sp, priv, command); 149 | if (reply != NULL) { 150 | freeReplyObject(reply); 151 | } 152 | } 153 | 154 | const char * 155 | vmod_call(struct sess *sp, struct vmod_priv *priv, const char *command) 156 | { 157 | redisReply *reply = NULL; 158 | const char *ret = NULL; 159 | char *digits; 160 | 161 | reply = redis_common(sp, priv, command); 162 | if (reply == NULL) { 163 | goto done; 164 | } 165 | 166 | switch (reply->type) { 167 | case REDIS_REPLY_STATUS: 168 | ret = strdup(reply->str); 169 | break; 170 | case REDIS_REPLY_ERROR: 171 | ret = strdup(reply->str); 172 | break; 173 | case REDIS_REPLY_INTEGER: 174 | digits = malloc(21); /* sizeof(long long) == 8; 20 digits + NUL */ 175 | if(digits) 176 | sprintf(digits, "%lld", reply->integer); 177 | ret = digits; 178 | break; 179 | case REDIS_REPLY_NIL: 180 | ret = NULL; 181 | break; 182 | case REDIS_REPLY_STRING: 183 | ret = strdup(reply->str); 184 | break; 185 | case REDIS_REPLY_ARRAY: 186 | ret = strdup("array"); 187 | break; 188 | default: 189 | ret = strdup("unexpected"); 190 | } 191 | 192 | done: 193 | if (reply) { 194 | freeReplyObject(reply); 195 | } 196 | 197 | return ret; 198 | } 199 | -------------------------------------------------------------------------------- /src/vmod_redis.vcc: -------------------------------------------------------------------------------- 1 | Module redis 2 | Init init_function 3 | Function VOID init_redis(PRIV_VCL, STRING, INT, INT) 4 | Function VOID send(PRIV_VCL, STRING) 5 | Function STRING call(PRIV_VCL, STRING) 6 | --------------------------------------------------------------------------------