Tarantool development patches archive
 help / color / mirror / Atom feed
* [PATCH 0/5] Transactional DDL
@ 2019-07-05 20:25 Vladimir Davydov
  2019-07-05 20:25 ` [PATCH 1/5] vinyl: don't sync WAL on space alter if not necessary Vladimir Davydov
                   ` (4 more replies)
  0 siblings, 5 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-05 20:25 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

Vladimir Davydov (5):
  vinyl: don't sync WAL on space alter if not necessary
  txn: run on_rollback triggers on txn_abort
  txn: fix execution order of commit triggers
  memtx: fix txn_on_yield for DDL transactions
  Allow to execute non-yielding DDL statements in transactions

 src/box/alter.cc              |  14 ----
 src/box/memtx_engine.c        |  10 +--
 src/box/memtx_space.c         |  10 +++
 src/box/txn.c                 |  33 +++++++-
 src/box/txn.h                 |  44 +++++++----
 src/box/user.cc               |   1 -
 src/box/vinyl.c               |  65 ++++++++++------
 src/box/vy_tx.c               |  11 ++-
 src/box/vy_tx.h               |   7 +-
 src/lib/core/trigger.h        |  11 +++
 test/box/on_replace.result    |  53 ++++++-------
 test/box/on_replace.test.lua  |  13 ++--
 test/box/transaction.result   | 171 ++++++++++++++++++++++++++++++++++++------
 test/box/transaction.test.lua |  92 ++++++++++++++++++++---
 test/engine/truncate.result   |  10 +--
 test/engine/truncate.test.lua |   6 +-
 16 files changed, 406 insertions(+), 145 deletions(-)

-- 
2.11.0

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

* [PATCH 1/5] vinyl: don't sync WAL on space alter if not necessary
  2019-07-05 20:25 [PATCH 0/5] Transactional DDL Vladimir Davydov
@ 2019-07-05 20:25 ` Vladimir Davydov
  2019-07-08  9:29   ` Konstantin Osipov
  2019-07-08 15:01   ` Vladimir Davydov
  2019-07-05 20:25 ` [PATCH 2/5] txn: run on_rollback triggers on txn_abort Vladimir Davydov
                   ` (3 subsequent siblings)
  4 siblings, 2 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-05 20:25 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches

Changes done to an altered space while a new index is being built or
the format is being checked are propagated via an on_replace trigger.
The problem is there may be transactions that started before the alter
request. Their working set can't be checked so we simply abort them.
We can't abort transactions that have reached WAL so we also call
wal_sync() to flush all pending WAL requests. This is a yielding
operation and we call it even if there's no transactions that need
to be flushed. As a result, vinyl space alter yields unconditionally,
even if the space is empty and there is no pending transactions
affecting it. This prevents us from implementing transactional DDL.
Let's call wal_sync() only if there's actually at least one pending
transaction affecting the altered space and waiting for WAL.
---
 src/box/vinyl.c | 51 ++++++++++++++++++++++++++++++---------------------
 src/box/vy_tx.c | 11 ++++++++++-
 src/box/vy_tx.h |  7 ++++++-
 3 files changed, 46 insertions(+), 23 deletions(-)

diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index 128b1199..e0de65d0 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -1052,25 +1052,8 @@ vinyl_space_invalidate(struct space *space)
 	 * soon as it's done reading disk, which will make the DML
 	 * request bail out early, without dereferencing the space.
 	 */
-	tx_manager_abort_writers_for_ddl(env->xm, space);
-}
-
-/**
- * This function is called after installing on_replace trigger
- * used for propagating changes done during DDL. It aborts all
- * rw transactions affecting the given space that began
- * before the trigger was installed so that DDL doesn't miss
- * their working set.
- */
-static void
-vy_abort_writers_for_ddl(struct vy_env *env, struct space *space)
-{
-	tx_manager_abort_writers_for_ddl(env->xm, space);
-	/*
-	 * Wait for prepared transactions to complete
-	 * (we can't abort them as they reached WAL).
-	 */
-	wal_sync();
+	bool unused;
+	tx_manager_abort_writers_for_ddl(env->xm, space, &unused);
 }
 
 /** Argument passed to vy_check_format_on_replace(). */
@@ -1131,6 +1114,13 @@ vinyl_space_check_format(struct space *space, struct tuple_format *format)
 	 */
 	struct vy_lsm *pk = vy_lsm(space->index[0]);
 
+	/*
+	 * Transactions started before the space alter request can't
+	 * be checked with on_replace trigger so we abort them.
+	 */
+	bool need_wal_sync;
+	tx_manager_abort_writers_for_ddl(env->xm, space, &need_wal_sync);
+
 	struct trigger on_replace;
 	struct vy_check_format_ctx ctx;
 	ctx.format = format;
@@ -1139,7 +1129,13 @@ vinyl_space_check_format(struct space *space, struct tuple_format *format)
 	trigger_create(&on_replace, vy_check_format_on_replace, &ctx, NULL);
 	trigger_add(&space->on_replace, &on_replace);
 
-	vy_abort_writers_for_ddl(env, space);
+	/*
+	 * Flush transactions waiting on WAL after installing on_replace
+	 * trigger so that changes made by newer transactions are checked
+	 * by the trigger callback.
+	 */
+	if (need_wal_sync)
+		wal_sync();
 
 	struct vy_read_iterator itr;
 	vy_read_iterator_open(&itr, pk, NULL, ITER_ALL, pk->env->empty_key,
@@ -4343,6 +4339,13 @@ vinyl_space_build_index(struct space *src_space, struct index *new_index,
 		return vy_build_recover(env, new_lsm, pk);
 
 	/*
+	 * Transactions started before the space alter request can't
+	 * be checked with on_replace trigger so we abort them.
+	 */
+	bool need_wal_sync;
+	tx_manager_abort_writers_for_ddl(env->xm, src_space, &need_wal_sync);
+
+	/*
 	 * Iterate over all tuples stored in the space and insert
 	 * each of them into the new LSM tree. Since read iterator
 	 * may yield, we install an on_replace trigger to forward
@@ -4359,7 +4362,13 @@ vinyl_space_build_index(struct space *src_space, struct index *new_index,
 	trigger_create(&on_replace, vy_build_on_replace, &ctx, NULL);
 	trigger_add(&src_space->on_replace, &on_replace);
 
-	vy_abort_writers_for_ddl(env, src_space);
+	/*
+	 * Flush transactions waiting on WAL after installing on_replace
+	 * trigger so that changes made by newer transactions are checked
+	 * by the trigger callback.
+	 */
+	if (need_wal_sync)
+		wal_sync();
 
 	struct vy_read_iterator itr;
 	vy_read_iterator_open(&itr, pk, NULL, ITER_ALL, pk->env->empty_key,
diff --git a/src/box/vy_tx.c b/src/box/vy_tx.c
index 56b9f292..ca8c57ad 100644
--- a/src/box/vy_tx.c
+++ b/src/box/vy_tx.c
@@ -1144,13 +1144,22 @@ vy_tx_set(struct vy_tx *tx, struct vy_lsm *lsm, struct tuple *stmt)
 }
 
 void
-tx_manager_abort_writers_for_ddl(struct tx_manager *xm, struct space *space)
+tx_manager_abort_writers_for_ddl(struct tx_manager *xm, struct space *space,
+				 bool *need_wal_sync)
 {
+	*need_wal_sync = false;
 	if (space->index_count == 0)
 		return; /* no indexes, no conflicts */
 	struct vy_lsm *lsm = vy_lsm(space->index[0]);
 	struct vy_tx *tx;
 	rlist_foreach_entry(tx, &xm->writers, in_writers) {
+		/*
+		 * We can't abort prepared transactions as they have
+		 * already reached WAL. The caller needs to sync WAL
+		 * to make sure they are gone.
+		 */
+		if (tx->state == VINYL_TX_COMMIT)
+			*need_wal_sync = true;
 		if (tx->state != VINYL_TX_READY)
 			continue;
 		if (tx->last_stmt_space == space ||
diff --git a/src/box/vy_tx.h b/src/box/vy_tx.h
index 9ee10755..376f4330 100644
--- a/src/box/vy_tx.h
+++ b/src/box/vy_tx.h
@@ -293,9 +293,14 @@ tx_manager_mem_used(struct tx_manager *xm);
  * Abort all rw transactions that affect the given space
  * and haven't reached WAL yet. Called before executing a DDL
  * operation.
+ *
+ * @need_wal_sync is set if at least one transaction can't be
+ * aborted, because it has reached WAL. The caller is supposed
+ * to call wal_sync() to flush them.
  */
 void
-tx_manager_abort_writers_for_ddl(struct tx_manager *xm, struct space *space);
+tx_manager_abort_writers_for_ddl(struct tx_manager *xm, struct space *space,
+				 bool *need_wal_sync);
 
 /**
  * Abort all local rw transactions that haven't reached WAL yet.
-- 
2.11.0

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

* [PATCH 2/5] txn: run on_rollback triggers on txn_abort
  2019-07-05 20:25 [PATCH 0/5] Transactional DDL Vladimir Davydov
  2019-07-05 20:25 ` [PATCH 1/5] vinyl: don't sync WAL on space alter if not necessary Vladimir Davydov
@ 2019-07-05 20:25 ` Vladimir Davydov
  2019-07-08  9:32   ` Konstantin Osipov
  2019-07-05 20:25 ` [PATCH 3/5] txn: fix execution order of commit triggers Vladimir Davydov
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-05 20:25 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches

When a memtx transaction is aborted on yield, it isn't enough to
rollback individual statements - we must also run on_rollback triggers,
otherwise changes done to the schema by an aborted DDL transaction will
be visible to other fibers until an attempt to commit it is made.
---
 src/box/txn.c                 |  7 ++++++-
 test/box/transaction.result   | 24 ++++++++++++++++++++++++
 test/box/transaction.test.lua | 10 ++++++++++
 3 files changed, 40 insertions(+), 1 deletion(-)

diff --git a/src/box/txn.c b/src/box/txn.c
index 818f405b..5d80833a 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -346,6 +346,7 @@ txn_run_triggers(struct txn *txn, struct rlist *trigger)
 	 * Some triggers require for in_txn variable to be set so
 	 * restore it for the time triggers are in progress.
 	 */
+	struct txn *old_txn = in_txn();
 	fiber_set_txn(fiber(), txn);
 	/* Rollback triggers must not throw. */
 	if (trigger_run(trigger, txn) != 0) {
@@ -357,7 +358,7 @@ txn_run_triggers(struct txn *txn, struct rlist *trigger)
 		unreachable();
 		panic("commit/rollback trigger failed");
 	}
-	fiber_set_txn(fiber(), NULL);
+	fiber_set_txn(fiber(), old_txn);
 }
 
 /**
@@ -556,6 +557,10 @@ void
 txn_abort(struct 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;
 }
 
diff --git a/test/box/transaction.result b/test/box/transaction.result
index 9da53e5b..857314b7 100644
--- a/test/box/transaction.result
+++ b/test/box/transaction.result
@@ -698,3 +698,27 @@ box.space.memtx:drop()
 box.space.vinyl:drop()
 ---
 ...
+--
+-- Check that changes done to the schema by a DDL statement are
+-- rolled back when the transaction is aborted on fiber yield.
+--
+s = box.schema.space.create('test')
+---
+...
+box.begin() s:create_index('pk') s:insert{1}
+---
+...
+fiber.sleep(0)
+---
+...
+s.index.pk == nil
+---
+- true
+...
+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 a6789316..8ffae2fe 100644
--- a/test/box/transaction.test.lua
+++ b/test/box/transaction.test.lua
@@ -363,3 +363,13 @@ if box.space.test then box.space.test:drop() end
 box.space.memtx:drop()
 box.space.vinyl:drop()
 
+--
+-- Check that changes done to the schema by a DDL statement are
+-- rolled back when the transaction is aborted on fiber yield.
+--
+s = box.schema.space.create('test')
+box.begin() s:create_index('pk') s:insert{1}
+fiber.sleep(0)
+s.index.pk == nil
+box.commit() -- error
+s:drop()
-- 
2.11.0

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

* [PATCH 3/5] txn: fix execution order of commit triggers
  2019-07-05 20:25 [PATCH 0/5] Transactional DDL Vladimir Davydov
  2019-07-05 20:25 ` [PATCH 1/5] vinyl: don't sync WAL on space alter if not necessary Vladimir Davydov
  2019-07-05 20:25 ` [PATCH 2/5] txn: run on_rollback triggers on txn_abort Vladimir Davydov
@ 2019-07-05 20:25 ` Vladimir Davydov
  2019-07-08 12:17   ` Konstantin Osipov
  2019-07-08 15:01   ` Vladimir Davydov
  2019-07-05 20:25 ` [PATCH 4/5] memtx: fix txn_on_yield for DDL transactions Vladimir Davydov
  2019-07-05 20:25 ` [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions Vladimir Davydov
  4 siblings, 2 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-05 20:25 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches

Both commit and rollback triggers are currently added to the list head.
As a result, they are both run in the reverse order. This is correct for
rollback triggers, because this matches the order in which statements
that added the triggers are rolled back, but this is wrong for commit
triggers. For example, suppose we create a space and then create an
index for it in the same transaction. We expect that on success we first
run the trigger that commits the space and only then the trigger that
commits the index, not vice versa. That said, reverse the order of
commit triggers in the scope of preparations for transactional DDL.
---
 src/box/txn.h          | 13 +++++++++++--
 src/lib/core/trigger.h | 11 +++++++++++
 2 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/src/box/txn.h b/src/box/txn.h
index a19becce..d1ef220a 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -198,7 +198,16 @@ struct txn {
 	 * in case a fiber stops (all engines).
 	 */
 	struct trigger fiber_on_stop;
-	 /** Commit and rollback triggers */
+	/**
+	 * Commit and rollback triggers.
+	 *
+	 * Note, we commit triggers are added to the tail of
+	 * the list while rollback triggers are added to the
+	 * head, see txn_on_commit() and txn_on_rollback().
+	 * This ensures that the triggers are fired in the
+	 * same order as statements that added them, both on
+	 * commit and on rollback.
+	 */
 	struct rlist on_commit, on_rollback;
 	struct sql_txn *psql_txn;
 };
@@ -279,7 +288,7 @@ static inline void
 txn_on_commit(struct txn *txn, struct trigger *trigger)
 {
 	txn_init_triggers(txn);
-	trigger_add(&txn->on_commit, trigger);
+	trigger_add_tail(&txn->on_commit, trigger);
 }
 
 static inline void
diff --git a/src/lib/core/trigger.h b/src/lib/core/trigger.h
index d3ffb9a8..4bc4da1a 100644
--- a/src/lib/core/trigger.h
+++ b/src/lib/core/trigger.h
@@ -84,6 +84,17 @@ trigger_add(struct rlist *list, struct trigger *trigger)
 	rlist_add_entry(list, trigger, link);
 }
 
+/**
+ * Same as trigger_add(), but adds a trigger to the tail of the list
+ * rather than to the head. Use it when you need to ensure a certain
+ * execution order among triggers.
+ */
+static inline void
+trigger_add_tail(struct rlist *list, struct trigger *trigger)
+{
+	rlist_add_tail_entry(list, trigger, link);
+}
+
 static inline void
 trigger_add_unique(struct rlist *list, struct trigger *trigger)
 {
-- 
2.11.0

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

* [PATCH 4/5] memtx: fix txn_on_yield for DDL transactions
  2019-07-05 20:25 [PATCH 0/5] Transactional DDL Vladimir Davydov
                   ` (2 preceding siblings ...)
  2019-07-05 20:25 ` [PATCH 3/5] txn: fix execution order of commit triggers Vladimir Davydov
@ 2019-07-05 20:25 ` Vladimir Davydov
  2019-07-08 12:22   ` Konstantin Osipov
  2019-07-05 20:25 ` [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions Vladimir Davydov
  4 siblings, 1 reply; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-05 20:25 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.
---
 src/box/memtx_engine.c        | 10 ++--------
 src/box/memtx_space.c         |  6 ++++++
 src/box/txn.c                 | 18 ++++++++++++++++++
 src/box/txn.h                 | 18 ++++++++++++++++++
 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, 78 insertions(+), 20 deletions(-)

diff --git a/src/box/memtx_engine.c b/src/box/memtx_engine.c
index c8376110..79cd26ad 100644
--- a/src/box/memtx_engine.c
+++ b/src/box/memtx_engine.c
@@ -403,14 +403,8 @@ 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);
-	}
+	if (txn->engine_tx == NULL)
+		memtx_init_txn(txn);
 	return 0;
 }
 
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 5d80833a..a70e50cc 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -575,6 +575,24 @@ txn_check_singlestatement(struct txn *txn, const char *where)
 	return 0;
 }
 
+void
+txn_enable_yield_for_ddl(void)
+{
+	struct txn *txn = in_txn();
+	/* See memtx_init_txn(). */
+	assert(txn != NULL && txn->engine_tx == txn);
+	trigger_clear(&txn->fiber_on_yield);
+}
+
+void
+txn_disable_yield_after_ddl(void)
+{
+	struct txn *txn = in_txn();
+	/* See memtx_init_txn(). */
+	assert(txn != NULL && txn->engine_tx == txn);
+	trigger_add(&fiber()->on_yield, &txn->fiber_on_yield);
+}
+
 int64_t
 box_txn_id(void)
 {
diff --git a/src/box/txn.h b/src/box/txn.h
index d1ef220a..5cbddc6f 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -371,6 +371,24 @@ int
 txn_check_singlestatement(struct txn *txn, const char *where);
 
 /**
+ * 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.
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] 37+ messages in thread

* [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-05 20:25 [PATCH 0/5] Transactional DDL Vladimir Davydov
                   ` (3 preceding siblings ...)
  2019-07-05 20:25 ` [PATCH 4/5] memtx: fix txn_on_yield for DDL transactions Vladimir Davydov
@ 2019-07-05 20:25 ` Vladimir Davydov
  2019-07-05 22:56   ` Konstantin Osipov
                     ` (2 more replies)
  4 siblings, 3 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-05 20:25 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 one thing that must be noted explicitly - it's 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.

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/memtx_space.c         |   8 ++-
 src/box/txn.c                 |  24 ++++----
 src/box/txn.h                 |  23 ++------
 src/box/user.cc               |   1 -
 src/box/vinyl.c               |  12 +++-
 test/box/on_replace.result    |  53 ++++++++---------
 test/box/on_replace.test.lua  |  13 ++---
 test/box/transaction.result   | 131 +++++++++++++++++++++++++++++++++++-------
 test/box/transaction.test.lua |  74 ++++++++++++++++++++----
 10 files changed, 237 insertions(+), 116 deletions(-)

diff --git a/src/box/alter.cc b/src/box/alter.cc
index 80fd50d1..1fbffbae 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -1798,7 +1798,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;
@@ -2095,7 +2094,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;
@@ -2288,7 +2286,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) {
@@ -2518,7 +2515,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;
 
@@ -2669,7 +2665,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;
@@ -2863,7 +2858,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 =
@@ -3161,7 +3155,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;
@@ -3210,7 +3203,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;
@@ -3288,7 +3280,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;
@@ -3441,7 +3432,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;
@@ -3644,7 +3634,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;
 
@@ -3789,7 +3778,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;
@@ -4172,7 +4160,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;
@@ -4463,7 +4450,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/memtx_space.c b/src/box/memtx_space.c
index 921dbcbf..14d440cf 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() != 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() != 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 a70e50cc..3f55e0fc 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -565,23 +565,25 @@ txn_abort(struct txn *txn)
 }
 
 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_enable_yield_for_ddl(void)
 {
 	struct txn *txn = in_txn();
 	/* See memtx_init_txn(). */
 	assert(txn != NULL && txn->engine_tx == txn);
+	/*
+	 * 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_UNSUPPORTED, "DDL",
+			 "yielding statements in transactions");
+		return -1;
+	}
 	trigger_clear(&txn->fiber_on_yield);
+	return 0;
 }
 
 void
diff --git a/src/box/txn.h b/src/box/txn.h
index 5cbddc6f..6f4ae005 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -364,13 +364,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);
-
-/**
  * 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
@@ -380,8 +373,12 @@ txn_check_singlestatement(struct txn *txn, const char *where);
  * 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.
  */
-void
+int
 txn_enable_yield_for_ddl(void);
 
 /** See txn_enable_yield_for_ddl(). */
@@ -527,16 +524,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..4e69d964 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -1121,7 +1121,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() != 0)
+		return -1;
 
 	struct trigger on_replace;
 	struct vy_check_format_ctx ctx;
@@ -4348,7 +4352,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() != 0)
+		return -1;
 
 	/*
 	 * Iterate over all tuples stored in the space and insert
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..96c75f98 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,109 @@ 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()
+---
+...
+--
+-- Only the first statement in a transaction is allowed to be
+-- a yielding DDL statement (index build, format check).
+--
+s = box.schema.space.create('test')
+---
+...
+_ = s:create_index('pk')
+---
+...
+s:insert{1, 1}
+---
+- [1, 1]
+...
+box.begin() s:create_index('sk', {parts = {2, 'unsigned'}}) s:insert{2, 2} box.commit()
+---
+...
+s.index.sk:drop()
+---
+...
+box.begin() s:format({{'a', 'unsigned'}, {'b', 'unsigned'}}) s:insert{3, 3} box.commit()
+---
+...
+s:format({})
+---
+...
+box.begin() s:insert{4, 4} s:create_index('sk', {parts = {2, 'unsigned'}}) -- error
+---
+- error: DDL does not support yielding statements in transactions
+...
+box.rollback()
+---
+...
+box.begin() s:insert{5, 5} s:format({{'a', 'unsigned'}, {'b', 'unsigned'}}) -- error
+---
+- error: DDL does not support yielding statements in transactions
+...
+box.rollback()
+---
+...
+s:select()
+---
+- - [1, 1]
+  - [2, 2]
+  - [3, 3]
+...
+s:drop()
+---
+...
diff --git a/test/box/transaction.test.lua b/test/box/transaction.test.lua
index 8cd3e8ba..9b360aed 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,62 @@ 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()
+
+--
+-- Only the first statement in a transaction is allowed to be
+-- a yielding DDL statement (index build, format check).
+--
+s = box.schema.space.create('test')
+_ = s:create_index('pk')
+s:insert{1, 1}
+box.begin() s:create_index('sk', {parts = {2, 'unsigned'}}) s:insert{2, 2} box.commit()
+s.index.sk:drop()
+box.begin() s:format({{'a', 'unsigned'}, {'b', 'unsigned'}}) s:insert{3, 3} box.commit()
+s:format({})
+box.begin() s:insert{4, 4} s:create_index('sk', {parts = {2, 'unsigned'}}) -- error
+box.rollback()
+box.begin() s:insert{5, 5} s:format({{'a', 'unsigned'}, {'b', 'unsigned'}}) -- error
+box.rollback()
+s:select()
+s:drop()
-- 
2.11.0

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-05 20:25 ` [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions Vladimir Davydov
@ 2019-07-05 22:56   ` Konstantin Osipov
  2019-07-08  8:09     ` Vladimir Davydov
  2019-07-08 12:26   ` Konstantin Osipov
  2019-07-08 12:31   ` Konstantin Osipov
  2 siblings, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-05 22:56 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:

Please work on a test case which not only ensures the statements
are allowed, but also produce desired results - as well as leave
no traces/partial effects on rollback.

-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-05 22:56   ` Konstantin Osipov
@ 2019-07-08  8:09     ` Vladimir Davydov
  2019-07-08  8:21       ` Konstantin Osipov
  0 siblings, 1 reply; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08  8:09 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Sat, Jul 06, 2019 at 01:56:35AM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> 
> Please work on a test case which not only ensures the statements
> are allowed, but also produce desired results - as well as leave
> no traces/partial effects on rollback.

As a matter of fact, this simple test does check that rollback doesn't
leave any traces - if it did, an attempt to commit the same statements
would fail:

  box.begin() create() box.rollback() -- suppose it leaved some traces
  box.begin() create() box.commit() -- then this would fail

It also checks that the DDL operations produce the desired results,
otherwise an attempt to drop created objects would fail:

  box.begin() create() box.commit() -- suppose it didn't create some objects
  box.begin() drop() box.commit() -- then this would fail

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-08  8:09     ` Vladimir Davydov
@ 2019-07-08  8:21       ` Konstantin Osipov
  2019-07-08  8:43         ` Vladimir Davydov
  0 siblings, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08  8:21 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 11:14]:
> On Sat, Jul 06, 2019 at 01:56:35AM +0300, Konstantin Osipov wrote:
> > * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> > 
> > Please work on a test case which not only ensures the statements
> > are allowed, but also produce desired results - as well as leave
> > no traces/partial effects on rollback.
> 
> As a matter of fact, this simple test does check that rollback doesn't
> leave any traces - if it did, an attempt to commit the same statements
> would fail:
> 
>   box.begin() create() box.rollback() -- suppose it leaved some traces
>   box.begin() create() box.commit() -- then this would fail
> 
> It also checks that the DDL operations produce the desired results,
> otherwise an attempt to drop created objects would fail:
> 
>   box.begin() create() box.commit() -- suppose it didn't create some objects
>   box.begin() drop() box.commit() -- then this would fail

In any case you test a fixed number of combinations, and should
come up with a randomized test. This feature encompasses all DDL
operators, including:

- roles
- users
- functions
- SQL triggers
- SQL foreign key constraints
- SQL views
- SQL check constraints

Every object should be involved in the test, and the generated
transactions should be random.

-- 
Konstantin Osipov, Moscow, Russia
https://scylladb.com

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-08  8:21       ` Konstantin Osipov
@ 2019-07-08  8:43         ` Vladimir Davydov
  2019-07-08  9:25           ` Konstantin Osipov
  0 siblings, 1 reply; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08  8:43 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Mon, Jul 08, 2019 at 11:21:16AM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 11:14]:
> > On Sat, Jul 06, 2019 at 01:56:35AM +0300, Konstantin Osipov wrote:
> > > * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> > > 
> > > Please work on a test case which not only ensures the statements
> > > are allowed, but also produce desired results - as well as leave
> > > no traces/partial effects on rollback.
> > 
> > As a matter of fact, this simple test does check that rollback doesn't
> > leave any traces - if it did, an attempt to commit the same statements
> > would fail:
> > 
> >   box.begin() create() box.rollback() -- suppose it leaved some traces
> >   box.begin() create() box.commit() -- then this would fail
> > 
> > It also checks that the DDL operations produce the desired results,
> > otherwise an attempt to drop created objects would fail:
> > 
> >   box.begin() create() box.commit() -- suppose it didn't create some objects
> >   box.begin() drop() box.commit() -- then this would fail
> 
> In any case you test a fixed number of combinations, and should
> come up with a randomized test. This feature encompasses all DDL
> operators, including:
> 
> - roles
> - users
> - functions
> - SQL triggers
> - SQL foreign key constraints
> - SQL views
> - SQL check constraints
> 
> Every object should be involved in the test, and the generated
> transactions should be random.

Sounds tricky :-/ May be, hand this over to Q&A?

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-08  8:43         ` Vladimir Davydov
@ 2019-07-08  9:25           ` Konstantin Osipov
  2019-07-08 16:46             ` Vladimir Davydov
  0 siblings, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08  9:25 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 11:45]:
> > In any case you test a fixed number of combinations, and should
> > come up with a randomized test. This feature encompasses all DDL
> > operators, including:
> > 
> > - roles
> > - users
> > - functions
> > - SQL triggers
> > - SQL foreign key constraints
> > - SQL views
> > - SQL check constraints
> > 
> > Every object should be involved in the test, and the generated
> > transactions should be random.
> 
> Sounds tricky :-/ May be, hand this over to Q&A?

Nobody is going to write the test better than you. You may even
hand this over to mermaids or some other mythical creature, if you
don't want this to happen.

-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 1/5] vinyl: don't sync WAL on space alter if not necessary
  2019-07-05 20:25 ` [PATCH 1/5] vinyl: don't sync WAL on space alter if not necessary Vladimir Davydov
@ 2019-07-08  9:29   ` Konstantin Osipov
  2019-07-08  9:50     ` Vladimir Davydov
  2019-07-08 15:01   ` Vladimir Davydov
  1 sibling, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08  9:29 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> Changes done to an altered space while a new index is being built or
> the format is being checked are propagated via an on_replace trigger.
> The problem is there may be transactions that started before the alter
> request. Their working set can't be checked so we simply abort them.
> We can't abort transactions that have reached WAL so we also call
> wal_sync() to flush all pending WAL requests. This is a yielding
> operation and we call it even if there's no transactions that need
> to be flushed. As a result, vinyl space alter yields unconditionally,
> even if the space is empty and there is no pending transactions
> affecting it. This prevents us from implementing transactional DDL.
> Let's call wal_sync() only if there's actually at least one pending
> transaction affecting the altered space and waiting for WAL.
> ---
>  src/box/vinyl.c | 51 ++++++++++++++++++++++++++++++---------------------
>  src/box/vy_tx.c | 11 ++++++++++-
>  src/box/vy_tx.h |  7 ++++++-
>  3 files changed, 46 insertions(+), 23 deletions(-)
> 
> diff --git a/src/box/vinyl.c b/src/box/vinyl.c
> index 128b1199..e0de65d0 100644
> --- a/src/box/vinyl.c
> +++ b/src/box/vinyl.c
> @@ -1052,25 +1052,8 @@ vinyl_space_invalidate(struct space *space)
>  	 * soon as it's done reading disk, which will make the DML
>  	 * request bail out early, without dereferencing the space.
>  	 */
> -	tx_manager_abort_writers_for_ddl(env->xm, space);
> -}

Please add a comment for vinyl_space_invalidate, explaining what
it is used for and who's using it, just like in the comment that
you removed:
> -
> -/**
> - * This function is called after installing on_replace trigger
> - * used for propagating changes done during DDL. It aborts all
> - * rw transactions affecting the given space that began
> - * before the trigger was installed so that DDL doesn't miss
> - * their working set.
> - */
> -static void
> -vy_abort_writers_for_ddl(struct vy_env *env, struct space *space)

Otherwise LGTM.

-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 2/5] txn: run on_rollback triggers on txn_abort
  2019-07-05 20:25 ` [PATCH 2/5] txn: run on_rollback triggers on txn_abort Vladimir Davydov
@ 2019-07-08  9:32   ` Konstantin Osipov
  2019-07-08  9:57     ` Vladimir Davydov
  0 siblings, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08  9:32 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> When a memtx transaction is aborted on yield, it isn't enough to
> rollback individual statements - we must also run on_rollback triggers,
> otherwise changes done to the schema by an aborted DDL transaction will
> be visible to other fibers until an attempt to commit it is made.
> ---
>  src/box/txn.c                 |  7 ++++++-
>  test/box/transaction.result   | 24 ++++++++++++++++++++++++
>  test/box/transaction.test.lua | 10 ++++++++++
>  3 files changed, 40 insertions(+), 1 deletion(-)
> 
> diff --git a/src/box/txn.c b/src/box/txn.c
> index 818f405b..5d80833a 100644
> --- a/src/box/txn.c
> +++ b/src/box/txn.c
> @@ -346,6 +346,7 @@ txn_run_triggers(struct txn *txn, struct rlist *trigger)
>  	 * Some triggers require for in_txn variable to be set so
>  	 * restore it for the time triggers are in progress.
>  	 */
> +	struct txn *old_txn = in_txn();

This manipulation should be in txn_abort(), not in
txn_run_triggers(). It's txn_abort().
>  	fiber_set_txn(fiber(), txn);
>  	/* Rollback triggers must not throw. */
>  	if (trigger_run(trigger, txn) != 0) {
> @@ -357,7 +358,7 @@ txn_run_triggers(struct txn *txn, struct rlist *trigger)
>  		unreachable();
>  		panic("commit/rollback trigger failed");
>  	}
> -	fiber_set_txn(fiber(), NULL);
> +	fiber_set_txn(fiber(), old_txn);

Ideally we should never need to restore old_txn. All transaction
statements, like txn_begin() or txn_abort() should set the txn, 
and whenever the transaction yields, the txn should be cleared.


-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 1/5] vinyl: don't sync WAL on space alter if not necessary
  2019-07-08  9:29   ` Konstantin Osipov
@ 2019-07-08  9:50     ` Vladimir Davydov
  0 siblings, 0 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08  9:50 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Mon, Jul 08, 2019 at 12:29:05PM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> > Changes done to an altered space while a new index is being built or
> > the format is being checked are propagated via an on_replace trigger.
> > The problem is there may be transactions that started before the alter
> > request. Their working set can't be checked so we simply abort them.
> > We can't abort transactions that have reached WAL so we also call
> > wal_sync() to flush all pending WAL requests. This is a yielding
> > operation and we call it even if there's no transactions that need
> > to be flushed. As a result, vinyl space alter yields unconditionally,
> > even if the space is empty and there is no pending transactions
> > affecting it. This prevents us from implementing transactional DDL.
> > Let's call wal_sync() only if there's actually at least one pending
> > transaction affecting the altered space and waiting for WAL.
> > ---
> >  src/box/vinyl.c | 51 ++++++++++++++++++++++++++++++---------------------
> >  src/box/vy_tx.c | 11 ++++++++++-
> >  src/box/vy_tx.h |  7 ++++++-
> >  3 files changed, 46 insertions(+), 23 deletions(-)
> > 
> > diff --git a/src/box/vinyl.c b/src/box/vinyl.c
> > index 128b1199..e0de65d0 100644
> > --- a/src/box/vinyl.c
> > +++ b/src/box/vinyl.c
> > @@ -1052,25 +1052,8 @@ vinyl_space_invalidate(struct space *space)
> >  	 * soon as it's done reading disk, which will make the DML
> >  	 * request bail out early, without dereferencing the space.
> >  	 */
> > -	tx_manager_abort_writers_for_ddl(env->xm, space);
> > -}
> 
> Please add a comment for vinyl_space_invalidate, explaining what
> it is used for and who's using it, just like in the comment that
> you removed:

I didn't remove any comments. I just moved a comment from
vy_abort_writers_for_ddl() to vinyl_space_build_index() and
vy_space_check_format().

Regarding vinyl_space_invalidate - I didn't touch it. And it seems to be
pretty well commented as it is, see comments to vinyl_space_invalidate()
and space_vtab.

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

* Re: [PATCH 2/5] txn: run on_rollback triggers on txn_abort
  2019-07-08  9:32   ` Konstantin Osipov
@ 2019-07-08  9:57     ` Vladimir Davydov
  2019-07-08 12:14       ` Konstantin Osipov
  0 siblings, 1 reply; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08  9:57 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Mon, Jul 08, 2019 at 12:32:01PM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> > When a memtx transaction is aborted on yield, it isn't enough to
> > rollback individual statements - we must also run on_rollback triggers,
> > otherwise changes done to the schema by an aborted DDL transaction will
> > be visible to other fibers until an attempt to commit it is made.
> > ---
> >  src/box/txn.c                 |  7 ++++++-
> >  test/box/transaction.result   | 24 ++++++++++++++++++++++++
> >  test/box/transaction.test.lua | 10 ++++++++++
> >  3 files changed, 40 insertions(+), 1 deletion(-)
> > 
> > diff --git a/src/box/txn.c b/src/box/txn.c
> > index 818f405b..5d80833a 100644
> > --- a/src/box/txn.c
> > +++ b/src/box/txn.c
> > @@ -346,6 +346,7 @@ txn_run_triggers(struct txn *txn, struct rlist *trigger)
> >  	 * Some triggers require for in_txn variable to be set so
> >  	 * restore it for the time triggers are in progress.
> >  	 */
> > +	struct txn *old_txn = in_txn();
> 
> This manipulation should be in txn_abort(), not in
> txn_run_triggers(). It's txn_abort().

But this function is also called from completion callback, where it has
to set the txn as well. That's why I put it there, otherwise we would
have to set/restore txn context in txn_complete as well. I'm not really
against it - just pointint it out. I'll prepare a patch that does that,
see how it looks.

> >  	fiber_set_txn(fiber(), txn);
> >  	/* Rollback triggers must not throw. */
> >  	if (trigger_run(trigger, txn) != 0) {
> > @@ -357,7 +358,7 @@ txn_run_triggers(struct txn *txn, struct rlist *trigger)
> >  		unreachable();
> >  		panic("commit/rollback trigger failed");
> >  	}
> > -	fiber_set_txn(fiber(), NULL);
> > +	fiber_set_txn(fiber(), old_txn);
> 
> Ideally we should never need to restore old_txn. All transaction
> statements, like txn_begin() or txn_abort() should set the txn, 
> and whenever the transaction yields, the txn should be cleared.

But we do want the transaction to remain attached to the fiber once
it resumes its execution so that we can raise an error on 'commit'.
Actually, we used to clear txn on yield, but then it was reworked
to make 'commit' more user-friendly.

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

* Re: [PATCH 2/5] txn: run on_rollback triggers on txn_abort
  2019-07-08  9:57     ` Vladimir Davydov
@ 2019-07-08 12:14       ` Konstantin Osipov
  2019-07-08 16:37         ` Vladimir Davydov
  0 siblings, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08 12:14 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 13:02]:
> > This manipulation should be in txn_abort(), not in
> > txn_run_triggers(). It's txn_abort().
> 
> But this function is also called from completion callback, where it has
> to set the txn as well. That's why I put it there, otherwise we would
> have to set/restore txn context in txn_complete as well. I'm not really
> against it - just pointint it out. I'll prepare a patch that does that,
> see how it looks.

Yes, so, basically, txn_begin(), txn_abort(), txn_complete()
should manage the fiber key.

> 
> > >  	fiber_set_txn(fiber(), txn);
> > >  	/* Rollback triggers must not throw. */
> > >  	if (trigger_run(trigger, txn) != 0) {
> > > @@ -357,7 +358,7 @@ txn_run_triggers(struct txn *txn, struct rlist *trigger)
> > >  		unreachable();
> > >  		panic("commit/rollback trigger failed");
> > >  	}
> > > -	fiber_set_txn(fiber(), NULL);
> > > +	fiber_set_txn(fiber(), old_txn);
> > 
> > Ideally we should never need to restore old_txn. All transaction
> > statements, like txn_begin() or txn_abort() should set the txn, 
> > and whenever the transaction yields, the txn should be cleared.
> 
> But we do want the transaction to remain attached to the fiber once
> it resumes its execution so that we can raise an error on 'commit'.
> Actually, we used to clear txn on yield, but then it was reworked
> to make 'commit' more user-friendly.

I don't get it. When a transaction is resumed after a yield, it
has to set its key again anyway. Basically, transaction == fiber
is no longer true, and each time a transaction starts running it
should update its key. Better yet, let's kill the transaction key
in the fiber altogether and pass the txn around by value.

-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 3/5] txn: fix execution order of commit triggers
  2019-07-05 20:25 ` [PATCH 3/5] txn: fix execution order of commit triggers Vladimir Davydov
@ 2019-07-08 12:17   ` Konstantin Osipov
  2019-07-08 15:01   ` Vladimir Davydov
  1 sibling, 0 replies; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08 12:17 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> Both commit and rollback triggers are currently added to the list head.
> As a result, they are both run in the reverse order. This is correct for
> rollback triggers, because this matches the order in which statements
> that added the triggers are rolled back, but this is wrong for commit
> triggers. For example, suppose we create a space and then create an
> index for it in the same transaction. We expect that on success we first
> run the trigger that commits the space and only then the trigger that
> commits the index, not vice versa. That said, reverse the order of
> commit triggers in the scope of preparations for transactional DDL.
> ---

lgtm.

-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 4/5] memtx: fix txn_on_yield for DDL transactions
  2019-07-05 20:25 ` [PATCH 4/5] memtx: fix txn_on_yield for DDL transactions Vladimir Davydov
@ 2019-07-08 12:22   ` Konstantin Osipov
  2019-07-08 16:41     ` Vladimir Davydov
  0 siblings, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08 12:22 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> 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.

Nit: I think since we set up triggers in memtx_* methods, we
should clear them inside memtx_* subsystem as well.

so it's not txn_*, it's memtx_{enable|disable}_yields

another way to do it is to clear fiber->txn key whenever
check_format /build_index functions yield. Basically these
functions run in the background, in a transaction that is
temporarily detached from the main fiber so should not pollute the
caller fiber key.

this is perhaps an over engineering at this stage, but I still
would want you to consider this.

-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-05 20:25 ` [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions Vladimir Davydov
  2019-07-05 22:56   ` Konstantin Osipov
@ 2019-07-08 12:26   ` Konstantin Osipov
  2019-07-08 16:51     ` Vladimir Davydov
  2019-07-08 12:31   ` Konstantin Osipov
  2 siblings, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08 12:26 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> +box.begin() s:insert{4, 4} s:create_index('sk', {parts = {2, 'unsigned'}}) -- error
> +---
> +- error: DDL does not support yielding statements in transactions

this message is rather obscure. Let's try to explain it in more
plain English. "Even if your engine allows a yield in transaction,
yielding in a transaction that has a DDL statement in it is not
uspported unless it's the first statement".

Is it what you wanted to say?


-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-05 20:25 ` [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions Vladimir Davydov
  2019-07-05 22:56   ` Konstantin Osipov
  2019-07-08 12:26   ` Konstantin Osipov
@ 2019-07-08 12:31   ` Konstantin Osipov
  2019-07-08 17:00     ` Vladimir Davydov
  2 siblings, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08 12:31 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> 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 one thing that must be noted explicitly - it's 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.
> 
> Closes #4083

also I would collapse this patch with the previous one, it doesn't
seem the previous one is worth it and fully clear without this
patch.


-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 1/5] vinyl: don't sync WAL on space alter if not necessary
  2019-07-05 20:25 ` [PATCH 1/5] vinyl: don't sync WAL on space alter if not necessary Vladimir Davydov
  2019-07-08  9:29   ` Konstantin Osipov
@ 2019-07-08 15:01   ` Vladimir Davydov
  1 sibling, 0 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08 15:01 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches

Pushed to master.

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

* Re: [PATCH 3/5] txn: fix execution order of commit triggers
  2019-07-05 20:25 ` [PATCH 3/5] txn: fix execution order of commit triggers Vladimir Davydov
  2019-07-08 12:17   ` Konstantin Osipov
@ 2019-07-08 15:01   ` Vladimir Davydov
  1 sibling, 0 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08 15:01 UTC (permalink / raw)
  To: kostja; +Cc: tarantool-patches

Pushed to master.

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

* Re: [PATCH 2/5] txn: run on_rollback triggers on txn_abort
  2019-07-08 12:14       ` Konstantin Osipov
@ 2019-07-08 16:37         ` Vladimir Davydov
  2019-07-08 21:56           ` Konstantin Osipov
  0 siblings, 1 reply; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08 16:37 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Mon, Jul 08, 2019 at 03:14:08PM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 13:02]:
