From 74fdd1c86af8b0cab531480bb33129cbc2713d0c Mon Sep 17 00:00:00 2001
From: Andrey Saranchin <Andrey22102001@gmail.com>
Date: Wed, 28 Aug 2024 15:36:06 +0300
Subject: [PATCH] memtx: remove all the space stories right on DDL

Currently, every DDL transaction has such properties:
1. DDL and DML operations can be mixed inside one transaction, order is
   not restricted.
2. DDL has an effect right after the operation, not after it's prepared
   or committed.
3. After the first DDL operation, transaction is not allowed to yield.
4. If DDL yields (format check, index build), operation must be the
   first in its transaction. If DDL yields, space cache is updated
   strictly after yields are over. In other words, transaction cannot
   yield after DDL changes became visible.

Keeping these properties in mind, we can recognize several flows in
the way memtx MVCC handles DDL now. Here is the approach:
1. All transactions concurrent with DDL are aborted when DDL gets
   prepared. Memtx stories are not deleted here.
2. If DDL gets committed, all memtx stories of the old space are
   deleted. It means that only stories created before DDL are deleted.

Such design is bad. Firstly, if transaction does a DDL and than a DML,
stories belonging to the new and the old schema will be mixed in the
indexes. Starting a DML transaction after DDL is prepared but before
it's committed leads to the same problem. If stories of different
schemas are mixed, Tarantool is most likely to crash since MVCC does
not handle this case at all. Secondly, transactions started after DDL is
prepared but before it's committed can read stories belonging to the old
schema. In this case, after DDL is committed and the stories are
deleted, we cannot do proper mvcc for such transactions anymore because
we've just deleted the tuple it has read.

Here is the new approach that current commit implements:
1. Abort all concurrent transactions and delete all memtx stories on
   every replace in space cache (right on DDL operation and on its
   rollback). In this case, every time a space and its schema is
   replaced with a new one, all the mvcc objects belonging to the old
   schema are deleted. In order not to break isolation of concurrent
   transactions, we have to abort them all right before deleting
   stories - it makes sense because we don't support old schema anymore
   and all its readers should be aborted.
2. When DDL gets committed, nothing happens since nothing has been
   changed.
3. Abort all concurrent transactions when DDL is being prepared. It is
   needed for DDL that does not update any space. For example, update in
   space `_cluster` or `_schema`. It worked this way before the commit
   so it is needed to provide the old behavior in such cases. Note that
   if DDL updates any space, all concurrent transactions are aborted
   right on DDL operation, when replace in space cache happens, and
   since DDL cannot yield, we have nothing to abort here so it's noop
   in such cases.

Since `memtx_tx_on_space_delete` was supposed to be called on commit, it
inserts prepared tuples to indexes. Since we want it to be called on
actual DDL, the commit makes it insert all tuples visible to the
transaction doing DDL.

Also, if we delete all the stories on DDL, we should correctly rollback
all non-committed DML statements - DML statements of the transaction
doing DDL if they happened before DDL and all transactions prepared
before DDL - if WAL write fails, we will need to roll them back without
stories. For this purpose, firstly, let's remove `engine_savepoint` only
for statements of aborted transactions in `memtx_tx_history_remove_stmt`
(it is needed not to roll them back because they are already handled) so
that statements of transaction doing DDL can be rolled back. And,
secondly, let's add a new helper `memtx_tx_history_rollback_empty_stmt`
that handles rollback of statements without stories (see helper's
description for elaboration).

Along the way, let's clean read lists of transactions aborted on DDL -
they are not needed anymore and potentially keeping them can lead to
use-after-free in future. Moreover, we should remove transactions
aborted by DDL from `read_view_txs` list so that they won't affect
memtx story GC until they are deleted - cleaning of transactions
does it as well.

Part of #10146
Part of #10474
Closes #10171
Closes #10096
Closes #10097

NO_CHANGELOG=later
NO_DOC=bugfix

(cherry picked from commit 959027de5553078dbe442aacb52e01e9b46c542c)
---
 src/box/memtx_space.c              |  19 +-
 src/box/memtx_tx.c                 | 212 +++++++++++-----------
 src/box/memtx_tx.h                 |  10 +-
 src/box/space.c                    |   2 +-
 test/box-luatest/mvcc_ddl_test.lua | 279 +++++++++++++++++++++++++++++
 5 files changed, 414 insertions(+), 108 deletions(-)

diff --git a/src/box/memtx_space.c b/src/box/memtx_space.c
index 17ae21102b..71c7fbbcdf 100644
--- a/src/box/memtx_space.c
+++ b/src/box/memtx_space.c
@@ -1397,6 +1397,23 @@ memtx_space_finish_alter(struct space *old_space, struct space *new_space)
 		new_memtx_space->bsize = old_memtx_space->bsize;
 }
 
+/**
+ * Invalidate the space in transaction manager.
+ */
+static void
+memtx_space_invalidate(struct space *space)
+{
+	/*
+	 * Abort all concurrent transactions and invalidate space in MVCC
+	 * only if it is enabled. Otherwise, all concurrent transactions
+	 * will be aborted when DDL will be prepared.
+	 */
+	if (memtx_tx_manager_use_mvcc_engine) {
+		memtx_tx_abort_all_for_ddl(in_txn());
+		memtx_tx_invalidate_space(space, in_txn());
+	}
+}
+
 /* }}} DDL */
 
 static const struct space_vtab memtx_space_vtab = {
@@ -1421,7 +1438,7 @@ static const struct space_vtab memtx_space_vtab = {
 	/* .prepare_alter = */ memtx_space_prepare_alter,
 	/* .finish_alter = */ memtx_space_finish_alter,
 	/* .prepare_upgrade = */ memtx_space_prepare_upgrade,
-	/* .invalidate = */ generic_space_invalidate,
+	/* .invalidate = */ memtx_space_invalidate,
 };
 
 struct space *
diff --git a/src/box/memtx_tx.c b/src/box/memtx_tx.c
index 1e776c584d..4c0980598c 100644
--- a/src/box/memtx_tx.c
+++ b/src/box/memtx_tx.c
@@ -682,6 +682,12 @@ memtx_tx_acquire_ddl(struct txn *tx)
 	(void) txn_can_yield(tx, false);
 }
 
