Merge branch 'ps/reftable-optimize-io'

Low-level I/O optimization for reftable.

* ps/reftable-optimize-io:
  reftable/stack: fix race in up-to-date check
  reftable/stack: unconditionally reload stack after commit
  reftable/blocksource: use mmap to read tables
  reftable/blocksource: refactor code to match our coding style
  reftable/stack: use stat info to avoid re-reading stack list
  reftable/stack: refactor reloading to use file descriptor
  reftable/stack: refactor stack reloading to have common exit path
This commit is contained in:
Junio C Hamano 2024-01-29 16:02:59 -08:00
commit 4d5a46ecb1
3 changed files with 171 additions and 69 deletions

View file

@ -76,8 +76,8 @@ struct reftable_block_source malloc_block_source(void)
}
struct file_block_source {
int fd;
uint64_t size;
unsigned char *data;
};
static uint64_t file_size(void *b)
@ -87,19 +87,12 @@ static uint64_t file_size(void *b)
static void file_return_block(void *b, struct reftable_block *dest)
{
if (dest->len)
memset(dest->data, 0xff, dest->len);
reftable_free(dest->data);
}
static void file_close(void *b)
static void file_close(void *v)
{
int fd = ((struct file_block_source *)b)->fd;
if (fd > 0) {
close(fd);
((struct file_block_source *)b)->fd = 0;
}
struct file_block_source *b = v;
munmap(b->data, b->size);
reftable_free(b);
}
@ -108,9 +101,7 @@ static int file_read_block(void *v, struct reftable_block *dest, uint64_t off,
{
struct file_block_source *b = v;
assert(off + size <= b->size);
dest->data = reftable_malloc(size);
if (pread_in_full(b->fd, dest->data, size, off) != size)
return -1;
dest->data = b->data + off;
dest->len = size;
return size;
}
@ -125,26 +116,26 @@ static struct reftable_block_source_vtable file_vtable = {
int reftable_block_source_from_file(struct reftable_block_source *bs,
const char *name)
{
struct stat st = { 0 };
int err = 0;
int fd = open(name, O_RDONLY);
struct file_block_source *p = NULL;
struct file_block_source *p;
struct stat st;
int fd;
fd = open(name, O_RDONLY);
if (fd < 0) {
if (errno == ENOENT) {
if (errno == ENOENT)
return REFTABLE_NOT_EXIST_ERROR;
}
return -1;
}
err = fstat(fd, &st);
if (err < 0) {
if (fstat(fd, &st) < 0) {
close(fd);
return REFTABLE_IO_ERROR;
}
p = reftable_calloc(sizeof(struct file_block_source));
p = reftable_calloc(sizeof(*p));
p->size = st.st_size;
p->fd = fd;
p->data = xmmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);
assert(!bs->ops);
bs->ops = &file_vtable;

View file

@ -66,6 +66,7 @@ int reftable_new_stack(struct reftable_stack **dest, const char *dir,
strbuf_addstr(&list_file_name, "/tables.list");
p->list_file = strbuf_detach(&list_file_name, NULL);
p->list_fd = -1;
p->reftable_dir = xstrdup(dir);
p->config = config;
@ -175,6 +176,12 @@ void reftable_stack_destroy(struct reftable_stack *st)
st->readers_len = 0;
FREE_AND_NULL(st->readers);
}
if (st->list_fd >= 0) {
close(st->list_fd);
st->list_fd = -1;
}
FREE_AND_NULL(st->list_file);
FREE_AND_NULL(st->reftable_dir);
reftable_free(st);
@ -304,69 +311,134 @@ static int tv_cmp(struct timeval *a, struct timeval *b)
static int reftable_stack_reload_maybe_reuse(struct reftable_stack *st,
int reuse_open)
{
struct timeval deadline = { 0 };
int err = gettimeofday(&deadline, NULL);
char **names = NULL, **names_after = NULL;
struct timeval deadline;
int64_t delay = 0;
int tries = 0;
int tries = 0, err;
int fd = -1;
err = gettimeofday(&deadline, NULL);
if (err < 0)
return err;
goto out;
deadline.tv_sec += 3;
while (1) {
char **names = NULL;
char **names_after = NULL;
struct timeval now = { 0 };
int err = gettimeofday(&now, NULL);
int err2 = 0;
if (err < 0) {
return err;
}
struct timeval now;
/* Only look at deadlines after the first few times. This
simplifies debugging in GDB */
tries++;
if (tries > 3 && tv_cmp(&now, &deadline) >= 0) {
break;
}
err = gettimeofday(&now, NULL);
if (err < 0)
goto out;
err = read_lines(st->list_file, &names);
if (err < 0) {
free_names(names);
return err;
}
err = reftable_stack_reload_once(st, names, reuse_open);
if (err == 0) {
free_names(names);
break;
}
if (err != REFTABLE_NOT_EXIST_ERROR) {
free_names(names);
return err;
}
/* err == REFTABLE_NOT_EXIST_ERROR can be caused by a concurrent
writer. Check if there was one by checking if the name list
changed.
/*
* Only look at deadlines after the first few times. This
* simplifies debugging in GDB.
*/
err2 = read_lines(st->list_file, &names_after);
if (err2 < 0) {
free_names(names);
return err2;
tries++;
if (tries > 3 && tv_cmp(&now, &deadline) >= 0)
goto out;
fd = open(st->list_file, O_RDONLY);
if (fd < 0) {
if (errno != ENOENT) {
err = REFTABLE_IO_ERROR;
goto out;
}
if (names_equal(names_after, names)) {
free_names(names);
free_names(names_after);
return err;
names = reftable_calloc(sizeof(char *));
} else {
err = fd_read_lines(fd, &names);
if (err < 0)
goto out;
}
err = reftable_stack_reload_once(st, names, reuse_open);
if (!err)
break;
if (err != REFTABLE_NOT_EXIST_ERROR)
goto out;
/*
* REFTABLE_NOT_EXIST_ERROR can be caused by a concurrent
* writer. Check if there was one by checking if the name list
* changed.
*/
err = read_lines(st->list_file, &names_after);
if (err < 0)
goto out;
if (names_equal(names_after, names)) {
err = REFTABLE_NOT_EXIST_ERROR;
goto out;
}
free_names(names);
names = NULL;
free_names(names_after);
names_after = NULL;
close(fd);
fd = -1;
delay = delay + (delay * rand()) / RAND_MAX + 1;
sleep_millisec(delay);
}
return 0;
out:
/*
* Invalidate the stat cache. It is sufficient to only close the file
* descriptor and keep the cached stat info because we never use the
* latter when the former is negative.
*/
if (st->list_fd >= 0) {
close(st->list_fd);
st->list_fd = -1;
}
/*
* Cache stat information in case it provides a useful signal to us.
* According to POSIX, "The st_ino and st_dev fields taken together
* uniquely identify the file within the system." That being said,
* Windows is not POSIX compliant and we do not have these fields
* available. So the information we have there is insufficient to
* determine whether two file descriptors point to the same file.
*
* While we could fall back to using other signals like the file's
* mtime, those are not sufficient to avoid races. We thus refrain from
* using the stat cache on such systems and fall back to the secondary
* caching mechanism, which is to check whether contents of the file
* have changed.
*
* On other systems which are POSIX compliant we must keep the file
* descriptor open. This is to avoid a race condition where two
* processes access the reftable stack at the same point in time:
*
* 1. A reads the reftable stack and caches its stat info.
*
* 2. B updates the stack, appending a new table to "tables.list".
* This will both use a new inode and result in a different file
* size, thus invalidating A's cache in theory.
*
* 3. B decides to auto-compact the stack and merges two tables. The
* file size now matches what A has cached again. Furthermore, the
* filesystem may decide to recycle the inode number of the file
* we have replaced in (2) because it is not in use anymore.
*
* 4. A reloads the reftable stack. Neither the inode number nor the
* file size changed. If the timestamps did not change either then
* we think the cached copy of our stack is up-to-date.
*
* By keeping the file descriptor open the inode number cannot be
* recycled, mitigating the race.
*/
if (!err && fd >= 0 && !fstat(fd, &st->list_st) &&
st->list_st.st_dev && st->list_st.st_ino) {
st->list_fd = fd;
fd = -1;
}
if (fd >= 0)
close(fd);
free_names(names);
free_names(names_after);
return err;
}
/* -1 = error
@ -375,8 +447,44 @@ static int reftable_stack_reload_maybe_reuse(struct reftable_stack *st,
static int stack_uptodate(struct reftable_stack *st)
{
char **names = NULL;
int err = read_lines(st->list_file, &names);
int err;
int i = 0;
/*
* When we have cached stat information available then we use it to
* verify whether the file has been rewritten.
*
* Note that we explicitly do not want to use `stat_validity_check()`
* and friends here because they may end up not comparing the `st_dev`
* and `st_ino` fields. These functions thus cannot guarantee that we
* indeed still have the same file.
*/
if (st->list_fd >= 0) {
struct stat list_st;
if (stat(st->list_file, &list_st) < 0) {
/*
* It's fine for "tables.list" to not exist. In that
* case, we have to refresh when the loaded stack has
* any readers.
*/
if (errno == ENOENT)
return !!st->readers_len;
return REFTABLE_IO_ERROR;
}
/*
* When "tables.list" refers to the same file we can assume
* that it didn't change. This is because we always use
* rename(3P) to update the file and never write to it
* directly.
*/
if (st->list_st.st_dev == list_st.st_dev &&
st->list_st.st_ino == list_st.st_ino)
return 0;
}
err = read_lines(st->list_file, &names);
if (err < 0)
return err;
@ -559,7 +667,7 @@ int reftable_addition_commit(struct reftable_addition *add)
add->new_tables = NULL;
add->new_tables_len = 0;
err = reftable_stack_reload(add->stack);
err = reftable_stack_reload_maybe_reuse(add->stack, 1);
if (err)
goto done;

View file

@ -14,7 +14,10 @@ license that can be found in the LICENSE file or at
#include "reftable-stack.h"
struct reftable_stack {
struct stat list_st;
char *list_file;
int list_fd;
char *reftable_dir;
int disable_auto_compact;