|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- 'use strict';
-
- const retrieveBSON = require('./connection/utils').retrieveBSON;
- const EventEmitter = require('events');
- const BSON = retrieveBSON();
- const Binary = BSON.Binary;
- const uuidV4 = require('./utils').uuidV4;
- const MongoError = require('./error').MongoError;
- const isRetryableError = require('././error').isRetryableError;
- const MongoNetworkError = require('./error').MongoNetworkError;
- const MongoWriteConcernError = require('./error').MongoWriteConcernError;
- const Transaction = require('./transactions').Transaction;
- const TxnState = require('./transactions').TxnState;
-
- function assertAlive(session, callback) {
- if (session.serverSession == null) {
- const error = new MongoError('Cannot use a session that has ended');
- if (typeof callback === 'function') {
- callback(error, null);
- return false;
- }
-
- throw error;
- }
-
- return true;
- }
-
-
-
-
-
-
- class ClientSession extends EventEmitter {
-
-
- constructor(topology, sessionPool, options, clientOptions) {
- super();
-
- if (topology == null) {
- throw new Error('ClientSession requires a topology');
- }
-
- if (sessionPool == null || !(sessionPool instanceof ServerSessionPool)) {
- throw new Error('ClientSession requires a ServerSessionPool');
- }
-
- options = options || {};
- this.topology = topology;
- this.sessionPool = sessionPool;
- this.hasEnded = false;
- this.serverSession = sessionPool.acquire();
- this.clientOptions = clientOptions;
-
- this.supports = {
- causalConsistency:
- typeof options.causalConsistency !== 'undefined' ? options.causalConsistency : true
- };
-
- options = options || {};
- if (typeof options.initialClusterTime !== 'undefined') {
- this.clusterTime = options.initialClusterTime;
- } else {
- this.clusterTime = null;
- }
-
- this.operationTime = null;
- this.explicit = !!options.explicit;
- this.owner = options.owner;
- this.defaultTransactionOptions = Object.assign({}, options.defaultTransactionOptions);
- this.transaction = new Transaction();
- }
-
-
-
- get id() {
- return this.serverSession.id;
- }
-
-
-
- endSession(options, callback) {
- if (typeof options === 'function') (callback = options), (options = {});
- options = options || {};
-
- if (this.hasEnded) {
- if (typeof callback === 'function') callback(null, null);
- return;
- }
-
- if (this.serverSession && this.inTransaction()) {
- this.abortTransaction();
- }
-
-
- this.hasEnded = true;
- this.emit('ended', this);
-
-
- this.sessionPool.release(this.serverSession);
-
-
- if (typeof callback === 'function') callback(null, null);
- }
-
-
-
- advanceOperationTime(operationTime) {
- if (this.operationTime == null) {
- this.operationTime = operationTime;
- return;
- }
-
- if (operationTime.greaterThan(this.operationTime)) {
- this.operationTime = operationTime;
- }
- }
-
-
-
- equals(session) {
- if (!(session instanceof ClientSession)) {
- return false;
- }
-
- return this.id.id.buffer.equals(session.id.id.buffer);
- }
-
-
-
- incrementTransactionNumber() {
- this.serverSession.txnNumber++;
- }
-
-
-
- inTransaction() {
- return this.transaction.isActive;
- }
-
-
-
- startTransaction(options) {
- assertAlive(this);
- if (this.inTransaction()) {
- throw new MongoError('Transaction already in progress');
- }
-
-
- this.incrementTransactionNumber();
-
-
- this.transaction = new Transaction(
- Object.assign({}, this.clientOptions, options || this.defaultTransactionOptions)
- );
-
- this.transaction.transition(TxnState.STARTING_TRANSACTION);
- }
-
-
-
- commitTransaction(callback) {
- if (typeof callback === 'function') {
- endTransaction(this, 'commitTransaction', callback);
- return;
- }
-
- return new Promise((resolve, reject) => {
- endTransaction(
- this,
- 'commitTransaction',
- (err, reply) => (err ? reject(err) : resolve(reply))
- );
- });
- }
-
-
-
- abortTransaction(callback) {
- if (typeof callback === 'function') {
- endTransaction(this, 'abortTransaction', callback);
- return;
- }
-
- return new Promise((resolve, reject) => {
- endTransaction(
- this,
- 'abortTransaction',
- (err, reply) => (err ? reject(err) : resolve(reply))
- );
- });
- }
-
-
-
- toBSON() {
- throw new Error('ClientSession cannot be serialized to BSON.');
- }
- }
-
- function endTransaction(session, commandName, callback) {
- if (!assertAlive(session, callback)) {
-
- return;
- }
-
-
- let txnState = session.transaction.state;
-
- if (txnState === TxnState.NO_TRANSACTION) {
- callback(new MongoError('No transaction started'));
- return;
- }
-
- if (commandName === 'commitTransaction') {
- if (
- txnState === TxnState.STARTING_TRANSACTION ||
- txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
- ) {
-
- session.transaction.transition(TxnState.TRANSACTION_COMMITTED_EMPTY);
- callback(null, null);
- return;
- }
-
- if (txnState === TxnState.TRANSACTION_ABORTED) {
- callback(new MongoError('Cannot call commitTransaction after calling abortTransaction'));
- return;
- }
- } else {
- if (txnState === TxnState.STARTING_TRANSACTION) {
-
- session.transaction.transition(TxnState.TRANSACTION_ABORTED);
- callback(null, null);
- return;
- }
-
- if (txnState === TxnState.TRANSACTION_ABORTED) {
- callback(new MongoError('Cannot call abortTransaction twice'));
- return;
- }
-
- if (
- txnState === TxnState.TRANSACTION_COMMITTED ||
- txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
- ) {
- callback(new MongoError('Cannot call abortTransaction after calling commitTransaction'));
- return;
- }
- }
-
-
- const command = { [commandName]: 1 };
-
-
- if (session.transaction.options.writeConcern) {
- Object.assign(command, { writeConcern: session.transaction.options.writeConcern });
- } else if (session.clientOptions && session.clientOptions.w) {
- Object.assign(command, { writeConcern: { w: session.clientOptions.w } });
- }
-
- function commandHandler(e, r) {
- if (commandName === 'commitTransaction') {
- session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
-
- if (
- e &&
- (e instanceof MongoNetworkError ||
- e instanceof MongoWriteConcernError ||
- isRetryableError(e))
- ) {
- if (e.errorLabels) {
- const idx = e.errorLabels.indexOf('TransientTransactionError');
- if (idx !== -1) {
- e.errorLabels.splice(idx, 1);
- }
- } else {
- e.errorLabels = [];
- }
-
- e.errorLabels.push('UnknownTransactionCommitResult');
- }
- } else {
- session.transaction.transition(TxnState.TRANSACTION_ABORTED);
- }
-
- callback(e, r);
- }
-
-
- function transactionError(err) {
- return commandName === 'commitTransaction' ? err : null;
- }
-
-
- session.topology.command('admin.$cmd', command, { session }, (err, reply) => {
- if (err && isRetryableError(err)) {
- return session.topology.command('admin.$cmd', command, { session }, (_err, _reply) =>
- commandHandler(transactionError(_err), _reply)
- );
- }
-
- commandHandler(transactionError(err), reply);
- });
- }
-
-
- class ServerSession {
- constructor() {
- this.id = { id: new Binary(uuidV4(), Binary.SUBTYPE_UUID) };
- this.lastUse = Date.now();
- this.txnNumber = 0;
- }
-
-
-
- hasTimedOut(sessionTimeoutMinutes) {
-
-
- const idleTimeMinutes = Math.round(
- (((Date.now() - this.lastUse) % 86400000) % 3600000) / 60000
- );
-
- return idleTimeMinutes > sessionTimeoutMinutes - 1;
- }
- }
-
-
- class ServerSessionPool {
- constructor(topology) {
- if (topology == null) {
- throw new Error('ServerSessionPool requires a topology');
- }
-
- this.topology = topology;
- this.sessions = [];
- }
-
-
-
- endAllPooledSessions() {
- if (this.sessions.length) {
- this.topology.endSessions(this.sessions.map(session => session.id));
- this.sessions = [];
- }
- }
-
-
-
- acquire() {
- const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
- while (this.sessions.length) {
- const session = this.sessions.shift();
- if (!session.hasTimedOut(sessionTimeoutMinutes)) {
- return session;
- }
- }
-
- return new ServerSession();
- }
-
-
-
- release(session) {
- const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
- while (this.sessions.length) {
- const session = this.sessions[this.sessions.length - 1];
- if (session.hasTimedOut(sessionTimeoutMinutes)) {
- this.sessions.pop();
- } else {
- break;
- }
- }
-
- if (!session.hasTimedOut(sessionTimeoutMinutes)) {
- this.sessions.unshift(session);
- }
- }
- }
-
- module.exports = {
- ClientSession,
- ServerSession,
- ServerSessionPool,
- TxnState
- };
|