git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH 7/8] reftable/stack: fix corruption on concurrent compaction

From
Patrick Steinhardt <ps@pks.im>
Date
Jul 31, 2024, 14:15 UTC
Message-ID
<b746565bf0ff80fff60821bdeb79867ad646c142.1722435214.git.ps@pks.im>
In-Reply-To
<cover.1722435214.git.ps@pks.im>
The locking employed by compaction uses the following schema:
  1. Lock "tables.list" and verify that it matches the version we have
     loaded in core.
  2. Lock each of the tables in the user-supplied range of tables that
     we are supposed to compact. These locks prohibit any concurrent
     process to compact those tables while we are doing that.
  3. Unlock "tables.list". This enables concurrent processes to add new
     tables to the stack, but also allows them to compact tables outside
     of the range of tables that we have locked.
  4. Perform the compaction.
  5. Lock "tables.list" again.
  6. Move the compacted table into place.
  7. Write the new order of tables, including the compacted table, into
     the lockfile.
  8. Commit the lockfile into place.

Letting concurrent processes modify the "tables.list" file while we are doing the compaction is very much part of the design and thus expected. After all, it may take some time to compact tables in the case where we are compacting a lot or very large tables.

But there is a bug in the code. Suppose we have two processes which are compacting two slices of the table. Given that we lock each of the tables before compacting them, we know that the slices must be disjunct from each other. But regardless of that, compaction performed by one process will always impact what the other process needs to write to the "tables.list" file.

Right now , we do not check whether the "tables.list" has been changed after we have locked it for the second time in (5). This has the consequence that we will always commit the old, cached in-core tables to disk without paying to respect what the other process has written. This scenario would then lead to data loss and corruption.

This can even happen in the simpler case of one compacting process and one writing process. The newly-appended table by the writing process would get discarded by the compacting process because it never sees the new table.

Fix this bug by re-checking whether our stack is still up to date after locking for the second time. If it isn't, then we adjust the indices of tables to replace in the updated stack.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 reftable/stack.c | 101 ++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 96 insertions(+), 5 deletions(-)
diff --git a/reftable/stack.c b/reftable/stack.c
index 9cc91a262c..2b1ac58120 100644
--- a/reftable/stack.c
+++ b/reftable/stack.c
@@ -1021,7 +1021,9 @@ static int stack_compact_range(struct reftable_stack *st,
 	struct lock_file *table_locks = NULL;
 	struct tempfile *new_table = NULL;
 	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;
@@ -1124,6 +1126,94 @@ static int stack_compact_range(struct reftable_stack *st,
 		}
 	}
 
+	/*
+	 * 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->readers[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->stack_len ?
+					st->readers[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.
+		 */
+		last_to_replace = last + (new_offset - first);
+		first_to_replace = new_offset;
+	} else {
+		REFTABLE_CALLOC_ARRAY(names, st->merged->stack_len + 1);
+		for (size_t i = 0; i < st->merged->stack_len; i++)
+			names[i] = xstrdup(st->readers[i]->name);
+		last_to_replace = last;
+		first_to_replace = first;
+	}
+
 	/*
 	 * If the resulting compacted table is not empty, then we need to move
 	 * it into place now.
@@ -1146,12 +1236,12 @@ static int stack_compact_range(struct reftable_stack *st,
 	 * have just written. In case the compacted table became empty we
 	 * simply skip writing it.
 	 */
-	for (i = 0; i < first; i++)
-		strbuf_addf(&tables_list_buf, "%s\n", st->readers[i]->name);
+	for (i = 0; i < first_to_replace; i++)
+		strbuf_addf(&tables_list_buf, "%s\n", names[i]);
 	if (!is_empty_table)
 		strbuf_addf(&tables_list_buf, "%s\n", new_table_name.buf);
-	for (i = last + 1; i < st->merged->stack_len; i++)
-		strbuf_addf(&tables_list_buf, "%s\n", st->readers[i]->name);
+	for (i = last_to_replace + 1; names[i]; i++)
+		strbuf_addf(&tables_list_buf, "%s\n", names[i]);
 
 	err = write_in_full(get_lock_file_fd(&tables_list_lock),
 			    tables_list_buf.buf, tables_list_buf.len);
@@ -1204,9 +1294,10 @@ static int stack_compact_range(struct reftable_stack *st,
 	delete_tempfile(&new_table);
 	strbuf_release(&new_table_name);
 	strbuf_release(&new_table_path);
-
 	strbuf_release(&tables_list_buf);
 	strbuf_release(&table_name);
+	free_names(names);
+
 	return err;
 }
 
