From 26a3c8cfafb072977d8c71d1f1f16a4f5d81a9c9 Mon Sep 17 00:00:00 2001
From: Vladimir Davydov <vdavydov@tarantool.org>
Date: Mon, 11 Nov 2024 18:14:24 +0300
Subject: [PATCH] vinyl: fix duplicate multikey stmt accounting with deferred
 deletes

`vy_mem_insert()` and `vy_mem_insert_upsert()` increment the row count
statistic of `vy_mem` only if no statement is replaced, which is
correct, while `vy_lsm_commit()` increments the row count of `vy_lsm`
unconditionally. As a result, `vy_lsm` may report a non-zero statement
count (via `index.stat()` or `index.len()`) after a dump. This may
happen only with a non-unique multikey index, when the statement has
duplicates in the indexed array, and only if the `deferred_deletes`
option is enabled, because otherwise we drop duplicates when we form
the transaction write set, see `vy_tx_set()`. With `deferred_deletes`,
we may create a `txv` for each multikey entry at the time when we
prepare to commit the transaction, see `vy_tx_handle_deferred_delete()`.

Another problem is that `vy_mem_rollback_stmt()` always decrements
the row count, even if it didn't find the rolled back statement in
the tree. As a result, if the transaction with duplicate multikey
entries is rolled back on WAL error, we'll decrement the row count
of `vy_mem` more times than necessary.

To fix this issue, let's make the `vy_mem` methods update the in-memory
statistic of `vy_lsm`. This way they should always stay in-sync. Also,
we make `vy_mem_rollback_stmt()` skip updating the statistics in case
the rolled back statement isn't present in the tree.

This issue results in `vinyl-luatest/select_consistency_test.lua`
flakiness when checking `index.len()` after compaction. Let's make
the test more thorough and also check that `index.len()` equals
`index.count()`.

Closes #10751
Part of #10752

NO_DOC=bug fix

(cherry picked from commit e8810c555d4e6ba56e6c798e04216aa11efb5304)
---
 ...-10751-fix-vinyl-memory-stmt-accounting.md |  5 ++
 src/box/vinyl.c                               |  4 +-
 src/box/vy_lsm.c                              | 24 +++---
 src/box/vy_mem.c                              | 39 ++++++---
 src/box/vy_mem.h                              | 12 ++-
 test/unit/vy_iterators_helper.c               |  5 +-
 test/unit/vy_iterators_helper.h               |  1 +
 test/unit/vy_mem.c                            |  2 +-
 ...uplicate_multikey_stmt_accounting_test.lua | 80 +++++++++++++++++++
 .../vinyl-luatest/select_consistency_test.lua | 15 +++-
 10 files changed, 149 insertions(+), 38 deletions(-)
 create mode 100644 changelogs/unreleased/gh-10751-fix-vinyl-memory-stmt-accounting.md
 create mode 100644 test/vinyl-luatest/gh_10751_duplicate_multikey_stmt_accounting_test.lua

diff --git a/changelogs/unreleased/gh-10751-fix-vinyl-memory-stmt-accounting.md b/changelogs/unreleased/gh-10751-fix-vinyl-memory-stmt-accounting.md
new file mode 100644
index 0000000000..830bb55940
--- /dev/null
+++ b/changelogs/unreleased/gh-10751-fix-vinyl-memory-stmt-accounting.md
@@ -0,0 +1,5 @@
+## bugfix/vinyl
+
+* Fixed a bug when `index.stat()` and `index.len()` could report a wrong number
+  of in-memory statements for a non-unique multi-key index of a space with
+  the `defer_deletes` option enabled (gh-10751).
diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index ebe59d4c0a..018f2a409b 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -4039,11 +4039,9 @@ vy_build_insert_stmt(struct vy_lsm *lsm, struct vy_mem *mem,
 	vy_stmt_set_lsn(region_stmt, lsn);
 	struct vy_entry entry;
 	vy_stmt_foreach_entry(entry, region_stmt, lsm->cmp_def) {
-		if (vy_mem_insert(mem, entry) != 0)
+		if (vy_mem_insert(mem, entry, &lsm->stat.memory.count) != 0)
 			return -1;
 		vy_mem_commit_stmt(mem, entry);
-		vy_stmt_counter_acct_tuple(&lsm->stat.memory.count,
-					   region_stmt);
 	}
 	return 0;
 }
diff --git a/src/box/vy_lsm.c b/src/box/vy_lsm.c
index 24aa50b52d..f4468aed62 100644
--- a/src/box/vy_lsm.c
+++ b/src/box/vy_lsm.c
@@ -906,6 +906,12 @@ vy_lsm_set(struct vy_lsm *lsm, struct vy_mem *mem,
 	assert(vy_stmt_is_refable(entry.stmt));
 	assert(*region_stmt == NULL || !vy_stmt_is_refable(*region_stmt));
 
+	/* Abort transaction if format was changed by DDL */
+	if (!vy_stmt_is_key(entry.stmt) &&
+	    format_id != tuple_format_id(mem->format)) {
+		diag_set(ClientError, ER_TRANSACTION_CONFLICT);
+		return -1;
+	}
 	/*
 	 * Allocate region_stmt on demand.
 	 *
@@ -923,19 +929,11 @@ vy_lsm_set(struct vy_lsm *lsm, struct vy_mem *mem,
 	}
 	entry.stmt = *region_stmt;
 
-	/* We can't free region_stmt below, so let's add it to the stats */
-	lsm->stat.memory.count.bytes += tuple_size(entry.stmt);
-
-	/* Abort transaction if format was changed by DDL */
-	if (!vy_stmt_is_key(entry.stmt) &&
-	    format_id != tuple_format_id(mem->format)) {
-		diag_set(ClientError, ER_TRANSACTION_CONFLICT);
-		return -1;
-	}
+	struct vy_stmt_counter *count = &lsm->stat.memory.count;
 	if (vy_stmt_type(*region_stmt) != IPROTO_UPSERT)
-		return vy_mem_insert(mem, entry);
+		return vy_mem_insert(mem, entry, count);
 	else
-		return vy_mem_insert_upsert(mem, entry);
+		return vy_mem_insert_upsert(mem, entry, count);
 }
 
 /**
@@ -1019,8 +1017,6 @@ vy_lsm_commit_stmt(struct vy_lsm *lsm, struct vy_mem *mem,
 {
 	vy_mem_commit_stmt(mem, entry);
 
-	lsm->stat.memory.count.rows++;
-
 	if (vy_stmt_type(entry.stmt) == IPROTO_UPSERT)
 		vy_lsm_commit_upsert(lsm, mem, entry);
 
@@ -1034,7 +1030,7 @@ void
 vy_lsm_rollback_stmt(struct vy_lsm *lsm, struct vy_mem *mem,
 		     struct vy_entry entry)
 {
-	vy_mem_rollback_stmt(mem, entry);
+	vy_mem_rollback_stmt(mem, entry, &lsm->stat.memory.count);
 
 	/* Invalidate cache element. */
 	vy_cache_on_write(&lsm->cache, entry, NULL);
diff --git a/src/box/vy_mem.c b/src/box/vy_mem.c
index 273c9b7fe4..07bf0d8ced 100644
--- a/src/box/vy_mem.c
+++ b/src/box/vy_mem.c
@@ -153,7 +153,8 @@ vy_mem_older_lsn(struct vy_mem *mem, struct vy_entry entry)
 }
 
 int
-vy_mem_insert_upsert(struct vy_mem *mem, struct vy_entry entry)
+vy_mem_insert_upsert(struct vy_mem *mem, struct vy_entry entry,
+		     struct vy_stmt_counter *count)
 {
 	assert(vy_stmt_type(entry.stmt) == IPROTO_UPSERT);
 	/* Check if the statement can be inserted in the vy_mem. */
@@ -169,9 +170,12 @@ vy_mem_insert_upsert(struct vy_mem *mem, struct vy_entry entry)
 	assert(! vy_mem_tree_iterator_is_invalid(&inserted));
 	assert(vy_entry_is_equal(entry,
 		*vy_mem_tree_iterator_get_elem(&mem->tree, &inserted)));
-	if (replaced.stmt == NULL)
+	if (replaced.stmt == NULL) {
 		mem->count.rows++;
+		count->rows++;
+	}
 	mem->count.bytes += size;
+	count->bytes += size;
 	/*
 	 * All iterators begin to see the new statement, and
 	 * will be aborted in case of rollback.
@@ -213,7 +217,8 @@ vy_mem_insert_upsert(struct vy_mem *mem, struct vy_entry entry)
 }
 
 int
-vy_mem_insert(struct vy_mem *mem, struct vy_entry entry)
+vy_mem_insert(struct vy_mem *mem, struct vy_entry entry,
+	      struct vy_stmt_counter *count)
 {
 	assert(vy_stmt_type(entry.stmt) != IPROTO_UPSERT);
 	/* Check if the statement can be inserted in the vy_mem. */
@@ -225,9 +230,12 @@ vy_mem_insert(struct vy_mem *mem, struct vy_entry entry)
 	struct vy_entry replaced = vy_entry_none();
 	if (vy_mem_tree_insert(&mem->tree, entry, &replaced, NULL))
 		return -1;
-	if (replaced.stmt == NULL)
+	if (replaced.stmt == NULL) {
 		mem->count.rows++;
+		count->rows++;
+	}
 	mem->count.bytes += size;
+	count->bytes += size;
 	/*
 	 * All iterators begin to see the new statement, and
 	 * will be aborted in case of rollback.
@@ -259,16 +267,23 @@ vy_mem_commit_stmt(struct vy_mem *mem, struct vy_entry entry)
 }
 
 void
-vy_mem_rollback_stmt(struct vy_mem *mem, struct vy_entry entry)
+vy_mem_rollback_stmt(struct vy_mem *mem, struct vy_entry entry,
+		     struct vy_stmt_counter *count)
 {
-	/* This is the statement we've inserted before. */
+	/* The statement must be from a lsregion. */
 	assert(!vy_stmt_is_refable(entry.stmt));
-	int rc = vy_mem_tree_delete(&mem->tree, entry);
-	assert(rc == 0);
-	(void) rc;
-	/* We can't free memory in case of rollback. */
-	mem->count.rows--;
-	mem->version++;
+	/*
+	 * The statement isn't present in the memory tree if it was replaced by
+	 * vy_mem_insert(). In this case we must not decrement the row count.
+	 *
+	 * Either way, we don't subtract the statement size because lsregion
+	 * doesn't support freeing memory.
+	 */
+	if (vy_mem_tree_delete(&mem->tree, entry) == 0) {
+		mem->version++;
+		mem->count.rows--;
+		count->rows--;
+	}
 }
 
 /* }}} vy_mem */
diff --git a/src/box/vy_mem.h b/src/box/vy_mem.h
index fa1bce3362..b1b9c39d5d 100644
--- a/src/box/vy_mem.h
+++ b/src/box/vy_mem.h
@@ -282,24 +282,28 @@ vy_mem_older_lsn(struct vy_mem *mem, struct vy_entry entry);
  * Insert a statement into the in-memory level.
  * @param mem        vy_mem.
  * @param entry      Vinyl statement.
+ * @param count      Statement counter to update.
  *
  * @retval  0 Success.
  * @retval -1 Memory error.
  */
 int
-vy_mem_insert(struct vy_mem *mem, struct vy_entry entry);
+vy_mem_insert(struct vy_mem *mem, struct vy_entry entry,
+	      struct vy_stmt_counter *count);
 
 /**
  * Insert an upsert statement into the mem.
  *
  * @param mem Mem to insert to.
  * @param entry Upsert statement to insert.
+ * @param count Statement counter to update.
  *
  * @retval  0 Success.
  * @retval -1 Memory error.
  */
 int
