Skip to content

Commit 7d6c8b2

Browse files
committed
Add support for Oracle Database 23ai Sessionless transactions
1 parent f420322 commit 7d6c8b2

File tree

14 files changed

+428
-16
lines changed

14 files changed

+428
-16
lines changed

lib/connection.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ class Connection extends EventEmitter {
110110
options.connection = this;
111111
if (options.keepInStmtCache === undefined)
112112
options.keepInStmtCache = true;
113+
if (options.suspendOnSuccess == undefined)
114+
options.suspendOnSuccess = false;
113115
settings.addToOptions(options,
114116
"autoCommit",
115117
"dbObjectAsPojo",
@@ -596,6 +598,11 @@ class Connection extends EventEmitter {
596598
outOptions.keepInStmtCache = options.keepInStmtCache;
597599
}
598600

601+
if (options.suspendOnSuccess !== undefined) {
602+
errors.assertParamPropBool(options, 2, "suspendOnSucess");
603+
outOptions.suspendOnSuccess = options.suspendOnSuccess;
604+
}
605+
599606
// handle options specific to executeMany()
600607
if (inExecuteMany) {
601608

@@ -706,6 +713,28 @@ class Connection extends EventEmitter {
706713
this._impl.setAction(value);
707714
}
708715

716+
//---------------------------------------------------------------------------
717+
// beginSessionlessTransaction()
718+
//
719+
// Begin a new sessionless transaction with provided transactionId.
720+
// If transactionId wasn't provided a random-generated transactionId will be
721+
// used and returned.
722+
//---------------------------------------------------------------------------
723+
async beginSessionlessTransaction(options) {
724+
errors.assertParamValue(nodbUtil.isObject(options), 1);
725+
if (options.transactionId != undefined)
726+
errors.assertParamValue(
727+
nodbUtil.isTransactionId(options.transactionId), 1);
728+
errors.assertParamPropUnsignedIntNonZero(options, 1, 'timeout');
729+
errors.assertParamPropBool(options, 1, 'deferRoundTrip');
730+
const normalizedTransactionId =
731+
nodbUtil.normalizeTransactionId(options.transactionId);
732+
const {timeout = 60, deferRoundTrip = false} = options;
733+
await this._impl.startSessionlessTransaction(normalizedTransactionId,
734+
timeout, constants.TPC_BEGIN_NEW, deferRoundTrip);
735+
return normalizedTransactionId;
736+
}
737+
709738
//---------------------------------------------------------------------------
710739
// breakExecution()
711740
//
@@ -1424,6 +1453,25 @@ class Connection extends EventEmitter {
14241453
return (stream);
14251454
}
14261455

1456+
//---------------------------------------------------------------------------
1457+
// resumeSessionlessTransaction()
1458+
//
1459+
// Resume an existing sessionlesss transaction using given transactionId
1460+
//---------------------------------------------------------------------------
1461+
async resumeSessionlessTransaction(options) {
1462+
errors.assertParamValue(nodbUtil.isObject(options), 1);
1463+
errors.assertParamPropUnsignedInt(options, 1, 'timeout');
1464+
errors.assertParamPropBool(options, 1, 'deferRoundTrip');
1465+
errors.assertParamValue(
1466+
nodbUtil.isTransactionId(options.transactionId), 1);
1467+
const normalizedTransactionId =
1468+
nodbUtil.normalizeTransactionId(options.transactionId);
1469+
const {timeout = 60, deferRoundTrip = false} = options;
1470+
await this._impl.startSessionlessTransaction(normalizedTransactionId,
1471+
timeout, constants.TPC_BEGIN_RESUME, deferRoundTrip);
1472+
return normalizedTransactionId;
1473+
}
1474+
14271475
//---------------------------------------------------------------------------
14281476
// rollback()
14291477
//
@@ -1538,6 +1586,16 @@ class Connection extends EventEmitter {
15381586
return outValue;
15391587
}
15401588

1589+
//---------------------------------------------------------------------------
1590+
// suspendSessionlessTransaction()
1591+
//
1592+
// Suspend any active sessionless transaction immediately
1593+
//---------------------------------------------------------------------------
1594+
async suspendSessionlessTransaction() {
1595+
errors.assert(this._impl, errors.ERR_INVALID_CONNECTION);
1596+
await this._impl.suspendSessionlessTransaction();
1597+
}
1598+
15411599
//---------------------------------------------------------------------------
15421600
// tag
15431601
//
@@ -1746,6 +1804,7 @@ Connection.prototype.break =
17461804
Connection.prototype.tpcRecover =
17471805
nodbUtil.callbackify(nodbUtil.wrapFn(Connection.prototype.tpcRecover));
17481806
nodbUtil.wrapFns(Connection.prototype,
1807+
"beginSessionlessTransaction",
17491808
"changePassword",
17501809
"close",
17511810
"commit",
@@ -1756,10 +1815,12 @@ nodbUtil.wrapFns(Connection.prototype,
17561815
"getQueue",
17571816
"getStatementInfo",
17581817
"ping",
1818+
"resumeSessionlessTransaction",
17591819
"rollback",
17601820
"shutdown",
17611821
"startup",
17621822
"subscribe",
1823+
"suspendSessionlessTransaction",
17631824
"tpcBegin",
17641825
"tpcCommit",
17651826
"tpcEnd",

lib/errors.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ const ERR_DB_FETCH_TYPE_HANDLER_CONVERTER = 166;
175175
const ERR_DB_FETCH_TYPE_HANDLER_RETURN_VALUE = 167;
176176
const ERR_ACCESS_TOKEN = 168;
177177
const ERR_CALLOUT_FN = 169;
178+
const ERR_SESSIONLESS_DIFFERING_METHODS = 170;
179+
const ERR_SESSIONLESS_ALREADY_ACTIVE = 171;
180+
const ERR_SESSIONLESS_INACTIVE = 172;
178181

179182
// Oracle Net layer errors start from 500
180183
const ERR_CONNECTION_CLOSED = 500;
@@ -254,6 +257,9 @@ adjustErrorXref.set("ORA-56600", ERR_CONNECTION_CLOSED);
254257
adjustErrorXref.set("ORA-24338", ERR_INVALID_REF_CURSOR);
255258
adjustErrorXref.set("ORA-25708", ERR_TOKEN_HAS_EXPIRED);
256259
adjustErrorXref.set("ORA-24344", WRN_COMPILATION_CREATE);
260+
adjustErrorXref.set("ORA-26202", ERR_SESSIONLESS_INACTIVE);
261+
adjustErrorXref.set("ORA-26211", ERR_SESSIONLESS_DIFFERING_METHODS);
262+
adjustErrorXref.set("ORA-26216", ERR_SESSIONLESS_ALREADY_ACTIVE);
257263

258264
// define mapping for error messages
259265
const messages = new Map();
@@ -519,14 +525,20 @@ messages.set(ERR_VECTOR_SPARSE_INVALID_INPUT, // NJS-164
519525
'SPARSE VECTOR Invalid Input Data');
520526
messages.set(ERR_VECTOR_SPARSE_INDICES_ELEM_IS_NOT_VALID, // NJS-165
521527
'SPARSE VECTOR indices element at index %d is not valid');
522-
messages.set(ERR_DB_FETCH_TYPE_HANDLER_CONVERTER, // NJS-166
528+
messages.set(ERR_DB_FETCH_TYPE_HANDLER_CONVERTER, // NJS-166
523529
'DBFetchTypeHandler return value attribute "converter" must be a function');
524-
messages.set(ERR_DB_FETCH_TYPE_HANDLER_RETURN_VALUE, // NJS-167
530+
messages.set(ERR_DB_FETCH_TYPE_HANDLER_RETURN_VALUE, // NJS-167
525531
'DBFetchTypeHandler return value must be an object');
526532
messages.set(ERR_ACCESS_TOKEN, // NJS-168
527533
'access token function failed.');
528-
messages.set(ERR_CALLOUT_FN, // NJS-169
534+
messages.set(ERR_CALLOUT_FN, // NJS-169
529535
'External function call failed.');
536+
messages.set(ERR_SESSIONLESS_DIFFERING_METHODS, //NJS-170
537+
'Different ways to start or suspend sessionless transactions are being used(server procedures and client APIs)');
538+
messages.set(ERR_SESSIONLESS_ALREADY_ACTIVE, //NJS-171
539+
'Suspend, commit or rollback the currently active sessionless transaction before beginning or resuming another one.');
540+
messages.set(ERR_SESSIONLESS_INACTIVE, //NJS-172
541+
'No sessionless transaction is active');
530542

531543
// Oracle Net layer errors
532544

@@ -984,6 +996,9 @@ module.exports = {
984996
ERR_UNKNOWN_TRANSACTION_STATE,
985997
ERR_INVALID_TRANSACTION_SIZE,
986998
ERR_INVALID_BRANCH_SIZE,
999+
ERR_SESSIONLESS_DIFFERING_METHODS,
1000+
ERR_SESSIONLESS_ALREADY_ACTIVE,
1001+
ERR_SESSIONLESS_INACTIVE,
9871002
ERR_CONNECTION_CLOSED_CODE: `${ERR_PREFIX}-${ERR_CONNECTION_CLOSED}`,
9881003
ERR_OPERATION_NOT_SUPPORTED_ON_BFILE,
9891004
ERR_OPERATION_ONLY_SUPPORTED_ON_BFILE,

lib/thin/connection.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,7 @@ class ThinConnectionImpl extends ConnectionImpl {
821821
this.remoteAddress = '';
822822
this.comboKey = null; // used in changePassword API
823823
this.tpcContext = null;
824+
this._sessionlessData = null;
824825

825826
this.nscon = new nsi();
826827
finalizationRegistry.register(this, this.nscon);
@@ -1282,6 +1283,55 @@ class ThinConnectionImpl extends ConnectionImpl {
12821283
errors.throwErr(errors.ERR_UNKNOWN_TRANSACTION_STATE, message.state);
12831284
}
12841285
}
1286+
//---------------------------------------------------------------------------
1287+
// Ensure no sessionless transaction was started through server procedure
1288+
//---------------------------------------------------------------------------
1289+
_validateSessionlessState() {
1290+
if (this._sessionlessData?.startedOnServer) {
1291+
errors.throwErr(errors.ERR_SESSIONLESS_DIFFERING_METHODS);
1292+
}
1293+
}
1294+
1295+
//---------------------------------------------------------------------------
1296+
// Begin/Resume a sessionless transaction with provided transactionId
1297+
//---------------------------------------------------------------------------
1298+
async startSessionlessTransaction(transactionId, timeout, flags,
1299+
deferRoundTrip) {
1300+
this._validateSessionlessState();
1301+
if (this._sessionlessData)
1302+
errors.throwErr(errors.ERR_SESSIONLESS_ALREADY_ACTIVE);
1303+
const message = new messages.TransactionSwitchMessage(this);
1304+
message.xid = {
1305+
globalTransactionId: transactionId,
1306+
branchQualifier: "",
1307+
formatId: constants.TNS_TPC_TRANS_SESSIONLESS_FORMAT
1308+
};
1309+
message.timeout = timeout;
1310+
message.operation = constants.TNS_TPC_TXN_START;
1311+
message.flags = constants.TNS_TPC_TRANS_SESSIONLESS | flags;
1312+
if (deferRoundTrip) {
1313+
message.messageType = constants.TNS_MSG_TYPE_PIGGYBACK;
1314+
this._sessionlessData = {
1315+
piggyback: message,
1316+
pending: true
1317+
};
1318+
} else {
1319+
await this._protocol._processMessage(message);
1320+
}
1321+
}
1322+
1323+
//---------------------------------------------------------------------------
1324+
// Suspend the active sessionless transaction
1325+
//---------------------------------------------------------------------------
1326+
async suspendSessionlessTransaction() {
1327+
this._validateSessionlessState();
1328+
if (!this._sessionlessData)
1329+
errors.throwErr(errors.ERR_SESSIONLESS_INACTIVE);
1330+
const message = new messages.TransactionSwitchMessage(this);
1331+
message.operation = constants.TNS_TPC_TXN_DETACH;
1332+
message.flags = constants.TNS_TPC_TRANS_SESSIONLESS;
1333+
await this._protocol._processMessage(message);
1334+
}
12851335

12861336

12871337
//---------------------------------------------------------------------------

lib/thin/protocol/capabilities.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class Capabilities {
105105
this.compileCaps[constants.TNS_CCAP_TTC3] =
106106
constants.TNS_CCAP_IMPLICIT_RESULTS | constants.TNS_CCAP_BIG_CHUNK_CLR |
107107
constants.TNS_CCAP_KEEP_OUT_ORDER | constants.TNS_CCAP_LTXID;
108+
this.compileCaps[constants.TNS_CCAP_OCI3] = constants.TNS_CCAP_OCI3_OCSSYNC;
108109
this.compileCaps[constants.TNS_CCAP_TTC2] = constants.TNS_CCAP_ZLNP;
109110
this.compileCaps[constants.TNS_CCAP_OCI2] = constants.TNS_CCAP_DRCP;
110111
this.compileCaps[constants.TNS_CCAP_CLIENT_FN] =
@@ -119,7 +120,8 @@ class Capabilities {
119120
this.compileCaps[constants.TNS_CCAP_CTB_FEATURE_BACKPORT] =
120121
constants.TNS_CCAP_CTB_IMPLICIT_POOL;
121122
this.compileCaps[constants.TNS_CCAP_TTC5] =
122-
constants.TNS_CCAP_VECTOR_SUPPORT;
123+
constants.TNS_CCAP_VECTOR_SUPPORT |
124+
constants.TNS_CCAP_TTC5_SESSIONLESS_TXNS;
123125
this.compileCaps[constants.TNS_CCAP_VECTOR_FEATURES] =
124126
constants.TNS_CCAP_VECTOR_FEATURE_BINARY | constants.TNS_CCAP_VECTOR_FEATURE_SPARSE;
125127
}

lib/thin/protocol/constants.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ module.exports = {
440440
// parameter keyword numbers,
441441
TNS_KEYWORD_NUM_CURRENT_SCHEMA: 168,
442442
TNS_KEYWORD_NUM_EDITION: 172,
443+
TNS_KEYWORD_NUM_TRANSACTION_ID: 201,
443444

444445
// bind flags
445446
TNS_BIND_USE_INDICATORS: 0x0001,
@@ -584,6 +585,7 @@ module.exports = {
584585
TNS_CCAP_UB2_DTY: 27,
585586
TNS_CCAP_OCI2: 31,
586587
TNS_CCAP_CLIENT_FN: 34,
588+
TNS_CCAP_OCI3: 35,
587589
TNS_CCAP_TTC3: 37,
588590
TNS_CCAP_SESS_SIGNATURE_VERSION: 39,
589591
TNS_CCAP_TTC4: 40,
@@ -649,6 +651,8 @@ module.exports = {
649651
TNS_CCAP_VECTOR_SUPPORT: 0x08,
650652
TNS_CCAP_VECTOR_FEATURE_BINARY: 0x01,
651653
TNS_CCAP_VECTOR_FEATURE_SPARSE: 0x02,
654+
TNS_CCAP_TTC5_SESSIONLESS_TXNS: 0x20,
655+
TNS_CCAP_OCI3_OCSSYNC: 0x20,
652656

653657
// runtime capability indices
654658
TNS_RCAP_COMPAT: 0,
@@ -678,6 +682,7 @@ module.exports = {
678682
// transaction switching op codes
679683
TNS_TPC_TXN_START: 0x01,
680684
TNS_TPC_TXN_DETACH: 0x02,
685+
TNS_TPC_TXN_POST_DETACH: 0x04,
681686

682687
// transaction change state op codes
683688
TNS_TPC_TXN_COMMIT: 0x01,
@@ -693,6 +698,25 @@ module.exports = {
693698
TNS_TPC_TXN_STATE_READ_ONLY: 4,
694699
TNS_TPC_TXN_STATE_FORGOTTEN: 5,
695700

701+
// sessionless transaction flag
702+
TNS_TPC_TRANS_SESSIONLESS: 0x00000010,
703+
704+
// sessionless transaction format
705+
TNS_TPC_TRANS_SESSIONLESS_FORMAT: 0x4e5c3e,
706+
707+
// sessionless sync version
708+
TNS_TPC_TRANS_TRANSACTION_ID_SYNC_VERSION_1: 0x01,
709+
710+
// sessionless server states
711+
TNS_TPC_TRANS_TRANSACTION_ID_SYNC_SET: 0x40,
712+
TNS_TPC_TRANS_TRANSACTION_ID_SYNC_UNSET: 0x80,
713+
714+
// sessionless state reason
715+
TNS_TPC_TRANS_TRANSACTION_ID_SYNC_SERVER: 0x01,
716+
TNS_TPC_TRANS_TRANSACTION_ID_SYNC_CLIENT: 0x02,
717+
TNS_TPC_TRANS_TRANSACTION_ID_SYNC_TXEND_XA: 0x03,
718+
719+
696720
// other constants
697721
TNS_ESCAPE_CHAR: 253,
698722
TNS_LONG_LENGTH_INDICATOR: dataHandlerConstants.TNS_LONG_LENGTH_INDICATOR,

lib/thin/protocol/messages/base.js

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -266,15 +266,22 @@ class Message {
266266
const num_elements = buf.readUB4();
267267
buf.skipBytes(1); // skip length
268268
for (let i = 0; i < num_elements; i++) {
269-
let temp16 = buf.readUB2();
270-
if (temp16 > 0) { // skip key
271-
buf.skipBytesChunked();
269+
let numBytes = buf.readUB2();
270+
let keyTextValue, value;
271+
if (numBytes > 0) { // read key
272+
keyTextValue = buf.readStr(constants.CSFRM_IMPLICIT);
272273
}
273-
temp16 = buf.readUB2();
274-
if (temp16 > 0) { // skip value
275-
buf.skipBytesChunked();
274+
numBytes = buf.readUB2();
275+
if (numBytes > 0) // read value
276+
value = buf.readBytesWithLength();
277+
const keywordNum = buf.readUB2(); // read keyword number
278+
if (keywordNum == constants.TNS_KEYWORD_NUM_TRANSACTION_ID) {
279+
this._updateSessionlessTxnState(value);
280+
} else if (keywordNum === constants.TNS_KEYWORD_NUM_CURRENT_SCHEMA) {
281+
this.connection.currentSchema = keyTextValue;
282+
} else if (keywordNum === constants.TNS_KEYWORD_NUM_EDITION) {
283+
this.connection._edition = keyTextValue;
276284
}
277-
buf.skipUB2(); // skip flags
278285
}
279286
buf.skipUB4(); // skip overall flags
280287
} else if (opcode === constants.TNS_SERVER_PIGGYBACK_EXT_SYNC) {
@@ -354,6 +361,11 @@ class Message {
354361
if (this.connection._tempLobsTotalSize > 0) {
355362
this.writeCloseTempLobsPiggyback(buf);
356363
}
364+
if (this.connection._sessionlessData?.pending) {
365+
this.connection._sessionlessData.pending = false;
366+
this.connection._sessionlessData.piggyback.encode(buf);
367+
this.connection._sessionlessData.piggyback = null;
368+
}
357369
}
358370

359371
writePiggybackHeader(buf, functionCode) {
@@ -515,6 +527,42 @@ class Message {
515527
this.deferredErr = errors.getErr(...arguments);
516528
}
517529
}
530+
531+
_updateSessionlessTxnState(buf) {
532+
const length = buf.length;
533+
const sessionlessState = buf.readUInt8(length - 2);
534+
const syncVersion = buf.readUInt8(length - 1);
535+
let startedOnServer;
536+
537+
if (syncVersion == constants.TNS_TPC_TRANS_TRANSACTION_ID_SYNC_VERSION_1) {
538+
// transactionId got unset for this session (txn ended or suspended)
539+
if (sessionlessState &
540+
constants.TNS_TPC_TRANS_TRANSACTION_ID_SYNC_UNSET) {
541+
this.connection._sessionlessData = null;
542+
this.connection._protocol.txnInProgress = false;
543+
// transactionId got set for this session
544+
} else if (sessionlessState &
545+
constants.TNS_TPC_TRANS_TRANSACTION_ID_SYNC_SET) {
546+
547+
// transaction initialized by PL/SQL procedure
548+
if (sessionlessState &
549+
constants.TNS_TPC_TRANS_TRANSACTION_ID_SYNC_SERVER)
550+
startedOnServer = true;
551+
// transaction initialized by calling client API(RPC call)
552+
else if (sessionlessState &
553+
constants.TNS_TPC_TRANS_TRANSACTION_ID_SYNC_CLIENT)
554+
startedOnServer = false;
555+
556+
this.connection._sessionlessData = {
557+
startedOnServer: startedOnServer,
558+
};
559+
this.connection._protocol.txnInProgress = true;
560+
}
561+
} else
562+
errors.throwErr(errors.ERR_INTERNAL, 'Unexpected TransactionId sync ' +
563+
'version in session sync piggyback.');
564+
}
565+
518566
}
519567

520568
module.exports = Message;

0 commit comments

Comments
 (0)