-- 
2.46.0.dirty
Previous: Patrick SteinhardtNext: Justin Tobler
Message 16 of 50 in “reftable: improvements and fixes for compaction”
  1. 0/8 reftable: improvements and fixes for compactionPatrick Steinhardt, Jul 31, 2024
  2. 1/8 reftable/stack: refactor function to gather table sizesPatrick Steinhardt, Jul 31, 2024
  3. Junio C HamanoAug 2, 2024
  4. 2/8 reftable/stack: test compaction with already-locked tablesPatrick Steinhardt, Jul 31, 2024
  5. Junio C HamanoAug 2, 2024
  6. Patrick SteinhardtAug 5, 2024
  7. 3/8 reftable/stack: update stats on failed full compactionPatrick Steinhardt, Jul 31, 2024
  8. 4/8 reftable/stack: simplify tracking of table locksPatrick Steinhardt, Jul 31, 2024
  9. Justin ToblerJul 31, 2024
  10. Junio C HamanoAug 2, 2024
  11. Patrick SteinhardtAug 5, 2024
  12. 5/8 reftable/stack: do not die when fsyncing lock file filesPatrick Steinhardt, Jul 31, 2024
  13. 6/8 reftable/stack: use lock_file when adding table to "tables.list"Patrick Steinhardt, Jul 31, 2024
  14. Justin ToblerJul 31, 2024
  15. Patrick SteinhardtAug 1, 2024
  16. 7/8 reftable/stack: fix corruption on concurrent compactionPatrick Steinhardt, Jul 31, 2024
  17. Justin ToblerAug 1, 2024
  18. Patrick SteinhardtAug 1, 2024
  19. 8/8 reftable/stack: handle locked tables during auto-compactionPatrick Steinhardt, Jul 31, 2024
  20. Junio C HamanoAug 2, 2024
  21. Patrick SteinhardtAug 5, 2024
  22. 0/9 reftable: improvements and fixes for compactionPatrick Steinhardt, Aug 5, 2024
  23. 1/9 reftable/stack: refactor function to gather table sizesPatrick Steinhardt, Aug 5, 2024
  24. 2/9 reftable/stack: extract function to setup stack with N tablesPatrick Steinhardt, Aug 5, 2024
  25. 3/9 reftable/stack: test compaction with already-locked tablesPatrick Steinhardt, Aug 5, 2024
  26. Karthik NayakAug 8, 2024
  27. 4/9 reftable/stack: update stats on failed full compactionPatrick Steinhardt, Aug 5, 2024
  28. 5/9 reftable/stack: simplify tracking of table locksPatrick Steinhardt, Aug 5, 2024
  29. 6/9 reftable/stack: do not die when fsyncing lock file filesPatrick Steinhardt, Aug 5, 2024
  30. 7/9 reftable/stack: use lock_file when adding table to "tables.list"Patrick Steinhardt, Aug 5, 2024
  31. 8/9 reftable/stack: fix corruption on concurrent compactionPatrick Steinhardt, Aug 5, 2024
  32. Karthik NayakAug 8, 2024
  33. Patrick SteinhardtAug 8, 2024
  34. 9/9 reftable/stack: handle locked tables during auto-compactionPatrick Steinhardt, Aug 5, 2024
  35. Justin ToblerAug 6, 2024
  36. Patrick SteinhardtAug 7, 2024
  37. Justin ToblerAug 7, 2024
  38. Karthik NayakAug 8, 2024
  39. Karthik NayakAug 8, 2024
  40. 0/9 reftable: improvements and fixes for compactionPatrick Steinhardt, Aug 8, 2024
  41. 1/9 reftable/stack: refactor function to gather table sizesPatrick Steinhardt, Aug 8, 2024
  42. 2/9 reftable/stack: extract function to setup stack with N tablesPatrick Steinhardt, Aug 8, 2024
  43. 3/9 reftable/stack: test compaction with already-locked tablesPatrick Steinhardt, Aug 8, 2024
  44. 4/9 reftable/stack: update stats on failed full compactionPatrick Steinhardt, Aug 8, 2024
  45. 5/9 reftable/stack: simplify tracking of table locksPatrick Steinhardt, Aug 8, 2024
  46. 6/9 reftable/stack: do not die when fsyncing lock file filesPatrick Steinhardt, Aug 8, 2024
  47. 7/9 reftable/stack: use lock_file when adding table to "tables.list"Patrick Steinhardt, Aug 8, 2024
  48. 8/9 reftable/stack: fix corruption on concurrent compactionPatrick Steinhardt, Aug 8, 2024
  49. 9/9 reftable/stack: handle locked tables during auto-compactionPatrick Steinhardt, Aug 8, 2024
  50. Karthik NayakAug 8, 2024

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.