Tarantool development patches archive
 help / color / mirror / Atom feed
* [PATCH v2 0/3] Transactional DDL
@ 2019-07-10 13:09 Vladimir Davydov
  2019-07-10 13:09 ` [PATCH v2 1/3] memtx: fix txn_on_yield for DDL transactions Vladimir Davydov
                   ` (3 more replies)
  0 siblings, 4 replies; 13+ messages in thread
From: Vladimir Davydov @ 2019-07-10 13:09 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches

This patch set implements transactional DDL for the case when all
statements are non-yielding. There are only two DDL statements that
may yield implicitly - building of a new index and checking a space
format - so it must cover most user stories. For more details re
the implementation, see comments to the individual patches.

https://github.com/tarantool/tarantool/issues/4083
https://github.com/tarantool/tarantool/commits/dv/gh-4083-transactional-ddl

v1: https://www.freelists.org/post/tarantool-patches/PATCH-05-Transactional-DDL

Changes in v2:
 - Committed approved patches and rebased.
 - Fixed wrong index usage in AlterSpaceOp::commit,rollback
 - Reworked error reporting.
 - Reworked txn_on_yield management.

Vladimir Davydov (3):
  memtx: fix txn_on_yield for DDL transactions
  ddl: don't use space_index from AlterSpaceOp::commit,rollback
  ddl: allow to execute non-yielding DDL statements in transactions

 src/box/alter.cc              | 91 ++++++++++++++--------------------------
 src/box/errcode.h             |  2 +-
 src/box/memtx_engine.c        | 96 ++----------------------------------------
 src/box/memtx_space.c         | 10 +++++
 src/box/txn.c                 | 89 ++++++++++++++++++++++++++++++++++++---
 src/box/txn.h                 | 63 +++++++++++++++-------------
 src/box/user.cc               |  1 -
 src/box/vinyl.c               | 25 ++++++++---
 test/box/misc.result          |  1 +
 test/box/on_replace.result    | 53 +++++++++++------------
 test/box/on_replace.test.lua  | 13 +++---
 test/box/transaction.result   | 98 +++++++++++++++++++++++++++++++++----------
 test/box/transaction.test.lua | 64 +++++++++++++++++++++++-----
 test/engine/ddl.result        | 58 ++++++++++++++++++++++++-
 test/engine/ddl.test.lua      | 39 ++++++++++++++++-
 test/engine/truncate.result   | 10 +----
 test/engine/truncate.test.lua |  6 +--
 17 files changed, 440 insertions(+), 279 deletions(-)

-- 
2.11.0

^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH v2 1/3] memtx: fix txn_on_yield for DDL transactions
  2019-07-10 13:09 [PATCH v2 0/3] Transactional DDL Vladimir Davydov
@ 2019-07-10 13:09 ` Vladimir Davydov
  2019-07-10 20:34   ` Konstantin Osipov
  2019-07-10 13:09 ` [PATCH v2 2/3] ddl: don't use space_index from AlterSpaceOp::commit,rollback Vladimir Davydov
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 13+ messages in thread
From: Vladimir Davydov @ 2019-07-10 13:09 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches

Memtx engine doesn't allow yielding inside a transaction. To achieve
that, it installs fiber->on_yield trigger that aborts the current
transaction (rolls it back, but leaves it be so that commit fails).

There's an exception though - DDL statements are allowed to yield.
This is required so as not to block the event loop while a new index
is built or a space format is checked. Currently, we handle this
exception by checking space id and omitting installation of the
trigger for system spaces. This isn't entirely correct, because we
may yield after a DDL statement is complete, in which case the
transaction won't be aborted though it should:

  box.begin()
  box.space.my_space:create_index('my_index')
  fiber.sleep(0) -- doesn't abort the transaction!

This patch fixes the problem by making the memtx engine install the
on_yield trigger unconditionally, for all kinds of transactions, and
instead explicitly disabling the trigger for yielding DDL operations.

In order not to spread the yield-in-transaction logic between memtx
and txn code, let's move all fiber_on_yield related stuff to txn,
export a method to disable yields, and use the method in memtx.
---
 src/box/memtx_engine.c        | 96 ++-----------------------------------------
 src/box/memtx_space.c         |  6 +++
 src/box/txn.c                 | 82 ++++++++++++++++++++++++++++++++++--
 src/box/txn.h                 | 50 ++++++++++++++--------
 src/box/vinyl.c               |  6 +++
 test/box/transaction.result   | 16 ++++++++
 test/box/transaction.test.lua |  8 ++++
 test/engine/truncate.result   | 10 +----
 test/engine/truncate.test.lua |  6 +--
 9 files changed, 155 insertions(+), 125 deletions(-)

diff --git a/src/box/memtx_engine.c b/src/box/memtx_engine.c
index c8376110..6ed38a91 100644
--- a/src/box/memtx_engine.c
+++ b/src/box/memtx_engine.c
@@ -55,58 +55,6 @@
 static void
 checkpoint_cancel(struct checkpoint *ckpt);
 
-/*
- * Memtx yield-in-transaction trigger: roll back the effects
- * of the transaction and mark the transaction as aborted.
- */
-static void
-txn_on_yield(struct trigger *trigger, void *event)
-{
-	(void) trigger;
-	(void) event;
-
-	struct txn *txn = in_txn();
-	assert(txn && txn->engine_tx);
-	if (txn == NULL || txn->engine_tx == NULL)
-		return;
-	txn_abort(txn);                 /* doesn't yield or fail */
-}
-
-/**
- * Initialize context for yield triggers.
- * In case of a yield inside memtx multi-statement transaction
- * we must, first of all, roll back the effects of the transaction
- * so that concurrent transactions won't see dirty, uncommitted
- * data.
- * Second, we must abort the transaction, since it has been rolled
- * back implicitly. The transaction can not be rolled back
- * completely from within a yield trigger, since a yield trigger
- * can't fail. Instead, we mark the transaction as aborted and
- * raise an error on attempt to commit it.
- *
- * So much hassle to be user-friendly until we have a true
- * interactive transaction support in memtx.
- */
-void
-memtx_init_txn(struct txn *txn)
-{
-	struct fiber *fiber = fiber();
-
-	trigger_create(&txn->fiber_on_yield, txn_on_yield,
-		       NULL, NULL);
-	/*
-	 * Memtx doesn't allow yields between statements of
-	 * a transaction. Set a trigger which would roll
-	 * back the transaction if there is a yield.
-	 */
-	trigger_add(&fiber->on_yield, &txn->fiber_on_yield);
-	/*
-	 * This serves as a marker that the triggers are
-	 * initialized.
-	 */
-	txn->engine_tx = txn;
-}
-
 struct PACKED memtx_tuple {
 	/*
 	 * sic: the header of the tuple is used
@@ -372,45 +320,10 @@ memtx_engine_create_space(struct engine *engine, struct space_def *def,
 }
 
 static int
-memtx_engine_prepare(struct engine *engine, struct txn *txn)
-{
-	(void)engine;
-	if (txn->engine_tx == 0)
-		return 0;
-	/*
-	 * These triggers are only used for memtx and only
-	 * when autocommit == false, so we are saving
-	 * on calls to trigger_create/trigger_clear.
-	 */
-	trigger_clear(&txn->fiber_on_yield);
-	if (txn->is_aborted) {
-		diag_set(ClientError, ER_TRANSACTION_YIELD);
-		diag_log();
-		return -1;
-	}
-	return 0;
-}
-
-static int
 memtx_engine_begin(struct engine *engine, struct txn *txn)
 {
 	(void)engine;
-	(void)txn;
-	return 0;
-}
-
-static int
-memtx_engine_begin_statement(struct engine *engine, struct txn *txn)
-{
-	(void)engine;
-	(void)txn;
-	if (txn->engine_tx == NULL) {
-		struct space *space = txn_last_stmt(txn)->space;
-
-		if (space->def->id > BOX_SYSTEM_ID_MAX)
-			/* Setup triggers for non-ddl transactions. */
-			memtx_init_txn(txn);
-	}
+	txn_disable_yield(txn);
 	return 0;
 }
 
@@ -459,9 +372,6 @@ memtx_engine_rollback_statement(struct engine *engine, struct txn *txn,
 static void
 memtx_engine_rollback(struct engine *engine, struct txn *txn)
 {
-	if (txn->engine_tx != NULL) {
-		trigger_clear(&txn->fiber_on_yield);
-	}
 	struct txn_stmt *stmt;
 	stailq_reverse(&txn->stmts);
 	stailq_foreach_entry(stmt, &txn->stmts, next)
@@ -948,8 +858,8 @@ static const struct engine_vtab memtx_engine_vtab = {
 	/* .create_space = */ memtx_engine_create_space,
 	/* .join = */ memtx_engine_join,
 	/* .begin = */ memtx_engine_begin,
-	/* .begin_statement = */ memtx_engine_begin_statement,
-	/* .prepare = */ memtx_engine_prepare,
+	/* .begin_statement = */ generic_engine_begin_statement,
+	/* .prepare = */ generic_engine_prepare,
 	/* .commit = */ generic_engine_commit,
 	/* .rollback_statement = */ memtx_engine_rollback_statement,
 	/* .rollback = */ memtx_engine_rollback,
diff --git a/src/box/memtx_space.c b/src/box/memtx_space.c
index f0e1cfd2..921dbcbf 100644
--- a/src/box/memtx_space.c
+++ b/src/box/memtx_space.c
@@ -888,6 +888,8 @@ memtx_space_check_format(struct space *space, struct tuple_format *format)
 	if (it == NULL)
 		return -1;
 
+	txn_enable_yield_for_ddl();
+
 	struct memtx_engine *memtx = (struct memtx_engine *)space->engine;
 	struct memtx_ddl_state state;
 	state.format = format;
@@ -930,6 +932,7 @@ memtx_space_check_format(struct space *space, struct tuple_format *format)
 	iterator_delete(it);
 	diag_destroy(&state.diag);
 	trigger_clear(&on_replace);
+	txn_disable_yield_after_ddl();
 	return rc;
 }
 
@@ -1027,6 +1030,8 @@ memtx_space_build_index(struct space *src_space, struct index *new_index,
 	if (it == NULL)
 		return -1;
 
+	txn_enable_yield_for_ddl();
+
 	struct memtx_engine *memtx = (struct memtx_engine *)src_space->engine;
 	struct memtx_ddl_state state;
 	state.index = new_index;
@@ -1103,6 +1108,7 @@ memtx_space_build_index(struct space *src_space, struct index *new_index,
 	iterator_delete(it);
 	diag_destroy(&state.diag);
 	trigger_clear(&on_replace);
+	txn_disable_yield_after_ddl();
 	return rc;
 }
 
diff --git a/src/box/txn.c b/src/box/txn.c
index c605345d..bab98d7e 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -40,6 +40,12 @@ double too_long_threshold;
 /* Txn cache. */
 static struct stailq txn_cache = {NULL, &txn_cache.first};
 
+static void
+txn_on_stop(struct trigger *trigger, void *event);
+
+static void
+txn_on_yield(struct trigger *trigger, void *event);
+
 static inline void
 fiber_set_txn(struct fiber *fiber, struct txn *txn)
 {
@@ -194,6 +200,7 @@ txn_begin()
 	txn->has_triggers  = false;
 	txn->is_done = false;
 	txn->is_aborted = false;
+	txn->is_yield_disabled = false;
 	txn->in_sub_stmt = 0;
 	txn->id = ++tsn;
 	txn->signature = -1;
@@ -201,8 +208,8 @@ txn_begin()
 	txn->engine_tx = NULL;
 	txn->psql_txn = NULL;
 	txn->fiber = NULL;
-	/* fiber_on_yield/fiber_on_stop initialized by engine on demand */
 	fiber_set_txn(fiber(), txn);
+	/* fiber_on_yield is initialized by engine on demand */
 	trigger_create(&txn->fiber_on_stop, txn_on_stop, NULL, NULL);
 	trigger_add(&fiber()->on_stop, &txn->fiber_on_stop);
 	return txn;
@@ -463,6 +470,12 @@ txn_write_to_wal(struct txn *txn)
 static int
 txn_prepare(struct txn *txn)
 {
+	if (txn->is_aborted) {
+		assert(txn->is_yield_disabled);
+		diag_set(ClientError, ER_TRANSACTION_YIELD);
+		diag_log();
+		return -1;
+	}
 	/*
 	 * If transaction has been started in SQL, deferred
 	 * foreign key constraints must not be violated.
@@ -484,6 +497,8 @@ txn_prepare(struct txn *txn)
 			return -1;
 	}
 	trigger_clear(&txn->fiber_on_stop);
+	if (txn->is_yield_disabled)
+		trigger_clear(&txn->fiber_on_yield);
 	return 0;
 }
 
@@ -550,12 +565,22 @@ txn_rollback(struct txn *txn)
 {
 	assert(txn == in_txn());
 	trigger_clear(&txn->fiber_on_stop);
+	if (txn->is_yield_disabled)
+		trigger_clear(&txn->fiber_on_yield);
 	txn->signature = -1;
 	txn_complete(txn);
 	fiber_set_txn(fiber(), NULL);
 }
 
-void
+/**
+ * Roll back the transaction but keep the object around.
+ * A special case for memtx transaction abort on yield. In this
+ * case we need to abort the transaction to avoid dirty reads but
+ * need to keep it around to ensure a new one is not implicitly
+ * started and committed by the user program. Later, at
+ * transaction commit we will raise an exception.
+ */
+static void
 txn_abort(struct txn *txn)
 {
 	assert(in_txn() == txn);
@@ -578,6 +603,31 @@ txn_check_singlestatement(struct txn *txn, const char *where)
 	return 0;
 }
 
+void
+txn_disable_yield(struct txn *txn)
+{
+	assert(!txn->is_yield_disabled);
+	txn->is_yield_disabled = true;
+	trigger_create(&txn->fiber_on_yield, txn_on_yield, NULL, NULL);
+	trigger_add(&fiber()->on_yield, &txn->fiber_on_yield);
+}
+
+void
+txn_enable_yield_for_ddl(void)
+{
+	struct txn *txn = in_txn();
+	assert(txn != NULL && txn->is_yield_disabled);
+	trigger_clear(&txn->fiber_on_yield);
+}
+
+void
+txn_disable_yield_after_ddl(void)
+{
+	struct txn *txn = in_txn();
+	assert(txn != NULL && txn->is_yield_disabled);
+	trigger_add(&fiber()->on_yield, &txn->fiber_on_yield);
+}
+
 int64_t
 box_txn_id(void)
 {
@@ -711,7 +761,7 @@ box_txn_rollback_to_savepoint(box_txn_savepoint_t *svp)
 	return 0;
 }
 
-void
+static void
 txn_on_stop(struct trigger *trigger, void *event)
 {
 	(void) trigger;
@@ -719,3 +769,29 @@ txn_on_stop(struct trigger *trigger, void *event)
 	txn_rollback(in_txn());                 /* doesn't yield or fail */
 }
 
+/**
+ * Memtx yield-in-transaction trigger callback.
+ *
+ * In case of a yield inside memtx multi-statement transaction
+ * we must, first of all, roll back the effects of the transaction
+ * so that concurrent transactions won't see dirty, uncommitted
+ * data.
+ *
+ * Second, we must abort the transaction, since it has been rolled
+ * back implicitly. The transaction can not be rolled back
+ * completely from within a yield trigger, since a yield trigger
+ * can't fail. Instead, we mark the transaction as aborted and
+ * raise an error on attempt to commit it.
+ *
+ * So much hassle to be user-friendly until we have a true
+ * interactive transaction support in memtx.
+ */
+static void
+txn_on_yield(struct trigger *trigger, void *event)
+{
+	(void) trigger;
+	(void) event;
+	struct txn *txn = in_txn();
+	assert(txn != NULL && txn->is_yield_disabled);
+	txn_abort(txn);                         /* doesn't yield or fail */
+}
diff --git a/src/box/txn.h b/src/box/txn.h
index d1ef220a..258a8db7 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -169,6 +169,11 @@ struct txn {
 	 * rolled back at commit.
 	 */
 	bool is_aborted;
+	/**
+	 * True if the engine doesn't support yields inside
+	 * transactions, see txn_disable_yield().
+	 */
+	bool is_yield_disabled;
 	/** True if on_commit and on_rollback lists are non-empty. */
 	bool has_triggers;
 	/** The number of active nested statement-level transactions. */
@@ -259,17 +264,6 @@ int
 txn_write(struct txn *txn);
 
 /**
- * Roll back the transaction but keep the object around.
- * A special case for memtx transaction abort on yield. In this
- * case we need to abort the transaction to avoid dirty reads but
- * need to keep it around to ensure a new one is not implicitly
- * started and committed by the user program. Later, at
- * transaction commit we will raise an exception.
- */
-void
-txn_abort(struct txn *txn);
-
-/**
  * Most txns don't have triggers, and txn objects
  * are created on every access to data, so txns
  * are partially initialized.
@@ -371,6 +365,34 @@ int
 txn_check_singlestatement(struct txn *txn, const char *where);
 
 /**
+ * Installs yield-in-transaction trigger: roll back the effects
+ * of the transaction and mark the transaction as aborted.
+ *
+ * Used by the memtx engine which doesn't support yields in
+ * transactions.
+ */
+void
+txn_disable_yield(struct txn *txn);
+
+/**
+ * Since memtx engine doesn't support yields inside transactions,
+ * it installs a trigger that aborts the current transaction on
+ * fiber yield. However, we want to allow yields while executing
+ * certain DDL statements, such as building an index or checking
+ * space format, so as not to block the event loop for too long.
+ *
+ * This function temporarily disables the trigger for this purpose.
+ * One must call txn_disable_yield_after_ddl() once the DDL request
+ * has been complete.
+ */
+void
+txn_enable_yield_for_ddl(void);
+
+/** See txn_enable_yield_for_ddl(). */
+void
+txn_disable_yield_after_ddl(void);
+
+/**
  * Returns true if the transaction has a single statement.
  * Supposed to be used from a space on_replace trigger to
  * detect transaction boundaries.
@@ -400,12 +422,6 @@ txn_last_stmt(struct txn *txn)
 }
 
 /**
- * Fiber-stop trigger: roll back the transaction.
- */
-void
-txn_on_stop(struct trigger *trigger, void *event);
-
-/**
  * Return VDBE that is being currently executed.
  *
  * @retval VDBE that is being currently executed.
diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index e0de65d0..8629aa8e 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -1121,6 +1121,8 @@ vinyl_space_check_format(struct space *space, struct tuple_format *format)
 	bool need_wal_sync;
 	tx_manager_abort_writers_for_ddl(env->xm, space, &need_wal_sync);
 
+	txn_enable_yield_for_ddl();
+
 	struct trigger on_replace;
 	struct vy_check_format_ctx ctx;
 	ctx.format = format;
@@ -1168,6 +1170,7 @@ vinyl_space_check_format(struct space *space, struct tuple_format *format)
 
 	diag_destroy(&ctx.diag);
 	trigger_clear(&on_replace);
+	txn_disable_yield_after_ddl();
 	return rc;
 }
 
@@ -4345,6 +4348,8 @@ vinyl_space_build_index(struct space *src_space, struct index *new_index,
 	bool need_wal_sync;
 	tx_manager_abort_writers_for_ddl(env->xm, src_space, &need_wal_sync);
 
+	txn_enable_yield_for_ddl();
+
 	/*
 	 * Iterate over all tuples stored in the space and insert
 	 * each of them into the new LSM tree. Since read iterator
@@ -4438,6 +4443,7 @@ vinyl_space_build_index(struct space *src_space, struct index *new_index,
 
 	diag_destroy(&ctx.diag);
 	trigger_clear(&on_replace);
+	txn_disable_yield_after_ddl();
 	return rc;
 }
 
diff --git a/test/box/transaction.result b/test/box/transaction.result
index 857314b7..ad2d650c 100644
--- a/test/box/transaction.result
+++ b/test/box/transaction.result
@@ -722,3 +722,19 @@ box.commit() -- error
 s:drop()
 ---
 ...
+--
+-- Check that a DDL transaction is rolled back on fiber yield.
+--
+s = box.schema.space.create('test')
+---
+...
+box.begin() s:create_index('pk') fiber.sleep(0)
+---
+...
+box.commit() -- error
+---
+- error: Transaction has been aborted by a fiber yield
+...
+s:drop()
+---
+...
diff --git a/test/box/transaction.test.lua b/test/box/transaction.test.lua
index 8ffae2fe..8cd3e8ba 100644
--- a/test/box/transaction.test.lua
+++ b/test/box/transaction.test.lua
@@ -373,3 +373,11 @@ fiber.sleep(0)
 s.index.pk == nil
 box.commit() -- error
 s:drop()
+
+--
+-- Check that a DDL transaction is rolled back on fiber yield.
+--
+s = box.schema.space.create('test')
+box.begin() s:create_index('pk') fiber.sleep(0)
+box.commit() -- error
+s:drop()
diff --git a/test/engine/truncate.result b/test/engine/truncate.result
index 277e7bda..d65a1df1 100644
--- a/test/engine/truncate.result
+++ b/test/engine/truncate.result
@@ -8,7 +8,7 @@ fiber =  require('fiber')
 ---
 ...
 --
--- Check that space truncation is forbidden in a transaction.
+-- Check that space truncation works fine in a transaction.
 --
 s = box.schema.create_space('test', {engine = engine})
 ---
@@ -19,13 +19,7 @@ _ = s:create_index('pk')
 _ = s:insert{123}
 ---
 ...
-box.begin()
----
-...
-s:truncate()
----
-...
-box.commit()
+box.begin() s:truncate() box.commit()
 ---
 ...
 s:select()
diff --git a/test/engine/truncate.test.lua b/test/engine/truncate.test.lua
index 74fdd52b..fe897fb6 100644
--- a/test/engine/truncate.test.lua
+++ b/test/engine/truncate.test.lua
@@ -4,14 +4,12 @@ engine = test_run:get_cfg('engine')
 fiber =  require('fiber')
 
 --
--- Check that space truncation is forbidden in a transaction.
+-- Check that space truncation works fine in a transaction.
 --
 s = box.schema.create_space('test', {engine = engine})
 _ = s:create_index('pk')
 _ = s:insert{123}
-box.begin()
-s:truncate()
-box.commit()
+box.begin() s:truncate() box.commit()
 s:select()
 s:drop()
 
-- 
2.11.0

^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH v2 2/3] ddl: don't use space_index from AlterSpaceOp::commit,rollback
  2019-07-10 13:09 [PATCH v2 0/3] Transactional DDL Vladimir Davydov
  2019-07-10 13:09 ` [PATCH v2 1/3] memtx: fix txn_on_yield for DDL transactions Vladimir Davydov
