| /* |
| * cache-memcache.c: memcached caching for Subversion |
| * |
| * ==================================================================== |
| * 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 <apr_md5.h> |
| |
| #include "svn_pools.h" |
| #include "svn_base64.h" |
| #include "svn_path.h" |
| |
| #include "svn_private_config.h" |
| #include "private/svn_cache.h" |
| #include "private/svn_dep_compat.h" |
| |
| #include "cache.h" |
| |
| #ifdef SVN_HAVE_MEMCACHE |
| |
| #include <apr_memcache.h> |
| |
| /* A note on thread safety: |
| |
| The apr_memcache_t object does its own mutex handling, and nothing |
| else in memcache_t is ever modified, so this implementation should |
| be fully thread-safe. |
| */ |
| |
| /* The (internal) cache object. */ |
| typedef struct memcache_t { |
| /* The memcached server set we're using. */ |
| apr_memcache_t *memcache; |
| |
| /* A prefix used to differentiate our data from any other data in |
| * the memcached (URI-encoded). */ |
| const char *prefix; |
| |
| /* The size of the key: either a fixed number of bytes or |
| * APR_HASH_KEY_STRING. */ |
| apr_ssize_t klen; |
| |
| |
| /* Used to marshal values in and out of the cache. */ |
| svn_cache__serialize_func_t serialize_func; |
| svn_cache__deserialize_func_t deserialize_func; |
| } memcache_t; |
| |
| /* The wrapper around apr_memcache_t. */ |
| struct svn_memcache_t { |
| apr_memcache_t *c; |
| }; |
| |
| |
| /* The memcached protocol says the maximum key length is 250. Let's |
| just say 249, to be safe. */ |
| #define MAX_MEMCACHED_KEY_LEN 249 |
| #define MEMCACHED_KEY_UNHASHED_LEN (MAX_MEMCACHED_KEY_LEN - \ |
| 2 * APR_MD5_DIGESTSIZE) |
| |
| |
| /* Set *MC_KEY to a memcache key for the given key KEY for CACHE, allocated |
| in POOL. */ |
| static svn_error_t * |
| build_key(const char **mc_key, |
| memcache_t *cache, |
| const void *raw_key, |
| apr_pool_t *pool) |
| { |
| const char *encoded_suffix; |
| const char *long_key; |
| apr_size_t long_key_len; |
| |
| if (cache->klen == APR_HASH_KEY_STRING) |
| encoded_suffix = svn_path_uri_encode(raw_key, pool); |
| else |
| { |
| const svn_string_t *raw = svn_string_ncreate(raw_key, cache->klen, pool); |
| const svn_string_t *encoded = svn_base64_encode_string2(raw, FALSE, |
| pool); |
| encoded_suffix = encoded->data; |
| } |
| |
| long_key = apr_pstrcat(pool, "SVN:", cache->prefix, ":", encoded_suffix, |
| SVN_VA_NULL); |
| long_key_len = strlen(long_key); |
| |
| /* We don't want to have a key that's too big. If it was going to |
| be too big, we MD5 the entire string, then replace the last bit |
| with the checksum. Note that APR_MD5_DIGESTSIZE is for the pure |
| binary digest; we have to double that when we convert to hex. |
| |
| Every key we use will either be at most |
| MEMCACHED_KEY_UNHASHED_LEN bytes long, or be exactly |
| MAX_MEMCACHED_KEY_LEN bytes long. */ |
| if (long_key_len > MEMCACHED_KEY_UNHASHED_LEN) |
| { |
| svn_checksum_t *checksum; |
| SVN_ERR(svn_checksum(&checksum, svn_checksum_md5, long_key, long_key_len, |
| pool)); |
| |
| long_key = apr_pstrcat(pool, |
| apr_pstrmemdup(pool, long_key, |
| MEMCACHED_KEY_UNHASHED_LEN), |
| svn_checksum_to_cstring_display(checksum, pool), |
| SVN_VA_NULL); |
| } |
| |
| *mc_key = long_key; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Core functionality of our getter functions: fetch DATA from the memcached |
| * given by CACHE_VOID and identified by KEY. Indicate success in FOUND and |
| * use a tempoary sub-pool of POOL for allocations. |
| */ |
| static svn_error_t * |
| memcache_internal_get(char **data, |
| apr_size_t *size, |
| svn_boolean_t *found, |
| void *cache_void, |
| const void *key, |
| apr_pool_t *pool) |
| { |
| memcache_t *cache = cache_void; |
| apr_status_t apr_err; |
| const char *mc_key; |
| apr_pool_t *subpool; |
| |
| if (key == NULL) |
| { |
| *found = FALSE; |
| return SVN_NO_ERROR; |
| } |
| |
| subpool = svn_pool_create(pool); |
| SVN_ERR(build_key(&mc_key, cache, key, subpool)); |
| |
| apr_err = apr_memcache_getp(cache->memcache, |
| pool, |
| mc_key, |
| data, |
| size, |
| NULL /* ignore flags */); |
| if (apr_err == APR_NOTFOUND) |
| { |
| *found = FALSE; |
| svn_pool_destroy(subpool); |
| return SVN_NO_ERROR; |
| } |
| else if (apr_err != APR_SUCCESS || !*data) |
| return svn_error_wrap_apr(apr_err, |
| _("Unknown memcached error while reading")); |
| |
| *found = TRUE; |
| |
| svn_pool_destroy(subpool); |
| return SVN_NO_ERROR; |
| } |
| |
| |
| static svn_error_t * |
| memcache_get(void **value_p, |
| svn_boolean_t *found, |
| void *cache_void, |
| const void *key, |
| apr_pool_t *result_pool) |
| { |
| memcache_t *cache = cache_void; |
| char *data; |
| apr_size_t data_len; |
| SVN_ERR(memcache_internal_get(&data, |
| &data_len, |
| found, |
| cache_void, |
| key, |
| result_pool)); |
| |
| /* If we found it, de-serialize it. */ |
| if (*found) |
| { |
| if (cache->deserialize_func) |
| { |
| SVN_ERR((cache->deserialize_func)(value_p, data, data_len, |
| result_pool)); |
| } |
| else |
| { |
| svn_stringbuf_t *value = svn_stringbuf_create_empty(result_pool); |
| value->data = data; |
| value->blocksize = data_len; |
| value->len = data_len - 1; /* account for trailing NUL */ |
| *value_p = value; |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implement vtable.has_key in terms of the getter. |
| */ |
| static svn_error_t * |
| memcache_has_key(svn_boolean_t *found, |
| void *cache_void, |
| const void *key, |
| apr_pool_t *scratch_pool) |
| { |
| char *data; |
| apr_size_t data_len; |
| SVN_ERR(memcache_internal_get(&data, |
| &data_len, |
| found, |
| cache_void, |
| key, |
| scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Core functionality of our setter functions: store LENGH bytes of DATA |
| * to be identified by KEY in the memcached given by CACHE_VOID. Use POOL |
| * for temporary allocations. |
| */ |
| static svn_error_t * |
| memcache_internal_set(void *cache_void, |
| const void *key, |
| const char *data, |
| apr_size_t len, |
| apr_pool_t *scratch_pool) |
| { |
| memcache_t *cache = cache_void; |
| const char *mc_key; |
| apr_status_t apr_err; |
| |
| SVN_ERR(build_key(&mc_key, cache, key, scratch_pool)); |
| apr_err = apr_memcache_set(cache->memcache, mc_key, (char *)data, len, 0, 0); |
| |
| /* ### Maybe write failures should be ignored (but logged)? */ |
| if (apr_err != APR_SUCCESS) |
| return svn_error_wrap_apr(apr_err, |
| _("Unknown memcached error while writing")); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| static svn_error_t * |
| memcache_set(void *cache_void, |
| const void *key, |
| void *value, |
| apr_pool_t *scratch_pool) |
| { |
| memcache_t *cache = cache_void; |
| apr_pool_t *subpool = svn_pool_create(scratch_pool); |
| void *data; |
| apr_size_t data_len; |
| svn_error_t *err; |
| |
| if (key == NULL) |
| return SVN_NO_ERROR; |
| |
| if (cache->serialize_func) |
| { |
| SVN_ERR((cache->serialize_func)(&data, &data_len, value, subpool)); |
| } |
| else |
| { |
| svn_stringbuf_t *value_str = value; |
| data = value_str->data; |
| data_len = value_str->len + 1; /* copy trailing NUL */ |
| } |
| |
| err = memcache_internal_set(cache_void, key, data, data_len, subpool); |
| |
| svn_pool_destroy(subpool); |
| return err; |
| } |
| |
| static svn_error_t * |
| memcache_get_partial(void **value_p, |
| svn_boolean_t *found, |
| void *cache_void, |
| const void *key, |
| svn_cache__partial_getter_func_t func, |
| void *baton, |
| apr_pool_t *result_pool) |
| { |
| svn_error_t *err = SVN_NO_ERROR; |
| |
| char *data; |
| apr_size_t size; |
| SVN_ERR(memcache_internal_get(&data, |
| &size, |
| found, |
| cache_void, |
| key, |
| result_pool)); |
| |
| /* If we found it, de-serialize it. */ |
| return *found |
| ? func(value_p, data, size, baton, result_pool) |
| : err; |
| } |
| |
| |
| static svn_error_t * |
| memcache_set_partial(void *cache_void, |
| const void *key, |
| svn_cache__partial_setter_func_t func, |
| void *baton, |
| apr_pool_t *scratch_pool) |
| { |
| svn_error_t *err = SVN_NO_ERROR; |
| |
| void *data; |
| apr_size_t size; |
| svn_boolean_t found = FALSE; |
| |
| apr_pool_t *subpool = svn_pool_create(scratch_pool); |
| SVN_ERR(memcache_internal_get((char **)&data, |
| &size, |
| &found, |
| cache_void, |
| key, |
| subpool)); |
| |
| /* If we found it, modify it and write it back to cache */ |
| if (found) |
| { |
| SVN_ERR(func(&data, &size, baton, subpool)); |
| err = memcache_internal_set(cache_void, key, data, size, subpool); |
| } |
| |
| svn_pool_destroy(subpool); |
| return err; |
| } |
| |
| |
| static svn_error_t * |
| memcache_iter(svn_boolean_t *completed, |
| void *cache_void, |
| svn_iter_apr_hash_cb_t user_cb, |
| void *user_baton, |
| apr_pool_t *scratch_pool) |
| { |
| return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, |
| _("Can't iterate a memcached cache")); |
| } |
| |
| static svn_boolean_t |
| memcache_is_cachable(void *unused, apr_size_t size) |
| { |
| SVN_UNUSED(unused); |
| |
| /* The memcached cutoff seems to be a bit (header length?) under a megabyte. |
| * We round down a little to be safe. |
| */ |
| return size < 1000000; |
| } |
| |
| static svn_error_t * |
| memcache_get_info(void *cache_void, |
| svn_cache__info_t *info, |
| svn_boolean_t reset, |
| apr_pool_t *result_pool) |
| { |
| memcache_t *cache = cache_void; |
| |
| info->id = apr_pstrdup(result_pool, cache->prefix); |
| |
| /* we don't have any memory allocation info */ |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_cache__vtable_t memcache_vtable = { |
| memcache_get, |
| memcache_has_key, |
| memcache_set, |
| memcache_iter, |
| memcache_is_cachable, |
| memcache_get_partial, |
| memcache_set_partial, |
| memcache_get_info |
| }; |
| |
| svn_error_t * |
| svn_cache__create_memcache(svn_cache__t **cache_p, |
| svn_memcache_t *memcache, |
| svn_cache__serialize_func_t serialize_func, |
| svn_cache__deserialize_func_t deserialize_func, |
| apr_ssize_t klen, |
| const char *prefix, |
| apr_pool_t *pool) |
| { |
| svn_cache__t *wrapper = apr_pcalloc(pool, sizeof(*wrapper)); |
| memcache_t *cache = apr_pcalloc(pool, sizeof(*cache)); |
| |
| cache->serialize_func = serialize_func; |
| cache->deserialize_func = deserialize_func; |
| cache->klen = klen; |
| cache->prefix = svn_path_uri_encode(prefix, pool); |
| cache->memcache = memcache->c; |
| |
| wrapper->vtable = &memcache_vtable; |
| wrapper->cache_internal = cache; |
| wrapper->error_handler = 0; |
| wrapper->error_baton = 0; |
| wrapper->pretend_empty = !!getenv("SVN_X_DOES_NOT_MARK_THE_SPOT"); |
| |
| *cache_p = wrapper; |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /*** Creating apr_memcache_t from svn_config_t. ***/ |
| |
| /* Baton for add_memcache_server. */ |
| struct ams_baton { |
| apr_memcache_t *memcache; |
| apr_pool_t *memcache_pool; |
| svn_error_t *err; |
| }; |
| |
| /* Implements svn_config_enumerator2_t. */ |
| static svn_boolean_t |
| add_memcache_server(const char *name, |
| const char *value, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| struct ams_baton *b = baton; |
| char *host, *scope; |
| apr_port_t port; |
| apr_status_t apr_err; |
| apr_memcache_server_t *server; |
| |
| apr_err = apr_parse_addr_port(&host, &scope, &port, |
| value, pool); |
| if (apr_err != APR_SUCCESS) |
| { |
| b->err = svn_error_wrap_apr(apr_err, |
| _("Error parsing memcache server '%s'"), |
| name); |
| return FALSE; |
| } |
| |
| if (scope) |
| { |
| b->err = svn_error_createf(SVN_ERR_BAD_SERVER_SPECIFICATION, NULL, |
| _("Scope not allowed in memcache server " |
| "'%s'"), |
| name); |
| return FALSE; |
| } |
| if (!host || !port) |
| { |
| b->err = svn_error_createf(SVN_ERR_BAD_SERVER_SPECIFICATION, NULL, |
| _("Must specify host and port for memcache " |
| "server '%s'"), |
| name); |
| return FALSE; |
| } |
| |
| /* Note: the four numbers here are only relevant when an |
| apr_memcache_t is being shared by multiple threads. */ |
| apr_err = apr_memcache_server_create(b->memcache_pool, |
| host, |
| port, |
| 0, /* min connections */ |
| 5, /* soft max connections */ |
| 10, /* hard max connections */ |
| /* time to live (in microseconds) */ |
| apr_time_from_sec(50), |
| &server); |
| if (apr_err != APR_SUCCESS) |
| { |
| b->err = svn_error_wrap_apr(apr_err, |
| _("Unknown error creating memcache server")); |
| return FALSE; |
| } |
| |
| apr_err = apr_memcache_add_server(b->memcache, server); |
| if (apr_err != APR_SUCCESS) |
| { |
| b->err = svn_error_wrap_apr(apr_err, |
| _("Unknown error adding server to memcache")); |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| #else /* ! SVN_HAVE_MEMCACHE */ |
| |
| /* Stubs for no apr memcache library. */ |
| |
| struct svn_memcache_t { |
| void *unused; /* Let's not have a size-zero struct. */ |
| }; |
| |
| svn_error_t * |
| svn_cache__create_memcache(svn_cache__t **cache_p, |
| svn_memcache_t *memcache, |
| svn_cache__serialize_func_t serialize_func, |
| svn_cache__deserialize_func_t deserialize_func, |
| apr_ssize_t klen, |
| const char *prefix, |
| apr_pool_t *pool) |
| { |
| return svn_error_create(SVN_ERR_NO_APR_MEMCACHE, NULL, NULL); |
| } |
| |
| #endif /* SVN_HAVE_MEMCACHE */ |
| |
| /* Implements svn_config_enumerator2_t. Just used for the |
| entry-counting return value of svn_config_enumerate2. */ |
| static svn_boolean_t |
| nop_enumerator(const char *name, |
| const char *value, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| return TRUE; |
| } |
| |
| svn_error_t * |
| svn_cache__make_memcache_from_config(svn_memcache_t **memcache_p, |
| svn_config_t *config, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| int server_count = |
| svn_config_enumerate2(config, |
| SVN_CACHE_CONFIG_CATEGORY_MEMCACHED_SERVERS, |
| nop_enumerator, NULL, scratch_pool); |
| |
| if (server_count == 0) |
| { |
| *memcache_p = NULL; |
| return SVN_NO_ERROR; |
| } |
| |
| if (server_count > APR_INT16_MAX) |
| return svn_error_create(SVN_ERR_TOO_MANY_MEMCACHED_SERVERS, NULL, NULL); |
| |
| #ifdef SVN_HAVE_MEMCACHE |
| { |
| struct ams_baton b; |
| svn_memcache_t *memcache = apr_pcalloc(result_pool, sizeof(*memcache)); |
| apr_status_t apr_err = apr_memcache_create(result_pool, |
| (apr_uint16_t)server_count, |
| 0, /* flags */ |
| &(memcache->c)); |
| if (apr_err != APR_SUCCESS) |
| return svn_error_wrap_apr(apr_err, |
| _("Unknown error creating apr_memcache_t")); |
| |
| b.memcache = memcache->c; |
| b.memcache_pool = result_pool; |
| b.err = SVN_NO_ERROR; |
| svn_config_enumerate2(config, |
| SVN_CACHE_CONFIG_CATEGORY_MEMCACHED_SERVERS, |
| add_memcache_server, &b, |
| scratch_pool); |
| |
| if (b.err) |
| return b.err; |
| |
| *memcache_p = memcache; |
| |
| return SVN_NO_ERROR; |
| } |
| #else /* ! SVN_HAVE_MEMCACHE */ |
| { |
| return svn_error_create(SVN_ERR_NO_APR_MEMCACHE, NULL, NULL); |
| } |
| #endif /* SVN_HAVE_MEMCACHE */ |
| } |