> > > This manipulation should be in txn_abort(), not in
> > > txn_run_triggers(). It's txn_abort().
> > 
> > But this function is also called from completion callback, where it has
> > to set the txn as well. That's why I put it there, otherwise we would
> > have to set/restore txn context in txn_complete as well. I'm not really
> > against it - just pointint it out. I'll prepare a patch that does that,
> > see how it looks.
> 
> Yes, so, basically, txn_begin(), txn_abort(), txn_complete()
> should manage the fiber key.

Okay, please take a look at the patch below.

> 
> > 
> > > >  	fiber_set_txn(fiber(), txn);
> > > >  	/* Rollback triggers must not throw. */
> > > >  	if (trigger_run(trigger, txn) != 0) {
> > > > @@ -357,7 +358,7 @@ txn_run_triggers(struct txn *txn, struct rlist *trigger)
> > > >  		unreachable();
> > > >  		panic("commit/rollback trigger failed");
> > > >  	}
> > > > -	fiber_set_txn(fiber(), NULL);
> > > > +	fiber_set_txn(fiber(), old_txn);
> > > 
> > > Ideally we should never need to restore old_txn. All transaction
> > > statements, like txn_begin() or txn_abort() should set the txn, 
> > > and whenever the transaction yields, the txn should be cleared.
> > 
> > But we do want the transaction to remain attached to the fiber once
> > it resumes its execution so that we can raise an error on 'commit'.
> > Actually, we used to clear txn on yield, but then it was reworked
> > to make 'commit' more user-friendly.
> 
> I don't get it. When a transaction is resumed after a yield, it
> has to set its key again anyway.

It's not a transaction that is resumed, it's fiber. So we do need to
know which transaction is assigned to a fiber.

> Basically, transaction == fiber
> is no longer true, and each time a transaction starts running it
> should update its key. Better yet, let's kill the transaction key
> in the fiber altogether and pass the txn around by value.

We can't kill the transaction key, because when transactions are used
from Lua it's the only way to get txn.

---
From 21f26c3437b3733ebd3acfb643886fce755db263 Mon Sep 17 00:00:00 2001
From: Vladimir Davydov <vdavydov.dev@gmail.com>
Date: Fri, 5 Jul 2019 17:08:45 +0300
Subject: [PATCH] txn: run on_rollback triggers on txn_abort

When a memtx transaction is aborted on yield, it isn't enough to
rollback individual statements - we must also run on_rollback triggers,
otherwise changes done to the schema by an aborted DDL transaction will
be visible to other fibers until an attempt to commit it is made.

diff --git a/src/box/txn.c b/src/box/txn.c
index 818f405b..c605345d 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -342,11 +342,6 @@ fail:
 static inline void
 txn_run_triggers(struct txn *txn, struct rlist *trigger)
 {
-	/*
-	 * Some triggers require for in_txn variable to be set so
-	 * restore it for the time triggers are in progress.
-	 */
-	fiber_set_txn(fiber(), txn);
 	/* Rollback triggers must not throw. */
 	if (trigger_run(trigger, txn) != 0) {
 		/*
@@ -357,7 +352,6 @@ txn_run_triggers(struct txn *txn, struct rlist *trigger)
 		unreachable();
 		panic("commit/rollback trigger failed");
 	}
-	fiber_set_txn(fiber(), NULL);
 }
 
 /**
@@ -412,7 +406,15 @@ txn_entry_done_cb(struct journal_entry *entry, void *data)
 {
 	struct txn *txn = data;
 	txn->signature = entry->res;
+	/*
+	 * Some commit/rollback triggers require for in_txn fiber
+	 * variable to be set so restore it for the time triggers
+	 * are in progress.
+	 */
+	assert(in_txn() == NULL);
+	fiber_set_txn(fiber(), txn);
 	txn_complete(txn);
+	fiber_set_txn(fiber(), NULL);
 }
 
 static int64_t
@@ -497,14 +499,15 @@ txn_write(struct txn *txn)
 	 * After this point the transaction must not be used
 	 * so reset the corresponding key in the fiber storage.
 	 */
-	fiber_set_txn(fiber(), NULL);
 	txn->start_tm = ev_monotonic_now(loop());
 	if (txn->n_new_rows + txn->n_applier_rows == 0) {
 		/* Nothing to do. */
 		txn->signature = 0;
 		txn_complete(txn);
+		fiber_set_txn(fiber(), NULL);
 		return 0;
 	}
+	fiber_set_txn(fiber(), NULL);
 	return txn_write_to_wal(txn);
 }
 
@@ -555,7 +558,12 @@ txn_rollback(struct txn *txn)
 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;
 }
 
diff --git a/test/box/transaction.result b/test/box/transaction.result
index 9da53e5b..857314b7 100644
--- a/test/box/transaction.result
+++ b/test/box/transaction.result
@@ -698,3 +698,27 @@ box.space.memtx:drop()
 box.space.vinyl:drop()
 ---
 ...
+--
+-- Check that changes done to the schema by a DDL statement are
+-- rolled back when the transaction is aborted on fiber yield.
+--
+s = box.schema.space.create('test')
+---
+...
+box.begin() s:create_index('pk') s:insert{1}
+---
+...
+fiber.sleep(0)
+---
+...
+s.index.pk == nil
+---
+- true
+...
+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 a6789316..8ffae2fe 100644
--- a/test/box/transaction.test.lua
+++ b/test/box/transaction.test.lua
@@ -363,3 +363,13 @@ if box.space.test then box.space.test:drop() end
 box.space.memtx:drop()
 box.space.vinyl:drop()
 
+--
+-- Check that changes done to the schema by a DDL statement are
+-- rolled back when the transaction is aborted on fiber yield.
+--
+s = box.schema.space.create('test')
+box.begin() s:create_index('pk') s:insert{1}
+fiber.sleep(0)
+s.index.pk == nil
+box.commit() -- error
+s:drop()

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

* Re: [PATCH 4/5] memtx: fix txn_on_yield for DDL transactions
  2019-07-08 12:22   ` Konstantin Osipov
@ 2019-07-08 16:41     ` Vladimir Davydov
  2019-07-08 16:58       ` Vladimir Davydov
  2019-07-08 21:57       ` Konstantin Osipov
  0 siblings, 2 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08 16:41 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Mon, Jul 08, 2019 at 03:22:48PM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> > 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.
> 
> Nit: I think since we set up triggers in memtx_* methods, we
> should clear them inside memtx_* subsystem as well.
> 
> so it's not txn_*, it's memtx_{enable|disable}_yields

Calling a memtx_* method from vinyl would look rather weird IMO.

> 
> another way to do it is to clear fiber->txn key whenever
> check_format /build_index functions yield. Basically these
> functions run in the background, in a transaction that is
> temporarily detached from the main fiber so should not pollute the
> caller fiber key.

I don't get it. Need to discuss f2f.