@ 2019-07-10 13:09 ` Vladimir Davydov
  2019-07-15 15:05   ` Konstantin Osipov
  2019-07-10 13:09 ` [PATCH v2 3/3] ddl: allow to execute non-yielding DDL statements in transactions Vladimir Davydov
  2019-07-15 16:23 ` [PATCH v2 0/3] Transactional DDL Vladimir Davydov
  3 siblings, 1 reply; 13+ messages in thread
From: Vladimir Davydov @ 2019-07-10 13:09 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches

If there are multiple DDL operations in the same transactions, which is
impossible now, but will be implemented soon, AlterSpaceOp::commit and
rollback methods must not access space index map. To understand that,
consider the following example:

  - on_replace: AlterSpaceOp1 creates index I1 for space S1
  - on_replace: AlterSpaceOp2 moves index I1 from space S1 to space S2
  - on_commit:  AlterSpaceOp1 commits creation of index I1

AlterSpaceOp1 can't lookup I1 in S1 by id, because the index was moved
from S1 to S2 by AlterSpaceOp2. If AlterSpaceOp1 attempts to look it up,
it will access a wrong index.

Fix that by storing pointers to old and new indexes in AlterSpaceOp if
required.
---
 src/box/alter.cc | 77 +++++++++++++++++++++++---------------------------------
 1 file changed, 32 insertions(+), 45 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index ce0cf2d9..612bcd89 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -1088,10 +1088,9 @@ ModifySpace::~ModifySpace()
 class DropIndex: public AlterSpaceOp
 {
 public:
-	DropIndex(struct alter_space *alter, struct index_def *def_arg)
-		:AlterSpaceOp(alter), old_index_def(def_arg) {}
-	/** A reference to the definition of the dropped index. */
-	struct index_def *old_index_def;
+	DropIndex(struct alter_space *alter, struct index *index)
+		:AlterSpaceOp(alter), old_index(index) {}
+	struct index *old_index;
 	virtual void alter_def(struct alter_space *alter);
 	virtual void prepare(struct alter_space *alter);
 	virtual void commit(struct alter_space *alter, int64_t lsn);
@@ -1104,24 +1103,22 @@ public:
 void
 DropIndex::alter_def(struct alter_space * /* alter */)
 {
-	rlist_del_entry(old_index_def, link);
+	rlist_del_entry(old_index->def, link);
 }
 
 /* Do the drop. */
 void
 DropIndex::prepare(struct alter_space *alter)
 {
-	if (old_index_def->iid == 0)
+	if (old_index->def->iid == 0)
 		space_drop_primary_key(alter->new_space);
 }
 
 void
 DropIndex::commit(struct alter_space *alter, int64_t signature)
 {
-	struct index *index = space_index(alter->old_space,
-					  old_index_def->iid);
-	assert(index != NULL);
-	index_commit_drop(index, signature);
+	(void)alter;
+	index_commit_drop(old_index, signature);
 }
 
 /**
@@ -1160,15 +1157,14 @@ class ModifyIndex: public AlterSpaceOp
 {
 public:
 	ModifyIndex(struct alter_space *alter,
-		    struct index_def *new_index_def_arg,
-		    struct index_def *old_index_def_arg)
-		: AlterSpaceOp(alter),new_index_def(new_index_def_arg),
-		  old_index_def(old_index_def_arg) {
+		    struct index *index, struct index_def *def)
+		: AlterSpaceOp(alter), old_index(index),
+		  new_index(NULL), new_index_def(def) {
 	        if (new_index_def->iid == 0 &&
 	            key_part_cmp(new_index_def->key_def->parts,
 	                         new_index_def->key_def->part_count,
-	                         old_index_def->key_def->parts,
-	                         old_index_def->key_def->part_count) != 0) {
+	                         old_index->def->key_def->parts,
+	                         old_index->def->key_def->part_count) != 0) {
 	                /*
 	                 * Primary parts have been changed -
 	                 * update secondary indexes.
@@ -1176,8 +1172,9 @@ public:
 	                alter->pk_def = new_index_def->key_def;
 	        }
 	}
+	struct index *old_index;
+	struct index *new_index;
 	struct index_def *new_index_def;
-	struct index_def *old_index_def;
 	virtual void alter_def(struct alter_space *alter);
 	virtual void alter(struct alter_space *alter);
 	virtual void commit(struct alter_space *alter, int64_t lsn);
@@ -1189,26 +1186,22 @@ public:
 void
 ModifyIndex::alter_def(struct alter_space *alter)
 {
-	rlist_del_entry(old_index_def, link);
+	rlist_del_entry(old_index->def, link);
 	index_def_list_add(&alter->key_list, new_index_def);
 }
 
 void
 ModifyIndex::alter(struct alter_space *alter)
 {
-	assert(old_index_def->iid == new_index_def->iid);
+	new_index = space_index(alter->new_space, new_index_def->iid);
+	assert(old_index->def->iid == new_index->def->iid);
 	/*
 	 * Move the old index to the new space to preserve the
 	 * original data, but use the new definition.
 	 */
 	space_swap_index(alter->old_space, alter->new_space,
-			 old_index_def->iid, new_index_def->iid);
-	struct index *old_index = space_index(alter->old_space,
-					      old_index_def->iid);
-	assert(old_index != NULL);
-	struct index *new_index = space_index(alter->new_space,
-					      new_index_def->iid);
-	assert(new_index != NULL);
+			 old_index->def->iid, new_index->def->iid);
+	SWAP(old_index, new_index);
 	SWAP(old_index->def, new_index->def);
 	index_update_def(new_index);
 }
@@ -1216,27 +1209,19 @@ ModifyIndex::alter(struct alter_space *alter)
 void
 ModifyIndex::commit(struct alter_space *alter, int64_t signature)
 {
-	struct index *new_index = space_index(alter->new_space,
-					      new_index_def->iid);
-	assert(new_index != NULL);
+	(void)alter;
 	index_commit_modify(new_index, signature);
 }
 
 void
 ModifyIndex::rollback(struct alter_space *alter)
 {
-	assert(old_index_def->iid == new_index_def->iid);
 	/*
 	 * Restore indexes.
 	 */
 	space_swap_index(alter->old_space, alter->new_space,
-			 old_index_def->iid, new_index_def->iid);
-	struct index *old_index = space_index(alter->old_space,
-					      old_index_def->iid);
-	assert(old_index != NULL);
-	struct index *new_index = space_index(alter->new_space,
-					      new_index_def->iid);
-	assert(new_index != NULL);
+			 old_index->def->iid, new_index->def->iid);
+	SWAP(old_index, new_index);
 	SWAP(old_index->def, new_index->def);
 	index_update_def(old_index);
 }
@@ -1400,10 +1385,12 @@ class TruncateIndex: public AlterSpaceOp
 	 * In case TRUNCATE fails, we need to clean up the new
 	 * index data in the engine.
 	 */
+	struct index *old_index;
 	struct index *new_index;
 public:
 	TruncateIndex(struct alter_space *alter, uint32_t iid)
-		: AlterSpaceOp(alter), iid(iid) {}
+		: AlterSpaceOp(alter), iid(iid),
+		  old_index(NULL), new_index(NULL) {}
 	virtual void prepare(struct alter_space *alter);
 	virtual void commit(struct alter_space *alter, int64_t signature);
 	virtual ~TruncateIndex();
@@ -1412,7 +1399,9 @@ public:
 void
 TruncateIndex::prepare(struct alter_space *alter)
 {
+	old_index = space_index(alter->old_space, iid);
 	new_index = space_index(alter->new_space, iid);
+
 	if (iid == 0) {
 		/*
 		 * Notify the engine that the primary index
@@ -1437,8 +1426,7 @@ TruncateIndex::prepare(struct alter_space *alter)
 void
 TruncateIndex::commit(struct alter_space *alter, int64_t signature)
 {
-	struct index *old_index = space_index(alter->old_space, iid);
-
+	(void)alter;
 	index_commit_drop(old_index, signature);
 	index_commit_create(new_index, signature);
 	new_index = NULL;
@@ -1655,7 +1643,7 @@ alter_space_move_indexes(struct alter_space *alter, uint32_t begin,
 				new_def = index_def_dup(old_def);
 				index_def_update_optionality(new_def,
 							     min_field_count);
-				(void) new ModifyIndex(alter, new_def, old_def);
+				(void) new ModifyIndex(alter, old_index, new_def);
 			} else {
 				(void) new MoveIndex(alter, old_def->iid);
 			}
@@ -1672,7 +1660,7 @@ alter_space_move_indexes(struct alter_space *alter, uint32_t begin,
 		index_def_update_optionality(new_def, min_field_count);
 		auto guard = make_scoped_guard([=] { index_def_delete(new_def); });
 		if (!index_def_change_requires_rebuild(old_index, new_def))
-			(void) new ModifyIndex(alter, new_def, old_def);
+			(void) new ModifyIndex(alter, old_index, new_def);
 		else
 			(void) new RebuildIndex(alter, new_def, old_def);
 		guard.is_active = false;
@@ -2207,7 +2195,7 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 				  "can not drop a referenced index");
 		}
 		alter_space_move_indexes(alter, 0, iid);
-		(void) new DropIndex(alter, old_index->def);
+		(void) new DropIndex(alter, old_index);
 	}
 	/* Case 2: create an index, if it is simply created. */
 	if (old_index == NULL && new_tuple != NULL) {
@@ -2285,8 +2273,7 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 			 * in the space conform to the new format.
 			 */
 			(void) new CheckSpaceFormat(alter);
-			(void) new ModifyIndex(alter, index_def,
-					       old_index->def);
+			(void) new ModifyIndex(alter, old_index, index_def);
 			index_def_guard.is_active = false;
 		}
 	}
-- 
2.11.0

^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH v2 3/3] ddl: allow to execute non-yielding DDL statements in transactions
  2019-07-10 13:09 [PATCH v2 0/3] Transactional DDL Vladimir Davydov
  2019-07-10 13:09 ` [PATCH v2 1/3] memtx: fix txn_on_yield for DDL transactions Vladimir Davydov
  2019-07-10 13:09 ` [PATCH v2 2/3] ddl: don't use space_index from AlterSpaceOp::commit,rollback Vladimir Davydov
@ 2019-07-10 13:09 ` Vladimir Davydov
  2019-07-10 20:43   ` Konstantin Osipov
                     ` (2 more replies)
  2019-07-15 16:23 ` [PATCH v2 0/3] Transactional DDL Vladimir Davydov
  3 siblings, 3 replies; 13+ messages in thread
From: Vladimir Davydov @ 2019-07-10 13:09 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches

The patch is pretty straightforward - all it does is moves checks for
single statement transactions from alter.cc to txn_enable_yield_for_ddl
so that now any DDL request may be executed in a transaction unless it
builds an index or checks the format of a non-empty space (those are the
only two operations that may yield).

There's two things that must be noted explicitly. The first is removal
of an assertion from priv_grant. The assertion ensured that a revoked
privilege was in the cache. The problem is the cache is built from the
contents of the space, see user_reload_privs. On rollback, we first
revert the content of the space to the original state, and only then
start invoking rollback triggers, which call priv_grant. As a result, we
will revert the cache to the original state right after the first
trigger is invoked and the following triggers will have no effect on it.
Thus we have to remove this assertion.

The second subtlety lays in vinyl_index_commit_modify. Before the commit
we assumed that if statement lsn is <= vy_lsm::commit_lsn, then it must
be local recovery from WAL. Now it's not true, because there may be
several operations for the same index in a transaction, and they all
will receive the same signature in on_commit trigger. We could, of
course, try to assign different signatures to them, but that would look
cumbersome - better simply allow lsn <= vy_lsm::commit_lsn after local
recovery, there's actually nothing wrong about that.

Closes #4083

@TarantoolBot document
Title: Transactional DDL

Now it's possible to group non-yielding DDL statements into
transactions, e.g.

```Lua
box.begin()
box.schema.space.create('my_space')
box.space.my_space:create_index('primary')
box.commit() -- or box.rollback()
```

Most DDL statements don't yield and hence can be run from transactions.
There are just two exceptions: creation of a new index and changing the
format of a non-empty space. Those are long operations that may yield
so as not to block the event loop for too long. Those statements can't
be executed from transactions (to be more exact, such a statement must
go first in any transaction).

Also, just like in case of DML transactions in memtx, it's forbidden to
explicitly yield in a DDL transaction by calling fiber.sleep or any
other yielding function. If this happens, the transaction will be
aborted and an attempt to commit it will fail.
---
 src/box/alter.cc              | 14 --------
 src/box/errcode.h             |  2 +-
 src/box/memtx_space.c         |  8 +++--
 src/box/txn.c                 | 27 +++++++-------
 src/box/txn.h                 | 25 ++++---------
 src/box/user.cc               |  1 -
 src/box/vinyl.c               | 23 ++++++++----
 test/box/misc.result          |  1 +
 test/box/on_replace.result    | 53 +++++++++++++---------------
 test/box/on_replace.test.lua  | 13 +++----
 test/box/transaction.result   | 82 ++++++++++++++++++++++++++++++++-----------
 test/box/transaction.test.lua | 56 +++++++++++++++++++++++------
 test/engine/ddl.result        | 58 ++++++++++++++++++++++++++++--
 test/engine/ddl.test.lua      | 39 ++++++++++++++++++--
 14 files changed, 273 insertions(+), 129 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index 612bcd89..c7be6b77 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -1816,7 +1816,6 @@ static void
 on_replace_dd_space(struct trigger * /* trigger */, void *event)
 {
 	struct txn *txn = (struct txn *) event;
-	txn_check_singlestatement_xc(txn, "Space _space");
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
@@ -2113,7 +2112,6 @@ static void
 on_replace_dd_index(struct trigger * /* trigger */, void *event)
 {
 	struct txn *txn = (struct txn *) event;
-	txn_check_singlestatement_xc(txn, "Space _index");
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
@@ -2305,7 +2303,6 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event)
 {
 	struct txn *txn = (struct txn *) event;
 	struct txn_stmt *stmt = txn_current_stmt(txn);
-	txn_check_singlestatement_xc(txn, "Space _truncate");
 	struct tuple *new_tuple = stmt->new_tuple;
 
 	if (new_tuple == NULL) {
@@ -2535,7 +2532,6 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
 {
 	struct txn *txn = (struct txn *) event;
 	struct txn_stmt *stmt = txn_current_stmt(txn);
-	txn_check_singlestatement_xc(txn, "Space _user");
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
 
@@ -2686,7 +2682,6 @@ static void
 on_replace_dd_func(struct trigger * /* trigger */, void *event)
 {
 	struct txn *txn = (struct txn *) event;
-	txn_check_singlestatement_xc(txn, "Space _func");
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
@@ -2880,7 +2875,6 @@ on_replace_dd_collation(struct trigger * /* trigger */, void *event)
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
-	txn_check_singlestatement_xc(txn, "Space _collation");
 	if (new_tuple == NULL && old_tuple != NULL) {
 		/* DELETE */
 		struct trigger *on_commit =
@@ -3178,7 +3172,6 @@ static void
 on_replace_dd_priv(struct trigger * /* trigger */, void *event)
 {
 	struct txn *txn = (struct txn *) event;
-	txn_check_singlestatement_xc(txn, "Space _priv");
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
@@ -3227,7 +3220,6 @@ static void
 on_replace_dd_schema(struct trigger * /* trigger */, void *event)
 {
 	struct txn *txn = (struct txn *) event;
-	txn_check_singlestatement_xc(txn, "Space _schema");
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
@@ -3305,7 +3297,6 @@ on_replace_dd_cluster(struct trigger *trigger, void *event)
 {
 	(void) trigger;
 	struct txn *txn = (struct txn *) event;
-	txn_check_singlestatement_xc(txn, "Space _cluster");
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
@@ -3458,7 +3449,6 @@ static void
 on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
 {
 	struct txn *txn = (struct txn *) event;
-	txn_check_singlestatement_xc(txn, "Space _sequence");
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
@@ -3661,7 +3651,6 @@ static void
 on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event)
 {
 	struct txn *txn = (struct txn *) event;
-	txn_check_singlestatement_xc(txn, "Space _space_sequence");
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *tuple = stmt->new_tuple ? stmt->new_tuple : stmt->old_tuple;
 
@@ -3806,7 +3795,6 @@ static void
 on_replace_dd_trigger(struct trigger * /* trigger */, void *event)
 {
 	struct txn *txn = (struct txn *) event;
-	txn_check_singlestatement_xc(txn, "Space _trigger");
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
@@ -4189,7 +4177,6 @@ static void
 on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
 {
 	struct txn *txn = (struct txn *) event;
-	txn_check_singlestatement_xc(txn, "Space _fk_constraint");
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
@@ -4480,7 +4467,6 @@ static void
 on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event)
 {
 	struct txn *txn = (struct txn *) event;
-	txn_check_singlestatement_xc(txn, "Space _ck_constraint");
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
diff --git a/src/box/errcode.h b/src/box/errcode.h
index be8dab27..97fc9675 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -106,7 +106,7 @@ struct errcode_record {
 	/* 51 */_(ER_NO_SUCH_FUNCTION,		"Function '%s' does not exist") \
 	/* 52 */_(ER_FUNCTION_EXISTS,		"Function '%s' already exists") \
 	/* 53 */_(ER_BEFORE_REPLACE_RET,	"Invalid return value of space:before_replace trigger: expected tuple or nil, got %s") \
-	/* 54 */_(ER_UNUSED2,			"") \
+	/* 54 */_(ER_MULTISTATEMENT_DDL,	"Can not perform %s in a multi-statement transaction") \
 	/* 55 */_(ER_TRIGGER_EXISTS,		"Trigger '%s' already exists") \
 	/* 56 */_(ER_USER_MAX,			"A limit on the total number of users has been reached: %u") \
 	/* 57 */_(ER_NO_SUCH_ENGINE,		"Space engine '%s' does not exist") \
diff --git a/src/box/memtx_space.c b/src/box/memtx_space.c
index 921dbcbf..47369994 100644
--- a/src/box/memtx_space.c
+++ b/src/box/memtx_space.c
@@ -888,7 +888,8 @@ memtx_space_check_format(struct space *space, struct tuple_format *format)
 	if (it == NULL)
 		return -1;
 
-	txn_enable_yield_for_ddl();
+	if (txn_enable_yield_for_ddl("space format check") != 0)
+		return -1;
 
 	struct memtx_engine *memtx = (struct memtx_engine *)space->engine;
 	struct memtx_ddl_state state;
@@ -1018,6 +1019,8 @@ memtx_space_build_index(struct space *src_space, struct index *new_index,
 	struct index *pk = index_find(src_space, 0);
 	if (pk == NULL)
 		return -1;
+	if (index_size(pk) == 0)
+		return 0;
 
 	struct errinj *inj = errinj(ERRINJ_BUILD_INDEX, ERRINJ_INT);
 	if (inj != NULL && inj->iparam == (int)new_index->def->iid) {
@@ -1030,7 +1033,8 @@ memtx_space_build_index(struct space *src_space, struct index *new_index,
 	if (it == NULL)
 		return -1;
 
-	txn_enable_yield_for_ddl();
+	if (txn_enable_yield_for_ddl("index build") != 0)
+		return -1;
 
 	struct memtx_engine *memtx = (struct memtx_engine *)src_space->engine;
 	struct memtx_ddl_state state;
diff --git a/src/box/txn.c b/src/box/txn.c
index bab98d7e..9ae11aa7 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -592,17 +592,6 @@ txn_abort(struct txn *txn)
 	txn->is_aborted = true;
 }
 
-int
-txn_check_singlestatement(struct txn *txn, const char *where)
-{
-	if (!txn_is_first_statement(txn)) {
-		diag_set(ClientError, ER_UNSUPPORTED,
-			 where, "multi-statement transactions");
-		return -1;
-	}
-	return 0;
-}
-
 void
 txn_disable_yield(struct txn *txn)
 {
@@ -612,12 +601,24 @@ txn_disable_yield(struct txn *txn)
 	trigger_add(&fiber()->on_yield, &txn->fiber_on_yield);
 }
 
-void
-txn_enable_yield_for_ddl(void)
+int
+txn_enable_yield_for_ddl(const char *what)
 {
 	struct txn *txn = in_txn();
 	assert(txn != NULL && txn->is_yield_disabled);
+	/*
+	 * It's okay to yield while executing the first DDL statement
+	 * in a transaction, because the schema hasn't been updated
+	 * yet and so other transactions can't see uncommitted objects.
+	 * Yielding in subsequent statements is not safe, as there
+	 * may be uncommitted objects in the schema cache.
+	 */
+	if (!txn_is_first_statement(txn)) {
+		diag_set(ClientError, ER_MULTISTATEMENT_DDL, what);
+		return -1;
+	}
 	trigger_clear(&txn->fiber_on_yield);
+	return 0;
 }
 
 void
diff --git a/src/box/txn.h b/src/box/txn.h
index 258a8db7..92366038 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -358,13 +358,6 @@ void
 txn_rollback_stmt(struct txn *txn);
 
 /**
- * Raise an error if this is a multi-statement transaction: DDL
- * can not be part of a multi-statement transaction.
- */
-int
-txn_check_singlestatement(struct txn *txn, const char *where);
-
-/**
  * Installs yield-in-transaction trigger: roll back the effects
  * of the transaction and mark the transaction as aborted.
  *
@@ -384,9 +377,13 @@ txn_disable_yield(struct txn *txn);
  * This function temporarily disables the trigger for this purpose.
  * One must call txn_disable_yield_after_ddl() once the DDL request
  * has been complete.
+ *
+ * Before enabling yields, this function checks if it doesn't
+ * violate transaction isolation. If it does, it returns -1 and
+ * sets diag. The 'what' message is used for error reporting.
  */
-void
-txn_enable_yield_for_ddl(void);
+int
+txn_enable_yield_for_ddl(const char *what);
 
 /** See txn_enable_yield_for_ddl(). */
 void
@@ -525,16 +522,6 @@ box_txn_rollback_to_savepoint(box_txn_savepoint_t *savepoint);
 
 #if defined(__cplusplus)
 } /* extern "C" */
-
-#include "diag.h"
-
-static inline void
-txn_check_singlestatement_xc(struct txn *txn, const char *where)
-{
-	if (txn_check_singlestatement(txn, where) != 0)
-		diag_raise();
-}
-
 #endif /* defined(__cplusplus) */
 
 #endif /* TARANTOOL_BOX_TXN_H_INCLUDED */
diff --git a/src/box/user.cc b/src/box/user.cc
index 48bdf18e..c46ff67d 100644
--- a/src/box/user.cc
+++ b/src/box/user.cc
@@ -709,7 +709,6 @@ priv_grant(struct user *grantee, struct priv_def *priv)
 	if (object == NULL)
 		return;
 	struct access *access = &object[grantee->auth_token];
-	assert(privset_search(&grantee->privs, priv) || access->granted == 0);
 	access->granted = priv->access;
 	rebuild_effective_grants(grantee);
 }
diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index 8629aa8e..af044f0c 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -919,16 +919,17 @@ vinyl_index_commit_modify(struct index *index, int64_t lsn)
 	       env->status == VINYL_FINAL_RECOVERY_LOCAL ||
 	       env->status == VINYL_FINAL_RECOVERY_REMOTE);
 
-	if (lsn <= lsm->commit_lsn) {
+	if (env->status == VINYL_FINAL_RECOVERY_LOCAL &&
+	    lsn <= lsm->commit_lsn) {
 		/*
-		 * This must be local recovery from WAL, when
-		 * the operation has already been committed to
-		 * vylog.
+		 * The statement we are recovering from WAL has
+		 * been successfully written to vylog so we must
+		 * not replay it.
 		 */
-		assert(env->status == VINYL_FINAL_RECOVERY_LOCAL);
 		return;
 	}
 
+	assert(lsm->commit_lsn <= lsn);
 	lsm->commit_lsn = lsn;
 
 	vy_log_tx_begin();
@@ -1121,7 +1122,11 @@ vinyl_space_check_format(struct space *space, struct tuple_format *format)
 	bool need_wal_sync;
 	tx_manager_abort_writers_for_ddl(env->xm, space, &need_wal_sync);
 
-	txn_enable_yield_for_ddl();
+	if (!need_wal_sync && vy_lsm_is_empty(pk))
+		return 0; /* space is empty, nothing to do */
+
+	if (txn_enable_yield_for_ddl("space format check") != 0)
+		return -1;
 
 	struct trigger on_replace;
 	struct vy_check_format_ctx ctx;
@@ -4348,7 +4353,11 @@ vinyl_space_build_index(struct space *src_space, struct index *new_index,
 	bool need_wal_sync;
 	tx_manager_abort_writers_for_ddl(env->xm, src_space, &need_wal_sync);
 
-	txn_enable_yield_for_ddl();
+	if (!need_wal_sync && vy_lsm_is_empty(pk))
+		return 0; /* space is empty, nothing to do */
+
+	if (txn_enable_yield_for_ddl("index build") != 0)
+		return -1;
 
 	/*
 	 * Iterate over all tuples stored in the space and insert
diff --git a/test/box/misc.result b/test/box/misc.result
index dab8549b..3c8e6994 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -385,6 +385,7 @@ t;
   51: box.error.NO_SUCH_FUNCTION
   52: box.error.FUNCTION_EXISTS
   53: box.error.BEFORE_REPLACE_RET
+  54: box.error.MULTISTATEMENT_DDL
   55: box.error.TRIGGER_EXISTS
   56: box.error.USER_MAX
   57: box.error.NO_SUCH_ENGINE
diff --git a/test/box/on_replace.result b/test/box/on_replace.result
index b71c9878..6334d9a2 100644
--- a/test/box/on_replace.result
+++ b/test/box/on_replace.result
@@ -465,86 +465,83 @@ s = box.schema.space.create('test_on_repl_ddl')
 _ = s:create_index('pk')
 ---
 ...
+_ = s:create_index('sk', {parts = {2, 'unsigned'}})
+---
+...
 t = s:on_replace(function () box.schema.space.create('some_space') end)
 ---
 ...
 s:replace({1, 2})
 ---
-- error: Space _schema does not support multi-statement transactions
+- [1, 2]
 ...
-t = s:on_replace(function () s:create_index('sec') end, t)
+t = s:on_replace(function () s:format({{'a', 'unsigned'}, {'b', 'unsigned'}}) end, t)
 ---
 ...
 s:replace({2, 3})
 ---
-- error: Space _index does not support multi-statement transactions
+- [2, 3]
 ...
-t = s:on_replace(function () box.schema.user.create('newu') end, t)
+t = s:on_replace(function () box.schema.user.create('newu') box.schema.role.create('newr') end, t)
 ---
 ...
 s:replace({3, 4})
 ---
-- error: Space _user does not support multi-statement transactions
+- [3, 4]
 ...
-t = s:on_replace(function () box.schema.role.create('newr') end, t)
+t = s:on_replace(function () box.schema.user.drop('newu') box.schema.role.drop('newr') end, t)
 ---
 ...
 s:replace({4, 5})
 ---
-- error: Space _user does not support multi-statement transactions
+- [4, 5]
 ...
-t = s:on_replace(function () box.space._user:delete{box.schema.GUEST_ID} end, t)
----
-...
-s:replace({4, 5})
----
-- error: Space _user does not support multi-statement transactions
-...
-t = s:on_replace(function () box.space._user:delete{box.schema.SUPER_ROLE_ID} end, t)
----
-...
-s:replace({4, 5})
----
-- error: Space _user does not support multi-statement transactions
-...
-t = s:on_replace(function () s:drop() end, t)
+t = s:on_replace(function () s.index.sk:drop() end, t)
 ---
 ...
 s:replace({5, 6})
 ---
-- error: Space _index does not support multi-statement transactions
+- [5, 6]
 ...
 t = s:on_replace(function () box.schema.func.create('newf') end, t)
 ---
 ...
 s:replace({6, 7})
 ---
-- error: Space _func does not support multi-statement transactions
+- [6, 7]
 ...
 t = s:on_replace(function () box.schema.user.grant('guest', 'read,write', 'space', 'test_on_repl_ddl') end, t)
 ---
 ...
 s:replace({7, 8})
 ---
-- error: Space _priv does not support multi-statement transactions
+- [7, 8]
 ...
 t = s:on_replace(function () s:rename('newname') end, t)
 ---
 ...
 s:replace({8, 9})
 ---
-- error: Space _space does not support multi-statement transactions
+- [8, 9]
 ...
 t = s:on_replace(function () s.index.pk:rename('newname') end, t)
 ---
 ...
 s:replace({9, 10})
 ---
-- error: Space _index does not support multi-statement transactions
+- [9, 10]
 ...
 s:select()
 ---
-- []
+- - [1, 2]
+  - [2, 3]
+  - [3, 4]
+  - [4, 5]
+  - [5, 6]
+  - [6, 7]
+  - [7, 8]
+  - [8, 9]
+  - [9, 10]
 ...
 s:drop() -- test_on_repl_ddl
 ---
diff --git a/test/box/on_replace.test.lua b/test/box/on_replace.test.lua
index 7fffc1e0..79c828da 100644
--- a/test/box/on_replace.test.lua
+++ b/test/box/on_replace.test.lua
@@ -181,19 +181,16 @@ second:drop()
 
 s = box.schema.space.create('test_on_repl_ddl')
 _ = s:create_index('pk')
+_ = s:create_index('sk', {parts = {2, 'unsigned'}})
 t = s:on_replace(function () box.schema.space.create('some_space') end)
 s:replace({1, 2})
-t = s:on_replace(function () s:create_index('sec') end, t)
+t = s:on_replace(function () s:format({{'a', 'unsigned'}, {'b', 'unsigned'}}) end, t)
 s:replace({2, 3})
-t = s:on_replace(function () box.schema.user.create('newu') end, t)
+t = s:on_replace(function () box.schema.user.create('newu') box.schema.role.create('newr') end, t)
 s:replace({3, 4})
-t = s:on_replace(function () box.schema.role.create('newr') end, t)
+t = s:on_replace(function () box.schema.user.drop('newu') box.schema.role.drop('newr') end, t)
 s:replace({4, 5})
-t = s:on_replace(function () box.space._user:delete{box.schema.GUEST_ID} end, t)
-s:replace({4, 5})
-t = s:on_replace(function () box.space._user:delete{box.schema.SUPER_ROLE_ID} end, t)
-s:replace({4, 5})
-t = s:on_replace(function () s:drop() end, t)
+t = s:on_replace(function () s.index.sk:drop() end, t)
 s:replace({5, 6})
 t = s:on_replace(function () box.schema.func.create('newf') end, t)
 s:replace({6, 7})
diff --git a/test/box/transaction.result b/test/box/transaction.result
index ad2d650c..9a48f2f7 100644
--- a/test/box/transaction.result
+++ b/test/box/transaction.result
@@ -84,22 +84,12 @@ while f:status() ~= 'dead' do fiber.sleep(0) end;
 ---
 ...
 -- transactions and system spaces
--- some operation involves more than one ddl spaces, so they should fail
-box.begin() box.schema.space.create('test');
+box.begin() box.schema.space.create('test') box.rollback();
 ---
-- error: Space _space does not support multi-statement transactions
 ...
-box.rollback();
+box.begin() box.schema.user.create('test') box.rollback();
 ---
 ...
-box.begin() box.schema.user.create('test');
----
-- error: Space _priv does not support multi-statement transactions
-...
-box.rollback();
----
-...
--- but this is Ok now
 box.begin() box.schema.func.create('test') box.rollback();
 ---
 ...
@@ -657,21 +647,14 @@ box.space.vinyl:select{};
 -- Two DDL satements in a row
 box.begin()
 box.space.test:truncate()
-box.space.test:truncate();
----
-- error: Space _truncate does not support multi-statement transactions
-...
--- A transaction is left open due to an exception in the above fragment
+box.space.test:truncate()
 box.rollback();
 ---
 ...
 -- Two DDL stateemnts on different engines
 box.begin()
 box.space.memtx:truncate()
-box.space.vinyl:truncate();
----
-- error: Space _truncate does not support multi-statement transactions
-...
+box.space.vinyl:truncate()
 box.rollback();
 ---
 ...
@@ -738,3 +721,60 @@ box.commit() -- error
 s:drop()
 ---
 ...
+--
+-- Multiple DDL statements in the same transaction.
+--
+test_run:cmd("setopt delimiter ';'")
+---
+- true
+...
+function create()
+    box.schema.role.create('my_role')
+    box.schema.user.create('my_user')
+    box.schema.user.grant('my_user', 'my_role')
+    box.schema.space.create('memtx_space', {engine = 'memtx'})
+    box.schema.space.create('vinyl_space', {engine = 'vinyl'})
+    box.schema.role.grant('my_role', 'read,write', 'space', 'vinyl_space')
+    box.schema.user.grant('my_user', 'read,write', 'space', 'memtx_space')
+    box.space.memtx_space:create_index('pk', {sequence = true})
+    box.space.memtx_space:create_index('sk', {parts = {2, 'unsigned'}})
+    box.space.vinyl_space:create_index('pk', {sequence = true})
+    box.space.vinyl_space:create_index('sk', {parts = {2, 'unsigned'}})
+    box.space.memtx_space:truncate()
+    box.space.vinyl_space:truncate()
+    box.space.memtx_space:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
+    box.space.vinyl_space:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
+    box.schema.func.create('my_func')
+end;
+---
+...
+function drop()
+    box.schema.func.drop('my_func')
+    box.space.memtx_space:truncate()
+    box.space.vinyl_space:truncate()
+    box.schema.user.revoke('my_user', 'read,write', 'space', 'memtx_space')
+    box.schema.role.revoke('my_role', 'read,write', 'space', 'vinyl_space')
+    box.space.memtx_space:drop()
+    box.space.vinyl_space:drop()
+    box.schema.user.revoke('my_user', 'my_role')
+    box.schema.user.drop('my_user')
+    box.schema.role.drop('my_role')
+end;
+---
+...
+test_run:cmd("setopt delimiter ''");
+---
+- true
+...
+box.begin() create() box.rollback()
+---
+...
+box.begin() create() box.commit()
+---
+...
+box.begin() drop() box.rollback()
+---
+...
+box.begin() drop() box.commit()
+---
+...
diff --git a/test/box/transaction.test.lua b/test/box/transaction.test.lua
index 8cd3e8ba..0792cc3c 100644
--- a/test/box/transaction.test.lua
+++ b/test/box/transaction.test.lua
@@ -41,12 +41,8 @@ f = fiber.create(sloppy);
 -- ensure it's rolled back automatically
 while f:status() ~= 'dead' do fiber.sleep(0) end;
 -- transactions and system spaces
--- some operation involves more than one ddl spaces, so they should fail
-box.begin() box.schema.space.create('test');
-box.rollback();
-box.begin() box.schema.user.create('test');
-box.rollback();
--- but this is Ok now
+box.begin() box.schema.space.create('test') box.rollback();
+box.begin() box.schema.user.create('test') box.rollback();
 box.begin() box.schema.func.create('test') box.rollback();
 box.begin() box.schema.user.grant('guest', 'read', 'space', '_priv') box.rollback();
 space = box.schema.space.create('test');
@@ -341,16 +337,13 @@ box.space.vinyl:select{};
 -- Two DDL satements in a row
 box.begin()
 box.space.test:truncate()
-box.space.test:truncate();
-
--- A transaction is left open due to an exception in the above fragment
+box.space.test:truncate()
 box.rollback();
 
 -- Two DDL stateemnts on different engines
 box.begin()
 box.space.memtx:truncate()
-box.space.vinyl:truncate();
-
+box.space.vinyl:truncate()
 box.rollback();
 
 box.space.memtx:select{};
@@ -381,3 +374,44 @@ s = box.schema.space.create('test')
 box.begin() s:create_index('pk') fiber.sleep(0)
 box.commit() -- error
 s:drop()
+
+--
+-- Multiple DDL statements in the same transaction.
+--
+test_run:cmd("setopt delimiter ';'")
+function create()
+    box.schema.role.create('my_role')
+    box.schema.user.create('my_user')
+    box.schema.user.grant('my_user', 'my_role')
+    box.schema.space.create('memtx_space', {engine = 'memtx'})
+    box.schema.space.create('vinyl_space', {engine = 'vinyl'})
+    box.schema.role.grant('my_role', 'read,write', 'space', 'vinyl_space')
+    box.schema.user.grant('my_user', 'read,write', 'space', 'memtx_space')
+    box.space.memtx_space:create_index('pk', {sequence = true})
+    box.space.memtx_space:create_index('sk', {parts = {2, 'unsigned'}})
+    box.space.vinyl_space:create_index('pk', {sequence = true})
+    box.space.vinyl_space:create_index('sk', {parts = {2, 'unsigned'}})
+    box.space.memtx_space:truncate()
+    box.space.vinyl_space:truncate()
+    box.space.memtx_space:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
+    box.space.vinyl_space:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
+    box.schema.func.create('my_func')
+end;
+function drop()
+    box.schema.func.drop('my_func')
+    box.space.memtx_space:truncate()
+    box.space.vinyl_space:truncate()
+    box.schema.user.revoke('my_user', 'read,write', 'space', 'memtx_space')
+    box.schema.role.revoke('my_role', 'read,write', 'space', 'vinyl_space')
+    box.space.memtx_space:drop()
+    box.space.vinyl_space:drop()
+    box.schema.user.revoke('my_user', 'my_role')
+    box.schema.user.drop('my_user')
+    box.schema.role.drop('my_role')
+end;
+test_run:cmd("setopt delimiter ''");
+
+box.begin() create() box.rollback()
+box.begin() create() box.commit()
+box.begin() drop() box.rollback()
+box.begin() drop() box.commit()
diff --git a/test/engine/ddl.result b/test/engine/ddl.result
index 30f305e9..c59c85f1 100644
--- a/test/engine/ddl.result
+++ b/test/engine/ddl.result
@@ -2215,8 +2215,62 @@ box.schema.user.revoke('guest', 'write', 'space', 'ddl_test')
 box.commit();
 ---
 ...
--- index and space drop are not currently supported (because of truncate)
-s.index.pk:drop();
+box.begin()
+s.index.pk:drop()
+s:drop()
+box.commit();
+---
+...
+--
+-- Only the first statement in a transaction is allowed to be
+-- a yielding DDL statement (index build, space format check).
+--
+s = box.schema.space.create('test', {engine = engine});
+---
+...
+_ = s:create_index('pk');
+---
+...
+s:insert{1, 1};
+---
+- [1, 1]
+...
+-- ok
+box.begin()
+s:create_index('sk', {parts = {2, 'unsigned'}})
+box.commit();
+---
+...
+s.index.sk:drop();
+---
+...
+-- ok
+box.begin()
+s:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
+box.commit();
+---
+...
+s:format({});
+---
+...
+-- error
+box.begin()
+s.index.pk:alter{sequence = true}
+s:create_index('sk', {parts = {2, 'unsigned'}});
+---
+- error: Can not perform index build in a multi-statement transaction
+...
+box.rollback();
+---
+...
+-- error
+box.begin()
+s.index.pk:alter{sequence = true}
+s:format({{'a', 'unsigned'}, {'b', 'unsigned'}});
+---
+- error: Can not perform space format check in a multi-statement transaction
+...
+box.rollback();
 ---
 ...
 s:drop();
diff --git a/test/engine/ddl.test.lua b/test/engine/ddl.test.lua
index 25ce8ee6..1670b548 100644
--- a/test/engine/ddl.test.lua
+++ b/test/engine/ddl.test.lua
@@ -827,8 +827,43 @@ box.begin()
 box.schema.user.revoke('guest', 'write', 'space', 'ddl_test')
 box.commit();
 
--- index and space drop are not currently supported (because of truncate)
-s.index.pk:drop();
+box.begin()
+s.index.pk:drop()
+s:drop()
+box.commit();
+
+--
+-- Only the first statement in a transaction is allowed to be
+-- a yielding DDL statement (index build, space format check).
+--
+s = box.schema.space.create('test', {engine = engine});
+_ = s:create_index('pk');
+s:insert{1, 1};
+
+-- ok
+box.begin()
+s:create_index('sk', {parts = {2, 'unsigned'}})
+box.commit();
+s.index.sk:drop();
+
+-- ok
+box.begin()
+s:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
+box.commit();
+s:format({});
+
+-- error
+box.begin()
+s.index.pk:alter{sequence = true}
+s:create_index('sk', {parts = {2, 'unsigned'}});
+box.rollback();
+
+-- error
+box.begin()
+s.index.pk:alter{sequence = true}
+s:format({{'a', 'unsigned'}, {'b', 'unsigned'}});
+box.rollback();
+
 s:drop();
 
 --
-- 
2.11.0

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 1/3] memtx: fix txn_on_yield for DDL transactions
  2019-07-10 13:09 ` [PATCH v2 1/3] memtx: fix txn_on_yield for DDL transactions Vladimir Davydov
@ 2019-07-10 20:34   ` Konstantin Osipov
  2019-07-12 14:54     ` Vladimir Davydov
  0 siblings, 1 reply; 13+ messages in thread
From: Konstantin Osipov @ 2019-07-10 20:34 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/10 16:13]:
>  static int
>  memtx_engine_begin(struct engine *engine, struct txn *txn)
>  {
>  	(void)engine;
> +	txn_disable_yield(txn);

This name is rather obscure.

txn_prohibit_yield()
txn_rollback_on_yield()
txn_disallow_yield()
txn_set_yield_trigger()
txn_set_rollback_on_yield_trigger()
txn_can_yield(false);

> -	/* .prepare = */ memtx_engine_prepare,
> +	/* .begin_statement = */ generic_engine_begin_statement,
> +	/* .prepare = */ generic_engine_prepare,

If some of these callbacks are now empty in all engines, they
should be removed from vtab.

> +	txn_enable_yield_for_ddl();
> +

Same here:

txn_allow_yield()

txn_yield_exception();
txn_can_yield(true);

>  	struct memtx_engine *memtx = (struct memtx_engine *)space->engine;
>  	struct memtx_ddl_state state;
>  	state.format = format;
> @@ -930,6 +932,7 @@ memtx_space_check_format(struct space *space, struct tuple_format *format)
>  	iterator_delete(it);
>  	diag_destroy(&state.diag);
>  	trigger_clear(&on_replace);
> +	txn_disable_yield_after_ddl();

txn_can_yield(false);

> +
>  static inline void
>  fiber_set_txn(struct fiber *fiber, struct txn *txn)
>  {
> @@ -194,6 +200,7 @@ txn_begin()
>  	txn->has_triggers  = false;
>  	txn->is_done = false;
>  	txn->is_aborted = false;
> +	txn->is_yield_disabled = false;

It is hight time to convert these to bit fields.

txn->can_yield

>  	txn->in_sub_stmt = 0;
>  	txn->id = ++tsn;
>  	txn->signature = -1;
> @@ -201,8 +208,8 @@ txn_begin()
>  	txn->engine_tx = NULL;
>  	txn->psql_txn = NULL;
>  	txn->fiber = NULL;
> -	/* fiber_on_yield/fiber_on_stop initialized by engine on demand */
>  	fiber_set_txn(fiber(), txn);
> +	/* fiber_on_yield is initialized by engine on demand */
>  	trigger_create(&txn->fiber_on_stop, txn_on_stop, NULL, NULL);
>  	trigger_add(&fiber()->on_stop, &txn->fiber_on_stop);
>  	return txn;
> @@ -463,6 +470,12 @@ txn_write_to_wal(struct txn *txn)
>  static int
>  txn_prepare(struct txn *txn)
>  {
> +	if (txn->is_aborted) {
> +		assert(txn->is_yield_disabled);

To anyone who doesn't know that is_aborted is only set by the
yield trigger, this is rather obscure. How are these variables
connected? 
is_aborted is a very general name, perhaps the context has changed
a bit but now when I read this code I don't immediately understadn
why is_aborted == ER_TRANACTION_YIELD. 

Perhaps is_aborted should be is_aborted_by_yield.

Overall I agree the approach is much better than before, at least
easier to track since the logic is not hidden in the engine.

Please let's make the names more evident now, and the api simple.
I think txn_can_yield(true/false) is good, and I also think that
we need to collapse all txn flags into a bit field now that we
have a critical mass.

-- 
Konstantin Osipov, Moscow, Russia

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 3/3] ddl: allow to execute non-yielding DDL statements in transactions
  2019-07-10 13:09 ` [PATCH v2 3/3] ddl: allow to execute non-yielding DDL statements in transactions Vladimir Davydov
@ 2019-07-10 20:43   ` Konstantin Osipov
  2019-07-12 14:55     ` Vladimir Davydov
  2019-07-10 21:07   ` Konstantin Osipov
  2019-07-15 15:03   ` Konstantin Osipov
  2 siblings, 1 reply; 13+ messages in thread
From: Konstantin Osipov @ 2019-07-10 20:43 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/10 16:13]:

Please write a follow up patch which changes box/lua/schema.lua
operations to use transactions wherever possible.

> The patch is pretty straightforward - all it does is moves checks for
> single statement transactions from alter.cc to txn_enable_yield_for_ddl
> so that now any DDL request may be executed in a transaction unless it
> builds an index or checks the format of a non-empty space (those are the
> only two operations that may yield).
> 
> There's two things that must be noted explicitly. The first is removal
> of an assertion from priv_grant. The assertion ensured that a revoked
> privilege was in the cache. The problem is the cache is built from the
> contents of the space, see user_reload_privs. On rollback, we first
> revert the content of the space to the original state, and only then
> start invoking rollback triggers, which call priv_grant. As a result, we
> will revert the cache to the original state right after the first
> trigger is invoked and the following triggers will have no effect on it.
> Thus we have to remove this assertion.
> 
> The second subtlety lays in vinyl_index_commit_modify. Before the commit
> we assumed that if statement lsn is <= vy_lsm::commit_lsn, then it must
> be local recovery from WAL. Now it's not true, because there may be
> several operations for the same index in a transaction, and they all
> will receive the same signature in on_commit trigger. We could, of
> course, try to assign different signatures to them, but that would look
> cumbersome - better simply allow lsn <= vy_lsm::commit_lsn after local
> recovery, there's actually nothing wrong about that.
> 
> Closes #4083
> 
> @TarantoolBot document
> Title: Transactional DDL
> 
> Now it's possible to group non-yielding DDL statements into
> transactions, e.g.
> 
> ```Lua
> box.begin()
> box.schema.space.create('my_space')
> box.space.my_space:create_index('primary')
> box.commit() -- or box.rollback()
> ```
> 
> Most DDL statements don't yield and hence can be run from transactions.
> There are just two exceptions: creation of a new index and changing the
> format of a non-empty space. Those are long operations that may yield
> so as not to block the event loop for too long. Those statements can't
> be executed from transactions (to be more exact, such a statement must
> go first in any transaction).
> 
> Also, just like in case of DML transactions in memtx, it's forbidden to
> explicitly yield in a DDL transaction by calling fiber.sleep or any
> other yielding function. If this happens, the transaction will be
> aborted and an attempt to commit it will fail.
> ---
>  src/box/alter.cc              | 14 --------
>  src/box/errcode.h             |  2 +-
>  src/box/memtx_space.c         |  8 +++--
>  src/box/txn.c                 | 27 +++++++-------
>  src/box/txn.h                 | 25 ++++---------
>  src/box/user.cc               |  1 -
>  src/box/vinyl.c               | 23 ++++++++----
>  test/box/misc.result          |  1 +
>  test/box/on_replace.result    | 53 +++++++++++++---------------
>  test/box/on_replace.test.lua  | 13 +++----
>  test/box/transaction.result   | 82 ++++++++++++++++++++++++++++++++-----------
>  test/box/transaction.test.lua | 56 +++++++++++++++++++++++------
>  test/engine/ddl.result        | 58 ++++++++++++++++++++++++++++--
>  test/engine/ddl.test.lua      | 39 ++++++++++++++++++--
>  14 files changed, 273 insertions(+), 129 deletions(-)
> 
> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index 612bcd89..c7be6b77 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> @@ -1816,7 +1816,6 @@ static void
>  on_replace_dd_space(struct trigger * /* trigger */, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
> -	txn_check_singlestatement_xc(txn, "Space _space");
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
> @@ -2113,7 +2112,6 @@ static void
>  on_replace_dd_index(struct trigger * /* trigger */, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
> -	txn_check_singlestatement_xc(txn, "Space _index");
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
> @@ -2305,7 +2303,6 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
> -	txn_check_singlestatement_xc(txn, "Space _truncate");
>  	struct tuple *new_tuple = stmt->new_tuple;
>  
>  	if (new_tuple == NULL) {
> @@ -2535,7 +2532,6 @@ on_replace_dd_user(struct trigger * /* trigger */, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
> -	txn_check_singlestatement_xc(txn, "Space _user");
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
>  
> @@ -2686,7 +2682,6 @@ static void
>  on_replace_dd_func(struct trigger * /* trigger */, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
> -	txn_check_singlestatement_xc(txn, "Space _func");
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
> @@ -2880,7 +2875,6 @@ on_replace_dd_collation(struct trigger * /* trigger */, void *event)
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
> -	txn_check_singlestatement_xc(txn, "Space _collation");
>  	if (new_tuple == NULL && old_tuple != NULL) {
>  		/* DELETE */
>  		struct trigger *on_commit =
> @@ -3178,7 +3172,6 @@ static void
>  on_replace_dd_priv(struct trigger * /* trigger */, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
> -	txn_check_singlestatement_xc(txn, "Space _priv");
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
> @@ -3227,7 +3220,6 @@ static void
>  on_replace_dd_schema(struct trigger * /* trigger */, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
> -	txn_check_singlestatement_xc(txn, "Space _schema");
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
> @@ -3305,7 +3297,6 @@ on_replace_dd_cluster(struct trigger *trigger, void *event)
>  {
>  	(void) trigger;
>  	struct txn *txn = (struct txn *) event;
> -	txn_check_singlestatement_xc(txn, "Space _cluster");
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
> @@ -3458,7 +3449,6 @@ static void
>  on_replace_dd_sequence(struct trigger * /* trigger */, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
> -	txn_check_singlestatement_xc(txn, "Space _sequence");
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
> @@ -3661,7 +3651,6 @@ static void
>  on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
> -	txn_check_singlestatement_xc(txn, "Space _space_sequence");
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *tuple = stmt->new_tuple ? stmt->new_tuple : stmt->old_tuple;
>  
> @@ -3806,7 +3795,6 @@ static void
>  on_replace_dd_trigger(struct trigger * /* trigger */, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
> -	txn_check_singlestatement_xc(txn, "Space _trigger");
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
> @@ -4189,7 +4177,6 @@ static void
>  on_replace_dd_fk_constraint(struct trigger * /* trigger*/, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
> -	txn_check_singlestatement_xc(txn, "Space _fk_constraint");
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
> @@ -4480,7 +4467,6 @@ static void
>  on_replace_dd_ck_constraint(struct trigger * /* trigger*/, void *event)
>  {
>  	struct txn *txn = (struct txn *) event;
> -	txn_check_singlestatement_xc(txn, "Space _ck_constraint");
>  	struct txn_stmt *stmt = txn_current_stmt(txn);
>  	struct tuple *old_tuple = stmt->old_tuple;
>  	struct tuple *new_tuple = stmt->new_tuple;
> diff --git a/src/box/errcode.h b/src/box/errcode.h
> index be8dab27..97fc9675 100644
> --- a/src/box/errcode.h
> +++ b/src/box/errcode.h
> @@ -106,7 +106,7 @@ struct errcode_record {
>  	/* 51 */_(ER_NO_SUCH_FUNCTION,		"Function '%s' does not exist") \
>  	/* 52 */_(ER_FUNCTION_EXISTS,		"Function '%s' already exists") \
>  	/* 53 */_(ER_BEFORE_REPLACE_RET,	"Invalid return value of space:before_replace trigger: expected tuple or nil, got %s") \
> -	/* 54 */_(ER_UNUSED2,			"") \
> +	/* 54 */_(ER_MULTISTATEMENT_DDL,	"Can not perform %s in a multi-statement transaction") \
>  	/* 55 */_(ER_TRIGGER_EXISTS,		"Trigger '%s' already exists") \
>  	/* 56 */_(ER_USER_MAX,			"A limit on the total number of users has been reached: %u") \
>  	/* 57 */_(ER_NO_SUCH_ENGINE,		"Space engine '%s' does not exist") \
> diff --git a/src/box/memtx_space.c b/src/box/memtx_space.c
> index 921dbcbf..47369994 100644
> --- a/src/box/memtx_space.c
> +++ b/src/box/memtx_space.c
> @@ -888,7 +888,8 @@ memtx_space_check_format(struct space *space, struct tuple_format *format)
>  	if (it == NULL)
>  		return -1;
>  
> -	txn_enable_yield_for_ddl();
> +	if (txn_enable_yield_for_ddl("space format check") != 0)
> +		return -1;
>  
>  	struct memtx_engine *memtx = (struct memtx_engine *)space->engine;
>  	struct memtx_ddl_state state;
> @@ -1018,6 +1019,8 @@ memtx_space_build_index(struct space *src_space, struct index *new_index,
>  	struct index *pk = index_find(src_space, 0);
>  	if (pk == NULL)
>  		return -1;
> +	if (index_size(pk) == 0)
> +		return 0;
>  
>  	struct errinj *inj = errinj(ERRINJ_BUILD_INDEX, ERRINJ_INT);
>  	if (inj != NULL && inj->iparam == (int)new_index->def->iid) {
> @@ -1030,7 +1033,8 @@ memtx_space_build_index(struct space *src_space, struct index *new_index,
>  	if (it == NULL)
>  		return -1;
>  
> -	txn_enable_yield_for_ddl();
> +	if (txn_enable_yield_for_ddl("index build") != 0)
> +		return -1;
>  
>  	struct memtx_engine *memtx = (struct memtx_engine *)src_space->engine;
>  	struct memtx_ddl_state state;
> diff --git a/src/box/txn.c b/src/box/txn.c
> index bab98d7e..9ae11aa7 100644
> --- a/src/box/txn.c
> +++ b/src/box/txn.c
> @@ -592,17 +592,6 @@ txn_abort(struct txn *txn)
>  	txn->is_aborted = true;
>  }
>  
> -int
> -txn_check_singlestatement(struct txn *txn, const char *where)
> -{
> -	if (!txn_is_first_statement(txn)) {
> -		diag_set(ClientError, ER_UNSUPPORTED,
> -			 where, "multi-statement transactions");
> -		return -1;
> -	}
> -	return 0;
> -}
> -
>  void
>  txn_disable_yield(struct txn *txn)
>  {
> @@ -612,12 +601,24 @@ txn_disable_yield(struct txn *txn)
>  	trigger_add(&fiber()->on_yield, &txn->fiber_on_yield);
>  }
>  
> -void
> -txn_enable_yield_for_ddl(void)
> +int
> +txn_enable_yield_for_ddl(const char *what)
>  {
>  	struct txn *txn = in_txn();
>  	assert(txn != NULL && txn->is_yield_disabled);
> +	/*
> +	 * It's okay to yield while executing the first DDL statement
> +	 * in a transaction, because the schema hasn't been updated
> +	 * yet and so other transactions can't see uncommitted objects.
> +	 * Yielding in subsequent statements is not safe, as there
> +	 * may be uncommitted objects in the schema cache.
> +	 */
> +	if (!txn_is_first_statement(txn)) {
> +		diag_set(ClientError, ER_MULTISTATEMENT_DDL, what);
> +		return -1;
> +	}
>  	trigger_clear(&txn->fiber_on_yield);
> +	return 0;
>  }
>  
>  void
> diff --git a/src/box/txn.h b/src/box/txn.h
> index 258a8db7..92366038 100644
> --- a/src/box/txn.h
> +++ b/src/box/txn.h
> @@ -358,13 +358,6 @@ void
>  txn_rollback_stmt(struct txn *txn);
>  
>  /**
> - * Raise an error if this is a multi-statement transaction: DDL
> - * can not be part of a multi-statement transaction.
> - */
> -int
> -txn_check_singlestatement(struct txn *txn, const char *where);
> -
> -/**
>   * Installs yield-in-transaction trigger: roll back the effects
>   * of the transaction and mark the transaction as aborted.
>   *
> @@ -384,9 +377,13 @@ txn_disable_yield(struct txn *txn);
>   * This function temporarily disables the trigger for this purpose.
>   * One must call txn_disable_yield_after_ddl() once the DDL request
>   * has been complete.
> + *
> + * Before enabling yields, this function checks if it doesn't
> + * violate transaction isolation. If it does, it returns -1 and
> + * sets diag. The 'what' message is used for error reporting.
>   */
> -void
> -txn_enable_yield_for_ddl(void);
> +int
> +txn_enable_yield_for_ddl(const char *what);
>  
>  /** See txn_enable_yield_for_ddl(). */
>  void
> @@ -525,16 +522,6 @@ box_txn_rollback_to_savepoint(box_txn_savepoint_t *savepoint);
>  
>  #if defined(__cplusplus)
>  } /* extern "C" */
> -
> -#include "diag.h"
> -
> -static inline void
> -txn_check_singlestatement_xc(struct txn *txn, const char *where)
> -{
> -	if (txn_check_singlestatement(txn, where) != 0)
> -		diag_raise();
> -}
> -
>  #endif /* defined(__cplusplus) */
>  
>  #endif /* TARANTOOL_BOX_TXN_H_INCLUDED */
> diff --git a/src/box/user.cc b/src/box/user.cc
> index 48bdf18e..c46ff67d 100644
> --- a/src/box/user.cc
> +++ b/src/box/user.cc
> @@ -709,7 +709,6 @@ priv_grant(struct user *grantee, struct priv_def *priv)
>  	if (object == NULL)
>  		return;
>  	struct access *access = &object[grantee->auth_token];
> -	assert(privset_search(&grantee->privs, priv) || access->granted == 0);
>  	access->granted = priv->access;
>  	rebuild_effective_grants(grantee);
>  }
> diff --git a/src/box/vinyl.c b/src/box/vinyl.c
> index 8629aa8e..af044f0c 100644
> --- a/src/box/vinyl.c
> +++ b/src/box/vinyl.c
> @@ -919,16 +919,17 @@ vinyl_index_commit_modify(struct index *index, int64_t lsn)
>  	       env->status == VINYL_FINAL_RECOVERY_LOCAL ||
>  	       env->status == VINYL_FINAL_RECOVERY_REMOTE);
>  
> -	if (lsn <= lsm->commit_lsn) {
> +	if (env->status == VINYL_FINAL_RECOVERY_LOCAL &&
> +	    lsn <= lsm->commit_lsn) {
>  		/*
> -		 * This must be local recovery from WAL, when
> -		 * the operation has already been committed to
> -		 * vylog.
> +		 * The statement we are recovering from WAL has
> +		 * been successfully written to vylog so we must
> +		 * not replay it.
>  		 */
> -		assert(env->status == VINYL_FINAL_RECOVERY_LOCAL);
>  		return;
>  	}
>  
> +	assert(lsm->commit_lsn <= lsn);
>  	lsm->commit_lsn = lsn;
>  
>  	vy_log_tx_begin();
> @@ -1121,7 +1122,11 @@ vinyl_space_check_format(struct space *space, struct tuple_format *format)
>  	bool need_wal_sync;
>  	tx_manager_abort_writers_for_ddl(env->xm, space, &need_wal_sync);
>  
> -	txn_enable_yield_for_ddl();
> +	if (!need_wal_sync && vy_lsm_is_empty(pk))
> +		return 0; /* space is empty, nothing to do */
> +
> +	if (txn_enable_yield_for_ddl("space format check") != 0)
> +		return -1;
>  
>  	struct trigger on_replace;
>  	struct vy_check_format_ctx ctx;
> @@ -4348,7 +4353,11 @@ vinyl_space_build_index(struct space *src_space, struct index *new_index,
>  	bool need_wal_sync;
>  	tx_manager_abort_writers_for_ddl(env->xm, src_space, &need_wal_sync);
>  
> -	txn_enable_yield_for_ddl();
> +	if (!need_wal_sync && vy_lsm_is_empty(pk))
> +		return 0; /* space is empty, nothing to do */
> +
> +	if (txn_enable_yield_for_ddl("index build") != 0)
> +		return -1;
>  
>  	/*
>  	 * Iterate over all tuples stored in the space and insert
> diff --git a/test/box/misc.result b/test/box/misc.result
> index dab8549b..3c8e6994 100644
> --- a/test/box/misc.result
> +++ b/test/box/misc.result
> @@ -385,6 +385,7 @@ t;
>    51: box.error.NO_SUCH_FUNCTION
>    52: box.error.FUNCTION_EXISTS
>    53: box.error.BEFORE_REPLACE_RET
> +  54: box.error.MULTISTATEMENT_DDL
>    55: box.error.TRIGGER_EXISTS
>    56: box.error.USER_MAX
>    57: box.error.NO_SUCH_ENGINE
> diff --git a/test/box/on_replace.result b/test/box/on_replace.result
> index b71c9878..6334d9a2 100644
> --- a/test/box/on_replace.result
> +++ b/test/box/on_replace.result
> @@ -465,86 +465,83 @@ s = box.schema.space.create('test_on_repl_ddl')
>  _ = s:create_index('pk')
>  ---
>  ...
> +_ = s:create_index('sk', {parts = {2, 'unsigned'}})
> +---
> +...
>  t = s:on_replace(function () box.schema.space.create('some_space') end)
>  ---
>  ...
>  s:replace({1, 2})
>  ---
> -- error: Space _schema does not support multi-statement transactions
> +- [1, 2]
>  ...
> -t = s:on_replace(function () s:create_index('sec') end, t)
> +t = s:on_replace(function () s:format({{'a', 'unsigned'}, {'b', 'unsigned'}}) end, t)
>  ---
>  ...
>  s:replace({2, 3})
>  ---
> -- error: Space _index does not support multi-statement transactions
> +- [2, 3]
>  ...
> -t = s:on_replace(function () box.schema.user.create('newu') end, t)
> +t = s:on_replace(function () box.schema.user.create('newu') box.schema.role.create('newr') end, t)
>  ---
>  ...
>  s:replace({3, 4})
>  ---
> -- error: Space _user does not support multi-statement transactions
> +- [3, 4]
>  ...
> -t = s:on_replace(function () box.schema.role.create('newr') end, t)
> +t = s:on_replace(function () box.schema.user.drop('newu') box.schema.role.drop('newr') end, t)
>  ---
>  ...
>  s:replace({4, 5})
>  ---
> -- error: Space _user does not support multi-statement transactions
> +- [4, 5]
>  ...
> -t = s:on_replace(function () box.space._user:delete{box.schema.GUEST_ID} end, t)
> ----
> -...
> -s:replace({4, 5})
> ----
> -- error: Space _user does not support multi-statement transactions
> -...
> -t = s:on_replace(function () box.space._user:delete{box.schema.SUPER_ROLE_ID} end, t)
> ----
> -...
> -s:replace({4, 5})
> ----
> -- error: Space _user does not support multi-statement transactions
> -...
> -t = s:on_replace(function () s:drop() end, t)
> +t = s:on_replace(function () s.index.sk:drop() end, t)
>  ---
>  ...
>  s:replace({5, 6})
>  ---
> -- error: Space _index does not support multi-statement transactions
> +- [5, 6]
>  ...
>  t = s:on_replace(function () box.schema.func.create('newf') end, t)
>  ---
>  ...
>  s:replace({6, 7})
>  ---
> -- error: Space _func does not support multi-statement transactions
> +- [6, 7]
>  ...
>  t = s:on_replace(function () box.schema.user.grant('guest', 'read,write', 'space', 'test_on_repl_ddl') end, t)
>  ---
>  ...
>  s:replace({7, 8})
>  ---
> -- error: Space _priv does not support multi-statement transactions
> +- [7, 8]
>  ...
>  t = s:on_replace(function () s:rename('newname') end, t)
>  ---
>  ...
>  s:replace({8, 9})
>  ---
> -- error: Space _space does not support multi-statement transactions
> +- [8, 9]
>  ...
>  t = s:on_replace(function () s.index.pk:rename('newname') end, t)
>  ---
>  ...
>  s:replace({9, 10})
>  ---
> -- error: Space _index does not support multi-statement transactions
> +- [9, 10]
>  ...
>  s:select()
>  ---
> -- []
> +- - [1, 2]
> +  - [2, 3]
> +  - [3, 4]
> +  - [4, 5]
> +  - [5, 6]
> +  - [6, 7]
> +  - [7, 8]
> +  - [8, 9]
> +  - [9, 10]
>  ...
>  s:drop() -- test_on_repl_ddl
>  ---
> diff --git a/test/box/on_replace.test.lua b/test/box/on_replace.test.lua
> index 7fffc1e0..79c828da 100644
> --- a/test/box/on_replace.test.lua
> +++ b/test/box/on_replace.test.lua
> @@ -181,19 +181,16 @@ second:drop()
>  
>  s = box.schema.space.create('test_on_repl_ddl')
>  _ = s:create_index('pk')
> +_ = s:create_index('sk', {parts = {2, 'unsigned'}})
>  t = s:on_replace(function () box.schema.space.create('some_space') end)
>  s:replace({1, 2})
> -t = s:on_replace(function () s:create_index('sec') end, t)
> +t = s:on_replace(function () s:format({{'a', 'unsigned'}, {'b', 'unsigned'}}) end, t)
>  s:replace({2, 3})
> -t = s:on_replace(function () box.schema.user.create('newu') end, t)
> +t = s:on_replace(function () box.schema.user.create('newu') box.schema.role.create('newr') end, t)
>  s:replace({3, 4})
> -t = s:on_replace(function () box.schema.role.create('newr') end, t)
> +t = s:on_replace(function () box.schema.user.drop('newu') box.schema.role.drop('newr') end, t)
>  s:replace({4, 5})
> -t = s:on_replace(function () box.space._user:delete{box.schema.GUEST_ID} end, t)
> -s:replace({4, 5})
> -t = s:on_replace(function () box.space._user:delete{box.schema.SUPER_ROLE_ID} end, t)
> -s:replace({4, 5})
> -t = s:on_replace(function () s:drop() end, t)
> +t = s:on_replace(function () s.index.sk:drop() end, t)
>  s:replace({5, 6})
>  t = s:on_replace(function () box.schema.func.create('newf') end, t)
>  s:replace({6, 7})
> diff --git a/test/box/transaction.result b/test/box/transaction.result
> index ad2d650c..9a48f2f7 100644
> --- a/test/box/transaction.result
> +++ b/test/box/transaction.result
> @@ -84,22 +84,12 @@ while f:status() ~= 'dead' do fiber.sleep(0) end;
>  ---
>  ...
>  -- transactions and system spaces
> --- some operation involves more than one ddl spaces, so they should fail
> -box.begin() box.schema.space.create('test');
> +box.begin() box.schema.space.create('test') box.rollback();
>  ---
> -- error: Space _space does not support multi-statement transactions
>  ...
> -box.rollback();
> +box.begin() box.schema.user.create('test') box.rollback();
>  ---
>  ...
> -box.begin() box.schema.user.create('test');
> ----
> -- error: Space _priv does not support multi-statement transactions
> -...
> -box.rollback();
> ----
> -...
> --- but this is Ok now
>  box.begin() box.schema.func.create('test') box.rollback();
>  ---
>  ...
> @@ -657,21 +647,14 @@ box.space.vinyl:select{};
>  -- Two DDL satements in a row
>  box.begin()
>  box.space.test:truncate()
> -box.space.test:truncate();
> ----
> -- error: Space _truncate does not support multi-statement transactions
> -...
> --- A transaction is left open due to an exception in the above fragment
> +box.space.test:truncate()
>  box.rollback();
>  ---
>  ...
>  -- Two DDL stateemnts on different engines
>  box.begin()
>  box.space.memtx:truncate()
> -box.space.vinyl:truncate();
> ----
> -- error: Space _truncate does not support multi-statement transactions
> -...
> +box.space.vinyl:truncate()
>  box.rollback();
>  ---
>  ...
> @@ -738,3 +721,60 @@ box.commit() -- error
>  s:drop()
>  ---
>  ...
> +--
> +-- Multiple DDL statements in the same transaction.
> +--
> +test_run:cmd("setopt delimiter ';'")
> +---
> +- true
> +...
> +function create()
> +    box.schema.role.create('my_role')
> +    box.schema.user.create('my_user')
> +    box.schema.user.grant('my_user', 'my_role')
> +    box.schema.space.create('memtx_space', {engine = 'memtx'})
> +    box.schema.space.create('vinyl_space', {engine = 'vinyl'})
> +    box.schema.role.grant('my_role', 'read,write', 'space', 'vinyl_space')
> +    box.schema.user.grant('my_user', 'read,write', 'space', 'memtx_space')
> +    box.space.memtx_space:create_index('pk', {sequence = true})
> +    box.space.memtx_space:create_index('sk', {parts = {2, 'unsigned'}})
> +    box.space.vinyl_space:create_index('pk', {sequence = true})
> +    box.space.vinyl_space:create_index('sk', {parts = {2, 'unsigned'}})
> +    box.space.memtx_space:truncate()
> +    box.space.vinyl_space:truncate()
> +    box.space.memtx_space:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
> +    box.space.vinyl_space:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
> +    box.schema.func.create('my_func')
> +end;
> +---
> +...
> +function drop()
> +    box.schema.func.drop('my_func')
> +    box.space.memtx_space:truncate()
> +    box.space.vinyl_space:truncate()
> +    box.schema.user.revoke('my_user', 'read,write', 'space', 'memtx_space')
> +    box.schema.role.revoke('my_role', 'read,write', 'space', 'vinyl_space')
> +    box.space.memtx_space:drop()
> +    box.space.vinyl_space:drop()
> +    box.schema.user.revoke('my_user', 'my_role')
> +    box.schema.user.drop('my_user')
> +    box.schema.role.drop('my_role')
> +end;
> +---
> +...
> +test_run:cmd("setopt delimiter ''");
> +---
> +- true
> +...
> +box.begin() create() box.rollback()
> +---
> +...
> +box.begin() create() box.commit()
> +---
> +...
> +box.begin() drop() box.rollback()
> +---
> +...
> +box.begin() drop() box.commit()
> +---
> +...
> diff --git a/test/box/transaction.test.lua b/test/box/transaction.test.lua
> index 8cd3e8ba..0792cc3c 100644
> --- a/test/box/transaction.test.lua
> +++ b/test/box/transaction.test.lua
> @@ -41,12 +41,8 @@ f = fiber.create(sloppy);
>  -- ensure it's rolled back automatically
>  while f:status() ~= 'dead' do fiber.sleep(0) end;
>  -- transactions and system spaces
> --- some operation involves more than one ddl spaces, so they should fail
> -box.begin() box.schema.space.create('test');
> -box.rollback();
> -box.begin() box.schema.user.create('test');
> -box.rollback();
> --- but this is Ok now
> +box.begin() box.schema.space.create('test') box.rollback();
> +box.begin() box.schema.user.create('test') box.rollback();
>  box.begin() box.schema.func.create('test') box.rollback();
>  box.begin() box.schema.user.grant('guest', 'read', 'space', '_priv') box.rollback();
>  space = box.schema.space.create('test');
> @@ -341,16 +337,13 @@ box.space.vinyl:select{};
>  -- Two DDL satements in a row
>  box.begin()
>  box.space.test:truncate()
> -box.space.test:truncate();
> -
> --- A transaction is left open due to an exception in the above fragment
> +box.space.test:truncate()
>  box.rollback();
>  
>  -- Two DDL stateemnts on different engines
>  box.begin()
>  box.space.memtx:truncate()
> -box.space.vinyl:truncate();
> -
> +box.space.vinyl:truncate()
>  box.rollback();
>  
>  box.space.memtx:select{};
> @@ -381,3 +374,44 @@ s = box.schema.space.create('test')
>  box.begin() s:create_index('pk') fiber.sleep(0)
>  box.commit() -- error
>  s:drop()
> +
> +--
> +-- Multiple DDL statements in the same transaction.
> +--
> +test_run:cmd("setopt delimiter ';'")
> +function create()
> +    box.schema.role.create('my_role')
> +    box.schema.user.create('my_user')
> +    box.schema.user.grant('my_user', 'my_role')
> +    box.schema.space.create('memtx_space', {engine = 'memtx'})
> +    box.schema.space.create('vinyl_space', {engine = 'vinyl'})
> +    box.schema.role.grant('my_role', 'read,write', 'space', 'vinyl_space')
> +    box.schema.user.grant('my_user', 'read,write', 'space', 'memtx_space')
> +    box.space.memtx_space:create_index('pk', {sequence = true})
> +    box.space.memtx_space:create_index('sk', {parts = {2, 'unsigned'}})
> +    box.space.vinyl_space:create_index('pk', {sequence = true})
> +    box.space.vinyl_space:create_index('sk', {parts = {2, 'unsigned'}})
> +    box.space.memtx_space:truncate()
> +    box.space.vinyl_space:truncate()
> +    box.space.memtx_space:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
> +    box.space.vinyl_space:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
> +    box.schema.func.create('my_func')
> +end;
> +function drop()
> +    box.schema.func.drop('my_func')
> +    box.space.memtx_space:truncate()
> +    box.space.vinyl_space:truncate()
> +    box.schema.user.revoke('my_user', 'read,write', 'space', 'memtx_space')
> +    box.schema.role.revoke('my_role', 'read,write', 'space', 'vinyl_space')
> +    box.space.memtx_space:drop()
> +    box.space.vinyl_space:drop()
> +    box.schema.user.revoke('my_user', 'my_role')
> +    box.schema.user.drop('my_user')
> +    box.schema.role.drop('my_role')
> +end;
> +test_run:cmd("setopt delimiter ''");
> +
> +box.begin() create() box.rollback()
> +box.begin() create() box.commit()
> +box.begin() drop() box.rollback()
> +box.begin() drop() box.commit()
> diff --git a/test/engine/ddl.result b/test/engine/ddl.result
> index 30f305e9..c59c85f1 100644
> --- a/test/engine/ddl.result
> +++ b/test/engine/ddl.result
> @@ -2215,8 +2215,62 @@ box.schema.user.revoke('guest', 'write', 'space', 'ddl_test')
>  box.commit();
>  ---
>  ...
> --- index and space drop are not currently supported (because of truncate)
> -s.index.pk:drop();
> +box.begin()
> +s.index.pk:drop()
> +s:drop()
> +box.commit();
> +---
> +...
> +--
> +-- Only the first statement in a transaction is allowed to be
> +-- a yielding DDL statement (index build, space format check).
> +--
> +s = box.schema.space.create('test', {engine = engine});
> +---
> +...
> +_ = s:create_index('pk');
> +---
> +...
> +s:insert{1, 1};
> +---
> +- [1, 1]
> +...
> +-- ok
> +box.begin()
> +s:create_index('sk', {parts = {2, 'unsigned'}})
> +box.commit();
> +---
> +...
> +s.index.sk:drop();
> +---
> +...
> +-- ok
> +box.begin()
> +s:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
> +box.commit();
> +---
> +...
> +s:format({});
> +---
> +...
> +-- error
> +box.begin()
> +s.index.pk:alter{sequence = true}
> +s:create_index('sk', {parts = {2, 'unsigned'}});
> +---
> +- error: Can not perform index build in a multi-statement transaction
> +...
> +box.rollback();
> +---
> +...
> +-- error
> +box.begin()
> +s.index.pk:alter{sequence = true}
> +s:format({{'a', 'unsigned'}, {'b', 'unsigned'}});
> +---
> +- error: Can not perform space format check in a multi-statement transaction
> +...
> +box.rollback();
>  ---
>  ...
>  s:drop();
> diff --git a/test/engine/ddl.test.lua b/test/engine/ddl.test.lua
> index 25ce8ee6..1670b548 100644
> --- a/test/engine/ddl.test.lua
> +++ b/test/engine/ddl.test.lua
> @@ -827,8 +827,43 @@ box.begin()
>  box.schema.user.revoke('guest', 'write', 'space', 'ddl_test')
>  box.commit();
>  
> --- index and space drop are not currently supported (because of truncate)
> -s.index.pk:drop();
> +box.begin()
> +s.index.pk:drop()
> +s:drop()
> +box.commit();
> +
> +--
> +-- Only the first statement in a transaction is allowed to be
> +-- a yielding DDL statement (index build, space format check).
> +--
> +s = box.schema.space.create('test', {engine = engine});
> +_ = s:create_index('pk');
> +s:insert{1, 1};
> +
> +-- ok
> +box.begin()
> +s:create_index('sk', {parts = {2, 'unsigned'}})
> +box.commit();
> +s.index.sk:drop();
> +
> +-- ok
> +box.begin()
> +s:format({{'a', 'unsigned'}, {'b', 'unsigned'}})
> +box.commit();
> +s:format({});
> +
> +-- error
> +box.begin()
> +s.index.pk:alter{sequence = true}
> +s:create_index('sk', {parts = {2, 'unsigned'}});
> +box.rollback();
> +
> +-- error
> +box.begin()
> +s.index.pk:alter{sequence = true}
> +s:format({{'a', 'unsigned'}, {'b', 'unsigned'}});
> +box.rollback();
> +
>  s:drop();
>  
>  --
> -- 
> 2.11.0
> 

-- 
Konstantin Osipov, Moscow, Russia

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 3/3] ddl: allow to execute non-yielding DDL statements in transactions
  2019-07-10 13:09 ` [PATCH v2 3/3] ddl: allow to execute non-yielding DDL statements in transactions Vladimir Davydov
  2019-07-10 20:43   ` Konstantin Osipov
@ 2019-07-10 21:07   ` Konstantin Osipov
  2019-07-15 15:03   ` Konstantin Osipov
  2 siblings, 0 replies; 13+ messages in thread
From: Konstantin Osipov @ 2019-07-10 21:07 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/10 16:13]:

A test plan for this feature:

1. A transaction which swaps two spaces by name.

name = foo.name;
foo.name = bar.name
bar.name = name

2. A transaction which creates a space, formats it to have an
   integer column, changes the format to 'any', changes the values
   to 'string', changes the format to 'string'

3. A transaction which creates a space, a user, and grants all privileges
   on this space to the user.Then sudoes to the user and tries to
   use the space.

4. A transaction, which revokes all privileges from a user and drops
   the space.

5. A transaction which creates a space and secondary indexes

6. A transaction which creates a space with a sequence.

7. Transactional drop a) empty b) nonempty space with all its belongings:
   secondary indexes, check constraints, sequences.

5. A transaction which creates a space and sets space triggers, as
   well as on_rollback trigger, then inserts a bunch of data and then
   a) commits b) rolls back.

6. An error injection crash test which starts 10 fibers which perform
   a random combination of transactional DDL in a loop, then,
   after 0.1 second ER_WAL_IO is enabled, fibers are joined,
   ER_WAL_IO is disabled. The DDL is using objects which names
   clash,  so that it does something useful, e.g:

   function name(object_type)
        return object_type_name ..  math.random(1, NUM_NAMES)
    end

    function ddl(object_type, object_name) 
        -- this function actually checks if the name exists 
        --  at the moment and only returns valid statements
        if object_type is user or role then
            return random("grant", "revoke", "create", "drop")
        end
        if object_type is table then 
            return random("create ", "format", "rename", "add
            index", "drop index", "rename index", "change index
            keys", "change unique -> non_unique", "add
            constraint", "drop constraint", "drop")
        end


    function gen_test()
        for each object type:
            eval = "box.begin()"
            eval = eval .. ddl(object_type, name(object_type))
            eval = eval .. "box.commit()"
        end
        return eval
    end
    function fiber()
        while true do
            eval(gen_test)
        end
    end

    for i in 1.. NUM_FIBERS do
        fibers[i] = fiber.create(fiber)
    end
    fiber.sleep(0.1)
    errinj.enable(er_wal_io)
    for i in 1.. NUM_FIBERS do
        fiber.join(fibers[i])
    end

    (cleanup all objects matching the name..$i pattern)


-- 
Konstantin Osipov, Moscow, Russia

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 1/3] memtx: fix txn_on_yield for DDL transactions
  2019-07-10 20:34   ` Konstantin Osipov
@ 2019-07-12 14:54     ` Vladimir Davydov
  2019-07-12 15:16       ` Konstantin Osipov
  0 siblings, 1 reply; 13+ messages in thread
From: Vladimir Davydov @ 2019-07-12 14:54 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Wed, Jul 10, 2019 at 11:34:39PM +0300, Konstantin Osipov wrote:
> > -	/* .prepare = */ memtx_engine_prepare,
> > +	/* .begin_statement = */ generic_engine_begin_statement,
> > +	/* .prepare = */ generic_engine_prepare,
> 
> If some of these callbacks are now empty in all engines, they
> should be removed from vtab.

Those methods are still used in Vinyl so we can't remove them :-(

> >  	txn->has_triggers  = false;
> >  	txn->is_done = false;
> >  	txn->is_aborted = false;
> > +	txn->is_yield_disabled = false;
> 
> It is hight time to convert these to bit fields.

NP but let's please do it in a separate patch if we have to.

> Overall I agree the approach is much better than before, at least
> easier to track since the logic is not hidden in the engine.
> 
> Please let's make the names more evident now, and the api simple.
> I think txn_can_yield(true/false) is good, and I also think that
> we need to collapse all txn flags into a bit field now that we
> have a critical mass.

Agree, the current API does look kinda crooked :-/ Reworked according to
your review. Please see the updated patch below and on the branch.
---

From 73cb8a22680bf362a5dce925b0aebd2e7508d334 Mon Sep 17 00:00:00 2001
From: Vladimir Davydov <vdavydov.dev@gmail.com>
Date: Fri, 5 Jul 2019 17:40:56 +0300
Subject: [PATCH] memtx: fix txn_on_yield for DDL transactions

Memtx engine doesn't allow yielding inside a transaction. To achieve
that, it installs fiber->on_yield trigger that aborts the current
transaction (rolls it back, but leaves it be so that commit fails).

There's an exception though - DDL statements are allowed to yield.
This is required so as not to block the event loop while a new index
is built or a space format is checked. Currently, we handle this
exception by checking space id and omitting installation of the
trigger for system spaces. This isn't entirely correct, because we
may yield after a DDL statement is complete, in which case the
transaction won't be aborted though it should:

  box.begin()
  box.space.my_space:create_index('my_index')
  fiber.sleep(0) -- doesn't abort the transaction!

This patch fixes the problem by making the memtx engine install the
on_yield trigger unconditionally, for all kinds of transactions, and
instead explicitly disabling the trigger for yielding DDL operations.

In order not to spread the yield-in-transaction logic between memtx
and txn code, let's move all fiber_on_yield related stuff to txn,
export a method to disable yields, and use the method in memtx.

diff --git a/src/box/memtx_engine.c b/src/box/memtx_engine.c
index c8376110..869cd343 100644
--- a/src/box/memtx_engine.c
+++ b/src/box/memtx_engine.c
@@ -55,58 +55,6 @@
 static void
 checkpoint_cancel(struct checkpoint *ckpt);
 
-/*
- * Memtx yield-in-transaction trigger: roll back the effects
- * of the transaction and mark the transaction as aborted.
- */
-static void
-txn_on_yield(struct trigger *trigger, void *event)
-{
-	(void) trigger;
-	(void) event;
-
-	struct txn *txn = in_txn();
-	assert(txn && txn->engine_tx);
-	if (txn == NULL || txn->engine_tx == NULL)
-		return;
-	txn_abort(txn);                 /* doesn't yield or fail */
-}
-
-/**
- * Initialize context for yield triggers.
- * In case of a yield inside memtx multi-statement transaction
- * we must, first of all, roll back the effects of the transaction
- * so that concurrent transactions won't see dirty, uncommitted
- * data.
- * Second, we must abort the transaction, since it has been rolled
- * back implicitly. The transaction can not be rolled back
- * completely from within a yield trigger, since a yield trigger
- * can't fail. Instead, we mark the transaction as aborted and
- * raise an error on attempt to commit it.
- *
- * So much hassle to be user-friendly until we have a true
- * interactive transaction support in memtx.
- */
-void
-memtx_init_txn(struct txn *txn)
-{
-	struct fiber *fiber = fiber();
-
-	trigger_create(&txn->fiber_on_yield, txn_on_yield,
-		       NULL, NULL);
-	/*
-	 * Memtx doesn't allow yields between statements of
-	 * a transaction. Set a trigger which would roll
-	 * back the transaction if there is a yield.
-	 */
-	trigger_add(&fiber->on_yield, &txn->fiber_on_yield);
-	/*
-	 * This serves as a marker that the triggers are
-	 * initialized.
-	 */
-	txn->engine_tx = txn;
-}
-
 struct PACKED memtx_tuple {
 	/*
 	 * sic: the header of the tuple is used
@@ -372,45 +320,10 @@ memtx_engine_create_space(struct engine *engine, struct space_def *def,
 }
 
 static int
-memtx_engine_prepare(struct engine *engine, struct txn *txn)
-{
-	(void)engine;
-	if (txn->engine_tx == 0)
-		return 0;
-	/*
-	 * These triggers are only used for memtx and only
-	 * when autocommit == false, so we are saving
-	 * on calls to trigger_create/trigger_clear.
-	 */
-	trigger_clear(&txn->fiber_on_yield);
-	if (txn->is_aborted) {
-		diag_set(ClientError, ER_TRANSACTION_YIELD);
-		diag_log();
-		return -1;
-	}
-	return 0;
-}
-
-static int
 memtx_engine_begin(struct engine *engine, struct txn *txn)
 {
 	(void)engine;
-	(void)txn;
-	return 0;
-}
-
-static int
-memtx_engine_begin_statement(struct engine *engine, struct txn *txn)
-{
-	(void)engine;
-	(void)txn;
-	if (txn->engine_tx == NULL) {
-		struct space *space = txn_last_stmt(txn)->space;
-
-		if (space->def->id > BOX_SYSTEM_ID_MAX)
-			/* Setup triggers for non-ddl transactions. */
-			memtx_init_txn(txn);
-	}
+	txn_can_yield(txn, false);
 	return 0;
 }
 
@@ -459,9 +372,6 @@ memtx_engine_rollback_statement(struct engine *engine, struct txn *txn,
 static void
 memtx_engine_rollback(struct engine *engine, struct txn *txn)
 {
-	if (txn->engine_tx != NULL) {
-		trigger_clear(&txn->fiber_on_yield);
-	}
 	struct txn_stmt *stmt;
 	stailq_reverse(&txn->stmts);
 	stailq_foreach_entry(stmt, &txn->stmts, next)
@@ -948,8 +858,8 @@ static const struct engine_vtab memtx_engine_vtab = {
 	/* .create_space = */ memtx_engine_create_space,
 	/* .join = */ memtx_engine_join,
 	/* .begin = */ memtx_engine_begin,
-	/* .begin_statement = */ memtx_engine_begin_statement,
-	/* .prepare = */ memtx_engine_prepare,
+	/* .begin_statement = */ generic_engine_begin_statement,
+	/* .prepare = */ generic_engine_prepare,
 	/* .commit = */ generic_engine_commit,
 	/* .rollback_statement = */ memtx_engine_rollback_statement,
 	/* .rollback = */ memtx_engine_rollback,
diff --git a/src/box/memtx_space.c b/src/box/memtx_space.c
index f0e1cfd2..926b3f18 100644
--- a/src/box/memtx_space.c
+++ b/src/box/memtx_space.c
@@ -878,6 +878,8 @@ memtx_check_on_replace(struct trigger *trigger, void *event)
 static int
 memtx_space_check_format(struct space *space, struct tuple_format *format)
 {
+	struct txn *txn = in_txn();
+
 	if (space->index_count == 0)
 		return 0;
 	struct index *pk = space->index[0];
@@ -888,6 +890,8 @@ memtx_space_check_format(struct space *space, struct tuple_format *format)
 	if (it == NULL)
 		return -1;
 
+	txn_can_yield(txn, true);
+
 	struct memtx_engine *memtx = (struct memtx_engine *)space->engine;
 	struct memtx_ddl_state state;
 	state.format = format;
@@ -930,6 +934,7 @@ memtx_space_check_format(struct space *space, struct tuple_format *format)
 	iterator_delete(it);
 	diag_destroy(&state.diag);
 	trigger_clear(&on_replace);
+	txn_can_yield(txn, false);
 	return rc;
 }
 
@@ -1002,6 +1007,7 @@ static int
 memtx_space_build_index(struct space *src_space, struct index *new_index,
 			struct tuple_format *new_format)
 {
+	struct txn *txn = in_txn();
 	/**
 	 * If it's a secondary key, and we're not building them
 	 * yet (i.e. it's snapshot recovery for memtx), do nothing.
@@ -1027,6 +1033,8 @@ memtx_space_build_index(struct space *src_space, struct index *new_index,
 	if (it == NULL)
 		return -1;
 
+	txn_can_yield(txn, true);
+
 	struct memtx_engine *memtx = (struct memtx_engine *)src_space->engine;
 	struct memtx_ddl_state state;
 	state.index = new_index;
@@ -1103,6 +1111,7 @@ memtx_space_build_index(struct space *src_space, struct index *new_index,
 	iterator_delete(it);
 	diag_destroy(&state.diag);
 	trigger_clear(&on_replace);
+	txn_can_yield(txn, false);
 	return rc;
 }
 
diff --git a/src/box/txn.c b/src/box/txn.c
index c605345d..5193b49c 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -40,6 +40,12 @@ double too_long_threshold;
 /* Txn cache. */
 static struct stailq txn_cache = {NULL, &txn_cache.first};
 
+static void
+txn_on_stop(struct trigger *trigger, void *event);
+
+static void
+txn_on_yield(struct trigger *trigger, void *event);
+
 static inline void
 fiber_set_txn(struct fiber *fiber, struct txn *txn)
 {
@@ -193,7 +199,8 @@ txn_begin()
 	txn->n_applier_rows = 0;
 	txn->has_triggers  = false;
 	txn->is_done = false;
-	txn->is_aborted = false;
+	txn->can_yield = true;
+	txn->is_aborted_by_yield = false;
 	txn->in_sub_stmt = 0;
 	txn->id = ++tsn;
 	txn->signature = -1;
@@ -201,8 +208,8 @@ txn_begin()
 	txn->engine_tx = NULL;
 	txn->psql_txn = NULL;
 	txn->fiber = NULL;
-	/* fiber_on_yield/fiber_on_stop initialized by engine on demand */
 	fiber_set_txn(fiber(), txn);
+	/* fiber_on_yield is initialized by engine on demand */
 	trigger_create(&txn->fiber_on_stop, txn_on_stop, NULL, NULL);
 	trigger_add(&fiber()->on_stop, &txn->fiber_on_stop);
 	return txn;
@@ -463,6 +470,12 @@ txn_write_to_wal(struct txn *txn)
 static int
 txn_prepare(struct txn *txn)
 {
+	if (txn->is_aborted_by_yield) {
+		assert(!txn->can_yield);
+		diag_set(ClientError, ER_TRANSACTION_YIELD);
+		diag_log();
+		return -1;
+	}
 	/*
 	 * If transaction has been started in SQL, deferred
 	 * foreign key constraints must not be violated.
@@ -484,6 +497,8 @@ txn_prepare(struct txn *txn)
 			return -1;
 	}
 	trigger_clear(&txn->fiber_on_stop);
+	if (!txn->can_yield)
+		trigger_clear(&txn->fiber_on_yield);
 	return 0;
 }
 
@@ -550,23 +565,13 @@ txn_rollback(struct txn *txn)
 {
 	assert(txn == in_txn());
 	trigger_clear(&txn->fiber_on_stop);
+	if (!txn->can_yield)
+		trigger_clear(&txn->fiber_on_yield);
 	txn->signature = -1;
 	txn_complete(txn);
 	fiber_set_txn(fiber(), NULL);
 }
 
-void
-txn_abort(struct txn *txn)
-{
-	assert(in_txn() == txn);
-	txn_rollback_to_svp(txn, NULL);
-	if (txn->has_triggers) {
-		txn_run_triggers(txn, &txn->on_rollback);
-		txn->has_triggers = false;
-	}
-	txn->is_aborted = true;
-}
-
 int
 txn_check_singlestatement(struct txn *txn, const char *where)
 {
@@ -578,6 +583,20 @@ txn_check_singlestatement(struct txn *txn, const char *where)
 	return 0;
 }
 
+void
+txn_can_yield(struct txn *txn, bool set)
+{
+	assert(txn == in_txn());
+	assert(txn->can_yield != set);
+	txn->can_yield = set;
+	if (set) {
+		trigger_clear(&txn->fiber_on_yield);
+	} else {
+		trigger_create(&txn->fiber_on_yield, txn_on_yield, NULL, NULL);
+		trigger_add(&fiber()->on_yield, &txn->fiber_on_yield);
+	}
+}
+
 int64_t
 box_txn_id(void)
 {
@@ -711,7 +730,7 @@ box_txn_rollback_to_savepoint(box_txn_savepoint_t *svp)
 	return 0;
 }
 
-void
+static void
 txn_on_stop(struct trigger *trigger, void *event)
 {
 	(void) trigger;
@@ -719,3 +738,34 @@ txn_on_stop(struct trigger *trigger, void *event)
 	txn_rollback(in_txn());                 /* doesn't yield or fail */
 }
 
+/**
+ * Memtx yield-in-transaction trigger callback.
+ *
+ * In case of a yield inside memtx multi-statement transaction
+ * we must, first of all, roll back the effects of the transaction
+ * so that concurrent transactions won't see dirty, uncommitted
+ * data.
+ *
+ * Second, we must abort the transaction, since it has been rolled
+ * back implicitly. The transaction can not be rolled back
+ * completely from within a yield trigger, since a yield trigger
+ * can't fail. Instead, we mark the transaction as aborted and
+ * raise an error on attempt to commit it.
+ *
+ * So much hassle to be user-friendly until we have a true
+ * interactive transaction support in memtx.
+ */
+static void
+txn_on_yield(struct trigger *trigger, void *event)
+{
+	(void) trigger;
+	(void) event;
+	struct txn *txn = in_txn();
+	assert(txn != NULL && !txn->can_yield);
+	txn_rollback_to_svp(txn, NULL);
+	if (txn->has_triggers) {
+		txn_run_triggers(txn, &txn->on_rollback);
+		txn->has_triggers = false;
+	}
+	txn->is_aborted_by_yield = true;
+}
diff --git a/src/box/txn.h b/src/box/txn.h
index d1ef220a..a6d6878b 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -168,7 +168,12 @@ struct txn {
 	 * True if the transaction was aborted so should be
 	 * rolled back at commit.
 	 */
-	bool is_aborted;
+	bool is_aborted_by_yield;
+	/**
+	 * True if yields are allowed inside a transaction,
+	 * see txn_can_yield().
+	 */
+	bool can_yield;
 	/** True if on_commit and on_rollback lists are non-empty. */
 	bool has_triggers;
 	/** The number of active nested statement-level transactions. */
@@ -259,17 +264,6 @@ int
 txn_write(struct txn *txn);
 
 /**
- * Roll back the transaction but keep the object around.
- * A special case for memtx transaction abort on yield. In this
- * case we need to abort the transaction to avoid dirty reads but
- * need to keep it around to ensure a new one is not implicitly
- * started and committed by the user program. Later, at
- * transaction commit we will raise an exception.
- */
-void
-txn_abort(struct txn *txn);
-
-/**
  * Most txns don't have triggers, and txn objects
  * are created on every access to data, so txns
  * are partially initialized.
@@ -371,6 +365,21 @@ int
 txn_check_singlestatement(struct txn *txn, const char *where);
 
 /**
+ * Enables or disables fiber yields inside the current transaction
+ * depending on the value of the given flag. Yields are disabled
+ * by installing a fiber-on-yield trigger that marks the transaction
+ * as aborted, which results in rolling back the transaction on
+ * commit.
+ *
+ * This function is used by the memtx engine, because it doesn't
+ * support yields inside transactions. It is also used to temporarily
+ * enable yields for long DDL operations such as building an index
+ * or checking a space format.
+ */
+void
+txn_can_yield(struct txn *txn, bool set);
+
+/**
  * Returns true if the transaction has a single statement.
  * Supposed to be used from a space on_replace trigger to
  * detect transaction boundaries.
@@ -400,12 +409,6 @@ txn_last_stmt(struct txn *txn)
 }
 
 /**
- * Fiber-stop trigger: roll back the transaction.
- */
-void
-txn_on_stop(struct trigger *trigger, void *event);
-
-/**
  * Return VDBE that is being currently executed.
  *
  * @retval VDBE that is being currently executed.
diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index e0de65d0..7635c84c 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -1093,6 +1093,7 @@ static int
 vinyl_space_check_format(struct space *space, struct tuple_format *format)
 {
 	struct vy_env *env = vy_env(space->engine);
+	struct txn *txn = in_txn();
 
 	/*
 	 * If this is local recovery, the space was checked before
@@ -1121,6 +1122,8 @@ vinyl_space_check_format(struct space *space, struct tuple_format *format)
 	bool need_wal_sync;
 	tx_manager_abort_writers_for_ddl(env->xm, space, &need_wal_sync);
 
+	txn_can_yield(txn, true);
+
 	struct trigger on_replace;
 	struct vy_check_format_ctx ctx;
 	ctx.format = format;
@@ -1168,6 +1171,7 @@ vinyl_space_check_format(struct space *space, struct tuple_format *format)
 
 	diag_destroy(&ctx.diag);
 	trigger_clear(&on_replace);
+	txn_can_yield(txn, false);
 	return rc;
 }
 
@@ -4314,6 +4318,7 @@ vinyl_space_build_index(struct space *src_space, struct index *new_index,
 {
 	struct vy_env *env = vy_env(src_space->engine);
 	struct vy_lsm *pk = vy_lsm(src_space->index[0]);
+	struct txn *txn = in_txn();
 
 	if (new_index->def->iid == 0 && !vy_lsm_is_empty(pk)) {
 		diag_set(ClientError, ER_UNSUPPORTED, "Vinyl",
@@ -4345,6 +4350,8 @@ vinyl_space_build_index(struct space *src_space, struct index *new_index,
 	bool need_wal_sync;
 	tx_manager_abort_writers_for_ddl(env->xm, src_space, &need_wal_sync);
 
+	txn_can_yield(txn, true);
+
 	/*
 	 * Iterate over all tuples stored in the space and insert
 	 * each of them into the new LSM tree. Since read iterator
@@ -4438,6 +4445,7 @@ vinyl_space_build_index(struct space *src_space, struct index *new_index,
 
 	diag_destroy(&ctx.diag);
 	trigger_clear(&on_replace);
+	txn_can_yield(txn, false);
 	return rc;
 }
 
diff --git a/test/box/transaction.result b/test/box/transaction.result
index 857314b7..ad2d650c 100644
--- a/test/box/transaction.result
+++ b/test/box/transaction.result
@@ -722,3 +722,19 @@ box.commit() -- error
 s:drop()
 ---
 ...
+--
+-- Check that a DDL transaction is rolled back on fiber yield.
+--
+s = box.schema.space.create('test')
+---
+...
+box.begin() s:create_index('pk') fiber.sleep(0)
+---
+...
+box.commit() -- error
+---
+- error: Transaction has been aborted by a fiber yield
+...
+s:drop()
+---
+...
diff --git a/test/box/transaction.test.lua b/test/box/transaction.test.lua
index 8ffae2fe..8cd3e8ba 100644
--- a/test/box/transaction.test.lua
+++ b/test/box/transaction.test.lua
@@ -373,3 +373,11 @@ fiber.sleep(0)
 s.index.pk == nil
 box.commit() -- error
 s:drop()
+
+--
+-- Check that a DDL transaction is rolled back on fiber yield.
+--
+s = box.schema.space.create('test')
+box.begin() s:create_index('pk') fiber.sleep(0)
+box.commit() -- error
+s:drop()
diff --git a/test/engine/truncate.result b/test/engine/truncate.result
index 277e7bda..d65a1df1 100644
--- a/test/engine/truncate.result
+++ b/test/engine/truncate.result
@@ -8,7 +8,7 @@ fiber =  require('fiber')
 ---
 ...
 --
--- Check that space truncation is forbidden in a transaction.
+-- Check that space truncation works fine in a transaction.
 --
 s = box.schema.create_space('test', {engine = engine})
 ---
@@ -19,13 +19,7 @@ _ = s:create_index('pk')
 _ = s:insert{123}
 ---
 ...
-box.begin()
----
-...
-s:truncate()
----
-...
-box.commit()
+box.begin() s:truncate() box.commit()
 ---
 ...
 s:select()
diff --git a/test/engine/truncate.test.lua b/test/engine/truncate.test.lua
index 74fdd52b..fe897fb6 100644
--- a/test/engine/truncate.test.lua
+++ b/test/engine/truncate.test.lua
@@ -4,14 +4,12 @@ engine = test_run:get_cfg('engine')
 fiber =  require('fiber')
 
 --
--- Check that space truncation is forbidden in a transaction.
+-- Check that space truncation works fine in a transaction.
 --
 s = box.schema.create_space('test', {engine = engine})
 _ = s:create_index('pk')
 _ = s:insert{123}
-box.begin()
-s:truncate()
-box.commit()
+box.begin() s:truncate() box.commit()
 s:select()
 s:drop()
 

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 3/3] ddl: allow to execute non-yielding DDL statements in transactions
  2019-07-10 20:43   ` Konstantin Osipov
@ 2019-07-12 14:55     ` Vladimir Davydov
  0 siblings, 0 replies; 13+ messages in thread
From: Vladimir Davydov @ 2019-07-12 14:55 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Wed, Jul 10, 2019 at 11:43:58PM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/10 16:13]:
> 
> Please write a follow up patch which changes box/lua/schema.lua
> operations to use transactions wherever possible.

Sure. I was actually planning to do that anyway once the patch set has
been committed.

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 1/3] memtx: fix txn_on_yield for DDL transactions
  2019-07-12 14:54     ` Vladimir Davydov
@ 2019-07-12 15:16       ` Konstantin Osipov
  0 siblings, 0 replies; 13+ messages in thread
From: Konstantin Osipov @ 2019-07-12 15:16 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/12 17:57]:

This is a very nice patch. Thank you for addressing my comments.
The bit fields is going to be trivial, so go ahead and don't
postpone it - it doesn't need a review.

One more comment below.

> diff --git a/src/box/vinyl.c b/src/box/vinyl.c
> index e0de65d0..7635c84c 100644
> --- a/src/box/vinyl.c
> +++ b/src/box/vinyl.c
> @@ -1093,6 +1093,7 @@ static int
>  vinyl_space_check_format(struct space *space, struct tuple_format *format)
>  {
>  	struct vy_env *env = vy_env(space->engine);
> +	struct txn *txn = in_txn();
>  
>  	/*
>  	 * If this is local recovery, the space was checked before
> @@ -1121,6 +1122,8 @@ vinyl_space_check_format(struct space *space, struct tuple_format *format)
>  	bool need_wal_sync;
>  	tx_manager_abort_writers_for_ddl(env->xm, space, &need_wal_sync);
>  
> +	txn_can_yield(txn, true);

I keep forgetting how this can happen, and then remember it's a
*memtx* transaction.

Please add this comment:

 /*
  * Tarantool doesn't allow multi-engine transactions, and DDL
  * system tables are memtx tables. Memtx transactions,
  * generally, can't yield.
  * So here we're in the middle of a *memtx* transaction. We don't
  * start a hidden vinyl transaction for DDL either, to avoid its
  * overhead. But some long DDL operations can yield, like
  * checking a format or building an index.
  * Unless we switch off memtx yield rollback triggers, such
  * yield leads to memtx transaction rollback. It is safe to switch
  * the trigger off though: it protects subsequent memtx
  * transactions from reading a dirty state, and at this phase
  * vinyl DDL does not change the data * dictionary, so there is
  * no dirty state that can be observed.
  */

-- 
Konstantin Osipov, Moscow, Russia

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 3/3] ddl: allow to execute non-yielding DDL statements in transactions
  2019-07-10 13:09 ` [PATCH v2 3/3] ddl: allow to execute non-yielding DDL statements in transactions Vladimir Davydov
  2019-07-10 20:43   ` Konstantin Osipov
  2019-07-10 21:07   ` Konstantin Osipov
@ 2019-07-15 15:03   ` Konstantin Osipov
  2 siblings, 0 replies; 13+ messages in thread
From: Konstantin Osipov @ 2019-07-15 15:03 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/10 16:13]:

LGTM.

lgtm


-- 
Konstantin Osipov, Moscow, Russia

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 2/3] ddl: don't use space_index from AlterSpaceOp::commit,rollback
  2019-07-10 13:09 ` [PATCH v2 2/3] ddl: don't use space_index from AlterSpaceOp::commit,rollback Vladimir Davydov
@ 2019-07-15 15:05   ` Konstantin Osipov
  0 siblings, 0 replies; 13+ messages in thread
From: Konstantin Osipov @ 2019-07-15 15:05 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/10 16:13]:
> If there are multiple DDL operations in the same transactions, which is
> impossible now, but will be implemented soon, AlterSpaceOp::commit and
> rollback methods must not access space index map. To understand that,
> consider the following example:
> 
>   - on_replace: AlterSpaceOp1 creates index I1 for space S1
>   - on_replace: AlterSpaceOp2 moves index I1 from space S1 to space S2
>   - on_commit:  AlterSpaceOp1 commits creation of index I1
> 
> AlterSpaceOp1 can't lookup I1 in S1 by id, because the index was moved
> from S1 to S2 by AlterSpaceOp2. If AlterSpaceOp1 attempts to look it up,
> it will access a wrong index.
> 
> Fix that by storing pointers to old and new indexes in AlterSpaceOp if
> required.

Please make this comment more clear.

LGTM.


-- 
Konstantin Osipov, Moscow, Russia

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH v2 0/3] Transactional DDL
  2019-07-10 13:09 [PATCH v2 0/3] Transactional DDL Vladimir Davydov
                   ` (2 preceding siblings ...)
  2019-07-10 13:09 ` [PATCH v2 3/3] ddl: allow to execute non-yielding DDL statements in transactions Vladimir Davydov
@ 2019-07-15 16:23 ` Vladimir Davydov
  3 siblings, 0 replies; 13+ messages in thread
From: Vladimir Davydov @ 2019-07-15 16:23 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches

On Wed, Jul 10, 2019 at 04:09:26PM +0300, Vladimir Davydov wrote:
> Vladimir Davydov (3):
>   memtx: fix txn_on_yield for DDL transactions
>   ddl: don't use space_index from AlterSpaceOp::commit,rollback
>   ddl: allow to execute non-yielding DDL statements in transactions

Pushed to master after addressing comments by Kostja.

^ permalink raw reply	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2019-07-15 16:23 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2019-07-10 13:09 [PATCH v2 0/3] Transactional DDL Vladimir Davydov
2019-07-10 13:09 ` [PATCH v2 1/3] memtx: fix txn_on_yield for DDL transactions Vladimir Davydov
2019-07-10 20:34   ` Konstantin Osipov
2019-07-12 14:54     ` Vladimir Davydov
2019-07-12 15:16       ` Konstantin Osipov
2019-07-10 13:09 ` [PATCH v2 2/3] ddl: don't use space_index from AlterSpaceOp::commit,rollback Vladimir Davydov
2019-07-15 15:05   ` Konstantin Osipov
2019-07-10 13:09 ` [PATCH v2 3/3] ddl: allow to execute non-yielding DDL statements in transactions Vladimir Davydov
2019-07-10 20:43   ` Konstantin Osipov
2019-07-12 14:55     ` Vladimir Davydov
2019-07-10 21:07   ` Konstantin Osipov
2019-07-15 15:03   ` Konstantin Osipov
2019-07-15 16:23 ` [PATCH v2 0/3] Transactional DDL Vladimir Davydov

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox