From 0d72c1b2715233d323aeb0fa6f8f3608aa0799ef Mon Sep 17 00:00:00 2001 From: Sharad Chandran R Date: Mon, 15 Jul 2024 13:03:52 +0530 Subject: [PATCH] Two Phase Commit support in Thin mode --- doc/src/api_manual/connection.rst | 35 ----- doc/src/release_notes.rst | 2 + doc/src/user_guide/appendix_a.rst | 2 +- doc/src/user_guide/two_phase_commit.rst | 5 - lib/connection.js | 33 ++++- lib/errors.js | 20 +++ lib/thin/connection.js | 121 ++++++++++++++- lib/thin/protocol/constants.js | 20 +++ lib/thin/protocol/messages/index.js | 6 +- .../messages/transactionChangeState.js | 99 +++++++++++++ .../protocol/messages/transactionSwitch.js | 139 ++++++++++++++++++ lib/util.js | 31 ++++ test/tpc.js | 57 +++++-- 13 files changed, 509 insertions(+), 61 deletions(-) create mode 100644 lib/thin/protocol/messages/transactionChangeState.js create mode 100644 lib/thin/protocol/messages/transactionSwitch.js diff --git a/doc/src/api_manual/connection.rst b/doc/src/api_manual/connection.rst index 98291a5b..2b94aef2 100644 --- a/doc/src/api_manual/connection.rst +++ b/doc/src/api_manual/connection.rst @@ -315,11 +315,6 @@ The properties of a *Connection* object are listed below. This read/write attribute is a string that specifies the internal name that is used by the connection when logging two-phase commit transactions. - .. note:: - - This property can only be used in the node-oracledb Thick mode. See - :ref:`enablingthick`. - .. attribute:: connection.tpcExternalName .. versionadded:: 5.3 @@ -327,11 +322,6 @@ The properties of a *Connection* object are listed below. This read/write attribute is a string that specifies the external name that is used by the connection when logging two-phase commit transactions. - .. note:: - - This property can only be used in the node-oracledb Thick mode. See - :ref:`enablingthick`. - .. attribute:: connection.transactionInProgress .. versionadded:: 6.3 @@ -2343,11 +2333,6 @@ Connection Methods See :ref:`Two-Phase Commits (TPC) `. - .. note:: - - This method is only supported in node-oracledb Thick mode. See - :ref:`enablingthick`. - The parameters of the ``connection.tpcBegin()`` method are: .. _tpcbegin: @@ -2427,11 +2412,6 @@ Connection Methods ignored and ``tpcCommit()`` has the same behavior as a regular ``connection.commit()`` call. - .. note:: - - This method is only supported in node-oracledb Thick mode. See - :ref:`enablingthick`. - Note: When using an external transaction manager with two-phase commits, :attr:`autocommitting ` should be disabled. @@ -2497,11 +2477,6 @@ Connection Methods If ``xid`` is not passed, the transaction identifier used by the previous ``connection.tpcBegin()`` call is used. - .. note:: - - This method is only supported in node-oracledb Thick mode. See - :ref:`enablingthick`. - The parameters of the ``connection.tpcEnd()`` method are: .. _tpcend: @@ -2623,11 +2598,6 @@ Connection Methods Returns a boolean indicating the transaction requires a commit. - .. note:: - - This method is only supported in node-oracledb Thick mode. See - :ref:`enablingthick`. - After calling this function, no further activity should take place on this connection until either :meth:`connection.tpcCommit()` or @@ -2700,11 +2670,6 @@ Connection Methods (XIDs) suitable for use with :meth:`connection.tpcCommit()` or :meth:`connection.tpcRollback()`. - .. note:: - - This method is only supported in node-oracledb Thick mode. See - :ref:`enablingthick`. - This function is a convenience wrapper that queries the view ``DBA_PENDING_TRANSACTIONS``. It requires SELECT privilege on that view. diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index cbb4c787..37989ec6 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -43,6 +43,8 @@ Thin Mode Changes provided for `Issue #1565 `__. +#) Added :ref:`Two-Phase Commits ` support. + Thick Mode Changes +++++++++++++++++++ diff --git a/doc/src/user_guide/appendix_a.rst b/doc/src/user_guide/appendix_a.rst index 8ba712f3..b5d0503a 100644 --- a/doc/src/user_guide/appendix_a.rst +++ b/doc/src/user_guide/appendix_a.rst @@ -243,7 +243,7 @@ node-oracledb Thin and Thick modes. For more details see :ref:`modediff`. - No - Yes * - Two-phase Commit (TPC) (see :ref:`twopc`) - - No + - Yes - Yes * - REF CURSORs (see :ref:`refcursors`) - Yes diff --git a/doc/src/user_guide/two_phase_commit.rst b/doc/src/user_guide/two_phase_commit.rst index b13930b4..0bed0567 100644 --- a/doc/src/user_guide/two_phase_commit.rst +++ b/doc/src/user_guide/two_phase_commit.rst @@ -9,11 +9,6 @@ support distributed transactions. See `Two-Phase Commit Mechanism `__ in the Oracle Database documentation. -.. note:: - - In this release, TPC is only supported in node-oracledb Thick mode. See - :ref:`enablingthick`. - Distributed transaction protocols attempt to keep multiple data sources consistent with one another by ensuring updates to the data sources participating in a distributed transaction are all performed, or none of diff --git a/lib/connection.js b/lib/connection.js index c6be7c63..f0e755eb 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1412,7 +1412,7 @@ class Connection extends EventEmitter { async tpcBegin(xid, flag, timeout) { errors.assertArgCount(arguments, 1, 3); errors.assertParamValue(nodbUtil.isXid(xid), 1); - + const normalizedXid = nodbUtil.normalizeXid(xid); if (arguments.length < 3) { timeout = 60; // seconds } else { @@ -1423,9 +1423,14 @@ class Connection extends EventEmitter { flag = constants.TPC_BEGIN_NEW; } else { errors.assertParamValue(typeof flag === 'number', 2); + const options = [constants.TPC_BEGIN_NEW, constants.TPC_BEGIN_JOIN, + constants.TPC_BEGIN_RESUME, constants.TPC_BEGIN_PROMOTE]; + if (options.indexOf(flag) < 0) { + errors.throwErr(errors.ERR_INVALID_TPC_BEGIN_FLAGS); + } } errors.assert(this._impl, errors.ERR_INVALID_CONNECTION); - await this._impl.tpcBegin(xid, flag, timeout); + await this._impl.tpcBegin(normalizedXid, flag, timeout); } //--------------------------------------------------------------------------- @@ -1441,11 +1446,13 @@ class Connection extends EventEmitter { } else { errors.assertParamValue(typeof onePhase === 'boolean', 2); } + let normalizedXid; if (arguments.length >= 1) { errors.assertParamValue(nodbUtil.isXid(xid), 1); + normalizedXid = nodbUtil.normalizeXid(xid); } errors.assert(this._impl, errors.ERR_INVALID_CONNECTION); - await this._impl.tpcCommit(xid, onePhase); + await this._impl.tpcCommit(normalizedXid, onePhase); } //--------------------------------------------------------------------------- @@ -1460,14 +1467,19 @@ class Connection extends EventEmitter { flag = constants.TPC_END_NORMAL; } else { errors.assertParamValue(typeof flag === 'number', 2); + const options = [constants.TPC_END_NORMAL, constants.TPC_END_SUSPEND]; + if (!options.includes(flag)) { + errors.throwErr(errors.ERR_INVALID_TPC_END_FLAGS); + } } - + let normalizedXid; if (arguments.length >= 1) { errors.assertParamValue(nodbUtil.isXid(xid), 1); + normalizedXid = nodbUtil.normalizeXid(xid); } errors.assert(this._impl, errors.ERR_INVALID_CONNECTION); - await this._impl.tpcEnd(xid, flag); + await this._impl.tpcEnd(normalizedXid, flag); } //--------------------------------------------------------------------------- @@ -1479,9 +1491,10 @@ class Connection extends EventEmitter { async tpcForget(xid) { errors.assertArgCount(arguments, 1, 1); errors.assertParamValue(nodbUtil.isXid(xid), 1); + const normalizedXid = nodbUtil.normalizeXid(xid); errors.assert(this._impl, errors.ERR_INVALID_CONNECTION); - await this._impl.tpcForget(xid); + await this._impl.tpcForget(normalizedXid); } //--------------------------------------------------------------------------- @@ -1491,12 +1504,14 @@ class Connection extends EventEmitter { //--------------------------------------------------------------------------- async tpcPrepare(xid) { errors.assertArgCount(arguments, 0, 1); + let normalizedXid; if (arguments.length >= 1) { errors.assertParamValue(nodbUtil.isXid(xid), 1); + normalizedXid = nodbUtil.normalizeXid(xid); } errors.assert(this._impl, errors.ERR_INVALID_CONNECTION); - return await this._impl.tpcPrepare(xid); + return await this._impl.tpcPrepare(normalizedXid); } //--------------------------------------------------------------------------- @@ -1541,12 +1556,14 @@ class Connection extends EventEmitter { //--------------------------------------------------------------------------- async tpcRollback(xid) { errors.assertArgCount(arguments, 0, 1); + let normalizedXid; if (arguments.length == 1) { errors.assertParamValue(nodbUtil.isXid(xid), 1); + normalizedXid = nodbUtil.normalizeXid(xid); } errors.assert(this._impl, errors.ERR_INVALID_CONNECTION); - await this._impl.tpcRollback(xid); + await this._impl.tpcRollback(normalizedXid); } //--------------------------------------------------------------------------- diff --git a/lib/errors.js b/lib/errors.js index ce26532a..67c470f6 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -156,6 +156,11 @@ const ERR_VECTOR_VERSION_NOT_SUPPORTED = 145; const ERR_OBJECT_IS_NOT_A_COLLECTION = 146; const ERR_CURSOR_HAS_BEEN_CLOSED = 147; const ERR_DML_RETURNING_DUP_BINDS = 149; +const ERR_INVALID_TPC_BEGIN_FLAGS = 150; +const ERR_INVALID_TPC_END_FLAGS = 151; +const ERR_UNKNOWN_TRANSACTION_STATE = 152; +const ERR_INVALID_TRANSACTION_SIZE = 153; +const ERR_INVALID_BRANCH_SIZE = 154; // Oracle Net layer errors start from 500 const ERR_CONNECTION_CLOSED = 500; @@ -441,6 +446,16 @@ messages.set(ERR_CURSOR_HAS_BEEN_CLOSED, // NJS-147 'cursor has been closed by the database'); messages.set(ERR_DML_RETURNING_DUP_BINDS, // NJS-149 'the bind variable placeholder "%s" cannot be used both before and after the RETURNING clause in a DML RETURNING statement'); +messages.set(ERR_INVALID_TPC_BEGIN_FLAGS, // NJS-150 + 'invalid flags for tpcBegin() in Two Phase Commit'); +messages.set(ERR_INVALID_TPC_END_FLAGS, // NJS-151 + 'invalid flags for tpcEnd() in Two Phase Commit'); +messages.set(ERR_UNKNOWN_TRANSACTION_STATE, // NJS-152 + 'internal error: unknown transaction state {state} in Two Phase Commit'); +messages.set(ERR_INVALID_TRANSACTION_SIZE, // NJS-153 + 'size of the transaction ID is %d and cannot exceed 64'); +messages.set(ERR_INVALID_BRANCH_SIZE, // NJS-154 + 'size of the branch ID is %d and cannot exceed 64'); // Oracle Net layer errors @@ -868,6 +883,11 @@ module.exports = { ERR_OBJECT_IS_NOT_A_COLLECTION, ERR_CURSOR_HAS_BEEN_CLOSED, ERR_DML_RETURNING_DUP_BINDS, + ERR_INVALID_TPC_BEGIN_FLAGS, + ERR_INVALID_TPC_END_FLAGS, + ERR_UNKNOWN_TRANSACTION_STATE, + ERR_INVALID_TRANSACTION_SIZE, + ERR_INVALID_BRANCH_SIZE, ERR_CONNECTION_CLOSED_CODE: `${ERR_PREFIX}-${ERR_CONNECTION_CLOSED}`, WRN_COMPILATION_CREATE, assert, diff --git a/lib/thin/connection.js b/lib/thin/connection.js index 25066963..6d9d4f05 100644 --- a/lib/thin/connection.js +++ b/lib/thin/connection.js @@ -60,7 +60,13 @@ class ThinConnectionImpl extends ConnectionImpl { async close() { try { if (this._protocol.txnInProgress) { - await this.rollback(); + if (this.tpcContext) { + const message = this.createTpcRollbackMessage(); + await this._protocol._processMessage(message); + } else { + await this.rollback(); + } + this.tpcContext = null; } if (this._drcpEnabled) { await this._sessRelease(); @@ -730,6 +736,7 @@ class ThinConnectionImpl extends ConnectionImpl { this.serviceName = ''; this.remoteAddress = ''; this.comboKey = null; // used in changePassword API + this.tpcContext = null; this.nscon = new nsi(); finalizationRegistry.register(this, this.nscon); @@ -1045,6 +1052,14 @@ class ThinConnectionImpl extends ConnectionImpl { this._dbOp = dbOp; } + setExternalName(value) { + this.externalName = value; + } + + setInternalName(value) { + this.internalName = value; + } + setClientInfo(clientInfo) { this._clientInfoModified = true; this._clientInfo = clientInfo; @@ -1083,6 +1098,102 @@ class ThinConnectionImpl extends ConnectionImpl { return lobImpl; } + // Check the state returned by the tpcCommit() call. + checkTpcCommitState(state, onePhase) { + if ((onePhase && state !== constants.TNS_TPC_TXN_STATE_READ_ONLY + && state !== constants.TNS_TPC_TXN_STATE_COMMITTED) || + (!onePhase && state !== constants.TNS_TPC_TXN_STATE_FORGOTTEN)) { + errors.throwErr(errors.ERR_UNKNOWN_TRANSACTION_STATE, state); + } + } + + // Creates a two-phase commit message suitable for committing a transaction. + createTpcCommitMessage(xid, onePhase) { + const message = new messages.TransactionChangeStateMessage(this); + message.operation = constants.TNS_TPC_TXN_COMMIT; + message.state = (onePhase == 0) ? constants.TNS_TPC_TXN_STATE_COMMITTED : + constants.TNS_TPC_TXN_STATE_READ_ONLY; + message.xid = xid; + message.context = this.tpcContext; + return message; + } + + // Creates a two-phase commit rollback message suitable for use in both + // the close() method and explicitly by the user. + createTpcRollbackMessage(xid = null) { + const message = new messages.TransactionChangeStateMessage(this); + message.operation = constants.TNS_TPC_TXN_ABORT; + message.state = constants.TNS_TPC_TXN_STATE_ABORTED; + message.xid = xid; + message.context = this.tpcContext; + return message; + } + + //--------------------------------------------------------------------------- + // tpcBegin() + //--------------------------------------------------------------------------- + async tpcBegin(xid, flags, timeout) { + const message = new messages.TransactionSwitchMessage(this); + message.operation = constants.TNS_TPC_TXN_START; + message.xid = xid; + message.flags = flags; + message.timeout = timeout; + await this._protocol._processMessage(message); + this.tpcContext = message.context; + } + + //--------------------------------------------------------------------------- + // tpcCommit() + //--------------------------------------------------------------------------- + async tpcCommit(xid, onePhase) { + const message = this.createTpcCommitMessage(xid, onePhase); + await this._protocol._processMessage(message); + this.checkTpcCommitState(message.state, onePhase); + } + + //--------------------------------------------------------------------------- + // tpcEnd() + //--------------------------------------------------------------------------- + async tpcEnd(xid, flags) { + const message = new messages.TransactionSwitchMessage(this); + message.operation = constants.TNS_TPC_TXN_DETACH; + message.xid = xid; + message.context = this.tpcContext; + message.flags = flags; + await this._protocol._processMessage(message); + this.tpcContext = null; + } + + //--------------------------------------------------------------------------- + // tpcPrepare() + //--------------------------------------------------------------------------- + async tpcPrepare(xid) { + const message = new messages.TransactionChangeStateMessage(this); + message.operation = constants.TNS_TPC_TXN_PREPARE; + message.xid = xid; + message.context = this.tpcContext; + await this._protocol._processMessage(message); + if (message.state === constants.TNS_TPC_TXN_STATE_REQUIRES_COMMIT) { + return true; + } else if (message.state === constants.TNS_TPC_TXN_STATE_READ_ONLY) { + return false; + } + + errors.throwErr(errors.ERR_UNKNOWN_TRANSACTION_STATE, message.state); + } + + //--------------------------------------------------------------------------- + // tpcRollback() + //--------------------------------------------------------------------------- + async tpcRollback(xid) { + const message = this.createTpcRollbackMessage(xid); + await this._protocol._processMessage(message); + if (message.state !== constants.TNS_TPC_TXN_STATE_ABORTED) { + errors.throwErr(errors.ERR_UNKNOWN_TRANSACTION_STATE, message.state); + } + } + + //--------------------------------------------------------------------------- // Returns the statement cache size for the statement cache maintained by // the connection object @@ -1106,6 +1217,10 @@ class ThinConnectionImpl extends ConnectionImpl { return ''; } + getExternalName() { + return this.externalName; + } + //--------------------------------------------------------------------------- // Returns the Oracle Database instance name associated with the connection. //--------------------------------------------------------------------------- @@ -1113,6 +1228,10 @@ class ThinConnectionImpl extends ConnectionImpl { return this.instanceName; } + getInternalName() { + return this.internalName; + } + //--------------------------------------------------------------------------- // Returns the Oracle Database domain name associated with the connection. //--------------------------------------------------------------------------- diff --git a/lib/thin/protocol/constants.js b/lib/thin/protocol/constants.js index 70855853..8574bd3e 100644 --- a/lib/thin/protocol/constants.js +++ b/lib/thin/protocol/constants.js @@ -548,6 +548,8 @@ module.exports = { TNS_FUNC_SESSION_RELEASE: 163, TNS_FUNC_SESSION_STATE: 176, // piggyback fn TNS_FUNC_CANCEL_ALL: 120, // piggyback fn + TNS_FUNC_TPC_TXN_SWITCH: 103, + TNS_FUNC_TPC_TXN_CHANGE_STATE: 104, // character sets and encodings TNS_CHARSET_UTF8: 873, @@ -660,6 +662,24 @@ module.exports = { TNS_EOCS_FLAGS_TXN_IN_PROGRESS: 0x00000002, TNS_EOCS_FLAGS_SESS_RELEASE: 0x00008000, + // transaction switching op codes + TNS_TPC_TXN_START: 0x01, + TNS_TPC_TXN_DETACH: 0x02, + + // transaction change state op codes + TNS_TPC_TXN_COMMIT: 0x01, + TNS_TPC_TXN_ABORT: 0x02, + TNS_TPC_TXN_PREPARE: 0x03, + TNS_TPC_TXN_FORGET: 0x04, + + // transaction states + TNS_TPC_TXN_STATE_PREPARE: 0, + TNS_TPC_TXN_STATE_REQUIRES_COMMIT: 1, + TNS_TPC_TXN_STATE_COMMITTED: 2, + TNS_TPC_TXN_STATE_ABORTED: 3, + TNS_TPC_TXN_STATE_READ_ONLY: 4, + TNS_TPC_TXN_STATE_FORGOTTEN: 5, + // other constants TNS_ESCAPE_CHAR: 253, TNS_LONG_LENGTH_INDICATOR: dataHandlerConstants.TNS_LONG_LENGTH_INDICATOR, diff --git a/lib/thin/protocol/messages/index.js b/lib/thin/protocol/messages/index.js index f46b8e58..d2dbdc27 100644 --- a/lib/thin/protocol/messages/index.js +++ b/lib/thin/protocol/messages/index.js @@ -38,6 +38,8 @@ const ProtocolMessage = require('./protocol.js'); const RollbackMessage = require('./rollback.js'); const SessionReleaseMessage = require('./sessionRelease.js'); const FastAuthMessage = require('./fastAuth.js'); +const TransactionChangeStateMessage = require('./transactionChangeState.js'); +const TransactionSwitchMessage = require('./transactionSwitch.js'); module.exports = { AuthMessage, @@ -51,5 +53,7 @@ module.exports = { PingMessage, ProtocolMessage, RollbackMessage, - SessionReleaseMessage + SessionReleaseMessage, + TransactionChangeStateMessage, + TransactionSwitchMessage }; diff --git a/lib/thin/protocol/messages/transactionChangeState.js b/lib/thin/protocol/messages/transactionChangeState.js new file mode 100644 index 00000000..a884a3ea --- /dev/null +++ b/lib/thin/protocol/messages/transactionChangeState.js @@ -0,0 +1,99 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. + +//----------------------------------------------------------------------------- +// +// This software is dual-licensed to you under the Universal Permissive License +// (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License +// 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose +// either license. +// +// If you elect to accept the software under the Apache License, Version 2.0, +// the following applies: +// +// 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 +// +// https://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. +// +//----------------------------------------------------------------------------- + +'use strict'; + +const Message = require("./base.js"); +const constants = require("../constants.js"); + +/** + * Used for two-phase commit (TPC) transaction change state: commit, rollback, + * forget, etc. + * + * @class TransactionChangeStateMessage + * @extends {Message} + */ +class TransactionChangeStateMessage extends Message { + + constructor(connImpl) { + super(connImpl); + this.functionCode = constants.TNS_FUNC_TPC_TXN_CHANGE_STATE; + } + + processReturnParameter(buf) { + // process the parameters returned by the datatabase + this.state = buf.readUB4(); + } + + encode(buf) { + // writes the message to the database + // acquire data to send to the server + let xidBytes; + if (this.xid) { + xidBytes = Buffer.alloc(128); + this.xid.globalTransactionId.copy(xidBytes); + this.xid.branchQualifier.copy(xidBytes, + this.xid.globalTransactionId.length); + } + + this.writeFunctionHeader(buf); + buf.writeUB4(this.operation); + if (this.context) { + buf.writeUInt8(1); + buf.writeUB4(this.context.length); + } else { + buf.writeUInt8(0); + buf.writeUB4(0); + } + + if (this.xid) { + buf.writeUB4(this.xid.formatId); + buf.writeUB4(this.xid.globalTransactionId.length); + buf.writeUB4(this.xid.branchQualifier.length); + buf.writeUInt8(1); + buf.writeUB4(xidBytes.length); + } else { + buf.writeUB4(0); + buf.writeUB4(0); + buf.writeUB4(0); + buf.writeUInt8(0); + buf.writeUB4(0); + } + buf.writeUB4(0); + buf.writeUB4(this.state); + buf.writeUInt8(1); + buf.writeUB4(this.flags); + + if (this.context) { + buf.writeBytes(this.context); + } + if (this.xid) { + buf.writeBytes(xidBytes); + } + } +} + +module.exports = TransactionChangeStateMessage; diff --git a/lib/thin/protocol/messages/transactionSwitch.js b/lib/thin/protocol/messages/transactionSwitch.js new file mode 100644 index 00000000..efef940d --- /dev/null +++ b/lib/thin/protocol/messages/transactionSwitch.js @@ -0,0 +1,139 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. + +//----------------------------------------------------------------------------- +// +// This software is dual-licensed to you under the Universal Permissive License +// (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License +// 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose +// either license. +// +// If you elect to accept the software under the Apache License, Version 2.0, +// the following applies: +// +// 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 +// +// https://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. +// +//----------------------------------------------------------------------------- + +'use strict'; + +const Message = require("./base.js"); +const constants = require("../constants.js"); + +/** + * Used for two-phase commit (TPC) transaction start, attach and detach. + * + * @class TransactionSwitchMessage + * @extends {Message} + */ +class TransactionSwitchMessage extends Message { + + constructor(connImpl) { + super(connImpl); + this.functionCode = constants.TNS_FUNC_TPC_TXN_SWITCH; + this.applicationValue = 0; + this.internalName = connImpl.internalName; + if (this.internalName) { + this.internalName = Buffer.from(this.internalName); + } + this.externalName = connImpl.externalName; + if (this.externalName) { + this.externalName = Buffer.from(this.externalName); + } + } + + processReturnParameter(buf) { + // process the parameters returned by the datatabase + this.applicationValue = buf.readUB4(); + this.contextLen = buf.readUB2(); + if (this.contextLen > 0) { + this.context = Buffer.from(buf.readBytes(this.contextLen)); + } + } + + encode(buf) { + // writes the message to the database + // acquire data to send to the server + let xidBytes; + if (this.xid) { + xidBytes = Buffer.alloc(128); + this.xid.globalTransactionId.copy(xidBytes); + this.xid.branchQualifier.copy(xidBytes, + this.xid.globalTransactionId.length); + } + + // write message + this.writeFunctionHeader(buf); + buf.writeUB4(this.operation); + if (this.context) { + buf.writeUInt8(1); // pointer (transaction context) + buf.writeUB4(this.context.length); + } else { + buf.writeUInt8(0); // pointer (transaction context) + buf.writeUB4(0); // transaction context length + } + + if (this.xid) { + buf.writeUB4(this.xid.formatId); + buf.writeUB4(this.xid.globalTransactionId.length); + buf.writeUB4(this.xid.branchQualifier.length); + buf.writeUInt8(1); // pointer (XID) + buf.writeUB4(xidBytes.length); + } else { + buf.writeUB4(0); // format id + buf.writeUB4(0); // global transaction id length + buf.writeUB4(0); // branch qualifier length + buf.writeUInt8(0); // pointer (XID) + buf.writeUB4(0); // XID length + } + + buf.writeUB4(this.flags); + buf.writeUB4(this.timeout); + buf.writeUInt8(1); // pointer (application value) + buf.writeUInt8(1); // pointer (return context) + buf.writeUInt8(1); // pointer (return context length) + + if (this.internalName) { + buf.writeUInt8(1); // pointer (internal name) + buf.writeUB4(this.internalName.length); + } else { + buf.writeUInt8(0); // pointer (internal name) + buf.writeUB4(0); // length of internal name + } + + if (this.externalName) { + buf.writeUInt8(1); // pointer (external name) + buf.writeUB4(this.externalName.length); + } else { + buf.writeUInt8(0); // pointer (external name) + buf.writeUB4(0); // length of external name + } + + if (this.context) { + buf.writeBytes(this.context); + } + if (this.xid) { + buf.writeBytes(xidBytes); + } + buf.writeUB4(this.applicationValue); + + if (this.internalName) { + buf.writeBytes(this.internalName); + } + + if (this.externalName) { + buf.writeBytes(this.externalName); + } + } +} + +module.exports = TransactionSwitchMessage; diff --git a/lib/util.js b/lib/util.js index 8b537a62..e7c13bf3 100644 --- a/lib/util.js +++ b/lib/util.js @@ -277,6 +277,36 @@ function isXid(value) { typeof value.branchQualifier === 'string')); } +function normalizeXid(value) { + let normalizedXid; + if (Buffer.isBuffer(value.globalTransactionId) && + Buffer.isBuffer(value.branchQualifier)) { + normalizedXid = value; + } else { + normalizedXid = { + formatId: value.formatId, + globalTransactionId: value.globalTransactionId, + branchQualifier: value.branchQualifier + }; + if (typeof value.globalTransactionId === 'string') { + normalizedXid.globalTransactionId = Buffer.from(value.globalTransactionId); + } + if (typeof value.branchQualifier === 'string') { + normalizedXid.branchQualifier = Buffer.from(value.branchQualifier); + } + } + if (normalizedXid.globalTransactionId.length > 64) { + errors.throwErr(errors.ERR_INVALID_TRANSACTION_SIZE, + normalizedXid.globalTransactionId.length); + } + if (normalizedXid.branchQualifier.length > 64) { + errors.throwErr(errors.ERR_INVALID_BRANCH_SIZE, + normalizedXid.branchQualifier.length); + } + + return normalizedXid; +} + function verifySodaDoc(content) { if (isSodaDocument(content)) return content._impl; @@ -403,6 +433,7 @@ module.exports = { isTokenValid, isVectorValue, isXid, + normalizeXid, makeDate, preventConcurrent, serialize, diff --git a/test/tpc.js b/test/tpc.js index 07b1cd8d..0906aa16 100644 --- a/test/tpc.js +++ b/test/tpc.js @@ -81,8 +81,6 @@ describe('259. tpc.js', function() { END;`; before(async function() { - if (oracledb.thin) - return this.skip(); conn = await oracledb.getConnection(dbConfig); await conn.execute(sql); @@ -234,6 +232,9 @@ describe('259. tpc.js', function() { }); it('259.2.8 negative tpcForget after tpcPrepare', async function() { + if (oracledb.thin) { + this.skip(); + } const xid = { formatId: 3998, globalTransactionId: "txn3998", @@ -258,6 +259,9 @@ describe('259. tpc.js', function() { }); it('259.2.9 negative tpcForget without tpcPrepare', async function() { + if (oracledb.thin) { + this.skip(); + } const xid = { formatId: 3999, globalTransactionId: "txn3999", @@ -289,12 +293,14 @@ describe('259. tpc.js', function() { }; await conn.tpcBegin(xid, oracledb.TPC_BEGIN_NEW, 60); + await conn.execute(`INSERT INTO TBL_259_2 VALUES (101, 'test#1')`); await conn.tpcPrepare(xid); const promise = dbaConn.tpcRecover(); const res = await Promise.resolve(promise); assert.strictEqual(res[0].formatId, 5000); assert.strictEqual(res[0].globalTransactionId, "txn5000"); assert.strictEqual(res[0].branchQualifier, "branchId"); + await conn.tpcCommit(res[0]); }); }); @@ -313,8 +319,6 @@ describe('259. tpc.js', function() { END;`; before(async function() { - if (oracledb.thin) - return this.skip(); conn = await oracledb.getConnection(dbConfig); await conn.execute(sql); }); @@ -461,8 +465,6 @@ describe('259. tpc.js', function() { let conn = null; before(async function() { - if (oracledb.thin) - return this.skip(); conn = await oracledb.getConnection(dbConfig); }); @@ -487,6 +489,9 @@ describe('259. tpc.js', function() { }); it('259.4.3 set and query ecid', async function() { + if (oracledb.thin) { + this.skip(); + } const sql = `SELECT USERNAME, SID, OSUSER, ECID FROM V$SESSION WHERE SID = :1`; @@ -518,8 +523,6 @@ describe('259. tpc.js', function() { END;`; before(async function() { - if (oracledb.thin) - return this.skip(); conn = await oracledb.getConnection(dbConfig); await conn.execute(sql); }); @@ -586,8 +589,6 @@ describe('259. tpc.js', function() { }; before(async function() { - if (oracledb.thin) - return this.skip(); conn = await oracledb.getConnection(dbConfig); }); @@ -645,4 +646,40 @@ describe('259. tpc.js', function() { ); }); }); + + describe('259.7 XID parameters validation', function() { + let conn = null; + before(async function() { + conn = await oracledb.getConnection(dbConfig); + }); + + after (async function() { + if (conn) + await conn.close(); + }); + + it('259.6.1 globalTransactionId size greater than 64', async function() { + const xid = { + formatId: 979797, + globalTransactionId: "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", + branchQualifier: "brancId1" + }; + await assert.rejects( + async () => await conn.tpcBegin(xid, oracledb.TPC_BEGIN_NEW, 60), + /NJS-153/ + ); + }); + + it('259.6.2 branchQualifier invalid number of arguments', async function() { + const xid = { + formatId: 979797, + globalTransactionId: "txn9999", + branchQualifier: "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" + }; + await assert.rejects( + async () => await conn.tpcBegin(xid, oracledb.TPC_BEGIN_NEW, 60), + /NJS-154/ + ); + }); + }); });