mirror of https://gitee.com/bigwinds/arangodb
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:
parent
78d12ee7b0
commit
9777f53878
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
// //////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
|
@ -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();
|
|
@ -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) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue