/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with this * work for additional information regarding copyright ownership. The ASF * licenses this file to You under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #include "mod_cache.h" #include "apr_strings.h" #include "apr_base64.h" #include "apr_md5.h" #include "apr_memcache.h" #include "mod_cache.h" #include "ap_provider.h" #include "ap_mpm.h" #define ME "Memcached" #ifndef MEMCACHED_MIN_SIZE #define MEMCACHED_MIN_SIZE (0 * 1024) #endif #ifndef MEMCACHED_MAX_SIZE #define MEMCACHED_MAX_SIZE (0 * 1024) #endif #ifndef MC_DEFAULT_SERVER_PORT #define MC_DEFAULT_SERVER_PORT (11211) #endif #ifndef MC_DEFAULT_SERVER_MIN #define MC_DEFAULT_SERVER_MIN (0) #endif #ifndef MC_DEFAULT_SERVER_SMAX #define MC_DEFAULT_SERVER_SMAX (1) #endif #ifndef MC_DEFAULT_SERVER_MAX #define MC_DEFAULT_SERVER_MAX (0) #endif #ifndef MC_DEFAULT_SERVER_TTL #define MC_DEFAULT_SERVER_TTL (600) #endif #ifndef MC_DEFAULT_SERVER_BLANKOUT #define MC_DEFAULT_SERVER_BLANKOUT (0) #endif module AP_MODULE_DECLARE_DATA memcached_cache_module; #define rvcheck(r,w,x) { \ if (rv != APR_SUCCESS) { \ ap_log_error(APLOG_MARK, APLOG_ERR, rv, (r)->server, \ ME ": Failed to %s for %s during %s", (w), h->cache_obj->key,(x)); \ return rv; \ }; \ } typedef struct { apr_array_header_t *servers; int minSize, maxSize, min, max, smax, ttl, blankout; apr_memcache_t *memctx; } mc_conf_t; typedef struct { int status; apr_time_t date, expire, request_time, response_time; } info_glob_t; typedef struct _cache_object { mc_conf_t *sconf; server_rec *server; /* XXX we're only doing this to let a ap_log * below NOTICE; which otherwise get * surpresed at s==null */ char * bname, * hname; apr_bucket_brigade *body_bb; /* tmp private copy of the whole * output string */ apr_size_t body_len; } mc_cache_object_t; static void * _create_config(apr_pool_t * p, server_rec * s) { mc_conf_t *sconf = apr_pcalloc(p, sizeof(mc_conf_t)); sconf->servers = apr_array_make(p, 10, sizeof(const char *)); sconf->minSize = MEMCACHED_MIN_SIZE; sconf->maxSize = MEMCACHED_MAX_SIZE; sconf->ttl = MC_DEFAULT_SERVER_TTL; sconf->min = MC_DEFAULT_SERVER_MIN; sconf->smax = MC_DEFAULT_SERVER_SMAX; sconf->max = MC_DEFAULT_SERVER_MAX; sconf->blankout = MC_DEFAULT_SERVER_BLANKOUT; sconf->memctx = NULL; /* initialized on post init - so we also have * a quick 'opt out' detection when not * configured. */ return sconf; }; /* These functions are pretty much identical to their ap_set_slot variants * EXCEPT for that they fill out the module_config rather than use the * struct_ptr which points into a directory config (and is 0x0 for RSRC_CONF * directives). One day we should add the module info to cmd_param->command * as to let this work for both types of directives. */ static const char * _set_int_slot(cmd_parms * cmd, void *struct_ptr, const char *arg) { mc_conf_t *sconf = ap_get_module_config(cmd->server->module_config, &memcached_cache_module); int offset = (int) (long) cmd->info; char *error_str = NULL; char *endptr; *(int *) ((char *) sconf + offset) = strtol(arg, &endptr, 10); if ((*arg == '\0') || (*endptr != '\0')) { error_str = apr_psprintf(cmd->pool, "Invalid value for directive %s, expected integer", cmd->directive->directive); } return error_str; }; static const char * _add_array_slot(cmd_parms * cmd, void *struct_ptr, const char *string) { mc_conf_t *sconf = ap_get_module_config(cmd->server->module_config, &memcached_cache_module); int offset = (int) (long) cmd->info; apr_array_header_t **arrayp; arrayp = (apr_array_header_t **) ((char *) sconf + offset); *(const char **) (apr_array_push(*arrayp)) = string; return NULL; }; #ifndef _ #define _(x) #x #endif static const command_rec _cmds[] = { AP_INIT_ITERATE("MemcachedServerList", _add_array_slot, (void *) APR_OFFSETOF(mc_conf_t, servers), RSRC_CONF, "Space separate list of memcached servers with optional colon " "separated port numbers (default port is " _(MC_DEFAULT_SERVER_PORT) ")."), AP_INIT_TAKE1("MemcachedMinObjectSize", _set_int_slot, (void *) APR_OFFSETOF(mc_conf_t, minSize), RSRC_CONF, "The minimum size (in bytes) of an object to be placed in the cache, " "0 implies no lower limit (default " _(MEMCACHED_MIN_SIZE) ")."), AP_INIT_TAKE1("MemcachedMaxObjectSize", _set_int_slot, (void *) APR_OFFSETOF(mc_conf_t, maxSize), RSRC_CONF, "The maximum size (in bytes) of an object to be placed in the cache, " "0 implies no upper limit (default " _(MEMCACHED_MAX_SIZE) ")."), AP_INIT_TAKE1("MemcachedMinServers", _set_int_slot, (void *) APR_OFFSETOF(mc_conf_t, min), RSRC_CONF, "Minimal number of memcached to keep open at all times" "0 implies no upper limit (default " _(MC_DEFAULT_SERVER_MIN) ")."), AP_INIT_TAKE1("MemcachedSoftMaxServers", _set_int_slot, (void *) APR_OFFSETOF(mc_conf_t, smax), RSRC_CONF, "Max number of connections which should be kept open, " "0 implies no upper limit (default " _(MC_DEFAULT_SERVER_SMAX) ")."), AP_INIT_TAKE1("MemcachedMaxServers", _set_int_slot, (void *) APR_OFFSETOF(mc_conf_t, max), RSRC_CONF, "Max number of connections which can be kept open; equal or smaller than the systems thread limit, " "0 implies no upper limit (default " _(MC_DEFAULT_SERVER_MAX) ")."), AP_INIT_TAKE1("MemcachedSoftExceededTTL", _set_int_slot, (void *) APR_OFFSETOF(mc_conf_t, ttl), RSRC_CONF, "Maximum amount of time (in seconds) that a connection may be kept open when exceeding the soft limit, " "0 implies no upper limit (default " _(MC_DEFAULT_SERVER_TTL) ")."), AP_INIT_TAKE1("MemcachedBlankout", _set_int_slot, (void *) APR_OFFSETOF(mc_conf_t, blankout), RSRC_CONF, "Time (in seconds) that a delete sticks - and during which other set/add's are ignored, " "0 implies no upper limit (default " _(MC_DEFAULT_SERVER_BLANKOUT) ")."), {NULL} }; static int _post_config(apr_pool_t * p, apr_pool_t * plog, apr_pool_t * ptemp, server_rec * s) { mc_conf_t *sconf = ap_get_module_config(s->module_config, &memcached_cache_module); apr_array_header_t *arr = sconf->servers; int i, nservers; apr_memcache_server_t *st; apr_status_t rv; int thread_limit = 0; nservers = arr->nelts; ap_mpm_query(AP_MPMQ_HARD_LIMIT_THREADS, &thread_limit); if (sconf->max) thread_limit = (sconf->max) > thread_limit ? thread_limit : sconf->max; if (thread_limit < sconf->max) { ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s, ME ": MemcachedMaxServers value (%d) capped to the MPM imposed thread limit of %d.", sconf->max, thread_limit); sconf->max = thread_limit; }; if (sconf->smax > sconf->max) { sconf->smax = sconf->max; ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, ME ": MemcachedSoftMaxServers value capped to MemcachedMaxServers (%d)", sconf->smax); } if (sconf->min > sconf->smax) { sconf->min = sconf->smax; ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, ME ": MemcachedMinServers value capped to MemcachedSoftMaxServers (%d)", sconf->min); } if (nservers == 0) { ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, ME ": no memcached servers specified (MemcachedServerList missing?)"); return -1; } rv = apr_memcache_create(p, nservers, 0, &(sconf->memctx)); if (rv != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, ME ": Failed to create Memcached for '%d' servers.", nservers); return -1; } for (i = 0; i < arr->nelts; i++) { int port = MC_DEFAULT_SERVER_PORT; char *q, *server = apr_pstrdup(p, ((const char **) arr->elts)[i]); if ((q = rindex(server, ':'))) { *q++ = '\0'; port = atoi(q); }; rv = apr_memcache_server_create(p, server, port, sconf->min, sconf->smax, thread_limit, sconf->ttl, &st); if (rv != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, ME ": Failed to create memcached: %s:%d", server, port); return -1; } rv = apr_memcache_add_server(sconf->memctx, st); if (rv != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, ME ": Failed to Add Server: %s:%d", server, port); return -1; } ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, "Memcache server <%s:%d> configured and " "added to the pool as # %d", server, port, i); } ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, ME ": %d Memcached server%s in the pool " "(connections min %d < soft max %d (for up to ttl %d sec) < hard max %d)", nservers, (nservers == 1) ? "" : "s", sconf->min, sconf->smax, sconf->ttl, thread_limit); return OK; } static cache_object_t * _make_cache_object(apr_pool_t * pool, mc_conf_t * sconf, server_rec * s, const char *key) { unsigned char digest[APR_MD5_DIGESTSIZE]; char tmp[22 + 2 + 1]; /* Allocate and initialize cache_object_t and my cache_object_t */ cache_object_t *obj = apr_pcalloc(pool, sizeof(*obj)); mc_cache_object_t *mc_obj; obj->key = apr_pstrdup(pool, key); obj->vobj = mc_obj = apr_pcalloc(pool, sizeof(*mc_obj)); mc_obj->sconf = sconf; mc_obj->server = s; apr_md5(digest, key, strlen(key)); apr_base64_encode(tmp, (char *)digest, APR_MD5_DIGESTSIZE); /* pre-create two keys - one for the header and one for the body. */ mc_obj->hname = apr_pstrcat(pool,"H_",tmp,NULL); mc_obj->bname = apr_pstrcat(pool,"B_",tmp,NULL); return obj; } static int _create_entity(cache_handle_t * h, request_rec * r, const char *key, apr_off_t len) { mc_conf_t *sconf = ap_get_module_config(r->server->module_config, &memcached_cache_module); mc_cache_object_t *mc_obj; if (sconf->memctx == NULL) { ap_log_error(APLOG_MARK, APLOG_WARNING, 0, r->server, ME ": CacheEnable memcached '%s' active " "but no memcached servers specified (Missing MemcachedServerList?)", r->uri); return DECLINED;/* we're not configured */ } /* Early cut-off if we already have a len set (len=-1 when not) */ if ((len > sconf->maxSize) && (sconf->maxSize)) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, ME ": URL %s failed the size check (too large, %" APR_OFF_T_FMT " > %d)", key, len, sconf->maxSize); return DECLINED; }; if (len >= 0 && len < sconf->minSize) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, ME ": URL %s failed the size check (too small) " "(%" APR_OFF_T_FMT " < %d)", key, len, sconf->minSize); return DECLINED; } h->cache_obj = _make_cache_object(r->pool, sconf, r->server, key); mc_obj = (mc_cache_object_t *) h->cache_obj->vobj; mc_obj->body_bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); mc_obj->body_len = 0; ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, ME ": created a cache object for %s, %s/%s", key, mc_obj->hname, mc_obj->bname); return OK; } static int _open_entity(cache_handle_t * h, request_rec * r, const char *key) { mc_conf_t *sconf = ap_get_module_config(r->server->module_config, &memcached_cache_module); mc_cache_object_t *mc_obj; if (sconf->memctx == NULL) { ap_log_error(APLOG_MARK, APLOG_WARNING, 0, r->server, ME ": CacheEnable memcached '%s' active " "but no memcached servers specified (Missing MemcachedServerList?)", r->uri); return DECLINED;/* we're not configured */ } h->cache_obj = _make_cache_object(r->pool, sconf, r->server, key); mc_obj = (mc_cache_object_t *) h->cache_obj->vobj; mc_obj->body_bb = NULL; mc_obj->body_len = 0; ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, ME ": opened a cache object for %s, %s/%s", key, mc_obj->hname, mc_obj->bname); return OK; } static apr_status_t _serialize_table(apr_bucket_brigade * bb, apr_table_t * table) { apr_table_entry_t *elts; apr_status_t rv; int i; if (table) { elts = (apr_table_entry_t *) apr_table_elts(table)->elts; for (i = 0; i < apr_table_elts(table)->nelts; ++i) { rv = apr_brigade_puts(bb, NULL, NULL, elts[i].key); if (rv != APR_SUCCESS) return rv; rv = apr_brigade_putc(bb, NULL, NULL, 0); if (rv != APR_SUCCESS) return rv; rv = apr_brigade_puts(bb, NULL, NULL, elts[i].val); if (rv != APR_SUCCESS) return rv; rv = apr_brigade_putc(bb, NULL, NULL, 0); if (rv != APR_SUCCESS) return rv; }; }; return apr_brigade_putc(bb, NULL, NULL, 0); } static apr_size_t _deserialize_table(apr_pool_t * pool, apr_table_t ** tablep, char *in, apr_size_t at, apr_size_t len) { int j = at; if (*tablep == NULL) *tablep = apr_table_make(pool, 20); while ((j < len) && in[j]) { char *key = in + j; j += strlen(key) + 1; char *val = in + j; j += strlen(val) + 1; apr_table_add(*tablep, key, val); } return j + 1; } static apr_status_t _store_headers(cache_handle_t * h, request_rec * r, cache_info * info) { cache_object_t *obj = h->cache_obj; mc_cache_object_t *mc_obj = (mc_cache_object_t *) obj->vobj; apr_table_t *headers_out = NULL, *headers_in = NULL; char *buff; apr_size_t len; apr_status_t rv; info_glob_t glob; if (r->headers_out) { headers_out = ap_cache_cacheable_hdrs_out(r); }; if (r->headers_in) { headers_in = ap_cache_cacheable_hdrs_in(r); }; /* and concatenate a binary glob with the key info data */ glob.status = htonl(info->status); glob.date = htonl(info->date); glob.expire = htonl(info->expire); glob.request_time = htonl(info->request_time); glob.response_time = htonl(info->response_time); apr_bucket_brigade *bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); rv = _serialize_table(bb, headers_out); rvcheck(r,"store headers", "serialization input headers"); rv = _serialize_table(bb, headers_in); rvcheck(r,"store headers", "serialization output headers"); rv = apr_brigade_write(bb, NULL, NULL, (char *) &glob, sizeof(glob)); rvcheck(r,"store headers", "brigade creation"); APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_eos_create(r->connection->bucket_alloc)); rv = apr_brigade_pflatten(bb, &buff, &len, r->pool); rvcheck(r,"store headers", "flatting of the brigade"); rv = apr_memcache_set(mc_obj->sconf->memctx, mc_obj->hname, buff, len, 0, 0); rvcheck(r,"store headers", "writing to memcached"); ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, ME ": Stored %" APR_SIZE_T_FMT " bytes of Header for URL %s, cached as %s.", len, h->cache_obj->key, mc_obj->hname); return APR_SUCCESS; } static apr_status_t _store_body(cache_handle_t * h, request_rec * r, apr_bucket_brigade * bb) { apr_bucket *e; apr_status_t rv; mc_cache_object_t *mc_obj = (mc_cache_object_t *) h->cache_obj->vobj; mc_conf_t *sconf = ap_get_module_config(r->server->module_config, &memcached_cache_module); if (sconf->memctx == NULL) { return APR_EGENERAL; /* we're not configured */ } if (mc_obj->body_bb == 0) abort(); /* * old approach of re-alloc pool avoided having 3 copies of same. * Not sure if this is better. */ for (e = APR_BRIGADE_FIRST(bb); e != APR_BRIGADE_SENTINEL(bb); e = APR_BUCKET_NEXT(e)) { apr_bucket *cpy; mc_obj->body_len += e->length; if ((sconf->maxSize) && (mc_obj->body_len > sconf->maxSize)) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, ME ":Not caching URL %s, too large", h->cache_obj->key); return APR_EGENERAL; }; rv = apr_bucket_copy(e, &cpy); if (rv == APR_ENOTIMPL) { const char *str; apr_size_t len; /* This takes care of uncopyable buckets. */ rv = apr_bucket_read(e, &str, &len, APR_BLOCK_READ); if (rv == APR_SUCCESS) { rv = apr_bucket_copy(e, &cpy); }; }; /* trap errors from both bucky copyes and reads */ if (rv != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_ERR, rv, r->server, ME ":Failed to copy bucket for %s", h->cache_obj->key); return rv; } APR_BRIGADE_INSERT_TAIL(mc_obj->body_bb, cpy); } /* * Was this the final bucket? If yes, try to store.. */ if (APR_BUCKET_IS_EOS(APR_BRIGADE_LAST(bb))) { char *buff; apr_size_t len; if (r->connection->aborted || r->no_cache) { ap_log_error(APLOG_MARK, APLOG_INFO, 0, r->server, ME ": Discarding body for URL %s " "because connection has been aborted.", h->cache_obj->key); return APR_EGENERAL; } if (mc_obj->body_len < sconf->minSize) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, ME ": URL %s failed the size check - really too small " "(%" APR_SIZE_T_FMT "<%d)", h->cache_obj->key, mc_obj->body_len, sconf->minSize); return APR_EGENERAL; } if ((sconf->maxSize) && (mc_obj->body_len > sconf->maxSize)) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, ME ": URL %s failed the size check - really too large " "(%" APR_SIZE_T_FMT "<%d)", h->cache_obj->key, mc_obj->body_len, sconf->maxSize); return APR_EGENERAL; } rv = apr_brigade_pflatten(bb, &buff, &len, r->pool); rvcheck(r,"store body", "flatten the body"); #if 0 apr_brigade_destroy(bb); /* rinse early - as may well * be very large */ #endif rv = apr_memcache_set(sconf->memctx, mc_obj->bname, buff, len, 0, 0); rvcheck(r,"store body", "storing into memcachd"); ap_log_error(APLOG_MARK, APLOG_INFO, 0, r->server, ME ": Stored %" APR_SIZE_T_FMT " bytes of Body for URL %s, cached as %s.", mc_obj->body_len, h->cache_obj->key, mc_obj->bname); } return APR_SUCCESS; } static int _remove_entity(cache_handle_t * h) { /* * Null out the cache object pointer so next time we start from * scratch */ mc_cache_object_t *mc_obj = (mc_cache_object_t *) h->cache_obj->vobj; h->cache_obj = NULL; ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, mc_obj->server, ME ": removed a cache object"); return OK; } static int _remove_url(cache_handle_t * h, apr_pool_t * p) { apr_status_t rv; mc_cache_object_t *mc_obj = (mc_cache_object_t *) h->cache_obj->vobj; if (!mc_obj || !mc_obj->sconf || !mc_obj->sconf->memctx) return DECLINED; rv = apr_memcache_delete(mc_obj->sconf->memctx, mc_obj->hname, mc_obj->sconf->blankout); rvcheck(mc_obj,"delete headers","remove url"); rv = apr_memcache_delete(mc_obj->sconf->memctx, mc_obj->bname, mc_obj->sconf->blankout); rvcheck(mc_obj,"delete body","remove url"); ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, mc_obj->server, ME ": removed a cache url"); return OK; } /* * @@@: XXX: FIXME: currently the headers are passed thru un-merged. Is that * okay, or should they be collapsed where possible? */ static apr_status_t _recall_headers(cache_handle_t * h, request_rec * r) { mc_cache_object_t *mc_obj = (mc_cache_object_t *) h->cache_obj->vobj; apr_size_t len, at; char *buff; info_glob_t glob; apr_status_t rv; static apr_uint16_t flags = 0; /* This case should not happen... */ if (!mc_obj || !mc_obj->sconf || !mc_obj->sconf->memctx) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, ME ": in _recall_headers() - but no private object (bug)"); return APR_NOTFOUND; }; h->req_hdrs = NULL; h->resp_hdrs = NULL; rv = apr_memcache_getp(mc_obj->sconf->memctx, r->pool, mc_obj->hname, &buff, &len, &flags); if (rv != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, ME ": header cache miss for %s", mc_obj->hname); return APR_NOTFOUND; } at = _deserialize_table(r->pool, &(h->resp_hdrs), buff, 0, len); at = _deserialize_table(r->pool, &(h->req_hdrs), buff, at, len); /* * XXX we propably want to rewrite this as ascii and/or combine with * open/create */ if (len - at != sizeof(info_glob_t)) { ap_log_error(APLOG_MARK, APLOG_CRIT, 0, r->server, "glob lost!"); return APR_NOTFOUND; }; memcpy((void *) &glob, buff + at, sizeof(glob)); h->cache_obj->info.status = ntohl(glob.status); h->cache_obj->info.date = ntohl(glob.date); h->cache_obj->info.expire = ntohl(glob.expire); h->cache_obj->info.request_time = ntohl(glob.request_time); h->cache_obj->info.response_time = ntohl(glob.response_time); ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, ME ": Recalled %" APR_SIZE_T_FMT " bytes of headers " "for URL %s from key %s", len, h->cache_obj->key, mc_obj->hname); return APR_SUCCESS; } static apr_status_t _recall_body(cache_handle_t * h, apr_pool_t * p, apr_bucket_brigade * bb) { char *buff; mc_cache_object_t *mc_obj = (mc_cache_object_t *) h->cache_obj->vobj; apr_size_t len; apr_bucket *e; apr_status_t rv; static apr_uint16_t flags = 0; /* This case should not happen... */ if (!mc_obj || !mc_obj->sconf || !mc_obj->sconf->memctx) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, mc_obj->server, ME ": in _recall_body() - but no private object (bug)"); return APR_NOTFOUND; }; rv = apr_memcache_getp(mc_obj->sconf->memctx, p, mc_obj->bname, &buff, &len, &flags); if (rv != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, mc_obj->server, ME ": body cache miss for %s", mc_obj->bname); return APR_NOTFOUND; }; e = apr_bucket_immortal_create(buff, len, bb->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, e); e = apr_bucket_eos_create(bb->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, e); /* * XXX this is the only reason we have ->server in the struct (DEBUG * level on a NULL is not logged as to supress 'startup' messages - * see log.c */ ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, mc_obj->server, ME ": Recalled %" APR_SIZE_T_FMT " bytes of body for URL %s from key %s", len, h->cache_obj->key, mc_obj->bname); return APR_SUCCESS; } static const cache_provider _provider = { &_remove_entity, &_store_headers, &_store_body, &_recall_headers, &_recall_body, &_create_entity, &_open_entity, &_remove_url, }; static void _hooks(apr_pool_t * p) { ap_hook_post_config(_post_config, NULL, NULL, APR_HOOK_MIDDLE); ap_register_provider(p, CACHE_PROVIDER_GROUP, "memcached", "0", &_provider); } module AP_MODULE_DECLARE_DATA memcached_cache_module = { STANDARD20_MODULE_STUFF, NULL, /* create per-directory config structure */ NULL, /* merge per-directory config structures */ _create_config, /* create per-server config structure */ NULL, /* merge per-server config structures */ _cmds, /* command apr_table_t */ _hooks };