diff --git a/CHANGELOG b/CHANGELOG index 56036d55c5..7d3996b427 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ v3.5.0-rc.6 (2019-XX-XX) ------------------------ +* Added missing REST API route GET /_api/transaction for retrieving the list of + currently ongoing transactions. + * Fixed issue #9558: RTRIM not working as expected. * Added startup error for bad temporary directory setting. @@ -15,7 +18,7 @@ v3.5.0-rc.6 (2019-XX-XX) will be a startup warning about potential data loss (though in ArangoDB 3.4 allowing to continue the startup - in 3.5 and higher we will abort the startup). -* Make TTL indexes behave like other indexes on creation +* Make TTL indexes behave like other indexes on creation. If a TTL index is already present on a collection, the previous behavior was to make subsequent calls to `ensureIndex` fail unconditionally with diff --git a/Documentation/DocuBlocks/Rest/Transactions/get_api_transactions.md b/Documentation/DocuBlocks/Rest/Transactions/get_api_transactions.md new file mode 100644 index 0000000000..eb4a1798d3 --- /dev/null +++ b/Documentation/DocuBlocks/Rest/Transactions/get_api_transactions.md @@ -0,0 +1,47 @@ + +@startDocuBlock get_api_transactions +@brief Return the currently running server-side transactions + +@RESTHEADER{GET /_api/transaction, Get currently running transactions, executeGetState:transactions} + +@RESTDESCRIPTION +The result is an object describing with the attribute *transactions*, which contains +an array of transactions. +In a cluster the array will contain the transactions from all coordinators. + +Each array entry contains an object with the following attributes: + +- *id*: the transaction's id +- *status*: the transaction's status + +@RESTRETURNCODES + +@RESTRETURNCODE{200} +If the list of transactions can be retrieved successfully, *HTTP 200* will be returned. + +@EXAMPLES + +Get currently running transactions + +@EXAMPLE_ARANGOSH_RUN{RestTransactionsGet} + db._drop("products"); + db._create("products"); + let body = { + collections: { + read : "products" + } + }; + let trx = db._createTransaction(body); + let url = "/_api/transaction"; + + let response = logCurlRequest('GET', url); + assert(response.code === 200); + + logJsonResponse(response); + + ~ trx.abort(); + ~ db._drop("products"); +@END_EXAMPLE_ARANGOSH_RUN + +@endDocuBlock + diff --git a/arangod/Cluster/ClusterComm.cpp b/arangod/Cluster/ClusterComm.cpp index 0a764029f2..69f9b632fb 100644 --- a/arangod/Cluster/ClusterComm.cpp +++ b/arangod/Cluster/ClusterComm.cpp @@ -875,9 +875,7 @@ void ClusterComm::drop(CoordTransactionID const coordTransactionID, /// then after another 2 seconds, 4 seconds and so on, until the overall /// timeout has been reached. A request that can connect and produces a /// result is simply reported back with no retry, even in an error case. -/// The method returns the number of successful requests and puts the -/// number of finished ones in nrDone. Thus, the timeout was triggered -/// if and only if nrDone < requests.size(). +/// The method returns the number of successful requests. //////////////////////////////////////////////////////////////////////////////// size_t ClusterComm::performRequests(std::vector& requests, @@ -885,7 +883,7 @@ size_t ClusterComm::performRequests(std::vector& requests, arangodb::LogTopic const& logTopic, bool retryOnCollNotFound, bool retryOnBackendUnavailable) { - if (requests.size() == 0) { + if (requests.empty()) { return 0; } diff --git a/arangod/RestHandler/RestTransactionHandler.cpp b/arangod/RestHandler/RestTransactionHandler.cpp index fe4b94052c..2e1625d480 100644 --- a/arangod/RestHandler/RestTransactionHandler.cpp +++ b/arangod/RestHandler/RestTransactionHandler.cpp @@ -27,7 +27,6 @@ #include "ApplicationFeatures/ApplicationServer.h" #include "Basics/ReadLocker.h" #include "Basics/WriteLocker.h" -#include "Cluster/ClusterInfo.h" #include "Cluster/ServerState.h" #include "StorageEngine/EngineSelectorFeature.h" #include "Transaction/Manager.h" @@ -85,6 +84,29 @@ RestStatus RestTransactionHandler::execute() { } void RestTransactionHandler::executeGetState() { + if (_request->suffixes().empty()) { + // no transaction id given - so list all the transactions + auto context = arangodb::ExecContext::CURRENT; + std::string user; + if (context != nullptr || arangodb::ExecContext::isAuthEnabled()) { + user = context->user(); + } + + VPackBuilder builder; + builder.openObject(); + builder.add("transactions", VPackValue(VPackValueType::Array)); + + bool const fanout = ServerState::instance()->isCoordinator() && !_request->parsedValue("local", false); + transaction::Manager* mgr = transaction::ManagerFeature::manager(); + mgr->toVelocyPack(builder, _vocbase.name(), user, fanout); + + builder.close(); // array + builder.close(); // object + + generateResult(rest::ResponseCode::OK, builder.slice()); + return; + } + if (_request->suffixes().size() != 1) { generateError(rest::ResponseCode::BAD, TRI_ERROR_BAD_PARAMETER, "expecting GET /_api/transaction/"); diff --git a/arangod/Transaction/Manager.cpp b/arangod/Transaction/Manager.cpp index 78a9739ade..b1934f4484 100644 --- a/arangod/Transaction/Manager.cpp +++ b/arangod/Transaction/Manager.cpp @@ -25,6 +25,10 @@ #include "Basics/ReadLocker.h" #include "Basics/WriteLocker.h" +#include "Cluster/ClusterComm.h" +#include "Cluster/ClusterInfo.h" +#include "Cluster/ServerState.h" +#include "GeneralServer/AuthenticationFeature.h" #include "Logger/Logger.h" #include "StorageEngine/EngineSelectorFeature.h" #include "StorageEngine/StorageEngine.h" @@ -32,6 +36,7 @@ #include "Transaction/Helpers.h" #include "Transaction/Methods.h" #include "Transaction/SmartContext.h" +#include "Transaction/Status.h" #include "Utils/CollectionNameResolver.h" #include @@ -621,6 +626,25 @@ Result Manager::updateTransaction(TRI_voc_tid_t tid, return res; } +/// @brief calls the callback function for each managed transaction +void Manager::iterateManagedTrx( + std::function const& callback) const { + READ_LOCKER(allTransactionsLocker, _allTransactionsLock); + + // iterate over all active transactions + for (size_t bucket = 0; bucket < numBuckets; ++bucket) { + READ_LOCKER(locker, _transactions[bucket]._lock); + + auto& buck = _transactions[bucket]; + for (auto const& it : buck._managed) { + if (it.second.type == MetaType::Managed) { + // we only care about managed transactions here + callback(it.first, it.second); + } + } + } +} + /// @brief collect forgotten transactions bool Manager::garbageCollect(bool abortAll) { bool didWork = false; @@ -723,5 +747,85 @@ bool Manager::abortManagedTrx(std::function cb) { return !toAbort.empty(); } +void Manager::toVelocyPack(VPackBuilder& builder, + std::string const& database, + std::string const& username, + bool fanout) const { + TRI_ASSERT(!builder.isClosed()); + + if (fanout) { + TRI_ASSERT(ServerState::instance()->isCoordinator()); + auto ci = ClusterInfo::instance(); + if (ci == nullptr) { + THROW_ARANGO_EXCEPTION(TRI_ERROR_SHUTTING_DOWN); + } + + std::shared_ptr cc = ClusterComm::instance(); + if (cc == nullptr) { + THROW_ARANGO_EXCEPTION(TRI_ERROR_SHUTTING_DOWN); + } + + std::vector requests; + auto auth = AuthenticationFeature::instance(); + + for (auto const& coordinator : ci->getCurrentCoordinators()) { + if (coordinator == ServerState::instance()->getId()) { + // ourselves! + continue; + } + + auto headers = std::make_unique>(); + if (auth != nullptr && auth->isActive()) { + // when in superuser mode, username is empty + // in this case ClusterComm will add the default superuser token + if (!username.empty()) { + VPackBuilder builder; + { + VPackObjectBuilder payload{&builder}; + payload->add("preferred_username", VPackValue(username)); + } + VPackSlice slice = builder.slice(); + headers->emplace(StaticStrings::Authorization, + "bearer " + auth->tokenCache().generateJwt(slice)); + } + } + + requests.emplace_back("server:" + coordinator, rest::RequestType::GET, + "/_db/" + database + "/_api/transaction?local=true", + std::make_shared(), std::move(headers)); + } + + if (!requests.empty()) { + size_t nrGood = cc->performRequests(requests, 30.0, Logger::COMMUNICATION, false); + + if (nrGood != requests.size()) { + THROW_ARANGO_EXCEPTION(TRI_ERROR_CLUSTER_BACKEND_UNAVAILABLE); + } + for (auto const& it : requests) { + if (it.result.result && it.result.result->getHttpReturnCode() == 200) { + auto const body = it.result.result->getBodyVelocyPack(); + VPackSlice slice = body->slice(); + if (slice.isObject()) { + slice = slice.get("transactions"); + if (slice.isArray()) { + for (auto const& it : VPackArrayIterator(slice)) { + builder.add(it); + } + } + } + } + } + } + } + + // merge with local transactions + iterateManagedTrx([&builder](TRI_voc_tid_t tid, ManagedTrx const& trx) { + builder.openObject(true); + builder.add("id", VPackValue(std::to_string(tid))); + builder.add("state", VPackValue(transaction::statusString(trx.state->status()))); + builder.close(); + }); +} + } // namespace transaction } // namespace arangodb diff --git a/arangod/Transaction/Manager.h b/arangod/Transaction/Manager.h index 40a091a332..160ab00aa5 100644 --- a/arangod/Transaction/Manager.h +++ b/arangod/Transaction/Manager.h @@ -44,6 +44,11 @@ struct TransactionData { virtual ~TransactionData() = default; }; +namespace velocypack { +class Builder; +class Slice; +} + namespace transaction { class Context; struct Options; @@ -53,17 +58,41 @@ class Manager final { static constexpr size_t numBuckets = 16; static constexpr double defaultTTL = 10.0 * 60.0; // 10 minutes static constexpr double tombstoneTTL = 5.0 * 60.0; // 5 minutes - + + enum class MetaType : uint8_t { + Managed = 1, /// global single shard db transaction + StandaloneAQL = 2, /// used for a standalone transaction (AQL standalone) + Tombstone = 3 /// used to ensure we can acknowledge double commits / aborts + }; + + struct ManagedTrx { + ManagedTrx(MetaType t, TransactionState* st, double ex) + : type(t), expires(ex), state(st), finalStatus(Status::UNDEFINED), + rwlock() {} + ~ManagedTrx(); + + MetaType type; + double expires; /// expiration timestamp, if 0 it expires immediately + TransactionState* state; /// Transaction, may be nullptr + /// @brief final TRX state that is valid if this is a tombstone + /// necessary to avoid getting error on a 'diamond' commit or accidantally + /// repeated commit / abort messages + transaction::Status finalStatus; + /// cheap usage lock for *state + mutable basics::ReadWriteSpinLock rwlock; + }; + public: + typedef std::function TrxCallback; + + Manager(Manager const&) = delete; + Manager& operator=(Manager const&) = delete; + explicit Manager(bool keepData) : _keepTransactionData(keepData), _nrRunning(0), _disallowInserts(false) {} - public: - typedef std::function TrxCallback; - - public: // register a list of failed transactions void registerFailedTransactions(std::unordered_set const& failedTransactions); @@ -84,8 +113,6 @@ class Manager final { uint64_t getActiveTransactionCount(); - public: - void disallowInserts() { _disallowInserts.store(true, std::memory_order_release); } @@ -121,6 +148,14 @@ class Manager final { /// @brief abort all transactions matching bool abortManagedTrx(std::function); + + /// @brief convert the list of running transactions to a VelocyPack array + /// the array must be opened already. + /// will use database and username to fan-out the request to the other + /// coordinators in a cluster + void toVelocyPack(arangodb::velocypack::Builder& builder, + std::string const& database, + std::string const& username, bool fanout) const; private: // hashes the transaction id into a bucket @@ -130,34 +165,12 @@ class Manager final { Result updateTransaction(TRI_voc_tid_t tid, transaction::Status status, bool clearServers); + + /// @brief calls the callback function for each managed transaction + void iterateManagedTrx(std::function const&) const; private: - - enum class MetaType : uint8_t { - Managed = 1, /// global single shard db transaction - StandaloneAQL = 2, /// used for a standalone transaction (AQL standalone) - Tombstone = 3 /// used to ensure we can acknowledge double commits / aborts - }; - struct ManagedTrx { - ManagedTrx(MetaType t, TransactionState* st, double ex) - : type(t), expires(ex), state(st), finalStatus(Status::UNDEFINED), - rwlock() {} - ~ManagedTrx(); - - MetaType type; - double expires; /// expiration timestamp, if 0 it expires immediately - TransactionState* state; /// Transaction, may be nullptr - /// @brief final TRX state that is valid if this is a tombstone - /// necessary to avoid getting error on a 'diamond' commit or accidantally - /// repeated commit / abort messages - transaction::Status finalStatus; - /// cheap usage lock for *state - mutable basics::ReadWriteSpinLock rwlock; - }; - -private: - - const bool _keepTransactionData; + bool const _keepTransactionData; // a lock protecting ALL buckets in _transactions mutable basics::ReadWriteLock _allTransactionsLock; diff --git a/arangod/V8Server/v8-vocbase.cpp b/arangod/V8Server/v8-vocbase.cpp index 6d9b9f6cef..37d3570900 100644 --- a/arangod/V8Server/v8-vocbase.cpp +++ b/arangod/V8Server/v8-vocbase.cpp @@ -61,6 +61,8 @@ #include "Statistics/StatisticsFeature.h" #include "StorageEngine/EngineSelectorFeature.h" #include "StorageEngine/StorageEngine.h" +#include "Transaction/Manager.h" +#include "Transaction/ManagerFeature.h" #include "Transaction/V8Context.h" #include "Utils/Events.h" #include "Utils/ExecContext.h" @@ -158,6 +160,38 @@ static void JS_Transaction(v8::FunctionCallbackInfo const& args) { TRI_V8_TRY_CATCH_END } +/// @brief returns the list of currently running managed transactions +static void JS_Transactions(v8::FunctionCallbackInfo const& args) { + TRI_V8_TRY_CATCH_BEGIN(isolate); + v8::HandleScope scope(isolate); + + auto& vocbase = GetContextVocBase(isolate); + + // check if we have some transaction object + if (args.Length() != 0) { + TRI_V8_THROW_EXCEPTION_USAGE("TRANSACTIONS()"); + } + + VPackBuilder builder; + builder.openArray(); + + bool const fanout = ServerState::instance()->isCoordinator(); + transaction::Manager* mgr = transaction::ManagerFeature::manager(); + auto context = arangodb::ExecContext::CURRENT; + std::string user; + if (context != nullptr || arangodb::ExecContext::isAuthEnabled()) { + user = context->user(); + } + mgr->toVelocyPack(builder, vocbase.name(), user, fanout); + + builder.close(); + + v8::Handle result = TRI_VPackToV8(isolate, builder.slice()); + + TRI_V8_RETURN(result); + TRI_V8_TRY_CATCH_END +} + //////////////////////////////////////////////////////////////////////////////// /// @brief normalize UTF 16 strings //////////////////////////////////////////////////////////////////////////////// @@ -1963,6 +1997,9 @@ void TRI_InitV8VocBridge(v8::Isolate* isolate, v8::Handle context, TRI_AddGlobalFunctionVocbase(isolate, TRI_V8_ASCII_STRING(isolate, "TRANSACTION"), JS_Transaction, true); + TRI_AddGlobalFunctionVocbase(isolate, + TRI_V8_ASCII_STRING(isolate, "TRANSACTIONS"), + JS_Transactions, true); TRI_AddGlobalFunctionVocbase(isolate, TRI_V8_ASCII_STRING(isolate, diff --git a/js/client/modules/@arangodb/arango-database.js b/js/client/modules/@arangodb/arango-database.js index 651ff6cfb1..89c9baead0 100644 --- a/js/client/modules/@arangodb/arango-database.js +++ b/js/client/modules/@arangodb/arango-database.js @@ -1186,6 +1186,17 @@ ArangoDatabase.prototype._createTransaction = function (data) { return new ArangoTransaction(this, data); }; +// ////////////////////////////////////////////////////////////////////////////// +// / @brief returns the currently ongoing managed transactions +// ////////////////////////////////////////////////////////////////////////////// + +ArangoDatabase.prototype._transactions = function () { + var requestResult = this._connection.GET("/_api/transaction"); + + arangosh.checkRequestResult(requestResult); + return requestResult.transactions; +}; + // ////////////////////////////////////////////////////////////////////////////// // / @brief creates a new view // ////////////////////////////////////////////////////////////////////////////// diff --git a/js/client/modules/@arangodb/arango-transaction.js b/js/client/modules/@arangodb/arango-transaction.js index 6793c132d2..3d533c12a8 100644 --- a/js/client/modules/@arangodb/arango-transaction.js +++ b/js/client/modules/@arangodb/arango-transaction.js @@ -29,13 +29,34 @@ const internal = require('internal'); const arangosh = require('@arangodb/arangosh'); const ArangoError = require('@arangodb').ArangoError; const ArangoQueryCursor = require('@arangodb/arango-query-cursor').ArangoQueryCursor; + +function throwNotRunning() { + throw new ArangoError({ + error: true, + code: internal.errors.ERROR_TRANSACTION_INTERNAL.code, + errorNum: internal.errors.ERROR_TRANSACTION_INTERNAL.code, + errorMessage: internal.errors.ERROR_TRANSACTION_INTERNAL.message + }); +}; function ArangoTransaction (database, data) { this._id = 0; + this._done = false; this._database = database; this._dbName = database._name(); this._dbPrefix = '/_db/' + encodeURIComponent(database._name()); + if (data && typeof data === 'object' && data._id) { + this._id = data._id; + return; + } else if (typeof data === 'string') { + this._id = data; + return; + } else if (typeof data === 'number') { + this._id = String(data); + return; + } + if (!data || typeof (data) !== 'object') { throw new ArangoError({ error: true, @@ -118,6 +139,17 @@ ArangoTransaction.prototype.id = function() { return this._id; }; +ArangoTransaction.prototype._PRINT = function (context) { + let colors = require('internal').COLORS; + let useColor = context.useColor; + + context.output += '[ArangoTransaction '; + if (useColor) { context.output += colors.COLOR_NUMBER; } + context.output += this._id; + if (useColor) { context.output += colors.COLOR_RESET; } + context.output += ']'; +}; + ArangoTransaction.prototype.collection = function(col) { if (col.isArangoCollection) { return new ArangoTransactionCollection(this, col); @@ -129,18 +161,33 @@ ArangoTransaction.prototype.commit = function() { let url = this._url() + '/' + this._id; var requestResult = this._database._connection.PUT(url, ""); arangosh.checkRequestResult(requestResult); - this._id = 0; + this._done = true; + return requestResult.result; }; ArangoTransaction.prototype.abort = function() { let url = this._url() + '/' + this._id; var requestResult = this._database._connection.DELETE(url, ""); arangosh.checkRequestResult(requestResult); - this._id = 0; + this._done = true; + return requestResult.result; +}; + +ArangoTransaction.prototype.status = function() { + let url = this._url() + '/' + this._id; + var requestResult = this._database._connection.GET(url); + arangosh.checkRequestResult(requestResult); + return requestResult.result; +}; + +ArangoTransaction.prototype.running = function() { + return this._id !== 0 && !this._done; }; ArangoTransaction.prototype.query = function(query, bindVars, cursorOptions, options) { - + if (!this.running()) { + throwNotRunning(); + } if (typeof query !== 'string' || query === undefined || query === '') { throw 'need a valid query string'; } @@ -183,13 +230,8 @@ ArangoTransactionCollection.prototype.name = function() { }; ArangoTransactionCollection.prototype.document = function(id) { - if (this._transaction.id() === 0) { - throw new ArangoError({ - error: true, - code: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorNum: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorMessage: 'transaction not running' - }); + if (!this._transaction.running()) { + throwNotRunning(); } let opts = { transactionId : this._transaction.id() }; return this._collection.document(id, opts); @@ -197,13 +239,8 @@ ArangoTransactionCollection.prototype.document = function(id) { ArangoTransactionCollection.prototype.save = ArangoTransactionCollection.prototype.insert = function(data, opts) { - if (this._transaction.id() === 0) { - throw new ArangoError({ - error: true, - code: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorNum: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorMessage: 'transaction not running' - }); + if (!this._transaction.running()) { + throwNotRunning(); } opts = opts || {}; opts.transactionId = this._transaction.id(); @@ -211,13 +248,8 @@ ArangoTransactionCollection.prototype.insert = function(data, opts) { }; ArangoTransactionCollection.prototype.remove = function(id, options) { - if (this._transaction.id() === 0) { - throw new ArangoError({ - error: true, - code: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorNum: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorMessage: 'transaction not running' - }); + if (!this._transaction.running()) { + throwNotRunning(); } if (!options) { options = {}; @@ -227,13 +259,8 @@ ArangoTransactionCollection.prototype.remove = function(id, options) { }; ArangoTransactionCollection.prototype.replace = function(id, data, options) { - if (this._transaction.id() === 0) { - throw new ArangoError({ - error: true, - code: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorNum: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorMessage: 'transaction not started yet' - }); + if (!this._transaction.running()) { + throwNotRunning(); } if (!options) { options = {}; @@ -243,13 +270,8 @@ ArangoTransactionCollection.prototype.replace = function(id, data, options) { }; ArangoTransactionCollection.prototype.update = function(id, data, options) { - if (this._transaction.id() === 0) { - throw new ArangoError({ - error: true, - code: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorNum: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorMessage: 'transaction not started yet' - }); + if (!this._transaction.running()) { + throwNotRunning(); } if (!options) { options = {}; @@ -259,13 +281,8 @@ ArangoTransactionCollection.prototype.update = function(id, data, options) { }; ArangoTransactionCollection.prototype.truncate = function(opts) { - if (this._transaction.id() === 0) { - throw new ArangoError({ - error: true, - code: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorNum: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorMessage: 'transaction not started yet' - }); + if (!this._transaction.running()) { + throwNotRunning(); } opts = opts || {}; opts.transactionId = this._transaction.id(); @@ -273,13 +290,8 @@ ArangoTransactionCollection.prototype.truncate = function(opts) { }; ArangoTransactionCollection.prototype.count = function() { - if (this._transaction.id() === 0) { - throw new ArangoError({ - error: true, - code: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorNum: internal.errors.ERROR_TRANSACTION_INTERNAL.code, - errorMessage: 'transaction not started yet' - }); + if (!this._transaction.running()) { + throwNotRunning(); } const url = this._collection._baseurl('count'); @@ -288,4 +300,4 @@ ArangoTransactionCollection.prototype.count = function() { arangosh.checkRequestResult(requestResult); return requestResult.count; -}; \ No newline at end of file +}; diff --git a/tests/js/client/load-balancing/load-balancing-transactions-auth-cluster.js b/tests/js/client/load-balancing/load-balancing-transactions-auth-cluster.js new file mode 100644 index 0000000000..d53226cf3b --- /dev/null +++ b/tests/js/client/load-balancing/load-balancing-transactions-auth-cluster.js @@ -0,0 +1,280 @@ +/* jshint globalstrict:true, strict:true, maxlen: 5000 */ +/* global assertTrue, assertFalse, assertEqual, require*/ + +//////////////////////////////////////////////////////////////////////////////// +/// DISCLAIMER +/// +/// Copyright 2018 ArangoDB GmbH, Cologne, Germany +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// +/// Copyright holder is ArangoDB GmbH, Cologne, Germany +/// +/// @author Jan Steemann +/// @author Copyright 2018, ArangoDB GmbH, Cologne, Germany +//////////////////////////////////////////////////////////////////////////////// + +'use strict'; + +const jsunity = require("jsunity"); + +const base64Encode = require('internal').base64Encode; +const db = require("internal").db; +const request = require("@arangodb/request"); +const url = require('url'); +const userModule = require("@arangodb/users"); +const _ = require("lodash"); + +function getCoordinators() { + const isCoordinator = (d) => (_.toLower(d.role) === 'coordinator'); + const toEndpoint = (d) => (d.endpoint); + const endpointToURL = (endpoint) => { + if (endpoint.substr(0, 6) === 'ssl://') { + return 'https://' + endpoint.substr(6); + } + var pos = endpoint.indexOf('://'); + if (pos === -1) { + return 'http://' + endpoint; + } + return 'http' + endpoint.substr(pos); + }; + + const instanceInfo = JSON.parse(require('internal').env.INSTANCEINFO); + return instanceInfo.arangods.filter(isCoordinator) + .map(toEndpoint) + .map(endpointToURL); +} + +const servers = getCoordinators(); + +function TransactionsSuite () { + 'use strict'; + const cn = 'UnitTestsCollection'; + let coordinators = []; + const users = [ + { username: 'alice', password: 'pass1' }, + { username: 'bob', password: 'pass2' }, + ]; + + function sendRequest(auth, method, endpoint, body, usePrimary) { + let res; + const i = usePrimary ? 0 : 1; + try { + const envelope = { + headers: { + authorization: + `Basic ${base64Encode(auth.username + ':' + auth.password)}` + }, + json: true, + method, + url: `${coordinators[i]}${endpoint}` + }; + if (method !== 'GET') { + envelope.body = body; + } + res = request(envelope); + } catch(err) { + console.error(`Exception processing ${method} ${endpoint}`, err.stack); + return {}; + } + + if (typeof res.body === "string") { + if (res.body === "") { + res.body = {}; + } else { + res.body = JSON.parse(res.body); + } + } + return res; + } + + let assertInList = function (list, trx) { + assertTrue(list.filter(function(data) { return data.id === trx.id; }).length > 0, + "transaction " + trx.id + " not found in list of transactions " + JSON.stringify(trx)); + }; + + let assertNotInList = function (list, trx) { + assertTrue(list.filter(function(data) { return data.id === trx.id; }).length === 0, + "transaction " + trx.id + " not found in list of transactions " + JSON.stringify(trx)); + }; + + return { + setUp: function() { + coordinators = getCoordinators(); + if (coordinators.length < 2) { + throw new Error('Expecting at least two coordinators'); + } + + db._drop(cn); + db._create(cn); + + try { + userModule.remove(users[0].username); + userModule.remove(users[1].username); + } catch (err) {} + userModule.save(users[0].username, users[0].password); + userModule.save(users[1].username, users[1].password); + + userModule.grantDatabase(users[0].username, '_system', 'rw'); + userModule.grantDatabase(users[1].username, '_system', 'rw'); + userModule.grantCollection(users[0].username, '_system', cn, 'rw'); + userModule.grantCollection(users[1].username, '_system', cn, 'rw'); + }, + + tearDown: function() { + coordinators = []; + db._drop(cn); + userModule.remove(users[0].username); + userModule.remove(users[1].username); + }, + + testListTransactions: function() { + const obj = { collections: { write: cn } }; + + let url = "/_api/transaction"; + let result = sendRequest(users[0], 'POST', url + "/begin", obj, true); + + assertEqual(result.status, 201); + assertFalse(result.body.result.id === undefined); + + let trx1 = result.body.result; + + try { + result = sendRequest(users[0], 'GET', url, {}, true); + + assertEqual(result.status, 200); + assertInList(result.body.transactions, trx1); + + result = sendRequest(users[0], 'GET', url, {}, false); + + assertEqual(result.status, 200); + assertInList(result.body.transactions, trx1); + } finally { + sendRequest(users[0], 'DELETE', '/_api/transaction/' + encodeURIComponent(trx1.id), {}, true); + } + }, + + testListTransactions2: function() { + const obj = { collections: { write: cn } }; + + let url = "/_api/transaction"; + let trx1, trx2; + + try { + let result = sendRequest(users[0], 'POST', url + "/begin", obj, true); + assertEqual(result.status, 201); + assertFalse(result.body.result.id === undefined); + trx1 = result.body.result; + + result = sendRequest(users[0], 'POST', url + "/begin", obj, false); + assertEqual(result.status, 201); + assertFalse(result.body.result.id === undefined); + trx2 = result.body.result; + + result = sendRequest(users[0], 'GET', url, {}, true); + + assertEqual(result.status, 200); + assertInList(result.body.transactions, trx1); + assertInList(result.body.transactions, trx2); + + result = sendRequest(users[0], 'GET', url, {}, false); + + assertEqual(result.status, 200); + assertInList(result.body.transactions, trx1); + assertInList(result.body.transactions, trx2); + + // commit trx1 on different coord + result = sendRequest(users[0], 'PUT', url + "/" + encodeURIComponent(trx1.id), {}, false); + assertEqual(trx1.id, result.body.result.id); + assertEqual("committed", result.body.result.status); + + result = sendRequest(users[0], 'GET', url, {}, false); + + assertEqual(result.status, 200); + assertNotInList(result.body.transactions, trx1); + assertInList(result.body.transactions, trx2); + + // abort trx2 on different coord + result = sendRequest(users[0], 'DELETE', url + "/" + encodeURIComponent(trx2.id), {}, true); + assertEqual(trx2.id, result.body.result.id); + assertEqual("aborted", result.body.result.status); + + result = sendRequest(users[0], 'GET', url, {}, false); + + assertEqual(result.status, 200); + assertNotInList(result.body.transactions, trx1); + assertNotInList(result.body.transactions, trx2); + + } finally { + sendRequest(users[0], 'DELETE', '/_api/transaction/' + encodeURIComponent(trx1.id), {}, true); + } + }, + + testCreateAndCommitElsewhere: function() { + const obj = { collections: { write: cn } }; + + let url = "/_api/transaction"; + let result = sendRequest(users[0], 'POST', url + "/begin", obj, true); + + assertEqual(result.status, 201); + assertFalse(result.body.result.id === undefined); + + let trx1 = result.body.result; + + try { + // commit on different coord + result = sendRequest(users[0], 'PUT', url + "/" + encodeURIComponent(trx1.id), {}, false); + + assertEqual(result.status, 200); + assertEqual(trx1.id, result.body.result.id); + assertEqual("committed", result.body.result.status); + + result = sendRequest(users[0], 'GET', url, {}, true); + assertNotInList(result.body.transactions, trx1); + } finally { + sendRequest(users[0], 'DELETE', '/_api/transaction/' + encodeURIComponent(trx1.id), {}, true); + } + }, + + testCreateAndAbortElsewhere: function() { + const obj = { collections: { write: cn } }; + + let url = "/_api/transaction"; + let result = sendRequest(users[0], 'POST', url + "/begin", obj, true); + + assertEqual(result.status, 201); + assertFalse(result.body.result.id === undefined); + + let trx1 = result.body.result; + + try { + // abort on different coord + result = sendRequest(users[0], 'DELETE', '/_api/transaction/' + encodeURIComponent(trx1.id), {}, true); + + assertEqual(result.status, 200); + assertEqual(trx1.id, result.body.result.id); + assertEqual("aborted", result.body.result.status); + + result = sendRequest(users[0], 'GET', url, {}, true); + assertNotInList(result.body.transactions, trx1); + } finally { + sendRequest(users[0], 'DELETE', '/_api/transaction/' + encodeURIComponent(trx1.id), {}, true); + } + }, + + }; +} + +jsunity.run(TransactionsSuite); + +return jsunity.done(); diff --git a/tests/js/client/load-balancing/load-balancing-transactions-noauth-cluster.js b/tests/js/client/load-balancing/load-balancing-transactions-noauth-cluster.js new file mode 100644 index 0000000000..90d3c338fd --- /dev/null +++ b/tests/js/client/load-balancing/load-balancing-transactions-noauth-cluster.js @@ -0,0 +1,256 @@ +/* jshint globalstrict:true, strict:true, maxlen: 5000 */ +/* global assertTrue, assertFalse, assertEqual, require*/ + +//////////////////////////////////////////////////////////////////////////////// +/// DISCLAIMER +/// +/// Copyright 2018 ArangoDB GmbH, Cologne, Germany +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// +/// Copyright holder is ArangoDB GmbH, Cologne, Germany +/// +/// @author Jan Steemann +/// @author Copyright 2018, ArangoDB GmbH, Cologne, Germany +//////////////////////////////////////////////////////////////////////////////// + +'use strict'; + +const jsunity = require("jsunity"); + +const db = require("internal").db; +const request = require("@arangodb/request"); +const url = require('url'); +const _ = require("lodash"); + +function getCoordinators() { + const isCoordinator = (d) => (_.toLower(d.role) === 'coordinator'); + const toEndpoint = (d) => (d.endpoint); + const endpointToURL = (endpoint) => { + if (endpoint.substr(0, 6) === 'ssl://') { + return 'https://' + endpoint.substr(6); + } + var pos = endpoint.indexOf('://'); + if (pos === -1) { + return 'http://' + endpoint; + } + return 'http' + endpoint.substr(pos); + }; + + const instanceInfo = JSON.parse(require('internal').env.INSTANCEINFO); + return instanceInfo.arangods.filter(isCoordinator) + .map(toEndpoint) + .map(endpointToURL); +} + +const servers = getCoordinators(); + +function TransactionsSuite () { + 'use strict'; + const cn = 'UnitTestsCollection'; + let coordinators = []; + + function sendRequest(method, endpoint, body, usePrimary) { + let res; + const i = usePrimary ? 0 : 1; + try { + const envelope = { + json: true, + method, + url: `${coordinators[i]}${endpoint}` + }; + if (method !== 'GET') { + envelope.body = body; + } + res = request(envelope); + } catch(err) { + console.error(`Exception processing ${method} ${endpoint}`, err.stack); + return {}; + } + + if (typeof res.body === "string") { + if (res.body === "") { + res.body = {}; + } else { + res.body = JSON.parse(res.body); + } + } + return res; + } + + let assertInList = function (list, trx) { + assertTrue(list.filter(function(data) { return data.id === trx.id; }).length > 0, + "transaction " + trx.id + " not found in list of transactions " + JSON.stringify(trx)); + }; + + let assertNotInList = function (list, trx) { + assertTrue(list.filter(function(data) { return data.id === trx.id; }).length === 0, + "transaction " + trx.id + " not found in list of transactions " + JSON.stringify(trx)); + }; + + return { + setUp: function() { + coordinators = getCoordinators(); + if (coordinators.length < 2) { + throw new Error('Expecting at least two coordinators'); + } + + db._drop(cn); + db._create(cn); + }, + + tearDown: function() { + coordinators = []; + db._drop(cn); + }, + + testListTransactions: function() { + const obj = { collections: { write: cn } }; + + let url = "/_api/transaction"; + let result = sendRequest('POST', url + "/begin", obj, true); + + assertEqual(result.status, 201); + assertFalse(result.body.result.id === undefined); + + let trx1 = result.body.result; + + try { + result = sendRequest('GET', url, {}, true); + + assertEqual(result.status, 200); + assertInList(result.body.transactions, trx1); + + result = sendRequest('GET', url, {}, false); + + assertEqual(result.status, 200); + assertInList(result.body.transactions, trx1); + } finally { + sendRequest('DELETE', '/_api/transaction/' + encodeURIComponent(trx1.id), {}, true); + } + }, + + testListTransactions2: function() { + const obj = { collections: { write: cn } }; + + let url = "/_api/transaction"; + let trx1, trx2; + + try { + let result = sendRequest('POST', url + "/begin", obj, true); + assertEqual(result.status, 201); + assertFalse(result.body.result.id === undefined); + trx1 = result.body.result; + + result = sendRequest('POST', url + "/begin", obj, false); + assertEqual(result.status, 201); + assertFalse(result.body.result.id === undefined); + trx2 = result.body.result; + + result = sendRequest('GET', url, {}, true); + + assertEqual(result.status, 200); + assertInList(result.body.transactions, trx1); + assertInList(result.body.transactions, trx2); + + result = sendRequest('GET', url, {}, false); + + assertEqual(result.status, 200); + assertInList(result.body.transactions, trx1); + assertInList(result.body.transactions, trx2); + + // commit trx1 on different coord + result = sendRequest('PUT', url + "/" + encodeURIComponent(trx1.id), {}, false); + assertEqual(trx1.id, result.body.result.id); + assertEqual("committed", result.body.result.status); + + result = sendRequest('GET', url, {}, false); + + assertEqual(result.status, 200); + assertNotInList(result.body.transactions, trx1); + assertInList(result.body.transactions, trx2); + + // abort trx2 on different coord + result = sendRequest('DELETE', url + "/" + encodeURIComponent(trx2.id), {}, true); + assertEqual(trx2.id, result.body.result.id); + assertEqual("aborted", result.body.result.status); + + result = sendRequest('GET', url, {}, false); + + assertEqual(result.status, 200); + assertNotInList(result.body.transactions, trx1); + assertNotInList(result.body.transactions, trx2); + + } finally { + sendRequest('DELETE', '/_api/transaction/' + encodeURIComponent(trx1.id), {}, true); + } + }, + + testCreateAndCommitElsewhere: function() { + const obj = { collections: { write: cn } }; + + let url = "/_api/transaction"; + let result = sendRequest('POST', url + "/begin", obj, true); + + assertEqual(result.status, 201); + assertFalse(result.body.result.id === undefined); + + let trx1 = result.body.result; + + try { + // commit on different coord + result = sendRequest('PUT', url + "/" + encodeURIComponent(trx1.id), {}, false); + + assertEqual(result.status, 200); + assertEqual(trx1.id, result.body.result.id); + assertEqual("committed", result.body.result.status); + + result = sendRequest('GET', url, {}, true); + assertNotInList(result.body.transactions, trx1); + } finally { + sendRequest('DELETE', '/_api/transaction/' + encodeURIComponent(trx1.id), {}, true); + } + }, + + testCreateAndAbortElsewhere: function() { + const obj = { collections: { write: cn } }; + + let url = "/_api/transaction"; + let result = sendRequest('POST', url + "/begin", obj, true); + + assertEqual(result.status, 201); + assertFalse(result.body.result.id === undefined); + + let trx1 = result.body.result; + + try { + // abort on different coord + result = sendRequest('DELETE', '/_api/transaction/' + encodeURIComponent(trx1.id), {}, true); + + assertEqual(result.status, 200); + assertEqual(trx1.id, result.body.result.id); + assertEqual("aborted", result.body.result.status); + + result = sendRequest('GET', url, {}, true); + assertNotInList(result.body.transactions, trx1); + } finally { + sendRequest('DELETE', '/_api/transaction/' + encodeURIComponent(trx1.id), {}, true); + } + }, + + }; +} + +jsunity.run(TransactionsSuite); + +return jsunity.done(); diff --git a/tests/js/client/shell/shell-transaction.js b/tests/js/client/shell/shell-transaction.js index d916fbe865..6ee1df9f97 100644 --- a/tests/js/client/shell/shell-transaction.js +++ b/tests/js/client/shell/shell-transaction.js @@ -291,6 +291,18 @@ function transactionRevisionsSuite () { function transactionInvocationSuite () { 'use strict'; + const cn = "UnitTestsCollection"; + + let assertInList = function(list, trx) { + assertTrue(list.filter(function(data) { return data.id === trx._id; }).length > 0, + "transaction " + trx._id + " is not contained in list of transactions " + JSON.stringify(list)); + }; + + let assertNotInList = function(list, trx) { + assertFalse(list.filter(function(data) { return data.id === trx._id; }).length > 0, + "transaction " + trx._id + " is contained in list of transactions " + JSON.stringify(list)); + }; + return { // ////////////////////////////////////////////////////////////////////////////// @@ -298,6 +310,11 @@ function transactionInvocationSuite () { // ////////////////////////////////////////////////////////////////////////////// setUp: function () { + db._drop(cn); + }, + + tearDown: function () { + db._drop(cn); }, // ////////////////////////////////////////////////////////////////////////////// @@ -310,9 +327,6 @@ function transactionInvocationSuite () { null, true, false, - 0, - 1, - 'foo', { }, { }, { }, { }, { }, false, true, @@ -367,7 +381,7 @@ function transactionInvocationSuite () { assertEqual(expected, err.errorNum); } finally { if (trx) { - trx.abort(); + try { trx.abort(); } catch (err) {} } } }); @@ -404,6 +418,236 @@ function transactionInvocationSuite () { } }); }, + + // ////////////////////////////////////////////////////////////////////////////// + // / @brief test: _transactions() function + // ////////////////////////////////////////////////////////////////////////////// + + testListTransactions: function () { + db._create(cn); + let trx1, trx2, trx3; + + let obj = { + collections: { + read: [ cn ] + } + }; + + try { + // create a single trx + trx1 = db._createTransaction(obj); + + let trx = db._transactions(); + assertInList(trx, trx1); + + trx1.commit(); + // trx is committed now - list should be empty + + trx = db._transactions(); + assertNotInList(trx, trx1); + + // create two more + trx2 = db._createTransaction(obj); + + trx = db._transactions(); + assertInList(trx, trx2); + assertNotInList(trx, trx1); + + trx3 = db._createTransaction(obj); + + trx = db._transactions(); + assertInList(trx, trx2); + assertInList(trx, trx3); + assertNotInList(trx, trx1); + + trx2.commit(); + + trx = db._transactions(); + assertInList(trx, trx3); + assertNotInList(trx, trx2); + assertNotInList(trx, trx1); + + trx3.commit(); + + trx = db._transactions(); + assertNotInList(trx, trx3); + assertNotInList(trx, trx2); + assertNotInList(trx, trx1); + } finally { + if (trx1 && trx1._id) { + try { trx1.abort(); } catch (err) {} + } + if (trx2 && trx2._id) { + try { trx2.abort(); } catch (err) {} + } + if (trx3 && trx3._id) { + try { trx3.abort(); } catch (err) {} + } + } + }, + + // ////////////////////////////////////////////////////////////////////////////// + // / @brief test: _createTransaction() function + // ////////////////////////////////////////////////////////////////////////////// + + testcreateTransaction: function () { + let values = [ "aaaaaaaaaaaaaaaaaaaaaaaa", "der-fuchs-der-fuchs", 99999999999999999999999, 1 ]; + + values.forEach(function(data) { + try { + let trx = db._createTransaction(data); + trx.status(); + fail(); + } catch (err) { + assertTrue(err.errorNum === internal.errors.ERROR_BAD_PARAMETER.code || + err.errorNum === internal.errors.ERROR_TRANSACTION_NOT_FOUND.code); + } + }); + }, + + // ////////////////////////////////////////////////////////////////////////////// + // / @brief test: abort + // ////////////////////////////////////////////////////////////////////////////// + + testAbortTransaction: function () { + db._create(cn); + let cleanup = []; + + let obj = { + collections: { + read: [ cn ] + } + }; + + try { + let trx1 = db._createTransaction(obj); + cleanup.push(trx1); + + assertInList(db._transactions(), trx1); + + // abort using trx object + let result = db._createTransaction(trx1).abort(); + assertEqual(trx1._id, result.id); + assertEqual("aborted", result.status); + + assertNotInList(db._transactions(), trx1); + + let trx2 = db._createTransaction(obj); + cleanup.push(trx2); + + assertInList(db._transactions(), trx2); + + // abort by id + result = db._createTransaction(trx2._id).abort(); + assertEqual(trx2._id, result.id); + assertEqual("aborted", result.status); + + assertNotInList(db._transactions(), trx1); + assertNotInList(db._transactions(), trx2); + + } finally { + cleanup.forEach(function(trx) { + try { trx.abort(); } catch (err) {} + }); + } + }, + + // ////////////////////////////////////////////////////////////////////////////// + // / @brief test: commit + // ////////////////////////////////////////////////////////////////////////////// + + testCommitTransaction: function () { + db._create(cn); + let cleanup = []; + + let obj = { + collections: { + read: [ cn ] + } + }; + + try { + let trx1 = db._createTransaction(obj); + cleanup.push(trx1); + + assertInList(db._transactions(), trx1); + + // commit using trx object + let result = db._createTransaction(trx1).commit(); + assertEqual(trx1._id, result.id); + assertEqual("committed", result.status); + + assertNotInList(db._transactions(), trx1); + + let trx2 = db._createTransaction(obj); + cleanup.push(trx2); + + assertInList(db._transactions(), trx2); + + // commit by id + result = db._createTransaction(trx2._id).commit(); + assertEqual(trx2._id, result.id); + assertEqual("committed", result.status); + + assertNotInList(db._transactions(), trx1); + assertNotInList(db._transactions(), trx2); + } finally { + cleanup.forEach(function(trx) { + try { trx.abort(); } catch (err) {} + }); + } + }, + + // ////////////////////////////////////////////////////////////////////////////// + // / @brief test: status + // ////////////////////////////////////////////////////////////////////////////// + + testStatusTransaction: function () { + db._create(cn); + let cleanup = []; + + let obj = { + collections: { + read: [ cn ] + } + }; + + try { + let trx1 = db._createTransaction(obj); + cleanup.push(trx1); + + let result = trx1.status(); + assertEqual(trx1._id, result.id); + assertEqual("running", result.status); + + result = db._createTransaction(trx1._id).commit(); + assertEqual(trx1._id, result.id); + assertEqual("committed", result.status); + + result = trx1.status(); + assertEqual(trx1._id, result.id); + assertEqual("committed", result.status); + + let trx2 = db._createTransaction(obj); + cleanup.push(trx2); + + result = trx2.status(); + assertEqual(trx2._id, result.id); + assertEqual("running", result.status); + + result = db._createTransaction(trx2._id).abort(); + assertEqual(trx2._id, result.id); + assertEqual("aborted", result.status); + + result = trx2.status(); + assertEqual(trx2._id, result.id); + assertEqual("aborted", result.status); + } finally { + cleanup.forEach(function(trx) { + try { trx.abort(); } catch (err) {} + }); + } + } }; }