all repos — cgit @ 4f6fb32f5881a093be4c2f41c72813b80404c569

a hyperfast web frontend for git written in c

cache.c (view raw)

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