-vy_mem_insert_upsert(struct vy_mem *mem, struct vy_entry entry);
+vy_mem_insert_upsert(struct vy_mem *mem, struct vy_entry entry,
+		     struct vy_stmt_counter *count);
 
 /**
  * Confirm insertion of a statement into the in-memory level.
@@ -313,9 +317,11 @@ vy_mem_commit_stmt(struct vy_mem *mem, struct vy_entry entry);
  * Remove a statement from the in-memory level.
  * @param mem        vy_mem.
  * @param entry      Vinyl statement.
+ * @param count      Statement counter to update.
  */
 void
-vy_mem_rollback_stmt(struct vy_mem *mem, struct vy_entry entry);
+vy_mem_rollback_stmt(struct vy_mem *mem, struct vy_entry entry,
+		     struct vy_stmt_counter *count);
 
 /**
  * Iterator for in-memory level.
diff --git a/test/unit/vy_iterators_helper.c b/test/unit/vy_iterators_helper.c
index 5c24d9d4d9..bcadf46741 100644
--- a/test/unit/vy_iterators_helper.c
+++ b/test/unit/vy_iterators_helper.c
@@ -11,6 +11,7 @@ struct vy_stmt_env stmt_env;
 struct vy_mem_env mem_env;
 struct vy_cache_env cache_env;
 struct mempool history_node_pool;
+struct vy_stmt_counter dummy_count;
 
 void
 vy_iterator_C_test_init(size_t cache_size)
@@ -149,9 +150,9 @@ vy_mem_insert_template(struct vy_mem *mem, const struct vy_stmt_template *templ)
 	tuple_unref(entry.stmt);
 	entry.stmt = region_stmt;
 	if (templ->type == IPROTO_UPSERT)
-		vy_mem_insert_upsert(mem, entry);
+		vy_mem_insert_upsert(mem, entry, &dummy_count);
 	else
-		vy_mem_insert(mem, entry);
+		vy_mem_insert(mem, entry, &dummy_count);
 	return entry;
 }
 
diff --git a/test/unit/vy_iterators_helper.h b/test/unit/vy_iterators_helper.h
index c6a7f506da..a3f7edf430 100644
--- a/test/unit/vy_iterators_helper.h
+++ b/test/unit/vy_iterators_helper.h
@@ -58,6 +58,7 @@ extern struct vy_stmt_env stmt_env;
 extern struct vy_mem_env mem_env;
 extern struct vy_cache_env cache_env;
 extern struct mempool history_node_pool;
+extern struct vy_stmt_counter dummy_count;
 
 #if defined(__cplusplus)
 extern "C" {
diff --git a/test/unit/vy_mem.c b/test/unit/vy_mem.c
index e91aa458a3..d3102a6909 100644
--- a/test/unit/vy_mem.c
+++ b/test/unit/vy_mem.c
@@ -42,7 +42,7 @@ test_basic(void)
 	entry = vy_mem_insert_template(mem, &stmts[3]);
 	ok(vy_entry_is_equal(vy_mem_older_lsn(mem, entry), older),
 	   "vy_mem_rollback 1");
-	vy_mem_rollback_stmt(mem, older);
+	vy_mem_rollback_stmt(mem, older, &dummy_count);
 	ok(vy_entry_is_equal(vy_mem_older_lsn(mem, entry), olderolder),
 	   "vy_mem_rollback 2");
 
diff --git a/test/vinyl-luatest/gh_10751_duplicate_multikey_stmt_accounting_test.lua b/test/vinyl-luatest/gh_10751_duplicate_multikey_stmt_accounting_test.lua
new file mode 100644
index 0000000000..a7bcd1a8ea
--- /dev/null
+++ b/test/vinyl-luatest/gh_10751_duplicate_multikey_stmt_accounting_test.lua
@@ -0,0 +1,80 @@
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group('vinyl.duplicate_multikey_stmt_accounting',
+                  t.helpers.matrix{defer_deletes = {false, true}})
+
+g.before_all(function(cg)
+    cg.server = server:new({
+        box_cfg = {vinyl_defer_deletes = cg.params.defer_deletes},
+    })
+    cg.server:start()
+end)
+
+g.after_all(function(cg)
+    cg.server:drop()
+end)
+
+g.before_each(function(cg)
+    cg.server:exec(function()
+        local s = box.schema.space.create('test', {engine = 'vinyl'})
+        s:create_index('primary')
+        s:create_index('secondary', {
+            unique = false,
+            parts = {{'[2][*]', 'unsigned'}},
+        })
+    end)
+end)
+
+g.after_each(function(cg)
+    cg.server:exec(function()
+        if box.space.test ~= nil then
+            box.space.test:drop()
+        end
+    end)
+end)
+
+g.test_commit = function(cg)
+    cg.server:exec(function()
+        local s = box.space.test
+        local i = s.index.secondary
+        s:replace({1, {1, 1}})
+        s:delete({1})
+        s:replace({1, {1, 1}})
+        box.snapshot()
+        t.assert_covers(i:stat(), {
+            memory = {rows = 0, bytes = 0},
+            disk = {rows = 1},
+        })
+        t.assert_equals(i:len(), 1)
+    end)
+end
+
+g.test_rollback = function(cg)
+    t.tarantool.skip_if_not_debug()
+    cg.server:exec(function()
+        local s = box.space.test
+        local i = s.index.secondary
+        s:replace({1, {1, 1}})
+        box.begin()
+        s:delete({1})
+        box.error.injection.set('ERRINJ_WAL_WRITE', true)
+        t.assert_error_covers({
+            type = 'ClientError',
+            code = box.error.WAL_IO,
+        }, box.commit)
+        box.error.injection.set('ERRINJ_WAL_WRITE', false)
+        box.snapshot()
+        t.assert_covers(i:stat(), {
+            memory = {rows = 0, bytes = 0},
+            disk = {rows = 1},
+        })
+        t.assert_equals(i:len(), 1)
+    end)
+end
+
+g.after_test('test_rollback', function(cg)
+    cg.server:exec(function()
+        box.error.injection.set('ERRINJ_WAL_WRITE', false)
+    end)
+end)
diff --git a/test/vinyl-luatest/select_consistency_test.lua b/test/vinyl-luatest/select_consistency_test.lua
index 88184f58da..b002b52045 100644
--- a/test/vinyl-luatest/select_consistency_test.lua
+++ b/test/vinyl-luatest/select_consistency_test.lua
@@ -232,8 +232,17 @@ g.test_select_consistency = function(cg)
             t.assert_equals(box.stat.vinyl().scheduler.compaction_queue, 0)
             t.assert_equals(box.stat.vinyl().scheduler.tasks_inprogress, 0)
         end)
-        t.assert_equals(s.index.i1:len(), s.index.i1:count())
-        t.assert_equals(s.index.i2:len(), s.index.i3:len())
-        t.assert_equals(s.index.i4:len(), s.index.i5:len())
+        local c1 = s.index.i1:count()
+        local c2 = s.index.i2:count()
+        local c3 = s.index.i3:count()
+        local c4 = s.index.i4:count()
+        local c5 = s.index.i5:count()
+        t.assert_equals(s.index.i1:len(), c1)
+        t.assert_equals(s.index.i2:len(), c2)
+        t.assert_equals(s.index.i3:len(), c3)
+        t.assert_equals(s.index.i4:len(), c4)
+        t.assert_equals(s.index.i5:len(), c5)
+        t.assert_equals(c2, c3)
+        t.assert_equals(c4, c5)
     end)
 end
-- 
GitLab