| /* | |
| * Copyright 2020 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style | |
| * license that can be found in the LICENSE file or at | |
| * https://developers.google.com/open-source/licenses/bsd | |
| */ | |
| #include "stack.h" | |
| #include "system.h" | |
| #include "constants.h" | |
| #include "merged.h" | |
| #include "reftable-error.h" | |
| #include "reftable-record.h" | |
| #include "reftable-merged.h" | |
| #include "table.h" | |
| #include "writer.h" | |
| static int stack_filename(struct reftable_buf *dest, struct reftable_stack *st, | |
| const char *name) | |
| { | |
| int err; | |
| reftable_buf_reset(dest); | |
| if ((err = reftable_buf_addstr(dest, st->reftable_dir)) < 0 || | |
| (err = reftable_buf_addstr(dest, "/")) < 0 || | |
| (err = reftable_buf_addstr(dest, name)) < 0) | |
| return err; | |
| return 0; | |
| } | |
| static ssize_t reftable_write_data(int fd, const void *data, size_t size) | |
| { | |
| size_t total_written = 0; | |
| const char *p = data; | |
| while (total_written < size) { | |
| ssize_t bytes_written = write(fd, p, size - total_written); | |
| if (bytes_written < 0 && (errno == EAGAIN || errno == EINTR)) | |
| continue; | |
| if (bytes_written < 0) | |
| return REFTABLE_IO_ERROR; | |
| total_written += bytes_written; | |
| p += bytes_written; | |
| } | |
| return total_written; | |
| } | |
| struct fd_writer { | |
| const struct reftable_write_options *opts; | |
| int fd; | |
| }; | |
| static ssize_t fd_writer_write(void *arg, const void *data, size_t sz) | |
| { | |
| struct fd_writer *writer = arg; | |
| return reftable_write_data(writer->fd, data, sz); | |
| } | |
| static int fd_writer_flush(void *arg) | |
| { | |
| struct fd_writer *writer = arg; | |
| return fsync(writer->fd); | |
| } | |
| static int fd_read_lines(int fd, char ***namesp) | |
| { | |
| char *buf = NULL; | |
| int err = 0; | |
| off_t size; | |
| size = lseek(fd, 0, SEEK_END); | |
| if (size < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| err = lseek(fd, 0, SEEK_SET); | |
| if (err < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| REFTABLE_ALLOC_ARRAY(buf, size + 1); | |
| if (!buf) { | |
| err = REFTABLE_OUT_OF_MEMORY_ERROR; | |
| goto done; | |
| } | |
| for (off_t total_read = 0; total_read < size; ) { | |
| ssize_t bytes_read = read(fd, buf + total_read, size - total_read); | |
| if (bytes_read < 0 && (errno == EAGAIN || errno == EINTR)) | |
| continue; | |
| if (bytes_read < 0 || !bytes_read) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| total_read += bytes_read; | |
| } | |
| buf[size] = 0; | |
| err = parse_names(buf, size, namesp); | |
| done: | |
| reftable_free(buf); | |
| return err; | |
| } | |
| int read_lines(const char *filename, char ***namesp) | |
| { | |
| int fd = open(filename, O_RDONLY); | |
| int err = 0; | |
| if (fd < 0) { | |
| if (errno == ENOENT) { | |
| REFTABLE_CALLOC_ARRAY(*namesp, 1); | |
| if (!*namesp) | |
| return REFTABLE_OUT_OF_MEMORY_ERROR; | |
| return 0; | |
| } | |
| return REFTABLE_IO_ERROR; | |
| } | |
| err = fd_read_lines(fd, namesp); | |
| close(fd); | |
| return err; | |
| } | |
| int reftable_stack_init_ref_iterator(struct reftable_stack *st, | |
| struct reftable_iterator *it) | |
| { | |
| return merged_table_init_iter(reftable_stack_merged_table(st), | |
| it, REFTABLE_BLOCK_TYPE_REF); | |
| } | |
| int reftable_stack_init_log_iterator(struct reftable_stack *st, | |
| struct reftable_iterator *it) | |
| { | |
| return merged_table_init_iter(reftable_stack_merged_table(st), | |
| it, REFTABLE_BLOCK_TYPE_LOG); | |
| } | |
| struct reftable_merged_table * | |
| reftable_stack_merged_table(struct reftable_stack *st) | |
| { | |
| return st->merged; | |
| } | |
| static int has_name(char **names, const char *name) | |
| { | |
| while (*names) { | |
| if (!strcmp(*names, name)) | |
| return 1; | |
| names++; | |
| } | |
| return 0; | |
| } | |
| /* Close and free the stack */ | |
| void reftable_stack_destroy(struct reftable_stack *st) | |
| { | |
| char **names = NULL; | |
| int err = 0; | |
| if (!st) | |
| return; | |
| if (st->merged) { | |
| reftable_merged_table_free(st->merged); | |
| st->merged = NULL; | |
| } | |
| err = read_lines(st->list_file, &names); | |
| if (err < 0) { | |
| REFTABLE_FREE_AND_NULL(names); | |
| } | |
| if (st->tables) { | |
| struct reftable_buf filename = REFTABLE_BUF_INIT; | |
| for (size_t i = 0; i < st->tables_len; i++) { | |
| const char *name = reftable_table_name(st->tables[i]); | |
| int try_unlinking = 1; | |
| reftable_buf_reset(&filename); | |
| if (names && !has_name(names, name)) { | |
| if (stack_filename(&filename, st, name) < 0) | |
| try_unlinking = 0; | |
| } | |
| reftable_table_decref(st->tables[i]); | |
| if (try_unlinking && filename.len) { | |
| /* On Windows, can only unlink after closing. */ | |
| unlink(filename.buf); | |
| } | |
| } | |
| reftable_buf_release(&filename); | |
| st->tables_len = 0; | |
| REFTABLE_FREE_AND_NULL(st->tables); | |
| } | |
| if (st->list_fd >= 0) { | |
| close(st->list_fd); | |
| st->list_fd = -1; | |
| } | |
| REFTABLE_FREE_AND_NULL(st->list_file); | |
| REFTABLE_FREE_AND_NULL(st->reftable_dir); | |
| reftable_free(st); | |
| free_names(names); | |
| } | |
| static struct reftable_table **stack_copy_tables(struct reftable_stack *st, | |
| size_t cur_len) | |
| { | |
| struct reftable_table **cur = reftable_calloc(cur_len, sizeof(*cur)); | |
| if (!cur) | |
| return NULL; | |
| for (size_t i = 0; i < cur_len; i++) | |
| cur[i] = st->tables[i]; | |
| return cur; | |
| } | |
| static int reftable_stack_reload_once(struct reftable_stack *st, | |
| const char **names, | |
| int reuse_open) | |
| { | |
| size_t cur_len = !st->merged ? 0 : st->merged->tables_len; | |
| struct reftable_table **cur = NULL; | |
| struct reftable_table **reused = NULL; | |
| struct reftable_table **new_tables = NULL; | |
| size_t reused_len = 0, reused_alloc = 0, names_len; | |
| size_t new_tables_len = 0; | |
| struct reftable_merged_table *new_merged = NULL; | |
| struct reftable_buf table_path = REFTABLE_BUF_INIT; | |
| int err = 0; | |
| size_t i; | |
| if (cur_len) { | |
| cur = stack_copy_tables(st, cur_len); | |
| if (!cur) { | |
| err = REFTABLE_OUT_OF_MEMORY_ERROR; | |
| goto done; | |
| } | |
| } | |
| names_len = names_length(names); | |
| if (names_len) { | |
| new_tables = reftable_calloc(names_len, sizeof(*new_tables)); | |
| if (!new_tables) { | |
| err = REFTABLE_OUT_OF_MEMORY_ERROR; | |
| goto done; | |
| } | |
| } | |
| while (*names) { | |
| struct reftable_table *table = NULL; | |
| const char *name = *names++; | |
| /* this is linear; we assume compaction keeps the number of | |
| tables under control so this is not quadratic. */ | |
| for (i = 0; reuse_open && i < cur_len; i++) { | |
| if (cur[i] && 0 == strcmp(cur[i]->name, name)) { | |
| table = cur[i]; | |
| cur[i] = NULL; | |
| /* | |
| * When reloading the stack fails, we end up | |
| * releasing all new tables. This also | |
| * includes the reused tables, even though | |
| * they are still in used by the old stack. We | |
| * thus need to keep them alive here, which we | |
| * do by bumping their refcount. | |
| */ | |
| REFTABLE_ALLOC_GROW_OR_NULL(reused, | |
| reused_len + 1, | |
| reused_alloc); | |
| if (!reused) { | |
| err = REFTABLE_OUT_OF_MEMORY_ERROR; | |
| goto done; | |
| } | |
| reused[reused_len++] = table; | |
| reftable_table_incref(table); | |
| break; | |
| } | |
| } | |
| if (!table) { | |
| struct reftable_block_source src = { NULL }; | |
| err = stack_filename(&table_path, st, name); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_block_source_from_file(&src, | |
| table_path.buf); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_table_new(&table, &src, name); | |
| if (err < 0) | |
| goto done; | |
| } | |
| new_tables[new_tables_len] = table; | |
| new_tables_len++; | |
| } | |
| /* success! */ | |
| err = reftable_merged_table_new(&new_merged, new_tables, | |
| new_tables_len, st->opts.hash_id); | |
| if (err < 0) | |
| goto done; | |
| /* | |
| * Close the old, non-reused tables and proactively try to unlink | |
| * them. This is done for systems like Windows, where the underlying | |
| * file of such an open table wouldn't have been possible to be | |
| * unlinked by the compacting process. | |
| */ | |
| for (i = 0; i < cur_len; i++) { | |
| if (cur[i]) { | |
| const char *name = reftable_table_name(cur[i]); | |
| err = stack_filename(&table_path, st, name); | |
| if (err < 0) | |
| goto done; | |
| reftable_table_decref(cur[i]); | |
| unlink(table_path.buf); | |
| } | |
| } | |
| /* Update the stack to point to the new tables. */ | |
| if (st->merged) | |
| reftable_merged_table_free(st->merged); | |
| new_merged->suppress_deletions = 1; | |
| st->merged = new_merged; | |
| if (st->tables) | |
| reftable_free(st->tables); | |
| st->tables = new_tables; | |
| st->tables_len = new_tables_len; | |
| new_tables = NULL; | |
| new_tables_len = 0; | |
| /* | |
| * Decrement the refcount of reused tables again. This only needs to | |
| * happen on the successful case, because on the unsuccessful one we | |
| * decrement their refcount via `new_tables`. | |
| */ | |
| for (i = 0; i < reused_len; i++) | |
| reftable_table_decref(reused[i]); | |
| done: | |
| for (i = 0; i < new_tables_len; i++) | |
| reftable_table_decref(new_tables[i]); | |
| reftable_free(new_tables); | |
| reftable_free(reused); | |
| reftable_free(cur); | |
| reftable_buf_release(&table_path); | |
| return err; | |
| } | |
| static int reftable_stack_reload_maybe_reuse(struct reftable_stack *st, | |
| int reuse_open) | |
| { | |
| char **names = NULL, **names_after = NULL; | |
| uint64_t deadline; | |
| int64_t delay = 0; | |
| int tries = 0, err; | |
| int fd = -1; | |
| deadline = reftable_time_ms() + 3000; | |
| while (1) { | |
| uint64_t now = reftable_time_ms(); | |
| /* | |
| * Only look at deadlines after the first few times. This | |
| * simplifies debugging in GDB. | |
| */ | |
| tries++; | |
| if (tries > 3 && now >= deadline) | |
| goto out; | |
| fd = open(st->list_file, O_RDONLY); | |
| if (fd < 0) { | |
| if (errno != ENOENT) { | |
| err = REFTABLE_IO_ERROR; | |
| goto out; | |
| } | |
| REFTABLE_CALLOC_ARRAY(names, 1); | |
| if (!names) { | |
| err = REFTABLE_OUT_OF_MEMORY_ERROR; | |
| goto out; | |
| } | |
| } else { | |
| err = fd_read_lines(fd, &names); | |
| if (err < 0) | |
| goto out; | |
| } | |
| err = reftable_stack_reload_once(st, (const char **) 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((const char **) names_after, | |
| (const char **) 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 * reftable_rand()) / UINT32_MAX + 1; | |
| poll(NULL, 0, delay); | |
| } | |
| 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); | |
| if (st->opts.on_reload) | |
| st->opts.on_reload(st->opts.on_reload_payload); | |
| return err; | |
| } | |
| int reftable_new_stack(struct reftable_stack **dest, const char *dir, | |
| const struct reftable_write_options *_opts) | |
| { | |
| struct reftable_buf list_file_name = REFTABLE_BUF_INIT; | |
| struct reftable_write_options opts = { 0 }; | |
| struct reftable_stack *p; | |
| int err; | |
| p = reftable_calloc(1, sizeof(*p)); | |
| if (!p) { | |
| err = REFTABLE_OUT_OF_MEMORY_ERROR; | |
| goto out; | |
| } | |
| if (_opts) | |
| opts = *_opts; | |
| if (opts.hash_id == 0) | |
| opts.hash_id = REFTABLE_HASH_SHA1; | |
| *dest = NULL; | |
| reftable_buf_reset(&list_file_name); | |
| if ((err = reftable_buf_addstr(&list_file_name, dir)) < 0 || | |
| (err = reftable_buf_addstr(&list_file_name, "/tables.list")) < 0) | |
| goto out; | |
| p->list_file = reftable_buf_detach(&list_file_name); | |
| p->list_fd = -1; | |
| p->opts = opts; | |
| p->reftable_dir = reftable_strdup(dir); | |
| if (!p->reftable_dir) { | |
| err = REFTABLE_OUT_OF_MEMORY_ERROR; | |
| goto out; | |
| } | |
| err = reftable_stack_reload_maybe_reuse(p, 1); | |
| if (err < 0) | |
| goto out; | |
| *dest = p; | |
| err = 0; | |
| out: | |
| if (err < 0) | |
| reftable_stack_destroy(p); | |
| return err; | |
| } | |
| /* | |
| * Check whether the given stack is up-to-date with what we have in memory. | |
| * Returns 0 if so, 1 if the stack is out-of-date or a negative error code | |
| * otherwise. | |
| */ | |
| static int stack_uptodate(struct reftable_stack *st) | |
| { | |
| char **names = NULL; | |
| int err; | |
| /* | |
| * 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 tables. | |
| */ | |
| if (errno == ENOENT) | |
| return !!st->tables_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; | |
| for (size_t i = 0; i < st->tables_len; i++) { | |
| if (!names[i]) { | |
| err = 1; | |
| goto done; | |
| } | |
| if (strcmp(st->tables[i]->name, names[i])) { | |
| err = 1; | |
| goto done; | |
| } | |
| } | |
| if (names[st->merged->tables_len]) { | |
| err = 1; | |
| goto done; | |
| } | |
| done: | |
| free_names(names); | |
| return err; | |
| } | |
| int reftable_stack_reload(struct reftable_stack *st) | |
| { | |
| int err = stack_uptodate(st); | |
| if (err > 0) | |
| return reftable_stack_reload_maybe_reuse(st, 1); | |
| return err; | |
| } | |
| struct reftable_addition { | |
| struct reftable_flock tables_list_lock; | |
| struct reftable_stack *stack; | |
| char **new_tables; | |
| size_t new_tables_len, new_tables_cap; | |
| uint64_t next_update_index; | |
| }; | |
| static void reftable_addition_close(struct reftable_addition *add) | |
| { | |
| struct reftable_buf nm = REFTABLE_BUF_INIT; | |
| size_t i; | |
| for (i = 0; i < add->new_tables_len; i++) { | |
| if (!stack_filename(&nm, add->stack, add->new_tables[i])) | |
| unlink(nm.buf); | |
| reftable_free(add->new_tables[i]); | |
| add->new_tables[i] = NULL; | |
| } | |
| reftable_free(add->new_tables); | |
| add->new_tables = NULL; | |
| add->new_tables_len = 0; | |
| add->new_tables_cap = 0; | |
| flock_release(&add->tables_list_lock); | |
| reftable_buf_release(&nm); | |
| } | |
| static int reftable_stack_init_addition(struct reftable_addition *add, | |
| struct reftable_stack *st, | |
| unsigned int flags) | |
| { | |
| struct reftable_buf lock_file_name = REFTABLE_BUF_INIT; | |
| int err; | |
| memset(add, 0, sizeof(*add)); | |
| add->stack = st; | |
| err = flock_acquire(&add->tables_list_lock, st->list_file, | |
| st->opts.lock_timeout_ms); | |
| if (err < 0) | |
| goto done; | |
| if (st->opts.default_permissions) { | |
| if (chmod(add->tables_list_lock.path, | |
| st->opts.default_permissions) < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| } | |
| err = stack_uptodate(st); | |
| if (err < 0) | |
| goto done; | |
| if (err > 0 && flags & REFTABLE_STACK_NEW_ADDITION_RELOAD) { | |
| err = reftable_stack_reload_maybe_reuse(add->stack, 1); | |
| if (err) | |
| goto done; | |
| } | |
| if (err > 0) { | |
| err = REFTABLE_OUTDATED_ERROR; | |
| goto done; | |
| } | |
| add->next_update_index = reftable_stack_next_update_index(st); | |
| done: | |
| if (err) | |
| reftable_addition_close(add); | |
| reftable_buf_release(&lock_file_name); | |
| return err; | |
| } | |
| static int stack_try_add(struct reftable_stack *st, | |
| int (*write_table)(struct reftable_writer *wr, | |
| void *arg), | |
| void *arg, unsigned flags) | |
| { | |
| struct reftable_addition add; | |
| int err; | |
| err = reftable_stack_init_addition(&add, st, flags); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_addition_add(&add, write_table, arg); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_addition_commit(&add); | |
| done: | |
| reftable_addition_close(&add); | |
| return err; | |
| } | |
| int reftable_stack_add(struct reftable_stack *st, | |
| int (*write)(struct reftable_writer *wr, void *arg), | |
| void *arg, unsigned flags) | |
| { | |
| int err = stack_try_add(st, write, arg, flags); | |
| if (err < 0) { | |
| if (err == REFTABLE_OUTDATED_ERROR) { | |
| /* Ignore error return, we want to propagate | |
| REFTABLE_OUTDATED_ERROR. | |
| */ | |
| reftable_stack_reload(st); | |
| } | |
| return err; | |
| } | |
| return 0; | |
| } | |
| static int format_name(struct reftable_buf *dest, uint64_t min, uint64_t max) | |
| { | |
| char buf[100]; | |
| uint32_t rnd = reftable_rand(); | |
| snprintf(buf, sizeof(buf), "0x%012" PRIx64 "-0x%012" PRIx64 "-%08x", | |
| min, max, rnd); | |
| reftable_buf_reset(dest); | |
| return reftable_buf_addstr(dest, buf); | |
| } | |
| void reftable_addition_destroy(struct reftable_addition *add) | |
| { | |
| if (!add) { | |
| return; | |
| } | |
| reftable_addition_close(add); | |
| reftable_free(add); | |
| } | |
| int reftable_addition_commit(struct reftable_addition *add) | |
| { | |
| struct reftable_buf table_list = REFTABLE_BUF_INIT; | |
| int err = 0; | |
| size_t i; | |
| if (add->new_tables_len == 0) | |
| goto done; | |
| for (i = 0; i < add->stack->merged->tables_len; i++) { | |
| if ((err = reftable_buf_addstr(&table_list, add->stack->tables[i]->name)) < 0 || | |
| (err = reftable_buf_addstr(&table_list, "\n")) < 0) | |
| goto done; | |
| } | |
| for (i = 0; i < add->new_tables_len; i++) { | |
| if ((err = reftable_buf_addstr(&table_list, add->new_tables[i])) < 0 || | |
| (err = reftable_buf_addstr(&table_list, "\n")) < 0) | |
| goto done; | |
| } | |
| err = reftable_write_data(add->tables_list_lock.fd, | |
| table_list.buf, table_list.len); | |
| reftable_buf_release(&table_list); | |
| if (err < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| err = fsync(add->tables_list_lock.fd); | |
| if (err < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| err = flock_commit(&add->tables_list_lock); | |
| if (err < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| /* success, no more state to clean up. */ | |
| for (i = 0; i < add->new_tables_len; i++) | |
| reftable_free(add->new_tables[i]); | |
| reftable_free(add->new_tables); | |
| add->new_tables = NULL; | |
| add->new_tables_len = 0; | |
| add->new_tables_cap = 0; | |
| err = reftable_stack_reload_maybe_reuse(add->stack, 1); | |
| if (err) | |
| goto done; | |
| if (!add->stack->opts.disable_auto_compact) { | |
| /* | |
| * Auto-compact the stack to keep the number of tables in | |
| * control. It is possible that a concurrent writer is already | |
| * trying to compact parts of the stack, which would lead to a | |
| * `REFTABLE_LOCK_ERROR` because parts of the stack are locked | |
| * already. Similarly, the stack may have been rewritten by a | |
| * concurrent writer, which causes `REFTABLE_OUTDATED_ERROR`. | |
| * Both of these errors are benign, so we simply ignore them. | |
| */ | |
| err = reftable_stack_auto_compact(add->stack); | |
| if (err < 0 && err != REFTABLE_LOCK_ERROR && | |
| err != REFTABLE_OUTDATED_ERROR) | |
| goto done; | |
| err = 0; | |
| } | |
| done: | |
| reftable_addition_close(add); | |
| return err; | |
| } | |
| int reftable_stack_new_addition(struct reftable_addition **dest, | |
| struct reftable_stack *st, | |
| unsigned int flags) | |
| { | |
| int err; | |
| REFTABLE_CALLOC_ARRAY(*dest, 1); | |
| if (!*dest) | |
| return REFTABLE_OUT_OF_MEMORY_ERROR; | |
| err = reftable_stack_init_addition(*dest, st, flags); | |
| if (err) { | |
| reftable_free(*dest); | |
| *dest = NULL; | |
| } | |
| return err; | |
| } | |
| int reftable_addition_add(struct reftable_addition *add, | |
| int (*write_table)(struct reftable_writer *wr, | |
| void *arg), | |
| void *arg) | |
| { | |
| struct reftable_buf temp_tab_file_name = REFTABLE_BUF_INIT; | |
| struct reftable_buf tab_file_name = REFTABLE_BUF_INIT; | |
| struct reftable_buf next_name = REFTABLE_BUF_INIT; | |
| struct reftable_writer *wr = NULL; | |
| struct reftable_tmpfile tab_file = REFTABLE_TMPFILE_INIT; | |
| struct fd_writer writer = { | |
| .opts = &add->stack->opts, | |
| }; | |
| int err = 0; | |
| reftable_buf_reset(&next_name); | |
| err = format_name(&next_name, add->next_update_index, add->next_update_index); | |
| if (err < 0) | |
| goto done; | |
| err = stack_filename(&temp_tab_file_name, add->stack, next_name.buf); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_buf_addstr(&temp_tab_file_name, ".temp.XXXXXX"); | |
| if (err < 0) | |
| goto done; | |
| err = tmpfile_from_pattern(&tab_file, temp_tab_file_name.buf); | |
| if (err < 0) | |
| goto done; | |
| if (add->stack->opts.default_permissions) { | |
| if (chmod(tab_file.path, | |
| add->stack->opts.default_permissions)) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| } | |
| writer.fd = tab_file.fd; | |
| err = reftable_writer_new(&wr, fd_writer_write, fd_writer_flush, | |
| &writer, &add->stack->opts); | |
| if (err < 0) | |
| goto done; | |
| err = write_table(wr, arg); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_writer_close(wr); | |
| if (err == REFTABLE_EMPTY_TABLE_ERROR) { | |
| err = 0; | |
| goto done; | |
| } | |
| if (err < 0) | |
| goto done; | |
| err = tmpfile_close(&tab_file); | |
| if (err < 0) | |
| goto done; | |
| if (wr->min_update_index < add->next_update_index) { | |
| err = REFTABLE_API_ERROR; | |
| goto done; | |
| } | |
| err = format_name(&next_name, wr->min_update_index, wr->max_update_index); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_buf_addstr(&next_name, ".ref"); | |
| if (err < 0) | |
| goto done; | |
| err = stack_filename(&tab_file_name, add->stack, next_name.buf); | |
| if (err < 0) | |
| goto done; | |
| /* | |
| On windows, this relies on rand() picking a unique destination name. | |
| Maybe we should do retry loop as well? | |
| */ | |
| err = tmpfile_rename(&tab_file, tab_file_name.buf); | |
| if (err < 0) | |
| goto done; | |
| REFTABLE_ALLOC_GROW_OR_NULL(add->new_tables, add->new_tables_len + 1, | |
| add->new_tables_cap); | |
| if (!add->new_tables) { | |
| err = REFTABLE_OUT_OF_MEMORY_ERROR; | |
| goto done; | |
| } | |
| add->new_tables[add->new_tables_len++] = reftable_buf_detach(&next_name); | |
| done: | |
| tmpfile_delete(&tab_file); | |
| reftable_buf_release(&temp_tab_file_name); | |
| reftable_buf_release(&tab_file_name); | |
| reftable_buf_release(&next_name); | |
| reftable_writer_free(wr); | |
| return err; | |
| } | |
| uint64_t reftable_stack_next_update_index(struct reftable_stack *st) | |
| { | |
| int sz = st->merged->tables_len; | |
| if (sz > 0) | |
| return reftable_table_max_update_index(st->tables[sz - 1]) + | |
| 1; | |
| return 1; | |
| } | |
| static int stack_write_compact(struct reftable_stack *st, | |
| struct reftable_writer *wr, | |
| size_t first, size_t last, | |
| struct reftable_log_expiry_config *config) | |
| { | |
| struct reftable_merged_table *mt = NULL; | |
| struct reftable_iterator it = { NULL }; | |
| struct reftable_ref_record ref = { NULL }; | |
| struct reftable_log_record log = { NULL }; | |
| size_t subtabs_len = last - first + 1; | |
| uint64_t entries = 0; | |
| int err = 0; | |
| for (size_t i = first; i <= last; i++) | |
| st->stats.bytes += st->tables[i]->size; | |
| err = reftable_writer_set_limits(wr, st->tables[first]->min_update_index, | |
| st->tables[last]->max_update_index); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_merged_table_new(&mt, st->tables + first, subtabs_len, | |
| st->opts.hash_id); | |
| if (err < 0) | |
| goto done; | |
| err = merged_table_init_iter(mt, &it, REFTABLE_BLOCK_TYPE_REF); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_iterator_seek_ref(&it, ""); | |
| if (err < 0) | |
| goto done; | |
| while (1) { | |
| err = reftable_iterator_next_ref(&it, &ref); | |
| if (err > 0) { | |
| err = 0; | |
| break; | |
| } | |
| if (err < 0) | |
| goto done; | |
| if (first == 0 && reftable_ref_record_is_deletion(&ref)) { | |
| continue; | |
| } | |
| err = reftable_writer_add_ref(wr, &ref); | |
| if (err < 0) | |
| goto done; | |
| entries++; | |
| } | |
| reftable_iterator_destroy(&it); | |
| err = merged_table_init_iter(mt, &it, REFTABLE_BLOCK_TYPE_LOG); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_iterator_seek_log(&it, ""); | |
| if (err < 0) | |
| goto done; | |
| while (1) { | |
| err = reftable_iterator_next_log(&it, &log); | |
| if (err > 0) { | |
| err = 0; | |
| break; | |
| } | |
| if (err < 0) | |
| goto done; | |
| if (first == 0 && reftable_log_record_is_deletion(&log)) { | |
| continue; | |
| } | |
| if (config && config->min_update_index > 0 && | |
| log.update_index < config->min_update_index) { | |
| continue; | |
| } | |
| if (config && config->time > 0 && | |
| log.value.update.time < config->time) { | |
| continue; | |
| } | |
| err = reftable_writer_add_log(wr, &log); | |
| if (err < 0) | |
| goto done; | |
| entries++; | |
| } | |
| done: | |
| reftable_iterator_destroy(&it); | |
| if (mt) | |
| reftable_merged_table_free(mt); | |
| reftable_ref_record_release(&ref); | |
| reftable_log_record_release(&log); | |
| st->stats.entries_written += entries; | |
| return err; | |
| } | |
| static int stack_compact_locked(struct reftable_stack *st, | |
| size_t first, size_t last, | |
| struct reftable_log_expiry_config *config, | |
| struct reftable_tmpfile *tab_file_out) | |
| { | |
| struct reftable_buf next_name = REFTABLE_BUF_INIT; | |
| struct reftable_buf tab_file_path = REFTABLE_BUF_INIT; | |
| struct reftable_writer *wr = NULL; | |
| struct fd_writer writer= { | |
| .opts = &st->opts, | |
| }; | |
| struct reftable_tmpfile tab_file = REFTABLE_TMPFILE_INIT; | |
| int err = 0; | |
| err = format_name(&next_name, reftable_table_min_update_index(st->tables[first]), | |
| reftable_table_max_update_index(st->tables[last])); | |
| if (err < 0) | |
| goto done; | |
| err = stack_filename(&tab_file_path, st, next_name.buf); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_buf_addstr(&tab_file_path, ".temp.XXXXXX"); | |
| if (err < 0) | |
| goto done; | |
| err = tmpfile_from_pattern(&tab_file, tab_file_path.buf); | |
| if (err < 0) | |
| goto done; | |
| if (st->opts.default_permissions && | |
| chmod(tab_file.path, st->opts.default_permissions) < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| writer.fd = tab_file.fd; | |
| err = reftable_writer_new(&wr, fd_writer_write, fd_writer_flush, | |
| &writer, &st->opts); | |
| if (err < 0) | |
| goto done; | |
| err = stack_write_compact(st, wr, first, last, config); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_writer_close(wr); | |
| if (err < 0) | |
| goto done; | |
| err = tmpfile_close(&tab_file); | |
| if (err < 0) | |
| goto done; | |
| *tab_file_out = tab_file; | |
| tab_file = REFTABLE_TMPFILE_INIT; | |
| done: | |
| tmpfile_delete(&tab_file); | |
| reftable_writer_free(wr); | |
| reftable_buf_release(&next_name); | |
| reftable_buf_release(&tab_file_path); | |
| return err; | |
| } | |
| enum stack_compact_range_flags { | |
| /* | |
| * Perform a best-effort compaction. That is, even if we cannot lock | |
| * all tables in the specified range, we will try to compact the | |
| * remaining slice. | |
| */ | |
| STACK_COMPACT_RANGE_BEST_EFFORT = (1 << 0), | |
| }; | |
| /* | |
| * Compact all tables in the range `[first, last)` into a single new table. | |
| * | |
| * This function returns `0` on success or a code `< 0` on failure. When the | |
| * stack or any of the tables in the specified range are already locked then | |
| * this function returns `REFTABLE_LOCK_ERROR`. This is a benign error that | |
| * callers can either ignore, or they may choose to retry compaction after some | |
| * amount of time. | |
| */ | |
| static int stack_compact_range(struct reftable_stack *st, | |
| size_t first, size_t last, | |
| struct reftable_log_expiry_config *expiry, | |
| unsigned int flags) | |
| { | |
| struct reftable_buf tables_list_buf = REFTABLE_BUF_INIT; | |
| struct reftable_buf new_table_name = REFTABLE_BUF_INIT; | |
| struct reftable_buf new_table_path = REFTABLE_BUF_INIT; | |
| struct reftable_buf table_name = REFTABLE_BUF_INIT; | |
| struct reftable_flock tables_list_lock = REFTABLE_FLOCK_INIT; | |
| struct reftable_flock *table_locks = NULL; | |
| struct reftable_tmpfile new_table = REFTABLE_TMPFILE_INIT; | |
| int is_empty_table = 0, err = 0; | |
| size_t first_to_replace, last_to_replace; | |
| size_t i, nlocks = 0; | |
| char **names = NULL; | |
| if (first > last || (!expiry && first == last)) { | |
| err = 0; | |
| goto done; | |
| } | |
| st->stats.attempts++; | |
| /* | |
| * Hold the lock so that we can read "tables.list" and lock all tables | |
| * which are part of the user-specified range. | |
| */ | |
| err = flock_acquire(&tables_list_lock, st->list_file, st->opts.lock_timeout_ms); | |
| if (err < 0) | |
| goto done; | |
| /* | |
| * Check whether the stack is up-to-date. We unfortunately cannot | |
| * handle the situation gracefully in case it's _not_ up-to-date | |
| * because the range of tables that the user has requested us to | |
| * compact may have been changed. So instead we abort. | |
| * | |
| * We could in theory improve the situation by having the caller not | |
| * pass in a range, but instead the list of tables to compact. If so, | |
| * we could check that relevant tables still exist. But for now it's | |
| * good enough to just abort. | |
| */ | |
| err = stack_uptodate(st); | |
| if (err < 0) | |
| goto done; | |
| if (err > 0) { | |
| err = REFTABLE_OUTDATED_ERROR; | |
| goto done; | |
| } | |
| /* | |
| * Lock all tables in the user-provided range. This is the slice of our | |
| * stack which we'll compact. | |
| * | |
| * Note that we lock tables in reverse order from last to first. The | |
| * intent behind this is to allow a newer process to perform best | |
| * effort compaction of tables that it has added in the case where an | |
| * older process is still busy compacting tables which are preexisting | |
| * from the point of view of the newer process. | |
| */ | |
| REFTABLE_ALLOC_ARRAY(table_locks, last - first + 1); | |
| if (!table_locks) { | |
| err = REFTABLE_OUT_OF_MEMORY_ERROR; | |
| goto done; | |
| } | |
| for (i = 0; i < last - first + 1; i++) | |
| table_locks[i] = REFTABLE_FLOCK_INIT; | |
| for (i = last + 1; i > first; i--) { | |
| err = stack_filename(&table_name, st, reftable_table_name(st->tables[i - 1])); | |
| if (err < 0) | |
| goto done; | |
| err = flock_acquire(&table_locks[nlocks], table_name.buf, 0); | |
| if (err < 0) { | |
| /* | |
| * When the table is locked already we may do a | |
| * best-effort compaction and compact only the tables | |
| * that we have managed to lock so far. This of course | |
| * requires that we have been able to lock at least two | |
| * tables, otherwise there would be nothing to compact. | |
| * In that case, we return a lock error to our caller. | |
| */ | |
| if (err == REFTABLE_LOCK_ERROR && last - (i - 1) >= 2 && | |
| flags & STACK_COMPACT_RANGE_BEST_EFFORT) { | |
| err = 0; | |
| /* | |
| * The subtraction is to offset the index, the | |
| * addition is to only compact up to the table | |
| * of the preceding iteration. They obviously | |
| * cancel each other out, but that may be | |
| * non-obvious when it was omitted. | |
| */ | |
| first = (i - 1) + 1; | |
| break; | |
| } | |
| goto done; | |
| } | |
| /* | |
| * We need to close the lockfiles as we might otherwise easily | |
| * run into file descriptor exhaustion when we compress a lot | |
| * of tables. | |
| */ | |
| err = flock_close(&table_locks[nlocks++]); | |
| if (err < 0) | |
| goto done; | |
| } | |
| /* | |
| * We have locked all tables in our range and can thus release the | |
| * "tables.list" lock while compacting the locked tables. This allows | |
| * concurrent updates to the stack to proceed. | |
| */ | |
| err = flock_release(&tables_list_lock); | |
| if (err < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| /* | |
| * Compact the now-locked tables into a new table. Note that compacting | |
| * these tables may end up with an empty new table in case tombstones | |
| * end up cancelling out all refs in that range. | |
| */ | |
| err = stack_compact_locked(st, first, last, expiry, &new_table); | |
| if (err < 0) { | |
| if (err != REFTABLE_EMPTY_TABLE_ERROR) | |
| goto done; | |
| is_empty_table = 1; | |
| } | |
| /* | |
| * Now that we have written the new, compacted table we need to re-lock | |
| * "tables.list". We'll then replace the compacted range of tables with | |
| * the new table. | |
| */ | |
| err = flock_acquire(&tables_list_lock, st->list_file, st->opts.lock_timeout_ms); | |
| if (err < 0) | |
| goto done; | |
| if (st->opts.default_permissions) { | |
| if (chmod(tables_list_lock.path, | |
| st->opts.default_permissions) < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| } | |
| /* | |
| * As we have unlocked the stack while compacting our slice of tables | |
| * it may have happened that a concurrently running process has updated | |
| * the stack while we were compacting. In that case, we need to check | |
| * whether the tables that we have just compacted still exist in the | |
| * stack in the exact same order as we have compacted them. | |
| * | |
| * If they do exist, then it is fine to continue and replace those | |
| * tables with our compacted version. If they don't, then we need to | |
| * abort. | |
| */ | |
| err = stack_uptodate(st); | |
| if (err < 0) | |
| goto done; | |
| if (err > 0) { | |
| ssize_t new_offset = -1; | |
| int fd; | |
| fd = open(st->list_file, O_RDONLY); | |
| if (fd < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| goto done; | |
| } | |
| err = fd_read_lines(fd, &names); | |
| close(fd); | |
| if (err < 0) | |
| goto done; | |
| /* | |
| * Search for the offset of the first table that we have | |
| * compacted in the updated "tables.list" file. | |
| */ | |
| for (size_t i = 0; names[i]; i++) { | |
| if (strcmp(names[i], st->tables[first]->name)) | |
| continue; | |
| /* | |
| * We have found the first entry. Verify that all the | |
| * subsequent tables we have compacted still exist in | |
| * the modified stack in the exact same order as we | |
| * have compacted them. | |
| */ | |
| for (size_t j = 1; j < last - first + 1; j++) { | |
| const char *old = first + j < st->merged->tables_len ? | |
| st->tables[first + j]->name : NULL; | |
| const char *new = names[i + j]; | |
| /* | |
| * If some entries are missing or in case the tables | |
| * have changed then we need to bail out. Again, this | |
| * shouldn't ever happen because we have locked the | |
| * tables we are compacting. | |
| */ | |
| if (!old || !new || strcmp(old, new)) { | |
| err = REFTABLE_OUTDATED_ERROR; | |
| goto done; | |
| } | |
| } | |
| new_offset = i; | |
| break; | |
| } | |
| /* | |
| * In case we didn't find our compacted tables in the stack we | |
| * need to bail out. In theory, this should have never happened | |
| * because we locked the tables we are compacting. | |
| */ | |
| if (new_offset < 0) { | |
| err = REFTABLE_OUTDATED_ERROR; | |
| goto done; | |
| } | |
| /* | |
| * We have found the new range that we want to replace, so | |
| * let's update the range of tables that we want to replace. | |
| */ | |
| first_to_replace = new_offset; | |
| last_to_replace = last + (new_offset - first); | |
| } else { | |
| /* | |
| * `fd_read_lines()` uses a `NULL` sentinel to indicate that | |
| * the array is at its end. As we use `free_names()` to free | |
| * the array, we need to include this sentinel value here and | |
| * thus have to allocate `tables_len + 1` many entries. | |
| */ | |
| REFTABLE_CALLOC_ARRAY(names, st->merged->tables_len + 1); | |
| if (!names) { | |
| err = REFTABLE_OUT_OF_MEMORY_ERROR; | |
| goto done; | |
| } | |
| for (size_t i = 0; i < st->merged->tables_len; i++) { | |
| names[i] = reftable_strdup(st->tables[i]->name); | |
| if (!names[i]) { | |
| err = REFTABLE_OUT_OF_MEMORY_ERROR; | |
| goto done; | |
| } | |
| } | |
| first_to_replace = first; | |
| last_to_replace = last; | |
| } | |
| /* | |
| * If the resulting compacted table is not empty, then we need to move | |
| * it into place now. | |
| */ | |
| if (!is_empty_table) { | |
| err = format_name(&new_table_name, st->tables[first]->min_update_index, | |
| st->tables[last]->max_update_index); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_buf_addstr(&new_table_name, ".ref"); | |
| if (err < 0) | |
| goto done; | |
| err = stack_filename(&new_table_path, st, new_table_name.buf); | |
| if (err < 0) | |
| goto done; | |
| err = tmpfile_rename(&new_table, new_table_path.buf); | |
| if (err < 0) | |
| goto done; | |
| } | |
| /* | |
| * Write the new "tables.list" contents with the compacted table we | |
| * have just written. In case the compacted table became empty we | |
| * simply skip writing it. | |
| */ | |
| for (i = 0; i < first_to_replace; i++) { | |
| if ((err = reftable_buf_addstr(&tables_list_buf, names[i])) < 0 || | |
| (err = reftable_buf_addstr(&tables_list_buf, "\n")) < 0) | |
| goto done; | |
| } | |
| if (!is_empty_table) { | |
| if ((err = reftable_buf_addstr(&tables_list_buf, new_table_name.buf)) < 0 || | |
| (err = reftable_buf_addstr(&tables_list_buf, "\n")) < 0) | |
| goto done; | |
| } | |
| for (i = last_to_replace + 1; names[i]; i++) { | |
| if ((err = reftable_buf_addstr(&tables_list_buf, names[i])) < 0 || | |
| (err = reftable_buf_addstr(&tables_list_buf, "\n")) < 0) | |
| goto done; | |
| } | |
| err = reftable_write_data(tables_list_lock.fd, | |
| tables_list_buf.buf, tables_list_buf.len); | |
| if (err < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| unlink(new_table_path.buf); | |
| goto done; | |
| } | |
| err = fsync(tables_list_lock.fd); | |
| if (err < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| unlink(new_table_path.buf); | |
| goto done; | |
| } | |
| err = flock_commit(&tables_list_lock); | |
| if (err < 0) { | |
| err = REFTABLE_IO_ERROR; | |
| unlink(new_table_path.buf); | |
| goto done; | |
| } | |
| /* | |
| * Reload the stack before deleting the compacted tables. We can only | |
| * delete the files after we closed them on Windows, so this needs to | |
| * happen first. | |
| */ | |
| err = reftable_stack_reload_maybe_reuse(st, first < last); | |
| if (err < 0) | |
| goto done; | |
| /* | |
| * Delete the old tables. They may still be in use by concurrent | |
| * readers, so it is expected that unlinking tables may fail. | |
| */ | |
| for (i = 0; i < nlocks; i++) { | |
| struct reftable_flock *table_lock = &table_locks[i]; | |
| reftable_buf_reset(&table_name); | |
| err = reftable_buf_add(&table_name, table_lock->path, | |
| strlen(table_lock->path) - strlen(".lock")); | |
| if (err) | |
| continue; | |
| unlink(table_name.buf); | |
| } | |
| done: | |
| flock_release(&tables_list_lock); | |
| for (i = 0; table_locks && i < nlocks; i++) | |
| flock_release(&table_locks[i]); | |
| reftable_free(table_locks); | |
| tmpfile_delete(&new_table); | |
| reftable_buf_release(&new_table_name); | |
| reftable_buf_release(&new_table_path); | |
| reftable_buf_release(&tables_list_buf); | |
| reftable_buf_release(&table_name); | |
| free_names(names); | |
| if (err == REFTABLE_LOCK_ERROR) | |
| st->stats.failures++; | |
| return err; | |
| } | |
| int reftable_stack_compact_all(struct reftable_stack *st, | |
| struct reftable_log_expiry_config *config) | |
| { | |
| size_t last = st->merged->tables_len ? st->merged->tables_len - 1 : 0; | |
| return stack_compact_range(st, 0, last, config, 0); | |
| } | |
| static int segment_size(struct segment *s) | |
| { | |
| return s->end - s->start; | |
| } | |
| struct segment suggest_compaction_segment(uint64_t *sizes, size_t n, | |
| uint8_t factor) | |
| { | |
| struct segment seg = { 0 }; | |
| uint64_t bytes; | |
| size_t i; | |
| if (!factor) | |
| factor = DEFAULT_GEOMETRIC_FACTOR; | |
| /* | |
| * If there are no tables or only a single one then we don't have to | |
| * compact anything. The sequence is geometric by definition already. | |
| */ | |
| if (n <= 1) | |
| return seg; | |
| /* | |
| * Find the ending table of the compaction segment needed to restore the | |
| * geometric sequence. Note that the segment end is exclusive. | |
| * | |
| * To do so, we iterate backwards starting from the most recent table | |
| * until a valid segment end is found. If the preceding table is smaller | |
| * than the current table multiplied by the geometric factor (2), the | |
| * compaction segment end has been identified. | |
| * | |
| * Tables after the ending point are not added to the byte count because | |
| * they are already valid members of the geometric sequence. Due to the | |
| * properties of a geometric sequence, it is not possible for the sum of | |
| * these tables to exceed the value of the ending point table. | |
| * | |
| * Example table size sequence requiring no compaction: | |
| * 64, 32, 16, 8, 4, 2, 1 | |
| * | |
| * Example table size sequence where compaction segment end is set to | |
| * the last table. Since the segment end is exclusive, the last table is | |
| * excluded during subsequent compaction and the table with size 3 is | |
| * the final table included: | |
| * 64, 32, 16, 8, 4, 3, 1 | |
| */ | |
| for (i = n - 1; i > 0; i--) { | |
| if (sizes[i - 1] < sizes[i] * factor) { | |
| seg.end = i + 1; | |
| bytes = sizes[i]; | |
| break; | |
| } | |
| } | |
| /* | |
| * Find the starting table of the compaction segment by iterating | |
| * through the remaining tables and keeping track of the accumulated | |
| * size of all tables seen from the segment end table. The previous | |
| * table is compared to the accumulated size because the tables from the | |
| * segment end are merged backwards recursively. | |
| * | |
| * Note that we keep iterating even after we have found the first | |
| * starting point. This is because there may be tables in the stack | |
| * preceding that first starting point which violate the geometric | |
| * sequence. | |
| * | |
| * Example compaction segment start set to table with size 32: | |
| * 128, 32, 16, 8, 4, 3, 1 | |
| */ | |
| for (; i > 0; i--) { | |
| uint64_t curr = bytes; | |
| bytes += sizes[i - 1]; | |
| if (sizes[i - 1] < curr * factor) { | |
| seg.start = i - 1; | |
| seg.bytes = bytes; | |
| } | |
| } | |
| return seg; | |
| } | |
| static int stack_segments_for_compaction(struct reftable_stack *st, | |
| struct segment *seg) | |
| { | |
| int version = (st->opts.hash_id == REFTABLE_HASH_SHA1) ? 1 : 2; | |
| int overhead = header_size(version) - 1; | |
| uint64_t *sizes; | |
| REFTABLE_CALLOC_ARRAY(sizes, st->merged->tables_len); | |
| if (!sizes) | |
| return REFTABLE_OUT_OF_MEMORY_ERROR; | |
| for (size_t i = 0; i < st->merged->tables_len; i++) | |
| sizes[i] = st->tables[i]->size - overhead; | |
| *seg = suggest_compaction_segment(sizes, st->merged->tables_len, | |
| st->opts.auto_compaction_factor); | |
| reftable_free(sizes); | |
| return 0; | |
| } | |
| static int update_segment_if_compaction_required(struct reftable_stack *st, | |
| struct segment *seg, | |
| bool use_geometric, | |
| bool *required) | |
| { | |
| int err; | |
| if (st->merged->tables_len < 2) { | |
| *required = false; | |
| return 0; | |
| } | |
| if (!use_geometric) { | |
| *required = true; | |
| return 0; | |
| } | |
| err = stack_segments_for_compaction(st, seg); | |
| if (err) | |
| return err; | |
| *required = segment_size(seg) > 0; | |
| return 0; | |
| } | |
| int reftable_stack_compaction_required(struct reftable_stack *st, | |
| bool use_heuristics, | |
| bool *required) | |
| { | |
| struct segment seg; | |
| return update_segment_if_compaction_required(st, &seg, use_heuristics, | |
| required); | |
| } | |
| int reftable_stack_auto_compact(struct reftable_stack *st) | |
| { | |
| struct segment seg; | |
| bool required; | |
| int err; | |
| err = update_segment_if_compaction_required(st, &seg, true, &required); | |
| if (err) | |
| return err; | |
| if (required) | |
| return stack_compact_range(st, seg.start, seg.end - 1, | |
| NULL, STACK_COMPACT_RANGE_BEST_EFFORT); | |
| return 0; | |
| } | |
| struct reftable_compaction_stats * | |
| reftable_stack_compaction_stats(struct reftable_stack *st) | |
| { | |
| return &st->stats; | |
| } | |
| int reftable_stack_read_ref(struct reftable_stack *st, const char *refname, | |
| struct reftable_ref_record *ref) | |
| { | |
| struct reftable_iterator it = { 0 }; | |
| int ret; | |
| ret = reftable_merged_table_init_ref_iterator(st->merged, &it); | |
| if (ret) | |
| goto out; | |
| ret = reftable_iterator_seek_ref(&it, refname); | |
| if (ret) | |
| goto out; | |
| ret = reftable_iterator_next_ref(&it, ref); | |
| if (ret) | |
| goto out; | |
| if (strcmp(ref->refname, refname) || | |
| reftable_ref_record_is_deletion(ref)) { | |
| reftable_ref_record_release(ref); | |
| ret = 1; | |
| goto out; | |
| } | |
| out: | |
| reftable_iterator_destroy(&it); | |
| return ret; | |
| } | |
| int reftable_stack_read_log(struct reftable_stack *st, const char *refname, | |
| struct reftable_log_record *log) | |
| { | |
| struct reftable_iterator it = {0}; | |
| int err; | |
| err = reftable_stack_init_log_iterator(st, &it); | |
| if (err) | |
| goto done; | |
| err = reftable_iterator_seek_log(&it, refname); | |
| if (err) | |
| goto done; | |
| err = reftable_iterator_next_log(&it, log); | |
| if (err) | |
| goto done; | |
| if (strcmp(log->refname, refname) || | |
| reftable_log_record_is_deletion(log)) { | |
| err = 1; | |
| goto done; | |
| } | |
| done: | |
| if (err) { | |
| reftable_log_record_release(log); | |
| } | |
| reftable_iterator_destroy(&it); | |
| return err; | |
| } | |
| static int is_table_name(const char *s) | |
| { | |
| const char *dot = strrchr(s, '.'); | |
| return dot && !strcmp(dot, ".ref"); | |
| } | |
| static void remove_maybe_stale_table(struct reftable_stack *st, uint64_t max, | |
| const char *name) | |
| { | |
| int err = 0; | |
| uint64_t update_idx = 0; | |
| struct reftable_block_source src = { NULL }; | |
| struct reftable_table *table = NULL; | |
| struct reftable_buf table_path = REFTABLE_BUF_INIT; | |
| err = stack_filename(&table_path, st, name); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_block_source_from_file(&src, table_path.buf); | |
| if (err < 0) | |
| goto done; | |
| err = reftable_table_new(&table, &src, name); | |
| if (err < 0) | |
| goto done; | |
| update_idx = reftable_table_max_update_index(table); | |
| reftable_table_decref(table); | |
| if (update_idx <= max) { | |
| unlink(table_path.buf); | |
| } | |
| done: | |
| reftable_buf_release(&table_path); | |
| } | |
| static int reftable_stack_clean_locked(struct reftable_stack *st) | |
| { | |
| uint64_t max = reftable_merged_table_max_update_index( | |
| reftable_stack_merged_table(st)); | |
| DIR *dir = opendir(st->reftable_dir); | |
| struct dirent *d = NULL; | |
| if (!dir) { | |
| return REFTABLE_IO_ERROR; | |
| } | |
| while ((d = readdir(dir))) { | |
| int found = 0; | |
| if (!is_table_name(d->d_name)) | |
| continue; | |
| for (size_t i = 0; !found && i < st->tables_len; i++) | |
| found = !strcmp(reftable_table_name(st->tables[i]), d->d_name); | |
| if (found) | |
| continue; | |
| remove_maybe_stale_table(st, max, d->d_name); | |
| } | |
| closedir(dir); | |
| return 0; | |
| } | |
| int reftable_stack_clean(struct reftable_stack *st) | |
| { | |
| struct reftable_addition *add = NULL; | |
| int err = reftable_stack_new_addition(&add, st, 0); | |
| if (err < 0) { | |
| goto done; | |
| } | |
| err = reftable_stack_reload(st); | |
| if (err < 0) { | |
| goto done; | |
| } | |
| err = reftable_stack_clean_locked(st); | |
| done: | |
| reftable_addition_destroy(add); | |
| return err; | |
| } | |
| enum reftable_hash reftable_stack_hash_id(struct reftable_stack *st) | |
| { | |
| return reftable_merged_table_hash_id(st->merged); | |
| } |