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 0000000000000000000000000000000000000000..e5e914a6d834068b128e7de666f65f9871f0eddd --- /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 e309cfea92246c85a66a73a8ccc08461a8d6643c..ec78ceaa52b243e524811765856d3bc945342bbd 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 0000000000000000000000000000000000000000..df440c7bf1beb036df4842dd7981024bcfb25c08 --- /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