| /* 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 "httpd.h" |
| #include "http_log.h" |
| #include "http_request.h" |
| #include "http_protocol.h" |
| #include "http_config.h" |
| #include "mod_status.h" |
| |
| #include "apr.h" |
| #include "apr_strings.h" |
| #include "apr_time.h" |
| #include "apr_shm.h" |
| #define APR_WANT_STRFUNC |
| #include "apr_want.h" |
| #include "apr_general.h" |
| |
| #if APR_HAVE_LIMITS_H |
| #include <limits.h> |
| #endif |
| |
| #include "ap_socache.h" |
| |
| /* XXX Unfortunately, there are still many unsigned ints in use here, so we |
| * XXX cannot allow more than UINT_MAX. Since some of the ints are exposed in |
| * XXX public interfaces, a simple search and replace is not enough. |
| * XXX It should be possible to extend that so that the total cache size can |
| * XXX be APR_SIZE_MAX and only the object size needs to be smaller than |
| * XXX UINT_MAX. |
| */ |
| #define SHMCB_MAX_SIZE (UINT_MAX<APR_SIZE_MAX ? UINT_MAX : APR_SIZE_MAX) |
| |
| #define DEFAULT_SHMCB_PREFIX "socache-shmcb-" |
| |
| #define DEFAULT_SHMCB_SUFFIX ".cache" |
| |
| #define ALIGNED_HEADER_SIZE APR_ALIGN_DEFAULT(sizeof(SHMCBHeader)) |
| #define ALIGNED_SUBCACHE_SIZE APR_ALIGN_DEFAULT(sizeof(SHMCBSubcache)) |
| #define ALIGNED_INDEX_SIZE APR_ALIGN_DEFAULT(sizeof(SHMCBIndex)) |
| |
| /* |
| * Header structure - the start of the shared-mem segment |
| */ |
| typedef struct { |
| /* Stats for cache operations */ |
| unsigned long stat_stores; |
| unsigned long stat_replaced; |
| unsigned long stat_expiries; |
| unsigned long stat_scrolled; |
| unsigned long stat_retrieves_hit; |
| unsigned long stat_retrieves_miss; |
| unsigned long stat_removes_hit; |
| unsigned long stat_removes_miss; |
| /* Number of subcaches */ |
| unsigned int subcache_num; |
| /* How many indexes each subcache's queue has */ |
| unsigned int index_num; |
| /* How large each subcache is, including the queue and data */ |
| unsigned int subcache_size; |
| /* How far into each subcache the data area is (optimisation) */ |
| unsigned int subcache_data_offset; |
| /* How large the data area in each subcache is (optimisation) */ |
| unsigned int subcache_data_size; |
| } SHMCBHeader; |
| |
| /* |
| * Subcache structure - the start of each subcache, followed by |
| * indexes then data |
| */ |
| typedef struct { |
| /* The start position and length of the cyclic buffer of indexes */ |
| unsigned int idx_pos, idx_used; |
| /* Same for the data area */ |
| unsigned int data_pos, data_used; |
| } SHMCBSubcache; |
| |
| /* |
| * Index structure - each subcache has an array of these |
| */ |
| typedef struct { |
| /* absolute time this entry expires */ |
| apr_time_t expires; |
| /* location within the subcache's data area */ |
| unsigned int data_pos; |
| /* size (most logic ignores this, we keep it only to minimise memcpy) */ |
| unsigned int data_used; |
| /* length of the used data which contains the id */ |
| unsigned int id_len; |
| /* Used to mark explicitly-removed socache entries */ |
| unsigned char removed; |
| } SHMCBIndex; |
| |
| struct ap_socache_instance_t { |
| apr_pool_t *pool; |
| const char *data_file; |
| apr_size_t shm_size; |
| apr_shm_t *shm; |
| SHMCBHeader *header; |
| }; |
| |
| /* The SHM data segment is of fixed size and stores data as follows. |
| * |
| * [ SHMCBHeader | Subcaches ] |
| * |
| * The SHMCBHeader header structure stores metadata concerning the |
| * cache and the contained subcaches. |
| * |
| * Subcaches is a hash table of header->subcache_num SHMCBSubcache |
| * structures. The hash table is indexed by SHMCB_MASK(id). Each |
| * SHMCBSubcache structure has a fixed size (header->subcache_size), |
| * which is determined at creation time, and looks like the following: |
| * |
| * [ SHMCBSubcache | Indexes | Data ] |
| * |
| * Each subcache is prefixed by the SHMCBSubcache structure. |
| * |
| * The subcache's "Data" segment is a single cyclic data buffer, of |
| * total size header->subcache_data_size; data inside is referenced |
| * using byte offsets. The offset marking the beginning of the cyclic |
| * buffer is subcache->data_pos; the buffer's length is |
| * subcache->data_used. |
| * |
| * "Indexes" is an array of header->index_num SHMCBIndex structures, |
| * which is used as a cyclic queue; subcache->idx_pos gives the array |
| * index of the first in use, subcache->idx_used gives the number in |
| * use. Both ->idx_* values have a range of [0, header->index_num) |
| * |
| * Each in-use SHMCBIndex structure represents a single cached object. |
| * The ID and data segment are stored consecutively in the subcache's |
| * cyclic data buffer. The "Data" segment can thus be seen to |
| * look like this, for example |
| * |
| * offset: [ 0 1 2 3 4 5 6 ... |
| * contents:[ ID1 Data1 ID2 Data2 ID3 ... |
| * |
| * where the corresponding indices would look like: |
| * |
| * idx1 = { data_pos = 0, data_used = 3, id_len = 1, ...} |
| * idx2 = { data_pos = 3, data_used = 3, id_len = 1, ...} |
| * ... |
| */ |
| |
| /* This macro takes a pointer to the header and a zero-based index and returns |
| * a pointer to the corresponding subcache. */ |
| #define SHMCB_SUBCACHE(pHeader, num) \ |
| (SHMCBSubcache *)(((unsigned char *)(pHeader)) + \ |
| ALIGNED_HEADER_SIZE + \ |
| (num) * ((pHeader)->subcache_size)) |
| |
| /* This macro takes a pointer to the header and an id and returns a |
| * pointer to the corresponding subcache. */ |
| #define SHMCB_MASK(pHeader, id) \ |
| SHMCB_SUBCACHE((pHeader), *(id) & ((pHeader)->subcache_num - 1)) |
| |
| /* This macro takes the same params as the last, generating two outputs for use |
| * in ap_log_error(...). */ |
| #define SHMCB_MASK_DBG(pHeader, id) \ |
| *(id), (*(id) & ((pHeader)->subcache_num - 1)) |
| |
| /* This macro takes a pointer to a subcache and a zero-based index and returns |
| * a pointer to the corresponding SHMCBIndex. */ |
| #define SHMCB_INDEX(pSubcache, num) \ |
| (SHMCBIndex *)(((unsigned char *)pSubcache) + \ |
| ALIGNED_SUBCACHE_SIZE + \ |
| (num) * ALIGNED_INDEX_SIZE) |
| |
| /* This macro takes a pointer to the header and a subcache and returns a |
| * pointer to the corresponding data area. */ |
| #define SHMCB_DATA(pHeader, pSubcache) \ |
| ((unsigned char *)(pSubcache) + (pHeader)->subcache_data_offset) |
| |
| /* |
| * Cyclic functions - assists in "wrap-around"/modulo logic |
| */ |
| |
| /* Addition modulo 'mod' */ |
| #define SHMCB_CYCLIC_INCREMENT(val,inc,mod) \ |
| (((val) + (inc)) % (mod)) |
| |
| /* Subtraction (or "distance between") modulo 'mod' */ |
| #define SHMCB_CYCLIC_SPACE(val1,val2,mod) \ |
| ((val2) >= (val1) ? ((val2) - (val1)) : \ |
| ((val2) + (mod) - (val1))) |
| |
| /* A "normal-to-cyclic" memcpy. */ |
| static void shmcb_cyclic_ntoc_memcpy(unsigned int buf_size, unsigned char *data, |
| unsigned int dest_offset, const unsigned char *src, |
| unsigned int src_len) |
| { |
| if (dest_offset + src_len < buf_size) |
| /* It be copied all in one go */ |
| memcpy(data + dest_offset, src, src_len); |
| else { |
| /* Copy the two splits */ |
| memcpy(data + dest_offset, src, buf_size - dest_offset); |
| memcpy(data, src + buf_size - dest_offset, |
| src_len + dest_offset - buf_size); |
| } |
| } |
| |
| /* A "cyclic-to-normal" memcpy. */ |
| static void shmcb_cyclic_cton_memcpy(unsigned int buf_size, unsigned char *dest, |
| const unsigned char *data, unsigned int src_offset, |
| unsigned int src_len) |
| { |
| if (src_offset + src_len < buf_size) |
| /* It be copied all in one go */ |
| memcpy(dest, data + src_offset, src_len); |
| else { |
| /* Copy the two splits */ |
| memcpy(dest, data + src_offset, buf_size - src_offset); |
| memcpy(dest + buf_size - src_offset, data, |
| src_len + src_offset - buf_size); |
| } |
| } |
| |
| /* A memcmp against a cyclic data buffer. Compares SRC of length |
| * SRC_LEN against the contents of cyclic buffer DATA (which is of |
| * size BUF_SIZE), starting at offset DEST_OFFSET. Got that? Good. */ |
| static int shmcb_cyclic_memcmp(unsigned int buf_size, unsigned char *data, |
| unsigned int dest_offset, |
| const unsigned char *src, |
| unsigned int src_len) |
| { |
| if (dest_offset + src_len < buf_size) |
| /* It be compared all in one go */ |
| return memcmp(data + dest_offset, src, src_len); |
| else { |
| /* Compare the two splits */ |
| int diff; |
| |
| diff = memcmp(data + dest_offset, src, buf_size - dest_offset); |
| if (diff) { |
| return diff; |
| } |
| return memcmp(data, src + buf_size - dest_offset, |
| src_len + dest_offset - buf_size); |
| } |
| } |
| |
| |
| /* Prototypes for low-level subcache operations */ |
| static void shmcb_subcache_expire(server_rec *, SHMCBHeader *, SHMCBSubcache *, |
| apr_time_t); |
| /* Returns zero on success, non-zero on failure. */ |
| static int shmcb_subcache_store(server_rec *s, SHMCBHeader *header, |
| SHMCBSubcache *subcache, |
| unsigned char *data, unsigned int data_len, |
| const unsigned char *id, unsigned int id_len, |
| apr_time_t expiry); |
| /* Returns zero on success, non-zero on failure. */ |
| static int shmcb_subcache_retrieve(server_rec *, SHMCBHeader *, SHMCBSubcache *, |
| const unsigned char *id, unsigned int idlen, |
| unsigned char *data, unsigned int *datalen); |
| /* Returns zero on success, non-zero on failure. */ |
| static int shmcb_subcache_remove(server_rec *, SHMCBHeader *, SHMCBSubcache *, |
| const unsigned char *, unsigned int); |
| |
| /* Returns result of the (iterator)() call, zero is success (continue) */ |
| static apr_status_t shmcb_subcache_iterate(ap_socache_instance_t *instance, |
| server_rec *s, |
| void *userctx, |
| SHMCBHeader *header, |
| SHMCBSubcache *subcache, |
| ap_socache_iterator_t *iterator, |
| unsigned char **buf, |
| apr_size_t *buf_len, |
| apr_pool_t *pool, |
| apr_time_t now); |
| |
| /* |
| * High-Level "handlers" as per ssl_scache.c |
| * subcache internals are deferred to shmcb_subcache_*** functions lower down |
| */ |
| |
| static const char *socache_shmcb_create(ap_socache_instance_t **context, |
| const char *arg, |
| apr_pool_t *tmp, apr_pool_t *p) |
| { |
| ap_socache_instance_t *ctx; |
| char *path, *cp, *cp2; |
| |
| /* Allocate the context. */ |
| *context = ctx = apr_pcalloc(p, sizeof *ctx); |
| ctx->pool = p; |
| |
| ctx->shm_size = 1024*512; /* 512KB */ |
| |
| if (!arg || *arg == '\0') { |
| /* Use defaults. */ |
| return NULL; |
| } |
| |
| ctx->data_file = path = ap_runtime_dir_relative(p, arg); |
| |
| cp = strrchr(path, '('); |
| cp2 = path + strlen(path) - 1; |
| if (cp) { |
| char *endptr; |
| if (*cp2 != ')') { |
| return "Invalid argument: no closing parenthesis or cache size " |
| "missing after pathname with parenthesis"; |
| } |
| *cp++ = '\0'; |
| *cp2 = '\0'; |
| |
| |
| ctx->shm_size = strtol(cp, &endptr, 10); |
| if (endptr != cp2) { |
| return "Invalid argument: cache size not numerical"; |
| } |
| |
| if (ctx->shm_size < 8192) { |
| return "Invalid argument: size has to be >= 8192 bytes"; |
| |
| } |
| |
| if (ctx->shm_size >= SHMCB_MAX_SIZE) { |
| return apr_psprintf(tmp, "Invalid argument: size has " |
| "to be < %" APR_SIZE_T_FMT " bytes on this platform", |
| SHMCB_MAX_SIZE); |
| } |
| } |
| else if (cp2 >= path && *cp2 == ')') { |
| return "Invalid argument: no opening parenthesis"; |
| } |
| |
| return NULL; |
| } |
| |
| static apr_status_t socache_shmcb_cleanup(void *arg) |
| { |
| ap_socache_instance_t *ctx = arg; |
| if (ctx->shm) { |
| apr_shm_destroy(ctx->shm); |
| ctx->shm = NULL; |
| } |
| return APR_SUCCESS; |
| } |
| |
| static apr_status_t socache_shmcb_init(ap_socache_instance_t *ctx, |
| const char *namespace, |
| const struct ap_socache_hints *hints, |
| server_rec *s, apr_pool_t *p) |
| { |
| void *shm_segment; |
| apr_size_t shm_segsize; |
| apr_status_t rv; |
| SHMCBHeader *header; |
| unsigned int num_subcache, num_idx, loop; |
| apr_size_t avg_obj_size, avg_id_len; |
| |
| /* Create shared memory segment */ |
| if (ctx->data_file == NULL) { |
| const char *path = apr_pstrcat(p, DEFAULT_SHMCB_PREFIX, namespace, |
| DEFAULT_SHMCB_SUFFIX, NULL); |
| |
| ctx->data_file = ap_runtime_dir_relative(p, path); |
| } |
| |
| /* Use anonymous shm by default, fall back on name-based. */ |
| rv = apr_shm_create(&ctx->shm, ctx->shm_size, NULL, p); |
| if (APR_STATUS_IS_ENOTIMPL(rv)) { |
| /* If anon shm isn't supported, fail if no named file was |
| * configured successfully; the ap_runtime_dir_relative call |
| * above will return NULL for invalid paths. */ |
| if (ctx->data_file == NULL) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(00818) |
| "Could not use anonymous shm for '%s' cache", |
| namespace); |
| ctx->shm = NULL; |
| return APR_EINVAL; |
| } |
| |
| /* For a name-based segment, remove it first in case of a |
| * previous unclean shutdown. */ |
| apr_shm_remove(ctx->data_file, p); |
| |
| rv = apr_shm_create(&ctx->shm, ctx->shm_size, ctx->data_file, p); |
| } |
| |
| if (rv != APR_SUCCESS) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(00819) |
| "Could not allocate shared memory segment for shmcb " |
| "socache"); |
| ctx->shm = NULL; |
| return rv; |
| } |
| apr_pool_cleanup_register(ctx->pool, ctx, socache_shmcb_cleanup, |
| apr_pool_cleanup_null); |
| |
| shm_segment = apr_shm_baseaddr_get(ctx->shm); |
| shm_segsize = apr_shm_size_get(ctx->shm); |
| if (shm_segsize < (5 * ALIGNED_HEADER_SIZE)) { |
| /* the segment is ridiculously small, bail out */ |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(00820) |
| "shared memory segment too small"); |
| return APR_ENOSPC; |
| } |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00821) |
| "shmcb_init allocated %" APR_SIZE_T_FMT |
| " bytes of shared memory", |
| shm_segsize); |
| /* Discount the header */ |
| shm_segsize -= ALIGNED_HEADER_SIZE; |
| /* Select index size based on average object size hints, if given. */ |
| avg_obj_size = hints && hints->avg_obj_size ? hints->avg_obj_size : 150; |
| avg_id_len = hints && hints->avg_id_len ? hints->avg_id_len : 30; |
| num_idx = (shm_segsize) / (avg_obj_size + avg_id_len); |
| num_subcache = 256; |
| while ((num_idx / num_subcache) < (2 * num_subcache)) |
| num_subcache /= 2; |
| num_idx /= num_subcache; |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00822) |
| "for %" APR_SIZE_T_FMT " bytes (%" APR_SIZE_T_FMT |
| " including header), recommending %u subcaches, " |
| "%u indexes each", shm_segsize, |
| shm_segsize + ALIGNED_HEADER_SIZE, |
| num_subcache, num_idx); |
| if (num_idx < 5) { |
| /* we're still too small, bail out */ |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(00823) |
| "shared memory segment too small"); |
| return APR_ENOSPC; |
| } |
| /* OK, we're sorted */ |
| ctx->header = header = shm_segment; |
| header->stat_stores = 0; |
| header->stat_replaced = 0; |
| header->stat_expiries = 0; |
| header->stat_scrolled = 0; |
| header->stat_retrieves_hit = 0; |
| header->stat_retrieves_miss = 0; |
| header->stat_removes_hit = 0; |
| header->stat_removes_miss = 0; |
| header->subcache_num = num_subcache; |
| /* Convert the subcache size (in bytes) to a value that is suitable for |
| * structure alignment on the host platform, by rounding down if necessary. */ |
| header->subcache_size = (size_t)(shm_segsize / num_subcache); |
| if (header->subcache_size != APR_ALIGN_DEFAULT(header->subcache_size)) { |
| header->subcache_size = APR_ALIGN_DEFAULT(header->subcache_size) - |
| APR_ALIGN_DEFAULT(1); |
| } |
| header->subcache_data_offset = ALIGNED_SUBCACHE_SIZE + |
| num_idx * ALIGNED_INDEX_SIZE; |
| header->subcache_data_size = header->subcache_size - |
| header->subcache_data_offset; |
| header->index_num = num_idx; |
| |
| /* Output trace info */ |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00824) |
| "shmcb_init_memory choices follow"); |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00825) |
| "subcache_num = %u", header->subcache_num); |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00826) |
| "subcache_size = %u", header->subcache_size); |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00827) |
| "subcache_data_offset = %u", header->subcache_data_offset); |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00828) |
| "subcache_data_size = %u", header->subcache_data_size); |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00829) |
| "index_num = %u", header->index_num); |
| /* The header is done, make the caches empty */ |
| for (loop = 0; loop < header->subcache_num; loop++) { |
| SHMCBSubcache *subcache = SHMCB_SUBCACHE(header, loop); |
| subcache->idx_pos = subcache->idx_used = 0; |
| subcache->data_pos = subcache->data_used = 0; |
| } |
| ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(00830) |
| "Shared memory socache initialised"); |
| /* Success ... */ |
| |
| return APR_SUCCESS; |
| } |
| |
| static void socache_shmcb_destroy(ap_socache_instance_t *ctx, server_rec *s) |
| { |
| if (ctx) { |
| apr_pool_cleanup_run(ctx->pool, ctx, socache_shmcb_cleanup); |
| } |
| } |
| |
| static apr_status_t socache_shmcb_store(ap_socache_instance_t *ctx, |
| server_rec *s, const unsigned char *id, |
| unsigned int idlen, apr_time_t expiry, |
| unsigned char *encoded, |
| unsigned int len_encoded, |
| apr_pool_t *p) |
| { |
| SHMCBHeader *header = ctx->header; |
| SHMCBSubcache *subcache = SHMCB_MASK(header, id); |
| int tryreplace; |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00831) |
| "socache_shmcb_store (0x%02x -> subcache %d)", |
| SHMCB_MASK_DBG(header, id)); |
| /* XXX: Says who? Why shouldn't this be acceptable, or padded if not? */ |
| if (idlen < 4) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(00832) "unusably short id provided " |
| "(%u bytes)", idlen); |
| return APR_EINVAL; |
| } |
| tryreplace = shmcb_subcache_remove(s, header, subcache, id, idlen); |
| if (shmcb_subcache_store(s, header, subcache, encoded, |
| len_encoded, id, idlen, expiry)) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(00833) |
| "can't store an socache entry!"); |
| return APR_ENOSPC; |
| } |
| if (tryreplace == 0) { |
| header->stat_replaced++; |
| } |
| else { |
| header->stat_stores++; |
| } |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00834) |
| "leaving socache_shmcb_store successfully"); |
| return APR_SUCCESS; |
| } |
| |
| static apr_status_t socache_shmcb_retrieve(ap_socache_instance_t *ctx, |
| server_rec *s, |
| const unsigned char *id, unsigned int idlen, |
| unsigned char *dest, unsigned int *destlen, |
| apr_pool_t *p) |
| { |
| SHMCBHeader *header = ctx->header; |
| SHMCBSubcache *subcache = SHMCB_MASK(header, id); |
| int rv; |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00835) |
| "socache_shmcb_retrieve (0x%02x -> subcache %d)", |
| SHMCB_MASK_DBG(header, id)); |
| |
| /* Get the entry corresponding to the id, if it exists. */ |
| rv = shmcb_subcache_retrieve(s, header, subcache, id, idlen, |
| dest, destlen); |
| if (rv == 0) |
| header->stat_retrieves_hit++; |
| else |
| header->stat_retrieves_miss++; |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00836) |
| "leaving socache_shmcb_retrieve successfully"); |
| |
| return rv == 0 ? APR_SUCCESS : APR_NOTFOUND; |
| } |
| |
| static apr_status_t socache_shmcb_remove(ap_socache_instance_t *ctx, |
| server_rec *s, const unsigned char *id, |
| unsigned int idlen, apr_pool_t *p) |
| { |
| SHMCBHeader *header = ctx->header; |
| SHMCBSubcache *subcache = SHMCB_MASK(header, id); |
| apr_status_t rv; |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00837) |
| "socache_shmcb_remove (0x%02x -> subcache %d)", |
| SHMCB_MASK_DBG(header, id)); |
| if (idlen < 4) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(00838) "unusably short id provided " |
| "(%u bytes)", idlen); |
| return APR_EINVAL; |
| } |
| if (shmcb_subcache_remove(s, header, subcache, id, idlen) == 0) { |
| header->stat_removes_hit++; |
| rv = APR_SUCCESS; |
| } else { |
| header->stat_removes_miss++; |
| rv = APR_NOTFOUND; |
| } |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00839) |
| "leaving socache_shmcb_remove successfully"); |
| |
| return rv; |
| } |
| |
| static void socache_shmcb_status(ap_socache_instance_t *ctx, |
| request_rec *r, int flags) |
| { |
| server_rec *s = r->server; |
| SHMCBHeader *header = ctx->header; |
| unsigned int loop, total = 0, cache_total = 0, non_empty_subcaches = 0; |
| apr_time_t idx_expiry, min_expiry = 0, max_expiry = 0; |
| apr_time_t now = apr_time_now(); |
| double expiry_total = 0; |
| int index_pct, cache_pct; |
| |
| AP_DEBUG_ASSERT(header->subcache_num > 0); |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00840) "inside shmcb_status"); |
| /* Perform the iteration inside the mutex to avoid corruption or invalid |
| * pointer arithmetic. The rest of our logic uses read-only header data so |
| * doesn't need the lock. */ |
| /* Iterate over the subcaches */ |
| for (loop = 0; loop < header->subcache_num; loop++) { |
| SHMCBSubcache *subcache = SHMCB_SUBCACHE(header, loop); |
| shmcb_subcache_expire(s, header, subcache, now); |
| total += subcache->idx_used; |
| cache_total += subcache->data_used; |
| if (subcache->idx_used) { |
| SHMCBIndex *idx = SHMCB_INDEX(subcache, subcache->idx_pos); |
| non_empty_subcaches++; |
| idx_expiry = idx->expires; |
| expiry_total += (double)idx_expiry; |
| max_expiry = ((idx_expiry > max_expiry) ? idx_expiry : max_expiry); |
| if (!min_expiry) |
| min_expiry = idx_expiry; |
| else |
| min_expiry = ((idx_expiry < min_expiry) ? idx_expiry : min_expiry); |
| } |
| } |
| index_pct = (100 * total) / (header->index_num * |
| header->subcache_num); |
| cache_pct = (100 * cache_total) / (header->subcache_data_size * |
| header->subcache_num); |
| /* Generate Output */ |
| if (!(flags & AP_STATUS_SHORT)) { |
| ap_rprintf(r, "cache type: <b>SHMCB</b>, shared memory: <b>%" APR_SIZE_T_FMT "</b> " |
| "bytes, current entries: <b>%d</b><br>", |
| ctx->shm_size, total); |
| ap_rprintf(r, "subcaches: <b>%d</b>, indexes per subcache: <b>%d</b><br>", |
| header->subcache_num, header->index_num); |
| if (non_empty_subcaches) { |
| apr_time_t average_expiry = (apr_time_t)(expiry_total / (double)non_empty_subcaches); |
| ap_rprintf(r, "time left on oldest entries' objects: "); |
| if (now < average_expiry) |
| ap_rprintf(r, "avg: <b>%d</b> seconds, (range: %d...%d)<br>", |
| (int)apr_time_sec(average_expiry - now), |
| (int)apr_time_sec(min_expiry - now), |
| (int)apr_time_sec(max_expiry - now)); |
| else |
| ap_rprintf(r, "expiry_threshold: <b>Calculation error!</b><br>"); |
| } |
| |
| ap_rprintf(r, "index usage: <b>%d%%</b>, cache usage: <b>%d%%</b><br>", |
| index_pct, cache_pct); |
| ap_rprintf(r, "total entries stored since starting: <b>%lu</b><br>", |
| header->stat_stores); |
| ap_rprintf(r, "total entries replaced since starting: <b>%lu</b><br>", |
| header->stat_replaced); |
| ap_rprintf(r, "total entries expired since starting: <b>%lu</b><br>", |
| header->stat_expiries); |
| ap_rprintf(r, "total (pre-expiry) entries scrolled out of the cache: " |
| "<b>%lu</b><br>", header->stat_scrolled); |
| ap_rprintf(r, "total retrieves since starting: <b>%lu</b> hit, " |
| "<b>%lu</b> miss<br>", header->stat_retrieves_hit, |
| header->stat_retrieves_miss); |
| ap_rprintf(r, "total removes since starting: <b>%lu</b> hit, " |
| "<b>%lu</b> miss<br>", header->stat_removes_hit, |
| header->stat_removes_miss); |
| } |
| else { |
| ap_rputs("CacheType: SHMCB\n", r); |
| ap_rprintf(r, "CacheSharedMemory: %" APR_SIZE_T_FMT "\n", |
| ctx->shm_size); |
| ap_rprintf(r, "CacheCurrentEntries: %d\n", total); |
| ap_rprintf(r, "CacheSubcaches: %d\n", header->subcache_num); |
| ap_rprintf(r, "CacheIndexesPerSubcaches: %d\n", header->index_num); |
| if (non_empty_subcaches) { |
| apr_time_t average_expiry = (apr_time_t)(expiry_total / (double)non_empty_subcaches); |
| if (now < average_expiry) { |
| ap_rprintf(r, "CacheTimeLeftOldestAvg: %d\n", (int)apr_time_sec(average_expiry - now)); |
| ap_rprintf(r, "CacheTimeLeftOldestMin: %d\n", (int)apr_time_sec(min_expiry - now)); |
| ap_rprintf(r, "CacheTimeLeftOldestMax: %d\n", (int)apr_time_sec(max_expiry - now)); |
| } |
| } |
| |
| ap_rprintf(r, "CacheIndexUsage: %d%%\n", index_pct); |
| ap_rprintf(r, "CacheUsage: %d%%\n", cache_pct); |
| ap_rprintf(r, "CacheStoreCount: %lu\n", header->stat_stores); |
| ap_rprintf(r, "CacheReplaceCount: %lu\n", header->stat_replaced); |
| ap_rprintf(r, "CacheExpireCount: %lu\n", header->stat_expiries); |
| ap_rprintf(r, "CacheDiscardCount: %lu\n", header->stat_scrolled); |
| ap_rprintf(r, "CacheRetrieveHitCount: %lu\n", header->stat_retrieves_hit); |
| ap_rprintf(r, "CacheRetrieveMissCount: %lu\n", header->stat_retrieves_miss); |
| ap_rprintf(r, "CacheRemoveHitCount: %lu\n", header->stat_removes_hit); |
| ap_rprintf(r, "CacheRemoveMissCount: %lu\n", header->stat_removes_miss); |
| } |
| ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00841) "leaving shmcb_status"); |
| } |
| |
| static apr_status_t socache_shmcb_iterate(ap_socache_instance_t *instance, |
| server_rec *s, void *userctx, |
| ap_socache_iterator_t *iterator, |
| apr_pool_t *pool) |
| { |
| SHMCBHeader *header = instance->header; |
| unsigned int loop; |
| apr_time_t now = apr_time_now(); |
| apr_status_t rv = APR_SUCCESS; |
| apr_size_t buflen = 0; |
| unsigned char *buf = NULL; |
| |
| /* Perform the iteration inside the mutex to avoid corruption or invalid |
| * pointer arithmetic. The rest of our logic uses read-only header data so |
| * doesn't need the lock. */ |
| /* Iterate over the subcaches */ |
| for (loop = 0; loop < header->subcache_num && rv == APR_SUCCESS; loop++) { |
| SHMCBSubcache *subcache = SHMCB_SUBCACHE(header, loop); |
| rv = shmcb_subcache_iterate(instance, s, userctx, header, subcache, |
| iterator, &buf, &buflen, pool, now); |
| } |
| return rv; |
| } |
| |
| /* |
| * Subcache-level cache operations |
| */ |
| |
| static void shmcb_subcache_expire(server_rec *s, SHMCBHeader *header, |
| SHMCBSubcache *subcache, apr_time_t now) |
| { |
| unsigned int loop = 0, freed = 0, expired = 0; |
| unsigned int new_idx_pos = subcache->idx_pos; |
| SHMCBIndex *idx = NULL; |
| |
| while (loop < subcache->idx_used) { |
| idx = SHMCB_INDEX(subcache, new_idx_pos); |
| if (idx->removed) |
| freed++; |
| else if (idx->expires <= now) |
| expired++; |
| else |
| /* not removed and not expired yet, we're done iterating */ |
| break; |
| loop++; |
| new_idx_pos = SHMCB_CYCLIC_INCREMENT(new_idx_pos, 1, header->index_num); |
| } |
| if (!loop) |
| /* Nothing to do */ |
| return; |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00842) |
| "expiring %u and reclaiming %u removed socache entries", |
| expired, freed); |
| if (loop == subcache->idx_used) { |
| /* We're expiring everything, piece of cake */ |
| subcache->idx_used = 0; |
| subcache->data_used = 0; |
| } else { |
| /* There remain other indexes, so we can use idx to adjust 'data' */ |
| unsigned int diff = SHMCB_CYCLIC_SPACE(subcache->data_pos, |
| idx->data_pos, |
| header->subcache_data_size); |
| /* Adjust the indexes */ |
| subcache->idx_used -= loop; |
| subcache->idx_pos = new_idx_pos; |
| /* Adjust the data area */ |
| subcache->data_used -= diff; |
| subcache->data_pos = idx->data_pos; |
| } |
| header->stat_expiries += expired; |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00843) |
| "we now have %u socache entries", subcache->idx_used); |
| } |
| |
| static int shmcb_subcache_store(server_rec *s, SHMCBHeader *header, |
| SHMCBSubcache *subcache, |
| unsigned char *data, unsigned int data_len, |
| const unsigned char *id, unsigned int id_len, |
| apr_time_t expiry) |
| { |
| unsigned int data_offset, new_idx, id_offset; |
| SHMCBIndex *idx; |
| unsigned int total_len = id_len + data_len; |
| |
| /* Sanity check the input */ |
| if (total_len > header->subcache_data_size) { |
| ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(00844) |
| "inserting socache entry larger (%d) than subcache data area (%d)", |
| total_len, header->subcache_data_size); |
| return -1; |
| } |
| |
| /* First reclaim space from removed and expired records. */ |
| shmcb_subcache_expire(s, header, subcache, apr_time_now()); |
| |
| /* Loop until there is enough space to insert |
| * XXX: This should first compress out-of-order expiries and |
| * removed records, and then force-remove oldest-first |
| */ |
| if (header->subcache_data_size - subcache->data_used < total_len |
| || subcache->idx_used == header->index_num) { |
| unsigned int loop = 0; |
| |
| idx = SHMCB_INDEX(subcache, subcache->idx_pos); |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00845) |
| "about to force-expire, subcache: idx_used=%d, " |
| "data_used=%d", subcache->idx_used, subcache->data_used); |
| do { |
| SHMCBIndex *idx2; |
| |
| /* Adjust the indexes by one */ |
| subcache->idx_pos = SHMCB_CYCLIC_INCREMENT(subcache->idx_pos, 1, |
| header->index_num); |
| subcache->idx_used--; |
| if (!subcache->idx_used) { |
| /* There's nothing left */ |
| subcache->data_used = 0; |
| break; |
| } |
| /* Adjust the data */ |
| idx2 = SHMCB_INDEX(subcache, subcache->idx_pos); |
| subcache->data_used -= SHMCB_CYCLIC_SPACE(idx->data_pos, idx2->data_pos, |
| header->subcache_data_size); |
| subcache->data_pos = idx2->data_pos; |
| /* Stats */ |
| header->stat_scrolled++; |
| /* Loop admin */ |
| idx = idx2; |
| loop++; |
| } while (header->subcache_data_size - subcache->data_used < total_len); |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00846) |
| "finished force-expire, subcache: idx_used=%d, " |
| "data_used=%d", subcache->idx_used, subcache->data_used); |
| } |
| |
| /* HERE WE ASSUME THAT THE NEW ENTRY SHOULD GO ON THE END! I'M NOT |
| * CHECKING WHETHER IT SHOULD BE GENUINELY "INSERTED" SOMEWHERE. |
| * |
| * We aught to fix that. httpd (never mind third party modules) |
| * does not promise to perform any processing in date order |
| * (c.f. FAQ "My log entries are not in date order!") |
| */ |
| /* Insert the id */ |
| id_offset = SHMCB_CYCLIC_INCREMENT(subcache->data_pos, subcache->data_used, |
| header->subcache_data_size); |
| shmcb_cyclic_ntoc_memcpy(header->subcache_data_size, |
| SHMCB_DATA(header, subcache), id_offset, |
| id, id_len); |
| subcache->data_used += id_len; |
| /* Insert the data */ |
| data_offset = SHMCB_CYCLIC_INCREMENT(subcache->data_pos, subcache->data_used, |
| header->subcache_data_size); |
| shmcb_cyclic_ntoc_memcpy(header->subcache_data_size, |
| SHMCB_DATA(header, subcache), data_offset, |
| data, data_len); |
| subcache->data_used += data_len; |
| /* Insert the index */ |
| new_idx = SHMCB_CYCLIC_INCREMENT(subcache->idx_pos, subcache->idx_used, |
| header->index_num); |
| idx = SHMCB_INDEX(subcache, new_idx); |
| idx->expires = expiry; |
| idx->data_pos = id_offset; |
| idx->data_used = total_len; |
| idx->id_len = id_len; |
| idx->removed = 0; |
| subcache->idx_used++; |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00847) |
| "insert happened at idx=%d, data=(%u:%u)", new_idx, |
| id_offset, data_offset); |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00848) |
| "finished insert, subcache: idx_pos/idx_used=%d/%d, " |
| "data_pos/data_used=%d/%d", |
| subcache->idx_pos, subcache->idx_used, |
| subcache->data_pos, subcache->data_used); |
| return 0; |
| } |
| |
| static int shmcb_subcache_retrieve(server_rec *s, SHMCBHeader *header, |
| SHMCBSubcache *subcache, |
| const unsigned char *id, unsigned int idlen, |
| unsigned char *dest, unsigned int *destlen) |
| { |
| unsigned int pos; |
| unsigned int loop = 0; |
| apr_time_t now = apr_time_now(); |
| |
| pos = subcache->idx_pos; |
| |
| while (loop < subcache->idx_used) { |
| SHMCBIndex *idx = SHMCB_INDEX(subcache, pos); |
| |
| /* Only consider 'idx' if the id matches, and the "removed" |
| * flag isn't set, and the record is not expired. |
| * Check the data length too to avoid a buffer overflow |
| * in case of corruption, which should be impossible, |
| * but it's cheap to be safe. */ |
| if (!idx->removed |
| && idx->id_len == idlen |
| && (idx->data_used - idx->id_len) <= *destlen |
| && shmcb_cyclic_memcmp(header->subcache_data_size, |
| SHMCB_DATA(header, subcache), |
| idx->data_pos, id, idx->id_len) == 0) { |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00849) |
| "match at idx=%d, data=%d", pos, idx->data_pos); |
| if (idx->expires > now) { |
| unsigned int data_offset; |
| |
| /* Find the offset of the data segment, after the id */ |
| data_offset = SHMCB_CYCLIC_INCREMENT(idx->data_pos, |
| idx->id_len, |
| header->subcache_data_size); |
| |
| *destlen = idx->data_used - idx->id_len; |
| |
| /* Copy out the data */ |
| shmcb_cyclic_cton_memcpy(header->subcache_data_size, |
| dest, SHMCB_DATA(header, subcache), |
| data_offset, *destlen); |
| |
| return 0; |
| } |
| else { |
| /* Already stale, quietly remove and treat as not-found */ |
| idx->removed = 1; |
| header->stat_expiries++; |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00850) |
| "shmcb_subcache_retrieve discarding expired entry"); |
| return -1; |
| } |
| } |
| /* Increment */ |
| loop++; |
| pos = SHMCB_CYCLIC_INCREMENT(pos, 1, header->index_num); |
| } |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00851) |
| "shmcb_subcache_retrieve found no match"); |
| return -1; |
| } |
| |
| static int shmcb_subcache_remove(server_rec *s, SHMCBHeader *header, |
| SHMCBSubcache *subcache, |
| const unsigned char *id, |
| unsigned int idlen) |
| { |
| unsigned int pos; |
| unsigned int loop = 0; |
| |
| pos = subcache->idx_pos; |
| while (loop < subcache->idx_used) { |
| SHMCBIndex *idx = SHMCB_INDEX(subcache, pos); |
| |
| /* Only consider 'idx' if the id matches, and the "removed" |
| * flag isn't set. */ |
| if (!idx->removed && idx->id_len == idlen |
| && shmcb_cyclic_memcmp(header->subcache_data_size, |
| SHMCB_DATA(header, subcache), |
| idx->data_pos, id, idx->id_len) == 0) { |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00852) |
| "possible match at idx=%d, data=%d", pos, idx->data_pos); |
| |
| /* Found the matching entry, remove it quietly. */ |
| idx->removed = 1; |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00853) |
| "shmcb_subcache_remove removing matching entry"); |
| return 0; |
| } |
| /* Increment */ |
| loop++; |
| pos = SHMCB_CYCLIC_INCREMENT(pos, 1, header->index_num); |
| } |
| |
| return -1; /* failure */ |
| } |
| |
| |
| static apr_status_t shmcb_subcache_iterate(ap_socache_instance_t *instance, |
| server_rec *s, |
| void *userctx, |
| SHMCBHeader *header, |
| SHMCBSubcache *subcache, |
| ap_socache_iterator_t *iterator, |
| unsigned char **buf, |
| apr_size_t *buf_len, |
| apr_pool_t *pool, |
| apr_time_t now) |
| { |
| unsigned int pos; |
| unsigned int loop = 0; |
| apr_status_t rv; |
| |
| pos = subcache->idx_pos; |
| while (loop < subcache->idx_used) { |
| SHMCBIndex *idx = SHMCB_INDEX(subcache, pos); |
| |
| /* Only consider 'idx' if the "removed" flag isn't set. */ |
| if (!idx->removed) { |
| |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00854) |
| "iterating idx=%d, data=%d", pos, idx->data_pos); |
| if (idx->expires > now) { |
| unsigned char *id = *buf; |
| unsigned char *dest; |
| unsigned int data_offset, dest_len; |
| apr_size_t buf_req; |
| |
| /* Find the offset of the data segment, after the id */ |
| data_offset = SHMCB_CYCLIC_INCREMENT(idx->data_pos, |
| idx->id_len, |
| header->subcache_data_size); |
| |
| dest_len = idx->data_used - idx->id_len; |
| |
| buf_req = APR_ALIGN_DEFAULT(idx->id_len + 1) |
| + APR_ALIGN_DEFAULT(dest_len + 1); |
| |
| if (buf_req > *buf_len) { |
| /* Grow to ~150% of this buffer requirement on resize |
| * always using APR_ALIGN_DEFAULT sized pages |
| */ |
| *buf_len = buf_req + APR_ALIGN_DEFAULT(buf_req / 2); |
| *buf = apr_palloc(pool, *buf_len); |
| id = *buf; |
| } |
| |
| dest = *buf + APR_ALIGN_DEFAULT(idx->id_len + 1); |
| |
| /* Copy out the data, because it's potentially cyclic */ |
| shmcb_cyclic_cton_memcpy(header->subcache_data_size, id, |
| SHMCB_DATA(header, subcache), |
| idx->data_pos, idx->id_len); |
| id[idx->id_len] = '\0'; |
| |
| shmcb_cyclic_cton_memcpy(header->subcache_data_size, dest, |
| SHMCB_DATA(header, subcache), |
| data_offset, dest_len); |
| dest[dest_len] = '\0'; |
| |
| rv = iterator(instance, s, userctx, id, idx->id_len, |
| dest, dest_len, pool); |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, APLOGNO(00855) |
| "shmcb entry iterated"); |
| if (rv != APR_SUCCESS) |
| return rv; |
| } |
| else { |
| /* Already stale, quietly remove and treat as not-found */ |
| idx->removed = 1; |
| header->stat_expiries++; |
| ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(00856) |
| "shmcb_subcache_iterate discarding expired entry"); |
| } |
| } |
| /* Increment */ |
| loop++; |
| pos = SHMCB_CYCLIC_INCREMENT(pos, 1, header->index_num); |
| } |
| |
| return APR_SUCCESS; |
| } |
| |
| static const ap_socache_provider_t socache_shmcb = { |
| "shmcb", |
| AP_SOCACHE_FLAG_NOTMPSAFE, |
| socache_shmcb_create, |
| socache_shmcb_init, |
| socache_shmcb_destroy, |
| socache_shmcb_store, |
| socache_shmcb_retrieve, |
| socache_shmcb_remove, |
| socache_shmcb_status, |
| socache_shmcb_iterate |
| }; |
| |
| static void register_hooks(apr_pool_t *p) |
| { |
| ap_register_provider(p, AP_SOCACHE_PROVIDER_GROUP, "shmcb", |
| AP_SOCACHE_PROVIDER_VERSION, |
| &socache_shmcb); |
| |
| /* Also register shmcb under the default provider name. */ |
| ap_register_provider(p, AP_SOCACHE_PROVIDER_GROUP, |
| AP_SOCACHE_DEFAULT_PROVIDER, |
| AP_SOCACHE_PROVIDER_VERSION, |
| &socache_shmcb); |
| } |
| |
| AP_DECLARE_MODULE(socache_shmcb) = { |
| STANDARD20_MODULE_STUFF, |
| NULL, NULL, NULL, NULL, NULL, |
| register_hooks |
| }; |