diff --git a/src/box/box.cc b/src/box/box.cc index ef048d4a7cf8abe98d1e4d3f5599bc20ec873752..38312ae23c626124e462ab9e81193c740eda4d68 100644 --- a/src/box/box.cc +++ b/src/box/box.cc @@ -81,7 +81,7 @@ process_rw(struct port *port, struct request *request) request->execute(request, port); port_eof(port); } catch (Exception *e) { - txn_rollback(); + txn_rollback_stmt(); throw; } } diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index f7c10d78e9daf0ccb1f0f578444007d092195830..cebfe88ad7f03b2daa3a7ff472dbb2b31545fb0f 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -45,6 +45,14 @@ ffi.cdef[[ const char *key, const char *key_end); void password_prepare(const char *password, int len, char *out, int out_len); + int + boxffi_txn_begin(); + + int + boxffi_txn_commit(); + + void + boxffi_txn_rollback(); ]] local function user_resolve(user) @@ -61,6 +69,10 @@ local function user_resolve(user) return tuple[1] end +box.begin = function() if ffi.C.boxffi_txn_begin() == -1 then box.raise() end end +box.commit = function() if ffi.C.boxffi_txn_commit() == -1 then box.raise() end end +box.rollback = ffi.C.boxffi_txn_rollback; + box.schema.space = {} box.schema.space.create = function(name, options) local _space = box.space[box.schema.SPACE_ID] diff --git a/src/box/txn.cc b/src/box/txn.cc index 24158fc352b2363b59dc6803404bd318a8fdf908..42d343cb74dde847c128efea2de8b1024c3654d8 100644 --- a/src/box/txn.cc +++ b/src/box/txn.cc @@ -174,6 +174,32 @@ txn_commit(struct txn *txn) in_txn() = NULL; } +/** + * Void all effects of the statement, but + * keep it in the list - to maintain + * limit on the number of statements in a + * trasnaction. + */ +void +txn_rollback_stmt() +{ + struct txn *txn = in_txn(); + if (txn == NULL) + return; + if (txn->autocommit) + return txn_rollback(); + struct txn_stmt *stmt = txn_stmt(txn); + if (stmt->old_tuple || stmt->new_tuple) { + space_replace(stmt->space, stmt->new_tuple, + stmt->old_tuple, DUP_INSERT); + if (stmt->new_tuple) + tuple_ref(stmt->new_tuple, -1); + } + stmt->old_tuple = stmt->new_tuple = NULL; + stmt->space = NULL; + stmt->row = NULL; +} + void txn_rollback() { @@ -206,3 +232,40 @@ txn_check_autocommit(struct txn *txn, const char *where) where, "multi-statement transactions"); } } + +extern "C" { + +int +boxffi_txn_begin() +{ + try { + if (in_txn()) + tnt_raise(ClientError, ER_ACTIVE_TRANSACTION); + (void) txn_begin(false); + } catch (Exception *e) { + return -1; /* pass exception through FFI */ + } + return 0; +} + +int +boxffi_txn_commit() +{ + try { + struct txn *txn = in_txn(); + if (txn == NULL) + tnt_raise(ClientError, ER_NO_ACTIVE_TRANSACTION); + txn_commit(txn); + } catch (Exception *e) { + return -1; /* pass exception through FFI */ + } + return 0; +} + +void +boxffi_txn_rollback() +{ + txn_rollback(); /* doesn't throw */ +} + +} /* extern "C" */ diff --git a/src/box/txn.h b/src/box/txn.h index 06cd9c20ba9a631cfee9375817764771b09bdbe3..3f9870b5fbb1b60e5a9450b8fb73bf780daad037 100644 --- a/src/box/txn.h +++ b/src/box/txn.h @@ -85,6 +85,13 @@ txn_begin_stmt(struct request *request); void txn_commit_stmt(struct txn *txn, struct port *port); +/** + * Rollback a statement. In autocommit mode, + * rolls back the entire transaction. + */ +void +txn_rollback_stmt(); + /** * Start a transaction explicitly. * @pre no transaction is active @@ -123,4 +130,30 @@ txn_stmt(struct txn *txn) return rlist_last_entry(&txn->stmts, struct txn_stmt, next); } +/** + * FFI bindings: do not throw exceptions, do not accept extra + * arguments + */ +extern "C" { + +/** + * @retval 0 - success + * @retval -1 - failed, perhaps a transaction has already been + * started + */ +int +boxffi_txn_begin(); + +/** + * @retval 0 - success + * @retval -1 - commit failed + */ +int +boxffi_txn_commit(); + +void +boxffi_txn_rollback(); + +} /* extern "C" */ + #endif /* TARANTOOL_BOX_TXN_H_INCLUDED */ diff --git a/src/errcode.h b/src/errcode.h index 052b02d631afdb14b21a1d016a8a30ee93c1ce9e..82ee385c324149861813149e3d07435bb70b2e4f 100644 --- a/src/errcode.h +++ b/src/errcode.h @@ -128,6 +128,8 @@ enum { TNT_ERRMSG_MAX = 512 }; /* 76 */_(ER_INVALID_XLOG_ORDER, 2, "Invalid xlog order: %lld and %lld") \ /* 77 */_(ER_NO_CONNECTION, 2, "Connection is not established") \ /* 78 */_(ER_TIMEOUT, 2, "Timeout exceeded") \ + /* 79 */_(ER_ACTIVE_TRANSACTION, 2, "Operation is not permitted when there is an active transaction ") \ + /* 80 */_(ER_NO_ACTIVE_TRANSACTION, 2, "Operation is not permitted when there is no active transaction ") \ /* * !IMPORTANT! Please follow instructions at start of the file diff --git a/src/session.cc b/src/session.cc index 7a089fc6626f7a5e9cfbccea7fbc406288f16d70..788933811d17ba6e33b223763672f60f2b2157f4 100644 --- a/src/session.cc +++ b/src/session.cc @@ -35,6 +35,7 @@ #include "exception.h" #include "random.h" #include <sys/socket.h> +#include "box/txn.h" static struct mh_i32ptr_t *session_registry; @@ -104,6 +105,10 @@ session_run_on_connect_triggers(struct session *session) void session_destroy(struct session *session) { + if (session->txn) { + assert(session->txn == in_txn()); + txn_rollback(); + } assert(session->txn == NULL); struct mh_i32ptr_node_t node = { session->id, NULL }; mh_i32ptr_remove(session_registry, &node, NULL); diff --git a/test/box/misc.result b/test/box/misc.result index 0e61e0aaa0fcd8aba6aca5db026e36089123ba83..ca8685af9e44dc9da165d1fc1836c74f382a1641 100644 --- a/test/box/misc.result +++ b/test/box/misc.result @@ -16,11 +16,14 @@ t = {} for n in pairs(box) do table.insert(t, tostring(n)) end table.sort(t) ... t --- -- - cfg +- - begin + - cfg + - commit - error - index - info - raise + - rollback - schema - slab - snapshot @@ -197,12 +200,14 @@ t; - 'box.error.CREATE_USER : 43' - 'box.error.CREATE_SPACE : 9' - 'box.error.UNKNOWN_SCHEMA_OBJECT : 49' + - 'box.error.NO_ACTIVE_TRANSACTION : 80' + - 'box.error.SPLICE : 25' - 'box.error.FIELD_TYPE_MISMATCH : 24' - 'box.error.UNSUPPORTED : 5' - 'box.error.INVALID_MSGPACK : 20' - 'box.error.KEY_PART_COUNT : 31' - 'box.error.ALTER_SPACE : 12' - - 'box.error.SPLICE : 25' + - 'box.error.ACTIVE_TRANSACTION : 79' - 'box.error.NO_CONNECTION : 77' - 'box.error.TUPLE_FOUND : 3' - 'box.error.INVALID_XLOG_NAME : 75' diff --git a/test/box/transaction.result b/test/box/transaction.result new file mode 100644 index 0000000000000000000000000000000000000000..c2f2e28ac6d1434618d4d567585011be348cc692 --- /dev/null +++ b/test/box/transaction.result @@ -0,0 +1,180 @@ +--# setopt delimiter ';' +-- empty transaction - ok +box.begin() box.commit(); +--- +... +-- double begin +box.begin() box.begin(); +--- +- error: 'Operation is not permitted when there is an active transaction ' +... +-- no active transaction since exception rolled it back +box.commit(); +--- +... +-- double commit - error +box.begin() box.commit() box.commit(); +--- +- error: 'Operation is not permitted when there is no active transaction ' +... +-- commit if not started - error +box.commit(); +--- +- error: 'Operation is not permitted when there is no active transaction ' +... +-- rollback if not started - ok +-- double rollback - ok +box.rollback() +box.begin() box.rollback() box.rollback(); +--- +... +-- rollback of an empty trans - ends transaction +box.begin() box.rollback(); +--- +... +-- no current transaction - error +box.commit(); +--- +- error: 'Operation is not permitted when there is no active transaction ' +... +fiber = require('fiber'); +--- +... +function sloppy() + box.begin() +end; +--- +... +f = fiber.wrap(sloppy); +--- +... +-- when the sloppy fiber ends, its session has an active transction +-- ensure it's rolled back automatically +fiber.sleep(0); +--- +... +fiber.sleep(0); +--- +... +-- transactions and system spaces +box.begin() box.schema.space.create('test'); +--- +- error: Space _space does not support multi-statement transactions +... +box.rollback(); +--- +... +box.begin() box.schema.func.create('test'); +--- +- error: Space _func does not support multi-statement transactions +... +box.rollback(); +--- +... +box.begin() box.schema.user.create('test'); +--- +- error: Space _user does not support multi-statement transactions +... +box.rollback(); +--- +... +box.begin() box.schema.user.grant('guest', 'read', 'universe'); +--- +- error: Space _priv does not support multi-statement transactions +... +box.rollback(); +--- +... +box.begin() box.space._schema:insert{'test'}; +--- +- error: Space _schema does not support multi-statement transactions +... +box.rollback(); +--- +... +box.begin() box.space._cluster:insert{123456789, 'abc'}; +--- +- error: Space _cluster does not support multi-statement transactions +... +box.rollback(); +--- +... +s = box.schema.space.create('test'); +--- +... +box.begin() s:create_index('primary'); +--- +- error: Space _index does not support multi-statement transactions +... +box.rollback(); +--- +... +s:create_index('primary'); +--- +... +function multi() + box.begin() + s:auto_increment{'first row'} + s:auto_increment{'second row'} + t = s:select{} + box.commit() +end; +--- +... +multi(); +--- +... +t; +--- +- - [1, 'first row'] + - [2, 'second row'] +... +s:select{}; +--- +- - [1, 'first row'] + - [2, 'second row'] +... +s:truncate(); +--- +... +function multi() + box.begin() + s:auto_increment{'first row'} + s:auto_increment{'second row'} + t = s:select{} + box.rollback() +end; +--- +... +multi(); +--- +... +t; +--- +- - [1, 'first row'] + - [2, 'second row'] +... +s:select{}; +--- +- [] +... +function multi() + box.begin() + s:insert{1, 'first row'} + pcall(s.insert, s, {1, 'duplicate'}) + t = s:select{} + box.commit() +end; +--- +... +multi(); +--- +... +t; +--- +- - [1, 'first row'] +... +s:select{}; +--- +- - [1, 'first row'] +... diff --git a/test/box/transaction.test.lua b/test/box/transaction.test.lua new file mode 100644 index 0000000000000000000000000000000000000000..981430f48d8d9e886f5cbb645f32eb1fc3f352b6 --- /dev/null +++ b/test/box/transaction.test.lua @@ -0,0 +1,76 @@ +--# setopt delimiter ';' +-- empty transaction - ok +box.begin() box.commit(); +-- double begin +box.begin() box.begin(); +-- no active transaction since exception rolled it back +box.commit(); +-- double commit - error +box.begin() box.commit() box.commit(); +-- commit if not started - error +box.commit(); +-- rollback if not started - ok +box.rollback() +-- double rollback - ok +box.begin() box.rollback() box.rollback(); +-- rollback of an empty trans - ends transaction +box.begin() box.rollback(); +-- no current transaction - error +box.commit(); +fiber = require('fiber'); +function sloppy() + box.begin() +end; +f = fiber.wrap(sloppy); +-- when the sloppy fiber ends, its session has an active transction +-- ensure it's rolled back automatically +fiber.sleep(0); +fiber.sleep(0); +-- transactions and system spaces +box.begin() box.schema.space.create('test'); +box.rollback(); +box.begin() box.schema.func.create('test'); +box.rollback(); +box.begin() box.schema.user.create('test'); +box.rollback(); +box.begin() box.schema.user.grant('guest', 'read', 'universe'); +box.rollback(); +box.begin() box.space._schema:insert{'test'}; +box.rollback(); +box.begin() box.space._cluster:insert{123456789, 'abc'}; +box.rollback(); +s = box.schema.space.create('test'); +box.begin() s:create_index('primary'); +box.rollback(); +s:create_index('primary'); +function multi() + box.begin() + s:auto_increment{'first row'} + s:auto_increment{'second row'} + t = s:select{} + box.commit() +end; +multi(); +t; +s:select{}; +s:truncate(); +function multi() + box.begin() + s:auto_increment{'first row'} + s:auto_increment{'second row'} + t = s:select{} + box.rollback() +end; +multi(); +t; +s:select{}; +function multi() + box.begin() + s:insert{1, 'first row'} + pcall(s.insert, s, {1, 'duplicate'}) + t = s:select{} + box.commit() +end; +multi(); +t; +s:select{};