1
0
Fork 0

Bug fix 3.5/add db list transactions function (#9575)

* added missing function db._transactions(), and equivalent REST API route
GET /_api/transaction

* updated

* use performRequests

* updated CHANGELOG

* added tests, fixed segfault

* use throwNotRunning

* use read transactions for testing (mmfiles blocks with multiple write
transactions on the same collection)
This commit is contained in:
Jan 2019-07-26 15:34:57 +02:00 committed by KVS85
parent 78d12ee7b0
commit 9777f53878
12 changed files with 1123 additions and 96 deletions

View File

@ -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

View File

@ -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

View File

@ -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<ClusterCommRequest>& requests,
@ -885,7 +883,7 @@ size_t ClusterComm::performRequests(std::vector<ClusterCommRequest>& requests,
arangodb::LogTopic const& logTopic,
bool retryOnCollNotFound,
bool retryOnBackendUnavailable) {
if (requests.size() == 0) {
if (requests.empty()) {
return 0;
}

View File

@ -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/<transaction-ID>");

View File

@ -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 <velocypack/Iterator.h>
@ -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<void(TRI_voc_tid_t, ManagedTrx const&)> 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<bool(TransactionState const&)> 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<ClusterComm> cc = ClusterComm::instance();
if (cc == nullptr) {
THROW_ARANGO_EXCEPTION(TRI_ERROR_SHUTTING_DOWN);
}
std::vector<ClusterCommRequest> requests;
auto auth = AuthenticationFeature::instance();
for (auto const& coordinator : ci->getCurrentCoordinators()) {
if (coordinator == ServerState::instance()->getId()) {
// ourselves!
continue;
}
auto headers = std::make_unique<std::unordered_map<std::string, std::string>>();
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::string>(), 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

View File

@ -44,6 +44,11 @@ struct TransactionData {
virtual ~TransactionData() = default;
};
namespace velocypack {
class Builder;
class Slice;
}
namespace transaction {
class Context;
struct Options;
@ -54,16 +59,40 @@ class Manager final {
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<void(TRI_voc_tid_t, TransactionData const*)> TrxCallback;
Manager(Manager const&) = delete;
Manager& operator=(Manager const&) = delete;
explicit Manager(bool keepData)
: _keepTransactionData(keepData),
_nrRunning(0),
_disallowInserts(false) {}
public:
typedef std::function<void(TRI_voc_tid_t, TransactionData const*)> TrxCallback;
public:
// register a list of failed transactions
void registerFailedTransactions(std::unordered_set<TRI_voc_tid_t> const& failedTransactions);
@ -84,8 +113,6 @@ class Manager final {
uint64_t getActiveTransactionCount();
public:
void disallowInserts() {
_disallowInserts.store(true, std::memory_order_release);
}
@ -122,6 +149,14 @@ class Manager final {
/// @brief abort all transactions matching
bool abortManagedTrx(std::function<bool(TransactionState const&)>);
/// @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
inline size_t getBucket(TRI_voc_tid_t tid) const {
@ -131,33 +166,11 @@ 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<void(TRI_voc_tid_t, ManagedTrx const&)> 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;

View File

@ -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<v8::Value> const& args) {
TRI_V8_TRY_CATCH_END
}
/// @brief returns the list of currently running managed transactions
static void JS_Transactions(v8::FunctionCallbackInfo<v8::Value> 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<v8::Value> 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<v8::Context> 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,

View File

@ -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
// //////////////////////////////////////////////////////////////////////////////

View File

@ -30,12 +30,33 @@ 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');

View File

@ -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();

View File

@ -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();

View File

@ -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) {}
}
}
});
@ -405,6 +419,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) {}
});
}
}
};
}