diff --git a/observability-test/database.ts b/observability-test/database.ts index 39ebe9afc..f8cc714fa 100644 --- a/observability-test/database.ts +++ b/observability-test/database.ts @@ -47,6 +47,7 @@ import {Instance, MutationGroup, Spanner} from '../src'; import * as pfy from '@google-cloud/promisify'; import {grpc} from 'google-gax'; import {MockError} from '../test/mockserver/mockspanner'; +import {FakeSessionFactory} from '../test/database'; const {generateWithAllSpansHaveDBName} = require('./helper'); const fakePfy = extend({}, pfy, { @@ -234,6 +235,7 @@ describe('Database', () => { './codec': {codec: fakeCodec}, './partial-result-stream': {partialResultStream: fakePartialResultStream}, './session-pool': {SessionPool: FakeSessionPool}, + './session-factory': {SessionFactory: FakeSessionFactory}, './session': {Session: FakeSession}, './table': {Table: FakeTable}, './transaction-runner': { diff --git a/src/database.ts b/src/database.ts index f98bb078d..edccf23c1 100644 --- a/src/database.ts +++ b/src/database.ts @@ -36,6 +36,7 @@ import { } from 'google-gax'; import {Backup} from './backup'; import {BatchTransaction, TransactionIdentifier} from './batch-transaction'; +import {SessionFactory, SessionFactoryInterface} from './session-factory'; import { google as databaseAdmin, google, @@ -111,7 +112,6 @@ import { setSpanErrorAndException, traceConfig, } from './instrument'; - export type GetDatabaseRolesCallback = RequestCallback< IDatabaseRole, databaseAdmin.spanner.admin.database.v1.IListDatabaseRolesResponse @@ -339,6 +339,7 @@ class Database extends common.GrpcServiceObject { private instance: Instance; formattedName_: string; pool_: SessionPoolInterface; + sessionFactory_: SessionFactoryInterface; queryOptions_?: spannerClient.spanner.v1.ExecuteSqlRequest.IQueryOptions; commonHeaders_: {[k: string]: string}; request: DatabaseRequest; @@ -450,15 +451,6 @@ class Database extends common.GrpcServiceObject { }, } as {} as ServiceObjectConfig); - this.pool_ = - typeof poolOptions === 'function' - ? new (poolOptions as SessionPoolConstructor)(this, null) - : new SessionPool(this, poolOptions); - const sessionPoolInstance = this.pool_ as SessionPool; - if (sessionPoolInstance) { - sessionPoolInstance._observabilityOptions = - instance._observabilityOptions; - } if (typeof poolOptions === 'object') { this.databaseRole = poolOptions.databaseRole || null; this.labels = poolOptions.labels || null; @@ -480,8 +472,13 @@ class Database extends common.GrpcServiceObject { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.requestStream = instance.requestStream as any; - this.pool_.on('error', this.emit.bind(this, 'error')); - this.pool_.open(); + this.sessionFactory_ = new SessionFactory(this, name, poolOptions); + this.pool_ = this.sessionFactory_.getPool(); + const sessionPoolInstance = this.pool_ as SessionPool; + if (sessionPoolInstance) { + sessionPoolInstance._observabilityOptions = + instance._observabilityOptions; + } this.queryOptions_ = Object.assign( Object.assign({}, queryOptions), Database.getEnvironmentQueryOptions() diff --git a/src/multiplexed-session.ts b/src/multiplexed-session.ts index 6b0deb71b..77f1cbb22 100644 --- a/src/multiplexed-session.ts +++ b/src/multiplexed-session.ts @@ -17,7 +17,7 @@ import {EventEmitter} from 'events'; import {Database} from './database'; import {Session} from './session'; -import {GetSessionCallback} from './session-pool'; +import {GetSessionCallback} from './session-factory'; import { ObservabilityOptions, getActiveOrNoopSpan, @@ -38,7 +38,7 @@ export const MUX_SESSION_CREATE_ERROR = 'mux-session-create-error'; * @constructs MultiplexedSessionInterface * @param {Database} database The database to create a multiplexed session for. */ -export interface MultiplexedSessionInterface { +export interface MultiplexedSessionInterface extends EventEmitter { /** * When called creates a multiplexed session. * @@ -71,6 +71,7 @@ export class MultiplexedSession database: Database; // frequency to create new mux session refreshRate: number; + isMultiplexedEnabled: boolean; _multiplexedSession: Session | null; _refreshHandle!: NodeJS.Timer; _observabilityOptions?: ObservabilityOptions; @@ -81,6 +82,9 @@ export class MultiplexedSession this.refreshRate = 7; this._multiplexedSession = null; this._observabilityOptions = database._observabilityOptions; + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS === 'true' + ? (this.isMultiplexedEnabled = true) + : (this.isMultiplexedEnabled = false); } /** diff --git a/src/session-factory.ts b/src/session-factory.ts new file mode 100644 index 000000000..a047dff0d --- /dev/null +++ b/src/session-factory.ts @@ -0,0 +1,161 @@ +/*! + * Copyright 2024 Google LLC. All Rights Reserved. + * + * 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. + */ + +import {Database, Session, Transaction} from '.'; +import { + MultiplexedSession, + MultiplexedSessionInterface, +} from './multiplexed-session'; +import { + SessionPool, + SessionPoolInterface, + SessionPoolOptions, +} from './session-pool'; +import {SessionPoolConstructor} from './database'; +import {ServiceObjectConfig} from '@google-cloud/common'; +const common = require('./common-grpc/service-object'); + +/** + * @callback GetSessionCallback + * @param {?Error} error Request error, if any. + * @param {Session} session The read-write session. + * @param {Transaction} transaction The transaction object. + */ +export interface GetSessionCallback { + ( + err: Error | null, + session?: Session | null, + transaction?: Transaction | null + ): void; +} + +/** + * Interface for implementing session-factory logic. + * + * @interface SessionFactoryInterface + */ +export interface SessionFactoryInterface { + /** + * When called returns a session. + * + * @name SessionFactoryInterface#getSession + * @param {GetSessionCallback} callback The callback function. + */ + getSession(callback: GetSessionCallback): void; + + /** + * When called returns the pool object. + * + * @name SessionFactoryInterface#getPool + */ + getPool(): SessionPoolInterface; + + /** + * To be called when releasing a session. + * + * @name SessionFactoryInterface#release + * @param {Session} session The session to be released. + */ + release(session: Session): void; +} + +/** + * Creates a SessionFactory object to manage the creation of + * session-pool and multiplexed session. + * + * @class + * + * @param {Database} database Database object. + * @param {String} name Name of the database. + * @param {SessionPoolOptions|SessionPoolInterface} options Session pool + * configuration options or custom pool inteface. + */ +export class SessionFactory + extends common.GrpcServiceObject + implements SessionFactoryInterface +{ + multiplexedSession_: MultiplexedSessionInterface; + pool_: SessionPoolInterface; + constructor( + database: Database, + name: String, + poolOptions?: SessionPoolConstructor | SessionPoolOptions + ) { + super({ + parent: database, + id: name, + } as {} as ServiceObjectConfig); + this.pool_ = + typeof poolOptions === 'function' + ? new (poolOptions as SessionPoolConstructor)(database, null) + : new SessionPool(database, poolOptions); + this.pool_.on('error', this.emit.bind(database, 'error')); + this.pool_.open(); + this.multiplexedSession_ = new MultiplexedSession(database); + // Multiplexed sessions should only be created if its enabled. + if ((this.multiplexedSession_ as MultiplexedSession).isMultiplexedEnabled) { + this.multiplexedSession_.on('error', this.emit.bind(database, 'error')); + this.multiplexedSession_.createSession(); + } + } + + /** + * Retrieves a session, either a regular session or a multiplexed session, based on the environment variable configuration. + * + * If the environment variable `GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS` is set to `true`, the method will attempt to + * retrieve a multiplexed session. Otherwise, it will retrieve a session from the regular pool. + * + * @param {GetSessionCallback} callback The callback function. + */ + + getSession(callback: GetSessionCallback): void { + const sessionHandler = (this.multiplexedSession_ as MultiplexedSession) + .isMultiplexedEnabled + ? this.multiplexedSession_ + : this.pool_; + + sessionHandler!.getSession((err, session) => callback(err, session)); + } + + /** + * Returns the regular session pool object. + * + * @returns {SessionPoolInterface} The session pool used by current instance. + */ + + getPool(): SessionPoolInterface { + return this.pool_; + } + + /** + * Releases a session back to the session pool. + * + * This method returns a session to the pool after it is no longer needed. + * It is a no-op for multiplexed sessions. + * + * @param {Session} session - The session to be released. This should be an instance of `Session` that was + * previously acquired from the session pool. + * + * @throws {Error} If the session is invalid or cannot be released. + */ + release(session: Session): void { + if ( + !(this.multiplexedSession_ as MultiplexedSession).isMultiplexedEnabled + ) { + this.pool_.release(session); + } + } +} diff --git a/src/session-pool.ts b/src/session-pool.ts index 9b75cdb9e..025216937 100644 --- a/src/session-pool.ts +++ b/src/session-pool.ts @@ -30,7 +30,7 @@ import { setSpanErrorAndException, startTrace, } from './instrument'; - +import {GetSessionCallback} from './session-factory'; import { isDatabaseNotFoundError, isInstanceNotFoundError, @@ -59,20 +59,6 @@ export interface GetWriteSessionCallback { ): void; } -/** - * @callback GetSessionCallback - * @param {?Error} error Request error, if any. - * @param {Session} session The read-write session. - * @param {Transaction} transaction The transaction object. - */ -export interface GetSessionCallback { - ( - err: Error | null, - session?: Session | null, - transaction?: Transaction | null - ): void; -} - /** * Interface for implementing custom session pooling logic, it should extend the * {@link https://nodejs.org/api/events.html|EventEmitter} class and emit any diff --git a/test/database.ts b/test/database.ts index 4f15fc93a..5b3ba6c97 100644 --- a/test/database.ts +++ b/test/database.ts @@ -46,7 +46,7 @@ import { CommitOptions, MutationSet, } from '../src/transaction'; - +import {SessionFactory} from '../src/session-factory'; let promisified = false; const fakePfy = extend({}, pfy, { promisifyAll(klass, options) { @@ -78,7 +78,7 @@ class FakeBatchTransaction { } } -class FakeGrpcServiceObject extends EventEmitter { +export class FakeGrpcServiceObject extends EventEmitter { calledWith_: IArguments; constructor() { super(); @@ -91,7 +91,7 @@ function fakePartialResultStream(this: Function & {calledWith_: IArguments}) { return this; } -class FakeSession { +export class FakeSession { calledWith_: IArguments; formattedName_: any; constructor() { @@ -109,7 +109,7 @@ class FakeSession { } } -class FakeSessionPool extends EventEmitter { +export class FakeSessionPool extends EventEmitter { calledWith_: IArguments; constructor() { super(); @@ -120,6 +120,37 @@ class FakeSessionPool extends EventEmitter { release() {} } +export class FakeMultiplexedSession extends EventEmitter { + calledWith_: IArguments; + constructor() { + super(); + this.calledWith_ = arguments; + } + createSession() {} + getSession() {} +} + +export class FakeSessionFactory extends EventEmitter { + calledWith_: IArguments; + constructor() { + super(); + this.calledWith_ = arguments; + } + getSession(): FakeSession | FakeMultiplexedSession { + if (process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS === 'false') { + return new FakeSession(); + } else { + return new FakeMultiplexedSession(); + } + } + getPool(): FakeSessionPool { + return new FakeSessionPool(); + } + getMultiplexedSession(): FakeMultiplexedSession { + return new FakeMultiplexedSession(); + } +} + class FakeTable { calledWith_: IArguments; constructor() { @@ -243,6 +274,7 @@ describe('Database', () => { './codec': {codec: fakeCodec}, './partial-result-stream': {partialResultStream: fakePartialResultStream}, './session-pool': {SessionPool: FakeSessionPool}, + './session-factory': {SessionFactory: FakeSessionFactory}, './session': {Session: FakeSession}, './table': {Table: FakeTable}, './transaction-runner': { @@ -295,43 +327,40 @@ describe('Database', () => { assert(database.formattedName_, formattedName); }); - it('should create a SessionPool object', () => { - assert(database.pool_ instanceof FakeSessionPool); - assert.strictEqual(database.pool_.calledWith_[0], database); - assert.strictEqual(database.pool_.calledWith_[1], POOL_OPTIONS); - }); - it('should accept a custom Pool class', () => { function FakePool() {} - FakePool.prototype.on = util.noop; - FakePool.prototype.open = util.noop; - const database = new Database( INSTANCE, NAME, FakePool as {} as db.SessionPoolConstructor ); - assert(database.pool_ instanceof FakePool); + assert(database.pool_ instanceof FakeSessionPool); }); it('should re-emit SessionPool errors', done => { const error = new Error('err'); + const sessionFactory = new SessionFactory(database, NAME); + database.on('error', err => { assert.strictEqual(err, error); done(); }); - database.pool_.emit('error', error); + sessionFactory.pool_.emit('error', error); }); - it('should open the pool', done => { - FakeSessionPool.prototype.open = () => { - FakeSessionPool.prototype.open = util.noop; - done(); - }; + it('should re-emit Multiplexed Session errors', done => { + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'true'; + const error = new Error('err'); + + const sessionFactory = new SessionFactory(database, NAME); - new Database(INSTANCE, NAME); + database.on('error', err => { + assert.strictEqual(err, error); + done(); + }); + sessionFactory.multiplexedSession_?.emit('error', error); }); it('should inherit from ServiceObject', done => { diff --git a/test/multiplexed-session.ts b/test/multiplexed-session.ts index ef71f7304..5e78d04ce 100644 --- a/test/multiplexed-session.ts +++ b/test/multiplexed-session.ts @@ -72,6 +72,24 @@ describe('MultiplexedSession', () => { assert.deepStrictEqual(multiplexedSession._multiplexedSession, null); assert(multiplexedSession instanceof events.EventEmitter); }); + + it('should correctly initialize the isMultiplexedEnabled field when GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS is enabled', () => { + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'true'; + const multiplexedSession = new MultiplexedSession(DATABASE); + assert.strictEqual( + (multiplexedSession as MultiplexedSession).isMultiplexedEnabled, + true + ); + }); + + it('should correctly initialize the isMultiplexedEnabled field when GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS is disabled', () => { + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'false'; + const multiplexedSession = new MultiplexedSession(DATABASE); + assert.strictEqual( + (multiplexedSession as MultiplexedSession).isMultiplexedEnabled, + false + ); + }); }); describe('createSession', () => { diff --git a/test/session-factory.ts b/test/session-factory.ts new file mode 100644 index 000000000..47aa231e8 --- /dev/null +++ b/test/session-factory.ts @@ -0,0 +1,253 @@ +/*! + * Copyright 2024 Google LLC. All Rights Reserved. + * + * 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. + */ + +import {Database, Session, SessionPool} from '../src'; +import {SessionFactory} from '../src/session-factory'; +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import {MultiplexedSession} from '../src/multiplexed-session'; +import {util} from '@google-cloud/common'; +import * as db from '../src/database'; +import {FakeTransaction} from './session-pool'; +import {ReleaseError} from '../src/session-pool'; + +describe('SessionFactory', () => { + let sessionFactory; + let fakeSession; + let fakeMuxSession; + const sandbox = sinon.createSandbox(); + const NAME = 'table-name'; + const POOL_OPTIONS = {}; + function noop() {} + const DATABASE = { + createSession: noop, + batchCreateSessions: noop, + databaseRole: 'parent_role', + } as unknown as Database; + + const createMuxSession = (name = 'id', props?): Session => { + props = props || {multiplexed: true}; + + return Object.assign(new Session(DATABASE, name), props, { + create: sandbox.stub().resolves(), + transaction: sandbox.stub().returns(new FakeTransaction()), + }); + }; + + const createSession = (name = 'id', props?): Session => { + props = props || {}; + + return Object.assign(new Session(DATABASE, name), props, { + create: sandbox.stub().resolves(), + transaction: sandbox.stub().returns(new FakeTransaction()), + }); + }; + + beforeEach(() => { + fakeSession = createSession(); + fakeMuxSession = createMuxSession(); + sandbox.stub(DATABASE, 'batchCreateSessions').callsFake(() => { + return Promise.resolve([[fakeSession, fakeSession, fakeSession]]); + }); + sandbox + .stub(DATABASE, 'createSession') + .withArgs({multiplexed: true}) + .callsFake(() => { + return Promise.resolve([fakeMuxSession]); + }); + sessionFactory = new SessionFactory(DATABASE, NAME, POOL_OPTIONS); + sessionFactory.parent = DATABASE; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('instantiation', () => { + describe('when GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS is disabled', () => { + before(() => { + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'false'; + }); + + it('should create a SessionPool object', () => { + assert(sessionFactory.pool_ instanceof SessionPool); + }); + + it('should accept a custom Pool class', () => { + function FakePool() {} + FakePool.prototype.on = util.noop; + FakePool.prototype.open = util.noop; + + const sessionFactory = new SessionFactory( + DATABASE, + NAME, + FakePool as {} as db.SessionPoolConstructor + ); + assert(sessionFactory.pool_ instanceof FakePool); + }); + + it('should open the pool', () => { + const openStub = sandbox + .stub(SessionPool.prototype, 'open') + .callsFake(() => {}); + + new SessionFactory(DATABASE, NAME, POOL_OPTIONS); + + assert.strictEqual(openStub.callCount, 1); + }); + }); + + describe('when GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS is enabled', () => { + before(() => { + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'true'; + }); + + it('should create a MultiplexedSession object', () => { + assert( + sessionFactory.multiplexedSession_ instanceof MultiplexedSession + ); + }); + + it('should initiate the multiplexed session creation', () => { + const createSessionStub = sandbox + .stub(MultiplexedSession.prototype, 'createSession') + .callsFake(() => {}); + + new SessionFactory(DATABASE, NAME, POOL_OPTIONS); + + assert.strictEqual(createSessionStub.callCount, 1); + }); + }); + }); + + describe('getSession', () => { + describe('when GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS is disabled', () => { + before(() => { + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'false'; + }); + + it('should retrieve a regular session from the pool', done => { + ( + sandbox.stub(sessionFactory.pool_, 'getSession') as sinon.SinonStub + ).callsFake(callback => callback(null, fakeSession)); + sessionFactory.getSession((err, resp) => { + assert.strictEqual(err, null); + assert.strictEqual(resp, fakeSession); + done(); + }); + }); + + it('should propagate errors when regular session retrieval fails', done => { + const fakeError = new Error(); + ( + sandbox.stub(sessionFactory.pool_, 'getSession') as sinon.SinonStub + ).callsFake(callback => callback(fakeError, null)); + sessionFactory.getSession((err, resp) => { + assert.strictEqual(err, fakeError); + assert.strictEqual(resp, null); + done(); + }); + }); + }); + + describe('when GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS is enabled', () => { + before(() => { + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'true'; + }); + + it('should return the multiplexed session', done => { + ( + sandbox.stub( + sessionFactory.multiplexedSession_, + 'getSession' + ) as sinon.SinonStub + ).callsFake(callback => callback(null, fakeMuxSession)); + sessionFactory.getSession((err, resp) => { + assert.strictEqual(err, null); + assert.strictEqual(resp, fakeMuxSession); + assert.strictEqual(resp?.multiplexed, true); + assert.strictEqual(fakeMuxSession.multiplexed, true); + done(); + }); + }); + + it('should propagate error when multiplexed session return fails', done => { + const fakeError = new Error(); + ( + sandbox.stub( + sessionFactory.multiplexedSession_, + 'getSession' + ) as sinon.SinonStub + ).callsFake(callback => callback(fakeError, null)); + sessionFactory.getSession((err, resp) => { + assert.strictEqual(err, fakeError); + assert.strictEqual(resp, null); + done(); + }); + }); + }); + }); + + describe('getPool', () => { + it('should return the session pool object', () => { + const pool = sessionFactory.getPool(); + assert(pool instanceof SessionPool); + assert.deepStrictEqual(pool, sessionFactory.pool_); + }); + }); + + describe('release', () => { + describe('when GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS is enabled', () => { + before(() => { + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'true'; + }); + + it('should not call the release method', () => { + const releaseStub = sandbox.stub(sessionFactory.pool_, 'release'); + const fakeSession = createSession(); + sessionFactory.release(fakeSession); + assert.strictEqual(releaseStub.callCount, 0); + }); + }); + + describe('when GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS is disabled', () => { + before(() => { + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'false'; + }); + + it('should call the release method to release a regular session', () => { + const releaseStub = sandbox.stub(sessionFactory.pool_, 'release'); + const fakeSession = createSession(); + sessionFactory.release(fakeSession); + assert.strictEqual(releaseStub.callCount, 1); + }); + + it('should propagate an error when release fails', () => { + const fakeSession = createSession(); + try { + sessionFactory.release(fakeSession); + assert.fail('Expected error was not thrown'); + } catch (error) { + assert.strictEqual( + (error as ReleaseError).message, + 'Unable to release unknown resource.' + ); + assert.strictEqual((error as ReleaseError).resource, fakeSession); + } + }); + }); + }); +}); diff --git a/test/spanner.ts b/test/spanner.ts index dcc8a3300..b6ee01024 100644 --- a/test/spanner.ts +++ b/test/spanner.ts @@ -66,6 +66,7 @@ import protobuf = google.spanner.v1; import Priority = google.spanner.v1.RequestOptions.Priority; import TypeCode = google.spanner.v1.TypeCode; import NullValue = google.protobuf.NullValue; +import {SessionFactory} from '../src/session-factory'; const { AlwaysOnSampler, @@ -5075,6 +5076,23 @@ describe('Spanner with mock server', () => { done(); }); }); + + describe('session-factory', () => { + after(() => { + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'false'; + }); + + it('should not propagate any error when enabling GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS after client initialization', done => { + const database = newTestDatabase(); + // enable env after database creation + process.env.GOOGLE_CLOUD_SPANNER_MULTIPLEXED_SESSIONS = 'true'; + const sessionFactory = database.sessionFactory_ as SessionFactory; + sessionFactory.getSession((err, _) => { + assert.ifError(err); + done(); + }); + }); + }); }); function executeSimpleUpdate(