> 
> this is perhaps an over engineering at this stage, but I still
> would want you to consider this.

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-08  9:25           ` Konstantin Osipov
@ 2019-07-08 16:46             ` Vladimir Davydov
  2019-07-08 21:59               ` Konstantin Osipov
  0 siblings, 1 reply; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08 16:46 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Mon, Jul 08, 2019 at 12:25:06PM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 11:45]:
> > > In any case you test a fixed number of combinations, and should
> > > come up with a randomized test. This feature encompasses all DDL
> > > operators, including:
> > > 
> > > - roles
> > > - users
> > > - functions
> > > - SQL triggers
> > > - SQL foreign key constraints
> > > - SQL views
> > > - SQL check constraints
> > > 
> > > Every object should be involved in the test, and the generated
> > > transactions should be random.
> > 
> > Sounds tricky :-/ May be, hand this over to Q&A?
> 
> Nobody is going to write the test better than you. You may even
> hand this over to mermaids or some other mythical creature, if you
> don't want this to happen.

Mermaids are cool ;-)

The problem is such a test is really difficult to come up with, because
we'd have to implement something like a dependency graph describing
which operations can be run after which. IMHO it isn't worth doing just
for the sake of a functional test.

Regarding SQL, the SQL team already has tests dealing with SQL related
objects (like constraints or triggers). Once they introduce support of
transactional DDL, we will get those tests for free.

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-08 12:26   ` Konstantin Osipov
@ 2019-07-08 16:51     ` Vladimir Davydov
  2019-07-08 22:02       ` Konstantin Osipov
  0 siblings, 1 reply; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08 16:51 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Mon, Jul 08, 2019 at 03:26:26PM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> > +box.begin() s:insert{4, 4} s:create_index('sk', {parts = {2, 'unsigned'}}) -- error
> > +---
> > +- error: DDL does not support yielding statements in transactions
> 
> this message is rather obscure. Let's try to explain it in more
> plain English. "Even if your engine allows a yield in transaction,
> yielding in a transaction that has a DDL statement in it is not
> uspported unless it's the first statement".
> 
> Is it what you wanted to say?

Basically, yes, but I'd prefer to keep the error message as brief as
possible. Besides we can't put such a text in ER_UNSUPPORTED error which
has the following template: "%s does not support %s". We can, of course,
introduce a separate error code, but I failed to come up with a good
name for it :-/

What about

  DDL does not support long operations, such as building an index or checking a space format, in a transaction

?

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

* Re: [PATCH 4/5] memtx: fix txn_on_yield for DDL transactions
  2019-07-08 16:41     ` Vladimir Davydov
@ 2019-07-08 16:58       ` Vladimir Davydov
  2019-07-09 10:12         ` Vladimir Davydov
  2019-07-08 21:57       ` Konstantin Osipov
  1 sibling, 1 reply; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08 16:58 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Mon, Jul 08, 2019 at 07:41:41PM +0300, Vladimir Davydov wrote:
> On Mon, Jul 08, 2019 at 03:22:48PM +0300, Konstantin Osipov wrote:
> > * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> > > 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.
> > 
> > Nit: I think since we set up triggers in memtx_* methods, we
> > should clear them inside memtx_* subsystem as well.
> > 
> > so it's not txn_*, it's memtx_{enable|disable}_yields
> 
> Calling a memtx_* method from vinyl would look rather weird IMO.

May be, instead we should move memtx stuff dealing with yields to
txn? Something like, txn_disable_yield() that would disable yields
altogether. Memtx would call this function while vinyl wouldn't.
Then everything related to yields in transactions would live in
txn.c. What do you think?

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-08 12:31   ` Konstantin Osipov
@ 2019-07-08 17:00     ` Vladimir Davydov
  0 siblings, 0 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-08 17:00 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Mon, Jul 08, 2019 at 03:31:25PM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> > 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 one thing that must be noted explicitly - it's 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.
> > 
> > Closes #4083
> 
> also I would collapse this patch with the previous one, it doesn't
> seem the previous one is worth it and fully clear without this
> patch.

I beg to disagree. The previous patch fixes certain bugs in the code
disabling yields in memtx transactions. This is covered by the new test.
Better keep it separated in case we want to cherry-pick it.

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

* Re: [PATCH 2/5] txn: run on_rollback triggers on txn_abort
  2019-07-08 16:37         ` Vladimir Davydov
@ 2019-07-08 21:56           ` Konstantin Osipov
  2019-07-09  8:49             ` Vladimir Davydov
  0 siblings, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08 21:56 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 22:14]:
> On Mon, Jul 08, 2019 at 03:14:08PM +0300, Konstantin Osipov wrote:
> > * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 13:02]:
> > > > This manipulation should be in txn_abort(), not in
> > > > txn_run_triggers(). It's txn_abort().
> > > 
> > > But this function is also called from completion callback, where it has
> > > to set the txn as well. That's why I put it there, otherwise we would
> > > have to set/restore txn context in txn_complete as well. I'm not really
> > > against it - just pointint it out. I'll prepare a patch that does that,
> > > see how it looks.
> > 
> > Yes, so, basically, txn_begin(), txn_abort(), txn_complete()
> > should manage the fiber key.
> 
> Okay, please take a look at the patch below.

Much safer. Thanks. LGTM.

-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 4/5] memtx: fix txn_on_yield for DDL transactions
  2019-07-08 16:41     ` Vladimir Davydov
  2019-07-08 16:58       ` Vladimir Davydov
@ 2019-07-08 21:57       ` Konstantin Osipov
  2019-07-09  7:51         ` Vladimir Davydov
  1 sibling, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08 21:57 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 22:14]:
> > another way to do it is to clear fiber->txn key whenever
> > check_format /build_index functions yield. Basically these
> > functions run in the background, in a transaction that is
> > temporarily detached from the main fiber so should not pollute the
> > caller fiber key.
> 
> I don't get it. Need to discuss f2f.

I don't get why you need to call this from vinyl.

If both engines use the api, it should be txn_* api of course. but
I don't get why both engines need to use it, vinyl doesn't set
yield triggers to my knowledge.


-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-08 16:46             ` Vladimir Davydov
@ 2019-07-08 21:59               ` Konstantin Osipov
  0 siblings, 0 replies; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08 21:59 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 22:14]:
> The problem is such a test is really difficult to come up with, because
> we'd have to implement something like a dependency graph describing
> which operations can be run after which. IMHO it isn't worth doing just
> for the sake of a functional test.

I volunteer to help :), and it is very much worth doing according
to my experience. 


-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-08 16:51     ` Vladimir Davydov
@ 2019-07-08 22:02       ` Konstantin Osipov
  2019-07-09  8:11         ` Vladimir Davydov
  0 siblings, 1 reply; 37+ messages in thread
From: Konstantin Osipov @ 2019-07-08 22:02 UTC (permalink / raw)
  To: Vladimir Davydov; +Cc: tarantool-patches

* Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 22:14]:
> What about
> 
>   DDL does not support long operations, such as building an index or checking a space format, in a transaction

It's not DDL, it's Tarantool :)

I think a separate error code is OK, a clear message has more
value than sticking to the same error code.

Can not perform %s in a multi-statement transaction. 

Is there a workaround? If yes, let's add it to the message.

-- 
Konstantin Osipov, Moscow, Russia

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

* Re: [PATCH 4/5] memtx: fix txn_on_yield for DDL transactions
  2019-07-08 21:57       ` Konstantin Osipov
@ 2019-07-09  7:51         ` Vladimir Davydov
  0 siblings, 0 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-09  7:51 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Tue, Jul 09, 2019 at 12:57:50AM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 22:14]:
> > > another way to do it is to clear fiber->txn key whenever
> > > check_format /build_index functions yield. Basically these
> > > functions run in the background, in a transaction that is
> > > temporarily detached from the main fiber so should not pollute the
> > > caller fiber key.
> > 
> > I don't get it. Need to discuss f2f.
> 
> I don't get why you need to call this from vinyl.
> 
> If both engines use the api, it should be txn_* api of course. but
> I don't get why both engines need to use it, vinyl doesn't set
> yield triggers to my knowledge.