+/**
+ * Clean and clear all read lists of @a txn.
+ */
+static void
+memtx_tx_clear_txn_read_lists(struct txn *txn);
+
 void
 memtx_tx_abort_all_for_ddl(struct txn *ddl_owner)
 {
@@ -696,7 +702,8 @@ memtx_tx_abort_all_for_ddl(struct txn *ddl_owner)
 			continue;
 		to_be_aborted->status = TXN_ABORTED;
 		txn_set_flags(to_be_aborted, TXN_IS_CONFLICTED);
-		say_warn("Transaction committing DDL (id=%lld) has aborted "
+		memtx_tx_clear_txn_read_lists(to_be_aborted);
+		say_warn("Transaction processing DDL (id=%lld) has aborted "
 			 "another TX (id=%lld)", (long long) ddl_owner->id,
 			 (long long) to_be_aborted->id);
 	}
@@ -1199,51 +1206,8 @@ static void
 memtx_tx_story_unlink_top_on_space_delete(struct memtx_story *story,
 					  uint32_t idx)
 {
-	assert(story != NULL);
-	assert(idx < story->index_count);
 	struct memtx_story_link *link = &story->link[idx];
-
-	assert(link->newer_story == NULL);
-	/*
-	 * Note that link[idx].in_index may not be the same as
-	 * story->space->index[idx] in case space is going to be deleted
-	 * in memtx_tx_on_space_delete(): during space alter operation we
-	 * swap all indexes to the new space object and instead use dummy
-	 * structs.
-	 */
-	struct index *index = story->link[idx].in_index;
-
 	struct memtx_story *old_story = link->older_story;
-	assert(old_story == NULL || old_story->link[idx].in_index == NULL);
-	struct tuple *old_tuple = old_story == NULL ? NULL : old_story->tuple;
-	struct tuple *removed, *unused;
-	if (index_replace(index, story->tuple, old_tuple,
-			  DUP_INSERT, &removed, &unused) != 0) {
-		diag_log();
-		unreachable();
-		panic("failed to rebind story in index");
-	}
-	assert(story->tuple == removed ||
-	       (removed == NULL && tuple_key_is_excluded(story->tuple,
-							 index->def->key_def,
-							 MULTIKEY_NONE)));
-	story->link[idx].in_index = NULL;
-	if (old_story != NULL)
-		old_story->link[idx].in_index = index;
-
-	/*
-	 * A space holds references to all his tuples.
-	 * All tuples that are physically in the primary index are referenced.
-	 * Thus we have to reference the tuple that was added to the primary
-	 * index and dereference the tuple that was removed from it.
-	 */
-	if (idx == 0) {
-		if (old_story != NULL)
-			memtx_tx_ref_to_primary(old_story);
-		memtx_tx_unref_from_primary(story);
-	}
-
-	/* Now simply unlink the story. */
 	if (old_story != NULL)
 		memtx_tx_story_unlink(story, old_story, idx);
 }
@@ -1279,7 +1243,6 @@ memtx_tx_story_unlink_both_on_space_delete(struct memtx_story *story,
 	assert(idx < story->index_count);
 	struct memtx_story_link *link = &story->link[idx];
 	if (link->newer_story == NULL) {
-		assert(link->in_index != NULL);
 		memtx_tx_story_unlink_top_on_space_delete(story, idx);
 	} else {
 		memtx_tx_story_unlink_both_common(story, idx);
@@ -1329,11 +1292,7 @@ memtx_tx_story_reorder(struct memtx_story *story,
 }
 
 /**
- * Unlink @a story from all chains and remove corresponding tuple from
- * indexes if necessary: used in `memtx_tx_on_space_delete` — intentionally
- * violates the top of the history chain invariant (see
- * `memtx_tx_story_full_unlink_story_gc_step`), since all stories are deleted
- * anyways.
+ * Unlink @a story from all chains, must be already removed from the index.
  */
 static void
 memtx_tx_story_full_unlink_on_space_delete(struct memtx_story *story)
@@ -1341,38 +1300,7 @@ memtx_tx_story_full_unlink_on_space_delete(struct memtx_story *story)
 	for (uint32_t i = 0; i < story->index_count; i++) {
 		struct memtx_story_link *link = &story->link[i];
 		if (link->newer_story == NULL) {
-			/*
-			 * We are at the top of the chain. That means
-			 * that story->tuple might be in index. If the story
-			 * actually deletes the tuple and is present in index,
-			 * it must be deleted from index.
-			 */
-			if (story->del_psn > 0 && link->in_index != NULL) {
-				struct index *index = link->in_index;
-				struct tuple *removed, *unused;
-				if (index_replace(index, story->tuple, NULL,
-						  DUP_INSERT,
-						  &removed, &unused) != 0) {
-					diag_log();
-					unreachable();
-					panic("failed to rollback change");
-				}
-				struct key_def *key_def = index->def->key_def;
-				assert(story->tuple == removed ||
-				       (removed == NULL &&
-					tuple_key_is_excluded(story->tuple,
-							      key_def,
-							      MULTIKEY_NONE)));
-				(void)key_def;
-				link->in_index = NULL;
-				/*
-				 * All tuples in pk are referenced.
-				 * Once removed it must be unreferenced.
-				 */
-				if (i == 0)
-					memtx_tx_unref_from_primary(story);
-			}
-
+			assert(link->in_index == NULL);
 			memtx_tx_story_unlink(story, link->older_story, i);
 		} else {
 			/* Just unlink from list */
@@ -2338,6 +2266,43 @@ memtx_tx_history_rollback_deleted_story(struct txn_stmt *stmt)
 	memtx_tx_story_unlink_deleted_by(del_story, stmt);
 }
 
+/**
+ * The helper rolls back a statements that is empty - has no stories
+ * linked. It can happen due to several reasons:
+ * 1. MVCC hasn't created stories for the stmt. It happens when space is
+ *    ephemeral or when the statement has deleted nothing. In this case
+ *    helper does nothing.
+ * 2. MVCC created stories for the statement, but they were deleted due to
+ *    DDL - here are 3 types of such transactions. First one is concurrent
+ *    with DDL. We shouldn't roll them back because we have already handled
+ *    them on DDL. Second one is DDL itself (`is_schema_changed` flag is set)
+ *    since stories of all the DML operations that happened before DDL were
+ *    deleted. We must roll its statements back because now the space contain
+ *    all its tuples. Third type is transactions prepared before DDL. We've
+ *    also removed their stories on DDL, so here we should roll them back
+ *    without stories if they have failed to commit.
+ */
+void
+memtx_tx_history_rollback_empty_stmt(struct txn_stmt *stmt)
+{
+	struct tuple *old_tuple = stmt->rollback_info.old_tuple;
+	struct tuple *new_tuple = stmt->rollback_info.new_tuple;
+	if (!stmt->txn->is_schema_changed && stmt->txn->psn == 0)
+		return;
+	if (stmt->space->def->opts.is_ephemeral ||
+	    (old_tuple == NULL && new_tuple == NULL))
+		return;
+	for (size_t i = 0; i < stmt->space->index_count; i++) {
+		struct tuple *unused;
+		if (index_replace(stmt->space->index[i], new_tuple, old_tuple,
+				  DUP_REPLACE_OR_INSERT, &unused,
+				  &unused) != 0) {
+			panic("failed to rebind story in index on "
+			      "rollback of statement without story");
+		}
+	}
+}
+
 void
 memtx_tx_history_rollback_stmt(struct txn_stmt *stmt)
 {
@@ -2362,6 +2327,8 @@ memtx_tx_history_rollback_stmt(struct txn_stmt *stmt)
 		memtx_tx_history_rollback_added_story(stmt);
 	else if (stmt->del_story != NULL)
 		memtx_tx_history_rollback_deleted_story(stmt);
+	else
+		memtx_tx_history_rollback_empty_stmt(stmt);
 	assert(stmt->add_story == NULL && stmt->del_story == NULL);
 }
 
@@ -2399,7 +2366,8 @@ memtx_tx_history_remove_stmt(struct txn_stmt *stmt)
 		memtx_tx_history_remove_added_story(stmt);
 	if (stmt->del_story != NULL)
 		memtx_tx_history_remove_deleted_story(stmt);
-	stmt->engine_savepoint = NULL;
+	if (stmt->txn->status == TXN_ABORTED)
+		stmt->engine_savepoint = NULL;
 }
 
 /**
@@ -2689,12 +2657,6 @@ memtx_tx_history_prepare_stmt(struct txn_stmt *stmt)
 	memtx_tx_story_gc();
 }
 
-/**
- * Clean and clear all read lists of @a txn.
- */
-static void
-memtx_tx_clear_txn_read_lists(struct txn *txn);
-
 void
 memtx_tx_prepare_finalize(struct txn *txn)
 {
@@ -2919,20 +2881,68 @@ memtx_tx_on_index_delete(struct index *index)
 }
 
 void
-memtx_tx_on_space_delete(struct space *space)
+memtx_tx_invalidate_space(struct space *space, struct txn *active_txn)
 {
+	struct memtx_story *story;
+	/*
+	 * Phase one: fill the indexes with actual tuples. Here we insert
+	 * all tuples visible to `active_txn`.
+	 */
+	rlist_foreach_entry(story, &space->memtx_stories, in_space_stories) {
+		assert(story->index_count == space->index_count);
+
+		for (uint32_t i = 0; i < story->index_count; i++) {
+			struct index *index = story->link[i].in_index;
+			if (index == NULL)
+				continue;
+
+			/* Mark as not in index. */
+			story->link[i].in_index = NULL;
+
+			/* Skip chains of excluded tuples. */
+			if (tuple_key_is_excluded(story->tuple,
+						  index->def->key_def,
+						  MULTIKEY_NONE))
+				continue;
+
+			struct tuple *new_tuple = NULL;
+			bool is_own_change;
+			memtx_tx_story_find_visible_tuple(story, active_txn, i,
+							  true, &new_tuple,
+							  &is_own_change);
+
+			/* Visible tuple is already in index - do nothing. */
+			if (new_tuple == story->tuple)
+				continue;
+
+			struct tuple *unused;
+			if (index_replace(index, story->tuple, new_tuple,
+					  DUP_REPLACE, &unused,
+					  &unused) != 0) {
+				diag_log();
+				unreachable();
+				panic("failed to rebind story in index on "
+				      "space invalidation");
+			}
+
+			if (i == 0) {
+				if (new_tuple != NULL) {
+					memtx_tx_ref_to_primary(
+						memtx_tx_story_get(new_tuple));
+				}
+				memtx_tx_unref_from_primary(story);
+			}
+		}
+	}
+
+	/*
+	 * Phase two: destroy all the stories. They are expected to be unlinked
+	 * from the indexes during the first phase.
+	 */
 	while (!rlist_empty(&space->memtx_stories)) {
-		struct memtx_story *story
-			= rlist_first_entry(&space->memtx_stories,
-					    struct memtx_story,
-					    in_space_stories);
-		/*
-		 * Space is to be altered (not necessarily dropped). Since
-		 * this operation is considered to be DDL, all other
-		 * transactions will be aborted anyway. We can't postpone
-		 * rollback till actual call of commit/rollback since stories
-		 * should be destroyed immediately.
-		 */
+		story = rlist_first_entry(&space->memtx_stories,
+					  struct memtx_story,
+					  in_space_stories);
 		if (story->add_stmt != NULL)
 			memtx_tx_history_remove_stmt(story->add_stmt);
 		while (story->del_stmt != NULL)
diff --git a/src/box/memtx_tx.h b/src/box/memtx_tx.h
index 5ab55817d5..992a126091 100644
--- a/src/box/memtx_tx.h
+++ b/src/box/memtx_tx.h
@@ -413,15 +413,15 @@ void
 memtx_tx_on_index_delete(struct index *index);
 
 /**
- * Notify manager the a space is deleted.
- * It's necessary because there is a chance that garbage collector hasn't
- * deleted all stories of that space and in that case some actions of
- * story's destructor are not applicable.
+ * Invalidate space in memtx tx: remove all the objects associated with
+ * space and its schema. The helper is supposed to be called when there is
+ * only one active transaction that is passed as `active_txn`. The indexes
+ * are populated with tuples according to what `active_txn` observes.
  *
  * NB: can trigger story garbage collection.
  */
 void
-memtx_tx_on_space_delete(struct space *space);
+memtx_tx_invalidate_space(struct space *space, struct txn *active_txn);
 
 /**
  * Create a snapshot cleaner.
diff --git a/src/box/space.c b/src/box/space.c
index 48a2d6240a..5477a58d90 100644
--- a/src/box/space.c
+++ b/src/box/space.c
@@ -442,7 +442,7 @@ void
 space_delete(struct space *space)
 {
 	assert(rlist_empty(&space->alter_stmts));
-	memtx_tx_on_space_delete(space);
+	assert(rlist_empty(&space->memtx_stories));
 	for (uint32_t j = 0; j <= space->index_id_max; j++) {
 		struct index *index = space->index_map[j];
 		if (index != NULL)
diff --git a/test/box-luatest/mvcc_ddl_test.lua b/test/box-luatest/mvcc_ddl_test.lua
index 0b6b1080fc..0c5c8f7afd 100644
--- a/test/box-luatest/mvcc_ddl_test.lua
+++ b/test/box-luatest/mvcc_ddl_test.lua
@@ -137,3 +137,282 @@ g.test_reader_list_use_after_free = function(cg)
         end
     end)
 end
+
+-- The test checks if stories associated with old schema cannot
+-- be access on index creation
+-- gh-10096
+g.test_dont_access_old_stories_on_index_create = function(cg)
+    cg.server:exec(function()
+        local fiber = require('fiber')
+
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1}}})
+        s:create_index('sk1', {parts = {{2}}, unique = true})
+
+        s:insert{1, 1, 1, 0}
+        -- Collect stories to be independent from GC steps
+        box.internal.memtx_tx_gc(100)
+
+        -- Create index in a separate transaction
+        -- Transaction creating index will create story for our tuple
+        -- when it reads it to insert into the new index
+        -- The problem is the story is created with old index_count and
+        -- does not have links for created index
+        local created_index = false
+        local f = fiber.create(function()
+            s:create_index('sk2', {parts = {{3}}, unique = true})
+            created_index = true
+        end)
+        f:set_joinable(true)
+
+        -- Make sure txn is still in progress
+        assert(not created_index)
+        s:replace{1, 1, 1, 1}
+        local ok = f:join()
+        t.assert(ok)
+    end)
+end
+
+-- The test checks if stories associated with old schema cannot
+-- be access when index is being altered
+-- gh-10097
+g.test_dont_access_old_stories_on_index_alter = function(cg)
+    cg.server:exec(function()
+        local fiber = require('fiber')
+
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1}}})
+        s:create_index('sk1', {parts = {{2}}, unique = true})
+
+        s:insert{1, 1, 1, 0}
+        -- Collect stories to be independent from GC steps
+        box.internal.memtx_tx_gc(100)
+
+        -- Alter index in a separate transaction
+        -- Transaction creating index will create story for our tuple
+        -- when it reads it to insert into the new index
+        -- The problem is the story link points to the old index, but
+        -- it was replaced with the new one (new index object, hence,
+        -- new address)
+        local altered_index = false
+        local f = fiber.create(function()
+            s.index.sk1:alter({parts = {{3}}, unique = true})
+            altered_index = true
+        end)
+        f:set_joinable(true)
+
+        -- Make sure txn is still in progress
+        assert(not altered_index)
+        s:replace{1, 1, 1, 1}
+        local ok = f:join()
+        t.assert(ok)
+    end)
+end
+
+-- The test covers a crash when using count after DDL because
+-- DDL has violated some MVCC invariants that are checked in count
+-- gh-10474, case 3
+g.test_count_crashes_after_ddl = function(cg)
+    cg.server:exec(function()
+        local fiber = require('fiber')
+
+        local s = box.schema.create_space('test')
+        s:create_index('pk')
+        s:create_index('sk', {parts={{2}}})
+
+        -- Do a sequence of operations under WAL delay:
+        -- Replace
+        -- DDL
+        -- Replace
+        box.error.injection.set('ERRINJ_WAL_DELAY', true)
+        fiber.create(function() box.space.test:replace{1, 1, 1} end)
+        local f = fiber.create(function()
+            box.space.test:format{{name = 'field1', type = 'scalar'}}
+        end)
+        fiber.create(function() box.space.test:replace{1, 1, 1} end)
+        f:set_joinable(true)
+        box.error.injection.set('ERRINJ_WAL_DELAY', false)
+
+        local ok = f:join()
+        t.assert(ok)
+        t.assert_equals(box.space.test:count(), 1)
+    end)
+end
+
+-- The test checks if isolation of transactions is not broken when DDL
+-- is committed. It happened because we removed all memtx stories on commit
+-- eariler.
+g.test_txn_isolation_on_ddl_commit = function(cg)
+    cg.server:exec(function()
+        local fiber = require('fiber')
+
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1}}})
+
+        -- Few tuples to do DDL without yields
+        for i = 1, 5 do
+            s:replace{i}
+        end
+
+        -- Collect stories to be independent from GC steps
+        box.internal.memtx_tx_gc(100)
+
+        -- Alter space in a separate transaction
+        local altered_space = false
+        local f = fiber.create(function()
+            s:format({{name = 'field1', type = 'unsigned'}})
+            altered_space = true
+        end)
+        f:set_joinable(true)
+        -- Make sure txn is still in progress
+        t.assert(not altered_space)
+
+        box.begin()
+        -- Drop all the tuples in the new transaction
+        for i = 1, 5 do
+            s:delete(i)
+        end
+        -- Check if tuples were deleted
+        t.assert_equals(s:select{}, {})
+
+        -- Wait for DDL to be committed and make sure it was successful
+        local ok = f:join()
+        t.assert(ok)
+
+        -- Check contents after yield
+        t.assert_equals(s:select{}, {})
+
+        box.commit()
+
+        -- We committed a transaction deleting all tuples just now, result
+        -- still must be empty
+        t.assert_equals(s:select{}, {})
+    end)
+end
+
+-- The test checks if DDL aborts all conflicting transactions - not preparing
+-- the DDL but the operation itself. We should check this behavior because we
+-- delete all memtx stories on DDL so we cannot support transaction isolation
+-- anymore. If we didn't abort active transactions, rollback of DDL could
+-- violate their isolation.
+g.test_abort_all_on_ddl = function(cg)
+    cg.server:exec(function()
+        local fiber = require('fiber')
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1}}})
+        local errmsg = 'Transaction has been aborted by conflict'
+
+        box.begin()
+        s:replace{1}
+        local ddl = fiber.create(function()
+            s:create_index('sk')
+        end)
+        ddl:set_joinable(true)
+        t.assert_error_msg_content_equals(errmsg, box.commit)
+        local ok = ddl:join()
+        t.assert(ok)
+    end)
+end
+
+-- The test checks if rollback of DDL aborts all conflicting transactions since
+-- they rely on new schema which is going to be rolled back. Since we already
+-- abort all transactions on DDL itself and transaction doing DDL cannot yield,
+-- the only way a concurrent transaction can appear is after DDL is prepared but
+-- before it is committed. If DDL wasn't prepared (manual rollback), there
+-- can't be concurrent transactions.
+g.test_abort_all_ddl_rollback = function(cg)
+    t.tarantool.skip_if_not_debug()
+    cg.server:exec(function()
+        local fiber = require('fiber')
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1}}})
+        local errmsg = 'Transaction has been aborted by conflict'
+
+        box.error.injection.set('ERRINJ_WAL_DELAY', true)
+
+        local ddl = fiber.create(function()
+            s:create_index('sk')
+        end)
+        ddl:set_joinable(true)
+
+        box.begin()
+        s:replace{1}
+        box.error.injection.set('ERRINJ_WAL_WRITE', true)
+        box.error.injection.set('ERRINJ_WAL_DELAY', false)
+        local ok = ddl:join()
+        t.assert_not(ok, 'DDL must fail and rollback due to WAL error')
+        box.error.injection.set('ERRINJ_WAL_WRITE', false)
+        t.assert_error_msg_content_equals(errmsg, box.commit)
+    end)
+end
+
+g.test_rollback_mixed_ddl_and_dml = function(cg)
+    cg.server:exec(function()
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1}}})
+        for i = 1, 10 do
+            s:replace{i}
+        end
+        local saved_select = s:select{}
+        box.begin()
+        s:replace{1, 1}
+        s:delete{1}
+        s:delete{2}
+        s:insert{11}
+        s:alter({is_sync = true})
+        s:replace{3, 1}
+        s:delete{4}
+        s:delete{5}
+        s:insert{12}
+        s:alter({is_sync = false})
+        box.rollback()
+        t.assert_equals(s:select{}, saved_select)
+    end)
+end
+
+g.test_rollback_prepared_dml_and_ddl = function(cg)
+    t.tarantool.skip_if_not_debug()
+    cg.server:exec(function()
+        local fiber = require('fiber')
+        local s = box.schema.space.create('test')
+        s:create_index('pk', {parts = {{1}}})
+        for i = 1, 10 do
+            s:replace{i}
+        end
+        local saved_select = s:select{}
+
+        box.error.injection.set('ERRINJ_WAL_DELAY', true)
+        local dml1 = fiber.create(function()
+            box.begin()
+            s:replace{1, 1}
+            s:replace{2, 2}
+            s:delete{3}
+            s:delete{4}
+            s:insert{100}
+            box.commit()
+        end)
+        dml1:set_joinable(true)
+        local ddl = fiber.create(function()
+            s:create_index('sk')
+        end)
+        ddl:set_joinable(true)
+        local dml2 = fiber.create(function()
+            box.begin()
+            s:replace{4, 4}
+            s:replace{5, 5}
+            s:delete{8}
+            s:insert{200}
+            box.commit()
+        end)
+        dml2:set_joinable(true)
+        box.error.injection.set('ERRINJ_WAL_WRITE', true)
+        box.error.injection.set('ERRINJ_WAL_DELAY', false)
+        local ok = dml1:join()
+        t.assert_not(ok)
+        ok = dml2:join()
+        t.assert_not(ok)
+        ok = ddl:join()
+        t.assert_not(ok)
+        t.assert_equals(s:select{}, saved_select)
+    end)
+end
-- 
GitLab