all repos — cgit @ a1b3938f711c9b0e5eedad1678535e5779da82c1

a hyperfast web frontend for git written in c

cache.c (view raw)

  1/* cache.c: cache management
  2 *
  3 * Copyright (C) 2006 Lars Hjemli
  4 *
  5 * Licensed under GNU General Public License v2
  6 *   (see COPYING for full license text)
  7 *
  8 *
  9 * The cache is just a directory structure where each file is a cache slot,
 10 * and each filename is based on the hash of some key (e.g. the cgit url).
 11 * Each file contains the full key followed by the cached content for that
 12 * key.
 13 *
 14 */
 15
 16#include "cgit.h"
 17#include "cache.h"
 18
 19#define CACHE_BUFSIZE (1024 * 4)
 20
 21struct cache_slot {
 22	const char *key;
 23	int keylen;
 24	int ttl;
 25	cache_fill_fn fn;
 26	void *cbdata;
 27	int cache_fd;
 28	int lock_fd;
 29	const char *cache_name;
 30	const char *lock_name;
 31	int match;
 32	struct stat cache_st;
 33	struct stat lock_st;
 34	int bufsize;
 35	char buf[CACHE_BUFSIZE];
 36};
 37
 38/* Open an existing cache slot and fill the cache buffer with
 39 * (part of) the content of the cache file. Return 0 on success
 40 * and errno otherwise.
 41 */
 42static int open_slot(struct cache_slot *slot)
 43{
 44	char *bufz;
 45	int bufkeylen = -1;
 46
 47	slot->cache_fd = open(slot->cache_name, O_RDONLY);
 48	if (slot->cache_fd == -1)
 49		return errno;
 50
 51	if (fstat(slot->cache_fd, &slot->cache_st))
 52		return errno;
 53
 54	slot->bufsize = xread(slot->cache_fd, slot->buf, sizeof(slot->buf));
 55	if (slot->bufsize < 0)
 56		return errno;
 57
 58	bufz = memchr(slot->buf, 0, slot->bufsize);
 59	if (bufz)
 60		bufkeylen = bufz - slot->buf;
 61
 62	slot->match = bufkeylen == slot->keylen &&
 63	    !memcmp(slot->key, slot->buf, bufkeylen + 1);
 64
 65	return 0;
 66}
 67
 68/* Close the active cache slot */
 69static int close_slot(struct cache_slot *slot)
 70{
 71	int err = 0;
 72	if (slot->cache_fd > 0) {
 73		if (close(slot->cache_fd))
 74			err = errno;
 75		else
 76			slot->cache_fd = -1;
 77	}
 78	return err;
 79}
 80
 81/* Print the content of the active cache slot (but skip the key). */
 82static int print_slot(struct cache_slot *slot)
 83{
 84	ssize_t i, j;
 85
 86	i = lseek(slot->cache_fd, slot->keylen + 1, SEEK_SET);
 87	if (i != slot->keylen + 1)
 88		return errno;
 89
 90	do {
 91		i = j = xread(slot->cache_fd, slot->buf, sizeof(slot->buf));
 92		if (i > 0)
 93			j = xwrite(STDOUT_FILENO, slot->buf, i);
 94	} while (i > 0 && j == i);
 95
 96	if (i < 0 || j != i)
 97		return errno;
 98	else
 99		return 0;
100}
101
102/* Check if the slot has expired */
103static int is_expired(struct cache_slot *slot)
104{
105	if (slot->ttl < 0)
106		return 0;
107	else
108		return slot->cache_st.st_mtime + slot->ttl*60 < time(NULL);
109}
110
111/* Check if the slot has been modified since we opened it.
112 * NB: If stat() fails, we pretend the file is modified.
113 */
114static int is_modified(struct cache_slot *slot)
115{
116	struct stat st;
117
118	if (stat(slot->cache_name, &st))
119		return 1;
120	return (st.st_ino != slot->cache_st.st_ino ||
121		st.st_mtime != slot->cache_st.st_mtime ||
122		st.st_size != slot->cache_st.st_size);
123}
124
125/* Close an open lockfile */
126static int close_lock(struct cache_slot *slot)
127{
128	int err = 0;
129	if (slot->lock_fd > 0) {
130		if (close(slot->lock_fd))
131			err = errno;
132		else
133			slot->lock_fd = -1;
134	}
135	return err;
136}
137
138/* Create a lockfile used to store the generated content for a cache
139 * slot, and write the slot key + \0 into it.
140 * Returns 0 on success and errno otherwise.
141 */
142static int lock_slot(struct cache_slot *slot)
143{
144	slot->lock_fd = open(slot->lock_name, O_RDWR|O_CREAT|O_EXCL,
145			     S_IRUSR|S_IWUSR);
146	if (slot->lock_fd == -1)
147		return errno;
148	if (xwrite(slot->lock_fd, slot->key, slot->keylen + 1) < 0)
149		return errno;
150	return 0;
151}
152
153/* Release the current lockfile. If `replace_old_slot` is set the
154 * lockfile replaces the old cache slot, otherwise the lockfile is
155 * just deleted.
156 */
157static int unlock_slot(struct cache_slot *slot, int replace_old_slot)
158{
159	int err;
160
161	if (replace_old_slot)
162		err = rename(slot->lock_name, slot->cache_name);
163	else
164		err = unlink(slot->lock_name);
165
166	if (err)
167		return errno;
168
169	return 0;
170}
171
172/* Generate the content for the current cache slot by redirecting
173 * stdout to the lock-fd and invoking the callback function
174 */
175static int fill_slot(struct cache_slot *slot)
176{
177	int tmp;
178
179	/* Preserve stdout */
180	tmp = dup(STDOUT_FILENO);
181	if (tmp == -1)
182		return errno;
183
184	/* Redirect stdout to lockfile */
185	if (dup2(slot->lock_fd, STDOUT_FILENO) == -1)
186		return errno;
187
188	/* Generate cache content */
189	slot->fn(slot->cbdata);
190
191	/* Restore stdout */
192	if (dup2(tmp, STDOUT_FILENO) == -1)
193		return errno;
194
195	/* Close the temporary filedescriptor */
196	if (close(tmp))
197		return errno;
198
199	return 0;
200}
201
202/* Crude implementation of 32-bit FNV-1 hash algorithm,
203 * see http://www.isthe.com/chongo/tech/comp/fnv/ for details
204 * about the magic numbers.
205 */
206#define FNV_OFFSET 0x811c9dc5
207#define FNV_PRIME  0x01000193
208
209unsigned long hash_str(const char *str)
210{
211	unsigned long h = FNV_OFFSET;
212	unsigned char *s = (unsigned char *)str;
213
214	if (!s)
215		return h;
216
217	while(*s) {
218		h *= FNV_PRIME;
219		h ^= *s++;
220	}
221	return h;
222}
223
224static int process_slot(struct cache_slot *slot)
225{
226	int err;
227
228	err = open_slot(slot);
229	if (!err && slot->match) {
230		if (is_expired(slot)) {
231			if (!lock_slot(slot)) {
232				/* If the cachefile has been replaced between
233				 * `open_slot` and `lock_slot`, we'll just
234				 * serve the stale content from the original
235				 * cachefile. This way we avoid pruning the
236				 * newly generated slot. The same code-path
237				 * is chosen if fill_slot() fails for some
238				 * reason.
239				 *
240				 * TODO? check if the new slot contains the
241				 * same key as the old one, since we would
242				 * prefer to serve the newest content.
243				 * This will require us to open yet another
244				 * file-descriptor and read and compare the
245				 * key from the new file, so for now we're
246				 * lazy and just ignore the new file.
247				 */
248				if (is_modified(slot) || fill_slot(slot)) {
249					unlock_slot(slot, 0);
250					close_lock(slot);
251				} else {
252					close_slot(slot);
253					unlock_slot(slot, 1);
254					slot->cache_fd = slot->lock_fd;
255				}
256			}
257		}
258		if ((err = print_slot(slot)) != 0) {
259			cache_log("[cgit] error printing cache %s: %s (%d)\n",
260				  slot->cache_name,
261				  strerror(err),
262				  err);
263		}
264		close_slot(slot);
265		return err;
266	}
267
268	/* If the cache slot does not exist (or its key doesn't match the
269	 * current key), lets try to create a new cache slot for this
270	 * request. If this fails (for whatever reason), lets just generate
271	 * the content without caching it and fool the caller to belive
272	 * everything worked out (but print a warning on stdout).
273	 */
274
275	close_slot(slot);
276	if ((err = lock_slot(slot)) != 0) {
277		cache_log("[cgit] Unable to lock slot %s: %s (%d)\n",
278			  slot->lock_name, strerror(err), err);
279		slot->fn(slot->cbdata);
280		return 0;
281	}
282
283	if ((err = fill_slot(slot)) != 0) {
284		cache_log("[cgit] Unable to fill slot %s: %s (%d)\n",
285			  slot->lock_name, strerror(err), err);
286		unlock_slot(slot, 0);
287		close_lock(slot);
288		slot->fn(slot->cbdata);
289		return 0;
290	}
291	// We've got a valid cache slot in the lock file, which
292	// is about to replace the old cache slot. But if we
293	// release the lockfile and then try to open the new cache
294	// slot, we might get a race condition with a concurrent
295	// writer for the same cache slot (with a different key).
296	// Lets avoid such a race by just printing the content of
297	// the lock file.
298	slot->cache_fd = slot->lock_fd;
299	unlock_slot(slot, 1);
300	if ((err = print_slot(slot)) != 0) {
301		cache_log("[cgit] error printing cache %s: %s (%d)\n",
302			  slot->cache_name,
303			  strerror(err),
304			  err);
305	}
306	close_slot(slot);
307	return err;
308}
309
310/* Print cached content to stdout, generate the content if necessary. */
311int cache_process(int size, const char *path, const char *key, int ttl,
312		  cache_fill_fn fn, void *cbdata)
313{
314	unsigned long hash;
315	int len, i;
316	char filename[1024];
317	char lockname[1024 + 5];  /* 5 = ".lock" */
318	struct cache_slot slot;
319
320	/* If the cache is disabled, just generate the content */
321	if (size <= 0) {
322		fn(cbdata);
323		return 0;
324	}
325
326	/* Verify input, calculate filenames */
327	if (!path) {
328		cache_log("[cgit] Cache path not specified, caching is disabled\n");
329		fn(cbdata);
330		return 0;
331	}
332	len = strlen(path);
333	if (len > sizeof(filename) - 10) { /* 10 = "/01234567\0" */
334		cache_log("[cgit] Cache path too long, caching is disabled: %s\n",
335			  path);
336		fn(cbdata);
337		return 0;
338	}
339	if (!key)
340		key = "";
341	hash = hash_str(key) % size;
342	strcpy(filename, path);
343	if (filename[len - 1] != '/')
344		filename[len++] = '/';
345	for(i = 0; i < 8; i++) {
346		sprintf(filename + len++, "%x",
347			(unsigned char)(hash & 0xf));
348		hash >>= 4;
349	}
350	filename[len] = '\0';
351	strcpy(lockname, filename);
352	strcpy(lockname + len, ".lock");
353	slot.fn = fn;
354	slot.cbdata = cbdata;
355	slot.ttl = ttl;
356	slot.cache_name = filename;
357	slot.lock_name = lockname;
358	slot.key = key;
359	slot.keylen = strlen(key);
360	return process_slot(&slot);
361}
362
363/* Return a strftime formatted date/time
364 * NB: the result from this function is to shared memory
365 */
366char *sprintftime(const char *format, time_t time)
367{
368	static char buf[64];
369	struct tm *tm;
370
371	if (!time)
372		return NULL;
373	tm = gmtime(&time);
374	strftime(buf, sizeof(buf)-1, format, tm);
375	return buf;
376}
377
378int cache_ls(const char *path)
379{
380	DIR *dir;
381	struct dirent *ent;
382	int err = 0;
383	struct cache_slot slot;
384	char fullname[1024];
385	char *name;
386
387	if (!path) {
388		cache_log("[cgit] cache path not specified\n");
389		return -1;
390	}
391	if (strlen(path) > 1024 - 10) {
392		cache_log("[cgit] cache path too long: %s\n",
393			  path);
394		return -1;
395	}
396	dir = opendir(path);
397	if (!dir) {
398		err = errno;
399		cache_log("[cgit] unable to open path %s: %s (%d)\n",
400			  path, strerror(err), err);
401		return err;
402	}
403	strcpy(fullname, path);
404	name = fullname + strlen(path);
405	if (*(name - 1) != '/') {
406		*name++ = '/';
407		*name = '\0';
408	}
409	slot.cache_name = fullname;
410	while((ent = readdir(dir)) != NULL) {
411		if (strlen(ent->d_name) != 8)
412			continue;
413		strcpy(name, ent->d_name);
414		if ((err = open_slot(&slot)) != 0) {
415			cache_log("[cgit] unable to open path %s: %s (%d)\n",
416				  fullname, strerror(err), err);
417			continue;
418		}
419		printf("%s %s %10"PRIuMAX" %s\n",
420		       name,
421		       sprintftime("%Y-%m-%d %H:%M:%S",
422				   slot.cache_st.st_mtime),
423		       (uintmax_t)slot.cache_st.st_size,
424		       slot.buf);
425		close_slot(&slot);
426	}
427	closedir(dir);
428	return 0;
429}
430
431/* Print a message to stdout */
432void cache_log(const char *format, ...)
433{
434	va_list args;
435	va_start(args, format);
436	vfprintf(stderr, format, args);
437	va_end(args);
438}
439