diff --git a/src/database.ts b/src/database.ts index 0048ec1c4..7e87940d4 100644 --- a/src/database.ts +++ b/src/database.ts @@ -43,6 +43,7 @@ import { google as spannerClient, } from '../protos/protos'; import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel; +import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; import { CreateDatabaseCallback, CreateDatabaseOptions, @@ -265,8 +266,6 @@ export interface GetIamPolicyOptions { /** * @typedef {object} GetTransactionOptions - * * @property {boolean} [optimisticLock] The optimistic lock a - * {@link Transaction} should use while running. */ export type GetTransactionOptions = Omit; @@ -325,6 +324,7 @@ export interface RestoreOptions { } export interface WriteAtLeastOnceOptions extends CallOptions { + readLockMode?: ReadLockMode; isolationLevel?: IsolationLevel; } diff --git a/src/index.ts b/src/index.ts index f187aa92a..e41d37465 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,6 +63,7 @@ import { } from 'google-gax'; import {google, google as instanceAdmin} from '../protos/protos'; import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel; +import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; import { PagedOptions, PagedResponse, @@ -159,7 +160,10 @@ export interface SpannerOptions extends GrpcClientOptions { sslCreds?: grpc.ChannelCredentials; routeToLeaderEnabled?: boolean; directedReadOptions?: google.spanner.v1.IDirectedReadOptions | null; - defaultTransactionOptions?: Pick; + defaultTransactionOptions?: Pick< + RunTransactionOptions, + 'isolationLevel' | 'readLockMode' + >; observabilityOptions?: ObservabilityOptions; disableBuiltInMetrics?: boolean; interceptors?: any[]; @@ -432,6 +436,7 @@ class Spanner extends GrpcService { ? options.defaultTransactionOptions : { isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED, }; delete options.defaultTransactionOptions; diff --git a/src/table.ts b/src/table.ts index e02a58934..837717f1d 100644 --- a/src/table.ts +++ b/src/table.ts @@ -39,6 +39,7 @@ import { } from './instrument'; import {google} from '../protos/protos'; import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel; +import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; export type Key = string | string[]; @@ -56,6 +57,7 @@ interface MutateRowsOptions extends CommitOptions { requestOptions?: Omit; excludeTxnFromChangeStreams?: boolean; isolationLevel?: IsolationLevel; + readLockMode?: ReadLockMode; } export type DeleteRowsCallback = CommitCallback; @@ -1110,11 +1112,17 @@ class Table { ? options.isolationLevel : IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED; + const readLockMode = + 'readLockMode' in options + ? options.readLockMode + : ReadLockMode.READ_LOCK_MODE_UNSPECIFIED; + this.database.runTransaction( { requestOptions: requestOptions, excludeTxnFromChangeStreams: excludeTxnFromChangeStreams, isolationLevel: isolationLevel, + readLockMode: readLockMode, }, (err, transaction) => { if (err) { diff --git a/src/transaction-runner.ts b/src/transaction-runner.ts index 3d78f36c7..4f569fcbf 100644 --- a/src/transaction-runner.ts +++ b/src/transaction-runner.ts @@ -27,6 +27,7 @@ import {Database} from './database'; import {google} from '../protos/protos'; import IRequestOptions = google.spanner.v1.IRequestOptions; import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel; +import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; // eslint-disable-next-line @typescript-eslint/no-var-requires const jsonProtos = require('../protos/protos.json'); @@ -45,9 +46,13 @@ const RetryInfo = Root.fromJSON(jsonProtos).lookup('google.rpc.RetryInfo'); export interface RunTransactionOptions { timeout?: number; requestOptions?: Pick; + /** + * @deprecated Use readLockMode instead. + */ optimisticLock?: boolean; excludeTxnFromChangeStreams?: boolean; isolationLevel?: IsolationLevel; + readLockMode?: ReadLockMode; } /** @@ -130,6 +135,7 @@ export abstract class Runner { const defaults = { timeout: 3600000, isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED, }; this.options = Object.assign(defaults, options); diff --git a/src/transaction.ts b/src/transaction.ts index 23e5992b7..9b5e0e266 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -2946,6 +2946,8 @@ export class Transaction extends Dml { * (when any needed locks are acquired). The validation process succeeds only * if there are no conflicting committed transactions (that committed * mutations to the read data at a commit timestamp after the read timestamp). + * + * @deprecated Set readLockMode through setReadWriteTransactionOptions instead. */ useOptimisticLock(): void { this._options.readWrite!.readLockMode = ReadLockMode.OPTIMISTIC; @@ -2964,12 +2966,6 @@ export class Transaction extends Dml { } setReadWriteTransactionOptions(options: RunTransactionOptions) { - /** - * Set optimistic concurrency control for the transaction. - */ - if (options?.optimisticLock) { - this._options.readWrite!.readLockMode = ReadLockMode.OPTIMISTIC; - } /** * Set option excludeTxnFromChangeStreams=true to exclude read/write transactions * from being tracked in change streams. @@ -2978,11 +2974,18 @@ export class Transaction extends Dml { this._options.excludeTxnFromChangeStreams = true; } /** - * Set isolation level . + * Set isolation level. */ this._options.isolationLevel = options?.isolationLevel ? options?.isolationLevel : this._getSpanner().defaultTransactionOptions.isolationLevel; + + /** + * Set read lock mode. + */ + this._options.readWrite!.readLockMode = options?.readLockMode + ? options?.readLockMode + : this._getSpanner().defaultTransactionOptions.readLockMode; } } diff --git a/system-test/spanner.ts b/system-test/spanner.ts index f8e8584d6..d3e36d40b 100644 --- a/system-test/spanner.ts +++ b/system-test/spanner.ts @@ -49,6 +49,7 @@ import {google} from '../protos/protos'; import CreateDatabaseMetadata = google.spanner.admin.database.v1.CreateDatabaseMetadata; import CreateBackupMetadata = google.spanner.admin.database.v1.CreateBackupMetadata; import CreateInstanceConfigMetadata = google.spanner.admin.instance.v1.CreateInstanceConfigMetadata; +import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; const singer = require('../test/data/singer'); const music = singer.examples.spanner.music; import {util} from 'protobufjs'; @@ -9152,7 +9153,7 @@ describe('Spanner', () => { it('GOOGLE_STANDARD_SQL should use getTransaction for executing sql', async () => { const transaction = ( - await DATABASE.getTransaction({optimisticLock: true}) + await DATABASE.getTransaction({readLockMode: ReadLockMode.OPTIMISTIC}) )[0]; try { diff --git a/test/database.ts b/test/database.ts index d666b2dce..04e485903 100644 --- a/test/database.ts +++ b/test/database.ts @@ -3402,6 +3402,17 @@ describe('Database', () => { assert.strictEqual(options, fakeOptions); }); + it('should optionally accept runner `option` readLockMode', async () => { + const fakeOptions = { + readLockMode: ReadLockMode.PESSIMISTIC, + }; + + await database.runTransaction(fakeOptions, assert.ifError); + + const options = fakeTransactionRunner.calledWith_[3]; + assert.strictEqual(options, fakeOptions); + }); + it('should release the session when finished', done => { const releaseStub = ( sandbox.stub(fakeSessionFactory, 'release') as sinon.SinonStub @@ -3519,6 +3530,17 @@ describe('Database', () => { assert.strictEqual(options, fakeOptions); }); + it('should optionally accept runner `option` readLockMode', async () => { + const fakeOptions = { + readLockMode: ReadLockMode.PESSIMISTIC, + }; + + await database.runTransactionAsync(fakeOptions, assert.ifError); + + const options = fakeAsyncTransactionRunner.calledWith_[3]; + assert.strictEqual(options, fakeOptions); + }); + it('should return the runners resolved value', async () => { const fakeValue = {}; diff --git a/test/index.ts b/test/index.ts index 05d5b0aa2..c578247d2 100644 --- a/test/index.ts +++ b/test/index.ts @@ -40,6 +40,7 @@ import { import {CLOUD_RESOURCE_HEADER, AFE_SERVER_TIMING_HEADER} from '../src/common'; import {MetricsTracerFactory} from '../src/metrics/metrics-tracer-factory'; import IsolationLevel = protos.google.spanner.v1.TransactionOptions.IsolationLevel; +import ReadLockMode = protos.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; const singer = require('./data/singer'); const music = singer.examples.spanner.music; @@ -357,6 +358,7 @@ describe('Spanner', () => { const fakeDefaultTxnOptions = { defaultTransactionOptions: { isolationLevel: IsolationLevel.REPEATABLE_READ, + readLockMode: ReadLockMode.PESSIMISTIC, }, }; diff --git a/test/spanner.ts b/test/spanner.ts index 3a042cfb8..d6f0ee062 100644 --- a/test/spanner.ts +++ b/test/spanner.ts @@ -78,6 +78,7 @@ import Priority = google.spanner.v1.RequestOptions.Priority; import TypeCode = google.spanner.v1.TypeCode; import NullValue = google.protobuf.NullValue; import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel; +import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; import {SessionFactory} from '../src/session-factory'; import {MultiplexedSession} from '../src/multiplexed-session'; import {WriteAtLeastOnceOptions} from '../src/database'; @@ -601,7 +602,7 @@ describe('Spanner with mock server', () => { const database = newTestDatabase(); await database.runTransactionAsync( { - optimisticLock: true, + readLockMode: ReadLockMode.OPTIMISTIC, requestOptions: {transactionTag: 'transaction-tag'}, }, async tx => { @@ -5000,7 +5001,7 @@ describe('Spanner with mock server', () => { const database = newTestDatabase(); await database.runTransactionAsync( { - optimisticLock: true, + readLockMode: ReadLockMode.OPTIMISTIC, }, async tx => { await tx!.run(selectSql); @@ -5105,7 +5106,7 @@ describe('Spanner with mock server', () => { const database = newTestDatabase(); await database.runTransactionAsync( { - optimisticLock: true, + readLockMode: ReadLockMode.OPTIMISTIC, excludeTxnFromChangeStreams: true, }, async tx => { @@ -5167,22 +5168,27 @@ describe('Spanner with mock server', () => { it('should use optimistic lock for runTransaction', done => { const database = newTestDatabase(); - database.runTransaction({optimisticLock: true}, async (err, tx) => { - assert.ifError(err); - await tx!.run(selectSql); - await tx!.commit(); - await database.close(); + database.runTransaction( + { + readLockMode: ReadLockMode.OPTIMISTIC, + }, + async (err, tx) => { + assert.ifError(err); + await tx!.run(selectSql); + await tx!.commit(); + await database.close(); - const request = spannerMock.getRequests().find(val => { - return (val as v1.ExecuteSqlRequest).sql; - }) as v1.ExecuteSqlRequest; - assert.ok(request, 'no ExecuteSqlRequest found'); - assert.strictEqual( - request.transaction!.begin!.readWrite!.readLockMode, - 'OPTIMISTIC', - ); - done(); - }); + const request = spannerMock.getRequests().find(val => { + return (val as v1.ExecuteSqlRequest).sql; + }) as v1.ExecuteSqlRequest; + assert.ok(request, 'no ExecuteSqlRequest found'); + assert.strictEqual( + request.transaction!.begin!.readWrite!.readLockMode, + 'OPTIMISTIC', + ); + done(); + }, + ); }); it('should use exclude transaction from change stream for runTransaction', done => { @@ -5234,7 +5240,7 @@ describe('Spanner with mock server', () => { it('should use optimistic lock and transaction tag for getTransaction', async () => { const database = newTestDatabase(); const promise = await database.getTransaction({ - optimisticLock: true, + readLockMode: ReadLockMode.OPTIMISTIC, requestOptions: {transactionTag: 'transaction-tag'}, }); const transaction = promise[0]; @@ -5275,11 +5281,16 @@ describe('Spanner with mock server', () => { const database = newTestDatabase({min: 1, max: 1}); let session1; let session2; - await database.runTransactionAsync({optimisticLock: true}, async tx => { - session1 = tx!.session.id; - await tx!.run(selectSql); - await tx.commit(); - }); + await database.runTransactionAsync( + { + readLockMode: ReadLockMode.OPTIMISTIC, + }, + async tx => { + session1 = tx!.session.id; + await tx!.run(selectSql); + await tx.commit(); + }, + ); spannerMock.resetRequests(); await database.runTransactionAsync(async tx => { session2 = tx!.session.id; @@ -5435,15 +5446,20 @@ describe('Spanner with mock server', () => { it('should use beginTransaction on retry with optimistic lock', async () => { const database = newTestDatabase(); let attempts = 0; - await database.runTransactionAsync({optimisticLock: true}, async tx => { - await tx!.run(selectSql); - if (!attempts) { - spannerMock.abortTransaction(tx); - } - attempts++; - await tx!.run(insertSql); - await tx.commit(); - }); + await database.runTransactionAsync( + { + readLockMode: ReadLockMode.OPTIMISTIC, + }, + async tx => { + await tx!.run(selectSql); + if (!attempts) { + spannerMock.abortTransaction(tx); + } + attempts++; + await tx!.run(insertSql); + await tx.commit(); + }, + ); await database.close(); const beginTxnRequest = spannerMock diff --git a/test/table.ts b/test/table.ts index 8d2ac7a26..9eb322621 100644 --- a/test/table.ts +++ b/test/table.ts @@ -28,6 +28,7 @@ import {TimestampBounds} from '../src/transaction'; import {google} from '../protos/protos'; import RequestOptions = google.spanner.v1.RequestOptions; import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel; +import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; let promisified = false; const fakePfy = extend({}, pfy, { @@ -408,6 +409,17 @@ describe('Table', () => { table.deleteRows(KEYS, deleteRowsOptions, assert.ifError); }); + it('should accept readLockMode option', done => { + const deleteRowsOptions = { + readLockMode: ReadLockMode.OPTIMISTIC, + }; + transaction.commit = options => { + assert.strictEqual(options, deleteRowsOptions); + done(); + }; + table.deleteRows(KEYS, deleteRowsOptions, assert.ifError); + }); + it('should delete the rows via transaction', done => { const stub = ( sandbox.stub(transaction, 'deleteRows') as sinon.SinonStub @@ -553,6 +565,22 @@ describe('Table', () => { table.insert(ROW, insertRowsOptions, assert.ifError); }); + + it('should accept readLockMode options', done => { + const insertRowsOptions = { + readLockMode: ReadLockMode.OPTIMISTIC, + }; + (sandbox.stub(transaction, 'insert') as sinon.SinonStub).withArgs( + table.name, + ROW, + ); + transaction.commit = options => { + assert.strictEqual(options, insertRowsOptions); + done(); + }; + + table.insert(ROW, insertRowsOptions, assert.ifError); + }); }); describe('read', () => { @@ -726,6 +754,22 @@ describe('Table', () => { table.replace(ROW, replaceRowsOptions, assert.ifError); }); + + it('should accept readLockMode options', done => { + const replaceRowsOptions = { + readLockMode: ReadLockMode.OPTIMISTIC, + }; + (sandbox.stub(transaction, 'replace') as sinon.SinonStub).withArgs( + table.name, + ROW, + ); + transaction.commit = options => { + assert.strictEqual(options, replaceRowsOptions); + done(); + }; + + table.replace(ROW, replaceRowsOptions, assert.ifError); + }); }); describe('update', () => { @@ -832,6 +876,22 @@ describe('Table', () => { table.update(ROW, updateRowsOptions, assert.ifError); }); + + it('should accept readLockMode option', done => { + const updateRowsOptions = { + readLockMode: ReadLockMode.OPTIMISTIC, + }; + (sandbox.stub(transaction, 'update') as sinon.SinonStub).withArgs( + table.name, + ROW, + ); + transaction.commit = options => { + assert.strictEqual(options, updateRowsOptions); + done(); + }; + + table.update(ROW, updateRowsOptions, assert.ifError); + }); }); describe('upsert', () => { @@ -938,5 +998,21 @@ describe('Table', () => { table.upsert(ROW, upsertRowsOptions, assert.ifError); }); + + it('should accept readLockMode option', done => { + const upsertRowsOptions = { + readLockMode: ReadLockMode.OPTIMISTIC, + }; + (sandbox.stub(transaction, 'upsert') as sinon.SinonStub).withArgs( + table.name, + ROW, + ); + transaction.commit = options => { + assert.strictEqual(options, upsertRowsOptions); + done(); + }; + + table.upsert(ROW, upsertRowsOptions, assert.ifError); + }); }); }); diff --git a/test/transaction-runner.ts b/test/transaction-runner.ts index 1aba522b2..bd3fac82c 100644 --- a/test/transaction-runner.ts +++ b/test/transaction-runner.ts @@ -24,6 +24,7 @@ import * as through from 'through2'; import {RunTransactionOptions} from '../src/transaction-runner'; import {google} from '../protos/protos'; import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel; +import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; import {randomUUID} from 'crypto'; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -120,6 +121,7 @@ describe('TransactionRunner', () => { const expectedOptions = { timeout: 3600000, isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED, }; assert.deepStrictEqual(runner.options, expectedOptions); @@ -127,7 +129,8 @@ describe('TransactionRunner', () => { it('should accept user `options`', () => { const options = { - isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + isolationLevel: IsolationLevel.SERIALIZABLE, + readLockMode: ReadLockMode.OPTIMISTIC, timeout: 1000, }; const r = new ExtendedRunner(SESSION, fakeTransaction, options); @@ -425,7 +428,8 @@ describe('TransactionRunner', () => { it('should pass `options` to `Runner`', () => { const options = { - isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + isolationLevel: IsolationLevel.REPEATABLE_READ, + readLockMode: ReadLockMode.PESSIMISTIC, timeout: 1, }; const r = new TransactionRunner( @@ -624,7 +628,8 @@ describe('TransactionRunner', () => { it('should pass `options` to `Runner`', () => { const options = { - isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + isolationLevel: IsolationLevel.REPEATABLE_READ, + readLockMode: ReadLockMode.OPTIMISTIC, timeout: 1, }; const r = new AsyncTransactionRunner( diff --git a/test/transaction.ts b/test/transaction.ts index 3e8a8f137..121c540ea 100644 --- a/test/transaction.ts +++ b/test/transaction.ts @@ -56,6 +56,7 @@ describe('Transaction', () => { directedReadOptions: {}, defaultTransactionOptions: { isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED, }, }; @@ -452,6 +453,7 @@ describe('Transaction', () => { directedReadOptions: fakeDirectedReadOptions, defaultTransactionOptions: { isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED, }, }; @@ -494,6 +496,7 @@ describe('Transaction', () => { directedReadOptions: fakeDirectedReadOptions, defaultTransactionOptions: { isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED, }, }; @@ -884,6 +887,7 @@ describe('Transaction', () => { directedReadOptions: fakeDirectedReadOptions, defaultTransactionOptions: { isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED, }, }; @@ -930,6 +934,7 @@ describe('Transaction', () => { directedReadOptions: fakeDirectedReadOptions, defaultTransactionOptions: { isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED, }, }; @@ -1655,12 +1660,14 @@ describe('Transaction', () => { transaction.begin(assert.ifError); }); - it('should set optimistic lock using useOptimisticLock', () => { + it('should set optimistic lock using setReadWriteTransactionOptions', () => { const rw = { readLockMode: ReadLockMode.OPTIMISTIC, }; transaction = new Transaction(SESSION); - transaction.useOptimisticLock(); + transaction.setReadWriteTransactionOptions({ + readLockMode: ReadLockMode.OPTIMISTIC, + }); const stub = sandbox.stub(transaction, 'request'); transaction.begin(); @@ -1679,18 +1686,45 @@ describe('Transaction', () => { ); }); - it('should set optimistic lock using setReadWriteTransactionOptions', () => { + it('should set repeatable read using setReadWriteTransactionOptions', () => { const rw = { - readLockMode: ReadLockMode.OPTIMISTIC, + readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED, }; transaction = new Transaction(SESSION); transaction.setReadWriteTransactionOptions({ - optimisticLock: ReadLockMode.OPTIMISTIC, + isolationLevel: IsolationLevel.REPEATABLE_READ, }); const stub = sandbox.stub(transaction, 'request'); transaction.begin(); - const expectedOptions = {isolationLevel: 0, readWrite: rw}; + const expectedOptions = {isolationLevel: 2, readWrite: rw}; + const {client, method, reqOpts, headers} = stub.lastCall.args[0]; + + assert.strictEqual(client, 'SpannerClient'); + assert.strictEqual(method, 'beginTransaction'); + assert.deepStrictEqual(reqOpts.options, expectedOptions); + assert.deepStrictEqual( + headers, + Object.assign( + {[LEADER_AWARE_ROUTING_HEADER]: true}, + transaction.commonHeaders_, + ), + ); + }); + + it('should set repeatable read isolation and pessimistic lock using setReadWriteTransactionOptions', () => { + const rw = { + readLockMode: ReadLockMode.PESSIMISTIC, + }; + transaction = new Transaction(SESSION); + transaction.setReadWriteTransactionOptions({ + isolationLevel: IsolationLevel.REPEATABLE_READ, + readLockMode: ReadLockMode.PESSIMISTIC, + }); + const stub = sandbox.stub(transaction, 'request'); + transaction.begin(); + + const expectedOptions = {isolationLevel: 2, readWrite: rw}; const {client, method, reqOpts, headers} = stub.lastCall.args[0]; assert.strictEqual(client, 'SpannerClient');