From de938a6f48c3593b999605d960fb5ca872d6d99a Mon Sep 17 00:00:00 2001
From: Georgiy Lebedev <g.lebedev@tarantool.org>
Date: Tue, 17 Jan 2023 11:58:12 +0300
Subject: [PATCH] memtx: fix rollback of prepared statements deleting MVCC
 stories

When we rollback a prepared statement that deletes an MVCC story, we need
to reset the deleted story's PSN.

Closes #7930

NO_DOC=bugfix
---
 ...mitted-tuple-after-prepared-tx-rollback.md |  4 ++
 src/box/memtx_tx.c                            | 10 +++-
 ..._tuple_after_prepared_tx_rollback_test.lua | 48 +++++++++++++++++++
 3 files changed, 60 insertions(+), 2 deletions(-)
 create mode 100644 changelogs/unreleased/gh-7930-memtx-mvcc-loss-of-committed-tuple-after-prepared-tx-rollback.md
 create mode 100644 test/box-luatest/gh_7930_memtx_mvcc_loss_of_committed_tuple_after_prepared_tx_rollback_test.lua

diff --git a/changelogs/unreleased/gh-7930-memtx-mvcc-loss-of-committed-tuple-after-prepared-tx-rollback.md b/changelogs/unreleased/gh-7930-memtx-mvcc-loss-of-committed-tuple-after-prepared-tx-rollback.md
new file mode 100644
index 0000000000..e5e914a6d8
--- /dev/null
+++ b/changelogs/unreleased/gh-7930-memtx-mvcc-loss-of-committed-tuple-after-prepared-tx-rollback.md
@@ -0,0 +1,4 @@
+## bugfix/memtx
+
+* Fixed possible loss of committed tuple after prepared transaction rollback
+  (gh-7930).
diff --git a/src/box/memtx_tx.c b/src/box/memtx_tx.c
index e309cfea92..ec78ceaa52 100644
--- a/src/box/memtx_tx.c
+++ b/src/box/memtx_tx.c
@@ -2324,8 +2324,14 @@ memtx_tx_history_rollback_added_story(struct txn_stmt *stmt)
 static void
 memtx_tx_history_rollback_deleted_story(struct txn_stmt *stmt)
 {
-	assert(stmt->del_story != NULL);
-	memtx_tx_story_unlink_deleted_by(stmt->del_story, stmt);
+	struct memtx_story *story = stmt->del_story;
+	/*
+	 * There can be no more than one prepared statement deleting a story at
+	 * any point in time.
+	 */
+	assert(story->del_psn == 0 || story->del_psn == stmt->txn->psn);
+	story->del_psn = 0;
+	memtx_tx_story_unlink_deleted_by(story, stmt);
 }
 
 void
diff --git a/test/box-luatest/gh_7930_memtx_mvcc_loss_of_committed_tuple_after_prepared_tx_rollback_test.lua b/test/box-luatest/gh_7930_memtx_mvcc_loss_of_committed_tuple_after_prepared_tx_rollback_test.lua
new file mode 100644
index 0000000000..df440c7bf1
--- /dev/null
+++ b/test/box-luatest/gh_7930_memtx_mvcc_loss_of_committed_tuple_after_prepared_tx_rollback_test.lua
@@ -0,0 +1,48 @@
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group()
+
+g.before_all(function(cg)
+    cg.server = server:new {
+        alias   = 'dflt',
+        box_cfg = {
+            memtx_use_mvcc_engine = true,
+            replication_synchro_quorum = 2,
+            replication_synchro_timeout = 0.0001,
+        }
+    }
+    cg.server:start()
+    cg.server:exec(function()
+        local s = box.schema.space.create("s", {is_sync = true})
+        s:create_index("pk")
+        local as = box.schema.space.create("as")
+        as:create_index("pk")
+
+        box.ctl.promote()
+    end)
+end)
+
+g.after_all(function(cg)
+    cg.server:drop()
+end)
+
+-- Checks that preparation of an insert statement with an older story deleted by
+-- a prepared transaction does not fail assertion.
+g.test_preparation_with_deleted_older_story_assertion = function(cg)
+    cg.server:exec(function()
+        local t = require('luatest')
+
+        box.space.as:replace{0}
+
+        t.assert_error_msg_content_equals(
+                'Quorum collection for a synchronous transaction is timed out',
+                function()
+                    box.atomic(function()
+                        box.space.s:replace{0}
+                        box.space.as:delete{0}
+                    end)
+                end)
+        t.assert_equals(box.space.as:get{0}, {0})
+    end)
+end
-- 
GitLab