DDL is a memtx operation, but actual index creation is done in
vinyl_space_build_index.

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-08 22:02       ` Konstantin Osipov
@ 2019-07-09  8:11         ` Vladimir Davydov
  2019-07-09 11:03           ` Vladimir Davydov
  0 siblings, 1 reply; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-09  8:11 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Tue, Jul 09, 2019 at 01:02:27AM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 22:14]:
> > What about
> > 
> >   DDL does not support long operations, such as building an index or checking a space format, in a transaction
> 
> It's not DDL, it's Tarantool :)

Well, all error codes are about Tarantool. However, this particular one
is about limitations of our data dictionarly language (DDL).

> 
> I think a separate error code is OK, a clear message has more
> value than sticking to the same error code.
> 
> Can not perform %s in a multi-statement transaction. 

Okay. Sounds good to me.

> 
> Is there a workaround? If yes, let's add it to the message.

No, there's no workaround. The only way to run index build or space
format check in a transaction is to execute it first, i.e. in fact in
a single-statement transaction.

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

* Re: [PATCH 2/5] txn: run on_rollback triggers on txn_abort
  2019-07-08 21:56           ` Konstantin Osipov
@ 2019-07-09  8:49             ` Vladimir Davydov
  0 siblings, 0 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-09  8:49 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Tue, Jul 09, 2019 at 12:56:18AM +0300, Konstantin Osipov wrote:
> * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 22:14]:
> > On Mon, Jul 08, 2019 at 03:14:08PM +0300, Konstantin Osipov wrote:
> > > * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 13:02]:
> > > > > This manipulation should be in txn_abort(), not in
> > > > > txn_run_triggers(). It's txn_abort().
> > > > 
> > > > But this function is also called from completion callback, where it has
> > > > to set the txn as well. That's why I put it there, otherwise we would
> > > > have to set/restore txn context in txn_complete as well. I'm not really
> > > > against it - just pointint it out. I'll prepare a patch that does that,
> > > > see how it looks.
> > > 
> > > Yes, so, basically, txn_begin(), txn_abort(), txn_complete()
> > > should manage the fiber key.
> > 
> > Okay, please take a look at the patch below.
> 
> Much safer. Thanks. LGTM.

Pushed to master.

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

* Re: [PATCH 4/5] memtx: fix txn_on_yield for DDL transactions
  2019-07-08 16:58       ` Vladimir Davydov
@ 2019-07-09 10:12         ` Vladimir Davydov
  0 siblings, 0 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-09 10:12 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Mon, Jul 08, 2019 at 07:58:10PM +0300, Vladimir Davydov wrote:
> On Mon, Jul 08, 2019 at 07:41:41PM +0300, Vladimir Davydov wrote:
> > On Mon, Jul 08, 2019 at 03:22:48PM +0300, Konstantin Osipov wrote:
> > > * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/05 23:27]:
> > > > 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.
> > > 
> > > Nit: I think since we set up triggers in memtx_* methods, we
> > > should clear them inside memtx_* subsystem as well.
> > > 
> > > so it's not txn_*, it's memtx_{enable|disable}_yields
> > 
> > Calling a memtx_* method from vinyl would look rather weird IMO.
> 
> May be, instead we should move memtx stuff dealing with yields to
> txn? Something like, txn_disable_yield() that would disable yields
> altogether. Memtx would call this function while vinyl wouldn't.
> Then everything related to yields in transactions would live in
> txn.c. What do you think?

The patch below illustrates what I mean. IMHO it looks much better
than the original one.
---

From 67e36c5a99065a17c391716972039f7e8fbb7691 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..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()
 

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

* Re: [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions
  2019-07-09  8:11         ` Vladimir Davydov
@ 2019-07-09 11:03           ` Vladimir Davydov
  0 siblings, 0 replies; 37+ messages in thread
From: Vladimir Davydov @ 2019-07-09 11:03 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tarantool-patches

On Tue, Jul 09, 2019 at 11:11:50AM +0300, Vladimir Davydov wrote:
> On Tue, Jul 09, 2019 at 01:02:27AM +0300, Konstantin Osipov wrote:
> > * Vladimir Davydov <vdavydov.dev@gmail.com> [19/07/08 22:14]:
> > > What about
> > > 
> > >   DDL does not support long operations, such as building an index or checking a space format, in a transaction
> > 
> > It's not DDL, it's Tarantool :)
> 
> Well, all error codes are about Tarantool. However, this particular one
> is about limitations of our data dictionarly language (DDL).
> 
> > 
> > I think a separate error code is OK, a clear message has more
> > value than sticking to the same error code.
> > 
> > Can not perform %s in a multi-statement transaction. 
> 
> Okay. Sounds good to me.
> 
> > 
> > Is there a workaround? If yes, let's add it to the message.
> 
> No, there's no workaround. The only way to run index build or space
> format check in a transaction is to execute it first, i.e. in fact in
> a single-statement transaction.

Here's the patch with the fixed error message. Please take a look once
time permits. Both remaining patches are available on the branch:

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

I can resend them in a separate thread if you wish.
---

From 19e11c407c95499a757c247903825d8265fec355 Mon Sep 17 00:00:00 2001
From: Vladimir Davydov <vdavydov.dev@gmail.com>
Date: Fri, 5 Jul 2019 20:13:16 +0300
Subject: [PATCH] Allow to execute non-yielding DDL statements in transactions

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 one thing that must be noted explicitly - it's 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.

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.

diff --git a/src/box/alter.cc b/src/box/alter.cc
index ce0cf2d9..2054ff6d 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -1828,7 +1828,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;
@@ -2125,7 +2124,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;
@@ -2318,7 +2316,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) {
@@ -2548,7 +2545,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;
 
@@ -2699,7 +2695,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;
@@ -2893,7 +2888,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 =
@@ -3191,7 +3185,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;
@@ -3240,7 +3233,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;
@@ -3318,7 +3310,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;
@@ -3471,7 +3462,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;
@@ -3674,7 +3664,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;
 
@@ -3819,7 +3808,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;
@@ -4202,7 +4190,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;
@@ -4493,7 +4480,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..dadaa230 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -1121,7 +1121,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 +4352,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();
 
 --

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

end of thread, other threads:[~2019-07-09 11:03 UTC | newest]

Thread overview: 37+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2019-07-05 20:25 [PATCH 0/5] Transactional DDL Vladimir Davydov
2019-07-05 20:25 ` [PATCH 1/5] vinyl: don't sync WAL on space alter if not necessary Vladimir Davydov
2019-07-08  9:29   ` Konstantin Osipov
2019-07-08  9:50     ` Vladimir Davydov
2019-07-08 15:01   ` Vladimir Davydov
2019-07-05 20:25 ` [PATCH 2/5] txn: run on_rollback triggers on txn_abort Vladimir Davydov
2019-07-08  9:32   ` Konstantin Osipov
2019-07-08  9:57     ` Vladimir Davydov
2019-07-08 12:14       ` Konstantin Osipov
2019-07-08 16:37         ` Vladimir Davydov
2019-07-08 21:56           ` Konstantin Osipov
2019-07-09  8:49             ` Vladimir Davydov
2019-07-05 20:25 ` [PATCH 3/5] txn: fix execution order of commit triggers Vladimir Davydov
2019-07-08 12:17   ` Konstantin Osipov
2019-07-08 15:01   ` Vladimir Davydov
2019-07-05 20:25 ` [PATCH 4/5] memtx: fix txn_on_yield for DDL transactions Vladimir Davydov
2019-07-08 12:22   ` Konstantin Osipov
2019-07-08 16:41     ` Vladimir Davydov
2019-07-08 16:58       ` Vladimir Davydov
2019-07-09 10:12         ` Vladimir Davydov
2019-07-08 21:57       ` Konstantin Osipov
2019-07-09  7:51         ` Vladimir Davydov
2019-07-05 20:25 ` [PATCH 5/5] Allow to execute non-yielding DDL statements in transactions Vladimir Davydov
2019-07-05 22:56   ` Konstantin Osipov
2019-07-08  8:09     ` Vladimir Davydov
2019-07-08  8:21       ` Konstantin Osipov
2019-07-08  8:43         ` Vladimir Davydov
2019-07-08  9:25           ` Konstantin Osipov
2019-07-08 16:46             ` Vladimir Davydov
2019-07-08 21:59               ` Konstantin Osipov
2019-07-08 12:26   ` Konstantin Osipov
2019-07-08 16:51     ` Vladimir Davydov
2019-07-08 22:02       ` Konstantin Osipov
2019-07-09  8:11         ` Vladimir Davydov
2019-07-09 11:03           ` Vladimir Davydov
2019-07-08 12:31   ` Konstantin Osipov
2019-07-08 17:00     ` Vladimir Davydov

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