[PATCH] txn: fix rollback in sub statement

Vladimir Davydov vdavydov.dev at gmail.com
Thu Feb 8 17:01:12 MSK 2018


There are two issues in the rollback code:

 - txn_rollback_stmt() rollbacks the current autocommit transaction even
   if it is called from a sub-statement. As a result, if a sub-statement
   (i.e. a statement called from a before_replace or on_replace trigger)
   fails (e.g. due to a conflict), it will trash the current transaction
   leading to a bad memory access upon returning from the trigger.

 - txn_begin_stmt() calls txn_rollback_stmt() on failure even if it did
   not instantiate the statement. So if it is called from a trigger and
   fails (e.g. due to nesting limit), it may trash the parent statement,
   again leading to a crash.

Fix them both and add some tests.

Closes #3127
---
Branch: gh-3127-txn-fix-rollback-in-sub-stmt

 src/box/txn.c                    | 22 ++++++++--------
 test/box/before_replace.result   | 57 +++++++++++++++++++++++++++++++++++++++-
 test/box/before_replace.test.lua | 21 ++++++++++++++-
 3 files changed, 87 insertions(+), 13 deletions(-)

diff --git a/src/box/txn.c b/src/box/txn.c
index 1af8243e..e25c0e0e 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -166,10 +166,10 @@ txn_begin_stmt(struct space *space)
 	if (txn == NULL) {
 		txn = txn_begin(true);
 		if (txn == NULL)
-			goto fail;
+			return NULL;
 	} else if (txn->in_sub_stmt > TXN_SUB_STMT_MAX) {
 		diag_set(ClientError, ER_SUB_STMT_MAX);
-		goto fail;
+		return NULL;
 	}
 
 	if (trigger_run(&space->on_stmt_begin, txn) != 0)
@@ -184,12 +184,14 @@ txn_begin_stmt(struct space *space)
 		goto fail;
 	stmt->space = space;
 
-	if (engine_begin_statement(engine, txn) != 0)
-		goto fail;
-
+	if (engine_begin_statement(engine, txn) != 0) {
+		txn_rollback_stmt();
+		return NULL;
+	}
 	return txn;
 fail:
-	txn_rollback_stmt();
+	if (txn->is_autocommit && txn->in_sub_stmt == 0)
+		txn_rollback();
 	return NULL;
 }
 
@@ -325,13 +327,11 @@ void
 txn_rollback_stmt()
 {
 	struct txn *txn = in_txn();
-	if (txn == NULL)
-		return;
-	if (txn->is_autocommit)
-		return txn_rollback();
-	if (txn->in_sub_stmt == 0)
+	if (txn == NULL || txn->in_sub_stmt == 0)
 		return;
 	txn->in_sub_stmt--;
+	if (txn->is_autocommit && txn->in_sub_stmt == 0)
+		return txn_rollback();
 	txn_rollback_to_svp(txn, txn->sub_stmt_begin[txn->in_sub_stmt]);
 }
 
diff --git a/test/box/before_replace.result b/test/box/before_replace.result
index 286b0c59..10f4699c 100644
--- a/test/box/before_replace.result
+++ b/test/box/before_replace.result
@@ -405,21 +405,31 @@ s2 = box.schema.space.create('test2')
 _ = s2:create_index('pk')
 ---
 ...
-cb = function(old, new) s2:auto_increment{old, new} end
+cb = function(old, new) s2:insert{i, old, new} end
 ---
 ...
 s:before_replace(cb) == cb
 ---
 - true
 ...
+i = 1
+---
+...
 s:insert{1, 1}
 ---
 - [1, 1]
 ...
+i = 2
+---
+...
 s:replace{1, 2}
 ---
 - [1, 2]
 ...
+s:replace{1, 3} -- error: conflict in s2
+---
+- error: Duplicate key exists in unique index 'pk' in space 'test2'
+...
 s:select()
 ---
 - - [1, 2]
@@ -568,6 +578,51 @@ s:delete(1)
 ---
 - [1, 2, 3]
 ...
+-- Nesting limit: space.before_replace
+cb = function(old, new) s:insert{1, 1} end
+---
+...
+s:before_replace(cb) == cb
+---
+- true
+...
+s:insert{1, 1} -- error
+---
+- error: 'Can not execute a nested statement: nesting limit reached'
+...
+s:select()
+---
+- []
+...
+s:before_replace(nil, cb)
+---
+...
+-- Nesting limit: space.before_replace + space.on_replace
+cb = function(old, new) s:delete(1) end
+---
+...
+s:before_replace(cb) == cb
+---
+- true
+...
+s:on_replace(cb) == cb
+---
+- true
+...
+s:insert{1, 1} -- error
+---
+- error: 'Can not execute a nested statement: nesting limit reached'
+...
+s:select()
+---
+- []
+...
+s:before_replace(nil, cb)
+---
+...
+s:on_replace(nil, cb)
+---
+...
 -- Make sure the server can recover from xlogs after
 -- using space:before_replace.
 test_run:cmd('restart server default')
diff --git a/test/box/before_replace.test.lua b/test/box/before_replace.test.lua
index 7514baaf..01a386d9 100644
--- a/test/box/before_replace.test.lua
+++ b/test/box/before_replace.test.lua
@@ -132,10 +132,13 @@ s:delete(1)
 -- Issue DML from trigger.
 s2 = box.schema.space.create('test2')
 _ = s2:create_index('pk')
-cb = function(old, new) s2:auto_increment{old, new} end
+cb = function(old, new) s2:insert{i, old, new} end
 s:before_replace(cb) == cb
+i = 1
 s:insert{1, 1}
+i = 2
 s:replace{1, 2}
+s:replace{1, 3} -- error: conflict in s2
 s:select()
 s2:select()
 
@@ -184,6 +187,22 @@ s:before_replace(nil, ret_update)
 s:on_replace(nil, save)
 s:delete(1)
 
+-- Nesting limit: space.before_replace
+cb = function(old, new) s:insert{1, 1} end
+s:before_replace(cb) == cb
+s:insert{1, 1} -- error
+s:select()
+s:before_replace(nil, cb)
+
+-- Nesting limit: space.before_replace + space.on_replace
+cb = function(old, new) s:delete(1) end
+s:before_replace(cb) == cb
+s:on_replace(cb) == cb
+s:insert{1, 1} -- error
+s:select()
+s:before_replace(nil, cb)
+s:on_replace(nil, cb)
+
 -- Make sure the server can recover from xlogs after
 -- using space:before_replace.
 test_run:cmd('restart server default')
-- 
2.11.0




More information about the Tarantool-patches mailing list