blob: a7f63391fbd841c4d02200d6f76c2c9d410729e1 [file]
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include "common/cachefilesd.h"
#include "common/debug.h"
#include "format_2_cull.h"
/******************************************************************************/
#define PAIR(Q,N) ((Q)->queue[(N)])
#define YOUNGEST(Q) ((Q)->queue[(Q)->youngest])
#define OLDEST(Q) ((Q)->queue[(Q)->oldest])
#define TIME(Q,N) (PAIR((Q), (N)).atime)
#define SLOT(Q,N) (PAIR((Q), (N)).slot)
#define QSIZE(Q) ((Q)->youngest - (Q)->oldest + 1)
static void cull_file(const char *filename);
static int cull_slot(slot_t slot);
static char in_queue(struct queue *cullq, slot_t slot, atime_t atime, unsigned i);
#ifdef FORCE_EVICT
/* Debugging values. */
size_t percent_evicted = 0;
#endif
/******************************************************************************/
/**
* Update a particular pair with new values.
* @param p A pointer to the pair to update.
* @param slot The slot number to store.
* @param atime The atime to store.
*/
static inline void pair_set(struct pair *p, slot_t slot, atime_t atime)
{
p->slot = slot;
p->atime = atime;
}
/**
* Returns a left-justified index of this item's correct
* position in the queue. This means it can be 0, or even
* past the end if the queue is full.
* @param cullq The queue to determine an insertion point for.
* @param atime The atime of the item to insert.
*/
static inline unsigned get_insert(struct queue *cullq, atime_t atime)
{
unsigned l = 0;
unsigned r = cullq->youngest;
unsigned m;
/* Older than the oldest item (or empty queue.) */
if (cullq->youngest == -1 ||
atime <= OLDEST(cullq).atime)
return 0;
/* Younger than the youngest item, add to end */
if (atime >= YOUNGEST(cullq).atime)
return cullq->youngest + 1;
/* Find insertion point. */
do {
m = (l + r) / 2;
if (atime > TIME(cullq,m))
l = m + 1;
else
r = m;
} while (r > l);
return l;
}
/**
* Compares two pair's atimes.
* @return 0 if they're equal, 1 if "a > b", -1 otherwise.
*/
static inline int pair_cmp(void const *a, void const *b)
{
#define timeof(X) (((struct pair *)X)->atime)
if (timeof(a) == timeof(b))
return 0;
return (timeof(a) > timeof(b)) ? 1 : -1;
}
#ifdef CONST_CHECK
/**
* Debug method. Ensures the validity of the queue ordering.
* @param cullq The queue to check the consistency of.
* @return 0 for OK, -1 otherwise.
*/
static char check_consistency(struct queue *cullq)
{
unsigned i;
if (cullq->youngest == -1)
return 0;
if (QSIZE(cullq) < 2)
return 0;
for (i = cullq->oldest + 1; i <= cullq->youngest; ++i) {
if (TIME(cullq,i) < TIME(cullq,i-1)) {
debug(0, "PROBLEM: [%u,%u] after [%u,%u]\n",
SLOT(cullq,i),
TIME(cullq,i),
SLOT(cullq,i-1),
TIME(cullq,i-1));
return -1;
}
}
return 0;
}
#endif
/**
* Insert a new slot+atime pair into the table.
* If the table is full, the table will make room only if
* the new value is old enough.
*
* @param cullq The queue to insert into
* @param slot The slot number of the new item
* @param atime The atime associated with this slot
*/
static void insert_into_cull_table(struct queue *cullq,
slot_t slot,
atime_t atime)
{
unsigned i;
/* Is the table full? */
if (cullq->youngest == cullq->size - 1) {
if (atime >= YOUNGEST(cullq).atime)
return;
/* last element gets pushed out */
cullq->youngest--;
}
/* Is the table overfull? ...
* We are careful to exclude cullq->youngest = -1. */
else if (unlikely(cullq->youngest + 1 > cullq->size)) {
debug(0, "youngest: %u; capacity: %u", cullq->youngest, cullq->size - 1);
error("Cull table overfull");
}
/* get insertion point.
* oldest and newest objects are fast searches. */
i = get_insert(cullq, atime);
if (in_queue(cullq, slot, atime, i))
return;
/* shift the Ith item to I+1, creating a new insertion point at I.
* The number of items currently in the table is (youngest + 1),
* because 'youngest' is stored as a zero index.
* We therefore move (num - i) objects. */
memmove(&PAIR(cullq,i+1),
&PAIR(cullq,i),
((cullq->youngest + 1) - i) * sizeof(PAIR(cullq,0)));
pair_set(&PAIR(cullq,i), slot, atime);
cullq->youngest++;
#ifdef CONST_CHECK
if (check_consistency(cullq)) {
debug(0,"Failed consistency check, i was %d, atime was %u", i, atime);
exit(254);
}
#endif
}
/**
* Build, or Rebuild, a culling queue based on the atimes file specified
* by the state structure associated with this culling queue.
* @param cullq The queue to build up
* @param randomize Optionally, should we read the source atimes file in a random order?
*/
void build_cull_queue(struct queue *cullq, char randomize)
{
/* Configuration (filesize, queuesize, readsizes) */
const size_t readnum = (1 << 12);
const size_t readbytes = readnum * sizeof(atime_t);
struct stat st;
/* Used for the actual reading */
int fd, n = readnum;
slot_t slot = 0;
atime_t *abuff;
unsigned i, p, chunk, chunks;
/* For randomization */
unsigned *readlist = NULL;
if (cullq->oldest)
error("Inconsistency: build_cull_queue called when the oldest element was not 0.");
/* Mark as non-ready while we're building ... */
cullq->ready = 0;
fd = open(cullq->state->atimefile, O_RDONLY);
if (fd < 0)
oserror("Failed to open atimes file (%s) to build a culling queue.",
cullq->state->atimefile);
abuff = (atime_t *)malloc(readbytes);
if (!abuff)
oserror("Failed to allocate space (%lub) for reading %lu atime entries",
readbytes, readnum);
fstat(fd, &st);
chunks = (st.st_size + readbytes - 1) / readbytes;
if (!chunks)
return;
/* This code randomizes the order of the chunks we read.
* It does unfortunately take up some space in memory,
* but it helps avoid the worst case buffer-building scenario. */
if (randomize) {
unsigned j;
readlist = (unsigned *)malloc(sizeof(unsigned) * chunks);
readlist[0] = 0;
/* inside-out Knuth-Fisher-Yates shuffle:
* read these chunks in a random order. */
for (i = 1; i < chunks; ++i) {
j = random() % (i+1);
readlist[i] = readlist[j];
readlist[j] = i;
}
}
/* Read the atimes file chunk-by-chunk, either in-order or
* a random chunk at a time. So either we use p linearly,
* or as an index into our randomized chunk-order list. */
for (p = 0; p < chunks; ++p) {
chunk = randomize ? readlist[p] : p;
lseek(fd, chunk * readbytes, SEEK_SET);
slot = chunk * readnum;
n = read(fd, abuff, readbytes) / sizeof(atime_t);
for (i = 0; i < n; ++i, ++slot)
insert_into_cull_table(cullq, slot, abuff[i] - 1);
}
/* Mark as ready! */
if (cullq->oldest == 0 && cullq->youngest != -1)
cullq->ready = 1;
close(fd);
free(abuff);
if (randomize)
free(readlist);
}
/**
* Instantiate a new queue associated with a particular cachefilesd cache.
* @param exponent How large should the queue be? It will be 2^exponent.
* @param state State information for a particular cachefilesd cache to associate this queue with.
* @return A pointer to the new queue.
*/
struct queue *new_queue(unsigned exponent, struct format_2_state *state)
{
struct queue *cullq = calloc(sizeof(struct queue),1);
if (!cullq) {
dperror("Failed to allocate memory for queue header");
return cullq;
}
cullq->size = 1 << exponent;
cullq->queue = calloc(sizeof(struct pair),cullq->size);
if (!cullq->queue) {
dperror("Failed to allocate memory for queue");
free(cullq);
return NULL;
}
cullq->ready = 0;
cullq->oldest = 0;
cullq->youngest = -1;
cullq->thrash = 0;
cullq->state = state;
return cullq;
}
/**
* Delete a queue. Simply frees memory.
* @param cullq The queue to delete.
*/
void delete_queue(struct queue *cullq)
{
free(cullq->queue);
free(cullq);
}
/**
* Advance the queue pointer by an arbitrary amount.
* Optionally, tell valgrind that the previous bits in memory are
* no longer valid queue slots.
* @param cullq The queue to advance
* @param by How many slots we should advance the pointer.
*/
static inline void advance_queue(struct queue *cullq, size_t by)
{
#ifdef _USE_VALGRIND
VALGRIND_MAKE_MEM_UNDEFINED(&OLDEST(cullq), sizeof(struct pair) * by);
#endif
cullq->oldest += by;
}
/**
* Invalidate the entire queue. Optionally, tell valgrind the queue is now invalid.
* @param cullq the queue to invalidate.
*/
static inline void erase_queue(struct queue *cullq)
{
#ifdef _USE_VALGRIND
VALGRIND_MAKE_MEM_UNDEFINED(cullq->queue, sizeof(struct pair) * cullq->size);
#endif
cullq->oldest = 0;
cullq->youngest = -1;
cullq->ready = 0;
}
/**
* cull_objects will mull over the culling queue, find the first valid object, and cull it.
* We will verify each item in the queue before attempting to cull it.
* It is possible we will run out of entries before we succeed in culling anything.
* @param cullq The queue to pick an item from.
* @return The size of the queue after we were done with it.
*/
size_t cull_objects(struct queue *cullq)
{
char active, fresh, success = 0;
slot_t slot;
atime_t atime, file_atime;
int cullfd, atimefd, rc;
size_t offset;
if (cullq->oldest >= cullq->size)
error("Cullable object count is inconsistent");
cullfd = open(cullq->state->indexfile, O_RDONLY);
if (cullfd < 0)
oserror("Failed to open culling index (%s) to verify slot before cull",
cullq->state->indexfile);
atimefd = open(cullq->state->atimefile, O_RDONLY);
if (atimefd < 0)
oserror("Failed to open atimes index (%s) to verify slot before cull",
cullq->state->atimefile);
/* Make a little note if this was a 'fresh' queue. */
fresh = (cullq->oldest == 0);
/* process the queue until we cull one item, or run out of items. */
do {
slot = OLDEST(cullq).slot;
atime = OLDEST(cullq).atime + 1;
offset = foffset(slot, cullq->state->pagesize,
cullq->state->num_perpage,
cullq->state->ent_size);
debug(3, "Considering culling %u", slot);
/* Is this atime even valid? If not,
* that means we've run out of valid entries in the queue.
* Because they are sorted off-by-one, a zero entry MUST
* mean the end of the queue. */
if (!atime) {
debug(3,"Empty atime. Considering queue empty.");
erase_queue(cullq);
break;
}
/* Seek and read first byte from cullslot to verify validity */
rc = lseek(cullfd, offset, SEEK_SET);
if (unlikely(rc != offset))
oserror("Failed to seek to correct slot in culling index");
rc = read(cullfd, &active, 1);
if (unlikely(rc != 1))
oserror("Failed to read slot status from culling index");
if (!active) {
debug(2, "Entry in cull_index is already gone.");
advance_queue(cullq, 1);
continue;
}
/* Seek and read atime to verify slot validity */
rc = lseek(atimefd, sizeof(atime_t) * slot, SEEK_SET);
if (unlikely(rc != sizeof(atime_t) * slot))
oserror("Failed to seek to this slot's atime when reading file...");
rc = read(atimefd, &file_atime, sizeof(atime_t));
if (unlikely(rc != sizeof(atime_t)))
oserror("Failed to read atime from file in order to verify slot.");
if (file_atime != atime) {
debug(2, "Slot %u was touched since we added it to the queue.",
slot);
advance_queue(cullq, 1);
continue;
}
/* Request the slot be culled. */
rc = cull_slot(slot);
advance_queue(cullq, 1);
/* This slot didn't work, silently roll on to the next one */
if (rc)
continue;
success = 1;
} while (cullq->oldest < cullq->size);
/* Keep track if we are being fruitful in our culling efforts. */
if (!success && fresh)
cullq->thrash++;
else if (success)
cullq->thrash = 0;
/* If we've emptied out our queue, notate it as needing to be re-filled. */
if (cullq->oldest == cullq->size) {
debug(1, "queue was depleted, marking it empty.");
erase_queue(cullq);
}
close(cullfd);
close(atimefd);
return QSIZE(cullq);
}
/**
* cull_file will cull a file representing an object in the current working directory.
* - requests CacheFiles rename the object "<cwd>/filename" to the graveyard
* @param filename The file to cull.
*/
__attribute__((unused))
static void cull_file(const char *filename)
{
char buffer[NAME_MAX + 30];
int ret, n;
n = sprintf(buffer, "cull %s", filename);
/* command the module */
ret = write(cachefd, buffer, n);
if (ret < 0 && errno != ESTALE && errno != ENOENT && errno != EBUSY)
oserror("Failed to cull object");
}
/**
* cull_slot will remove a file from the cache via its index slot number.
* @param slot The slot to cull.
*/
static int cull_slot(slot_t slot)
{
char buffer[20];
int rc, len;
len = snprintf(buffer, 20, "cullslot %u", slot);
debug(2, "%s", buffer);
rc = write(cachefd, buffer, len);
if (rc < len) {
rc = errno;
debug(1, "cmd(%s) failed: %m", buffer);
return rc;
}
return 0;
}
/**
* in_queue is a best-effort function that detects if an object is
* already in the culling queue or not. It may return a false negative
* if the atime associated with the object has changed inbetween its
* addition to the queue and the verification.
* @param cullq The queue to search.
* @param slot The slot number we seek to verify.
* @param atime That slot's atime.
* @param i The insertion index of this hypothetical slot.
* Retrieve this value by using get_insert.
* @return True/False. True indicates the item was definitely found in the cache.
*/
char in_queue(struct queue *cullq, slot_t slot, atime_t atime, unsigned i)
{
unsigned j = i;
/* Ensure the insertion point, i, is a valid array index. */
if (j >= QSIZE(cullq) || j > cullq->youngest)
return 0;
/* Bullseye. */
if (SLOT(cullq, j) == slot)
return 1;
/* Might be duplicate atimes, so search to the left and right until
* our ship runs ashore, or we find the man we're looking for.
*
* Note:
* (A) the +1 lets us use lt instead of lte.
* (B) the +1 overflows the -1 sentinel to be 0, and the loop is avoided.
*/
while (++j < cullq->youngest + 1 && TIME(cullq, j) == atime)
if (SLOT(cullq, j) == slot)
return 1;
for (j = i; --j != -1 && TIME(cullq, j) == atime;)
if (SLOT(cullq, j) == slot)
return 1;
/* Couldn't find it! */
return 0;
}
/**
* queue_refresh will freshen the queue with new items,
* and make sure the existing items are valid.
* @param cullq the queue to freshen.
* @return The number of queue items evicted.
*/
size_t queue_refresh(struct queue *cullq)
{
int fd, rc;
atime_t new_atime;
unsigned i, evicted = 0;
#ifdef FORCE_EVICT
/* For testing, compute an eviction cutoff for simulating ... */
size_t evict = (((float) percent_evicted / 100.0) * (float)cullq->size);
#endif
cullq->ready = 0;
fd = open(cullq->state->atimefile, O_RDONLY);
if (fd < 0)
oserror("Failed to open atimes file during queue refresh");
/* The queue refresh proceeds in three steps. */
/* Step one: iterate through the queue and refresh the atimes present. */
for (i = cullq->oldest;
i <= cullq->youngest && i < cullq->size;
++i) {
/* Read the current atime in from file. */
rc = lseek(fd, sizeof(atime_t) * SLOT(cullq,i), SEEK_SET);
if (unlikely(rc < 0))
oserror("Failed to seek to position in atime file (%s)",
cullq->state->atimefile);
rc = read(fd, &new_atime, sizeof(atime_t));
if (unlikely(rc != sizeof(atime_t)))
oserror("Failed to retrieve atime from file (%s)",
cullq->state->atimefile);
#ifdef FORCE_EVICT
/* Debug note: we use 'evict' to simulate a
* forced percentage of evictions. */
if (i < evict)
new_atime = 0;
#endif
/* Update the atime in the queue. */
if (new_atime - 1 != TIME(cullq,i)) {
evicted++;
debug(4,"Freshen: had (%u), updated to (%u)", TIME(cullq,i), new_atime);
}
TIME(cullq,i) = new_atime - 1;
}
close(fd);
/* The queue has been previously nibbled at, shift it down. */
if (cullq->oldest != 0) {
memmove(&PAIR(cullq,0),
&OLDEST(cullq),
sizeof(PAIR(cullq,0)) * (cullq->youngest - cullq->oldest + 1));
evicted += cullq->oldest;
cullq->youngest -= cullq->oldest;
cullq->oldest = 0;
}
/* If nothing changed at all, AND the queue is full,
* there's nothing further we need to do at all. */
if (!evicted && cullq->youngest == cullq->size - 1)
return 0;
/* Step Two: Re-sort the queue so that it is at least internally consistent.
* We only need to do this if anything changed. */
else if (evicted)
qsort(&OLDEST(cullq), (cullq->youngest - cullq->oldest) + 1,
sizeof(struct pair), pair_cmp);
#ifdef CONST_CHECK
if (check_consistency(cullq)) {
for (i = 0 ; i < cullq->size; ++i)
debug(0,"{%8u}", TIME(cullq,i));
debug(0, "Failed consistency check after qsort");
exit(10);
}
#endif
/* Step 3: Re-read the cull_atimes file and ensure our queue is
* accurate. The randomized-read ability of build_cull_queue
* is left as an option: it's possible that with a low eviction
* rate that setting this to 0 might yield better speeds. */
build_cull_queue(cullq, 1);
return evicted;
}