Skip to content

Commit c0a4363

Browse files
authored
feat: Support for Directed Reads (#1966)
1 parent 6c0eef1 commit c0a4363

17 files changed

+617
-18
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
**/node_modules
33
/.coverage
44
/coverage
5+
/.idea
6+
/.vscode
57
/.nyc_output
68
/docs/
79
/out/

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/nodejs-spanner/tre
112112
| Updates the default leader of an existing database | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/database-update-default-leader.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/database-update-default-leader.js,samples/README.md) |
113113
| Updates a Cloud Spanner Database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/database-update.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/database-update.js,samples/README.md) |
114114
| Datatypes | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/datatypes.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/datatypes.js,samples/README.md) |
115+
| Runs an execute sql request with directed read options | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/directed-reads.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/directed-reads.js,samples/README.md) |
115116
| Delete using DML returning. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-delete.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml-returning-delete.js,samples/README.md) |
116117
| Insert using DML returning. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-insert.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml-returning-insert.js,samples/README.md) |
117118
| Update using DML returning. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-update.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml-returning-update.js,samples/README.md) |

protos/protos.d.ts

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

protos/protos.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/README.md

+18
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ and automatic, synchronous replication for high availability.
3737
* [Updates the default leader of an existing database](#updates-the-default-leader-of-an-existing-database)
3838
* [Updates a Cloud Spanner Database.](#updates-a-cloud-spanner-database.)
3939
* [Datatypes](#datatypes)
40+
* [Runs an execute sql request with directed read options](#runs-an-execute-sql-request-with-directed-read-options)
4041
* [Delete using DML returning.](#delete-using-dml-returning.)
4142
* [Insert using DML returning.](#insert-using-dml-returning.)
4243
* [Update using DML returning.](#update-using-dml-returning.)
@@ -517,6 +518,23 @@ __Usage:__
517518

518519

519520

521+
### Runs an execute sql request with directed read options
522+
523+
View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/directed-reads.js).
524+
525+
[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/directed-reads.js,samples/README.md)
526+
527+
__Usage:__
528+
529+
530+
`node directed-reads.js <INSTANCE_ID> <DATABASE_ID> <PROJECT_ID>`
531+
532+
533+
-----
534+
535+
536+
537+
520538
### Delete using DML returning.
521539

522540
View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-delete.js).

samples/directed-reads.js

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// sample-metadata:
16+
// title: Runs an execute sql request with directed read options
17+
// usage: node directed-reads.js <INSTANCE_ID> <DATABASE_ID> <PROJECT_ID>
18+
19+
'use strict';
20+
21+
function main(
22+
instanceId = 'my-instance',
23+
databaseId = 'my-database',
24+
projectId = 'my-project-id'
25+
) {
26+
// [START spanner_directed_read]
27+
// Imports the Google Cloud Spanner client library
28+
const {Spanner, protos} = require('@google-cloud/spanner');
29+
30+
// Only one of excludeReplicas or includeReplicas can be set
31+
// Each accepts a list of replicaSelections which contains location and type
32+
// * `location` - The location must be one of the regions within the
33+
// multi-region configuration of your database.
34+
// * `type` - The type of the replica
35+
// Some examples of using replicaSelectors are:
36+
// * `location:us-east1` --> The "us-east1" replica(s) of any available type
37+
// will be used to process the request.
38+
// * `type:READ_ONLY` --> The "READ_ONLY" type replica(s) in nearest
39+
//. available location will be used to process the
40+
// request.
41+
// * `location:us-east1 type:READ_ONLY` --> The "READ_ONLY" type replica(s)
42+
// in location "us-east1" will be used to process
43+
// the request.
44+
// includeReplicas also contains an option for autoFailover which when set
45+
// Spanner will not route requests to a replica outside the
46+
// includeReplicas list when all the specified replicas are unavailable
47+
// or unhealthy. The default value is `false`
48+
const directedReadOptionsForClient = {
49+
excludeReplicas: {
50+
replicaSelections: [
51+
{
52+
location: 'us-east4',
53+
},
54+
],
55+
},
56+
};
57+
58+
// Instantiates a client with directedReadOptions
59+
const spanner = new Spanner({
60+
projectId: projectId,
61+
directedReadOptions: directedReadOptionsForClient,
62+
});
63+
64+
async function spannerDirectedReads() {
65+
// Gets a reference to a Cloud Spanner instance and backup
66+
const instance = spanner.instance(instanceId);
67+
const database = instance.database(databaseId);
68+
const directedReadOptionsForRequest = {
69+
includeReplicas: {
70+
replicaSelections: [
71+
{
72+
type: protos.google.spanner.v1.DirectedReadOptions.ReplicaSelection
73+
.Type.READ_ONLY,
74+
},
75+
],
76+
autoFailoverDisabled: true,
77+
},
78+
};
79+
80+
await database.getSnapshot(async (err, transaction) => {
81+
if (err) {
82+
console.error(err);
83+
return;
84+
}
85+
try {
86+
// Read rows while passing directedReadOptions directly to the query.
87+
// These will override the options passed at Client level.
88+
const [rows] = await transaction.run({
89+
sql: 'SELECT SingerId, AlbumId, AlbumTitle FROM Albums',
90+
directedReadOptions: directedReadOptionsForRequest,
91+
});
92+
rows.forEach(row => {
93+
const json = row.toJSON();
94+
console.log(
95+
`SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`
96+
);
97+
});
98+
console.log(
99+
'Successfully executed read-only transaction with directedReadOptions'
100+
);
101+
} catch (err) {
102+
console.error('ERROR:', err);
103+
} finally {
104+
transaction.end();
105+
// Close the database when finished.
106+
await database.close();
107+
}
108+
});
109+
}
110+
spannerDirectedReads();
111+
// [END spanner_directed_read]
112+
}
113+
process.on('unhandledRejection', err => {
114+
console.error(err.message);
115+
process.exitCode = 1;
116+
});
117+
main(...process.argv.slice(2));

samples/system-test/spanner.test.js

+20
Original file line numberDiff line numberDiff line change
@@ -1985,5 +1985,25 @@ describe('Spanner', () => {
19851985
)
19861986
);
19871987
});
1988+
1989+
// directed_read_options
1990+
it('should run read-only transaction with directed read options set', async () => {
1991+
const output = execSync(
1992+
`node directed-reads.js ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}`
1993+
);
1994+
console.log(output);
1995+
assert.match(
1996+
output,
1997+
new RegExp(
1998+
'SingerId: 2, AlbumId: 2, AlbumTitle: Forever Hold your Peace'
1999+
)
2000+
);
2001+
assert.match(
2002+
output,
2003+
new RegExp(
2004+
'Successfully executed read-only transaction with directedReadOptions'
2005+
)
2006+
);
2007+
});
19882008
});
19892009
});

src/batch-transaction.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import * as extend from 'extend';
2020
import * as is from 'is';
2121
import {Snapshot} from './transaction';
2222
import {google} from '../protos/protos';
23-
import {Session, Database, Spanner} from '.';
23+
import {Session, Database} from '.';
2424
import {
2525
CLOUD_RESOURCE_HEADER,
2626
addLeaderAwareRoutingHeader,

src/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,17 @@ export type GetInstanceConfigOperationsCallback = PagedCallback<
110110
* Session pool configuration options.
111111
* @property {boolean} [routeToLeaderEnabled=True] If set to false leader aware routing will be disabled.
112112
* Disabling leader aware routing would route all requests in RW/PDML transactions to any region.
113+
* @property {google.spanner.v1.IDirectedReadOptions} [directedReadOptions] Sets the DirectedReadOptions for all ReadRequests and ExecuteSqlRequests for the Client.
114+
* Indicates which replicas or regions should be used for non-transactional reads or queries.
115+
* DirectedReadOptions won't be set for readWrite transactions"
113116
*/
114117
export interface SpannerOptions extends GrpcClientOptions {
115118
apiEndpoint?: string;
116119
servicePath?: string;
117120
port?: number;
118121
sslCreds?: grpc.ChannelCredentials;
119122
routeToLeaderEnabled?: boolean;
123+
directedReadOptions?: google.spanner.v1.IDirectedReadOptions | null;
120124
}
121125
export interface RequestConfig {
122126
client: string;
@@ -217,6 +221,7 @@ class Spanner extends GrpcService {
217221
projectFormattedName_: string;
218222
resourceHeader_: {[k: string]: string};
219223
routeToLeaderEnabled = true;
224+
directedReadOptions: google.spanner.v1.IDirectedReadOptions | null;
220225

221226
/**
222227
* Placeholder used to auto populate a column with the commit timestamp.
@@ -291,6 +296,12 @@ class Spanner extends GrpcService {
291296
},
292297
options || {}
293298
) as {} as SpannerOptions;
299+
300+
const directedReadOptions = options.directedReadOptions
301+
? options.directedReadOptions
302+
: null;
303+
delete options.directedReadOptions;
304+
294305
const emulatorHost = Spanner.getSpannerEmulatorHost();
295306
if (
296307
emulatorHost &&
@@ -332,6 +343,7 @@ class Spanner extends GrpcService {
332343
this.resourceHeader_ = {
333344
[CLOUD_RESOURCE_HEADER]: this.projectFormattedName_,
334345
};
346+
this.directedReadOptions = directedReadOptions;
335347
}
336348

337349
/** Closes this Spanner client and cleans up all resources used by it. */

src/session-pool.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,9 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
10811081
* @private
10821082
*/
10831083
_stopHouseKeeping(): void {
1084+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10841085
clearInterval(this._pingHandle as any);
1086+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10851087
clearInterval(this._evictHandle as any);
10861088
}
10871089
}

src/transaction.ts

+40
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export interface ExecuteSqlRequest extends Statement, RequestOptions {
8686
queryOptions?: IQueryOptions;
8787
requestOptions?: Omit<IRequestOptions, 'transactionTag'>;
8888
dataBoostEnabled?: boolean | null;
89+
directedReadOptions?: google.spanner.v1.IDirectedReadOptions;
8990
}
9091

9192
export interface KeyRange {
@@ -107,6 +108,7 @@ export interface ReadRequest extends RequestOptions {
107108
partitionToken?: Uint8Array | null;
108109
requestOptions?: Omit<IRequestOptions, 'transactionTag'>;
109110
dataBoostEnabled?: boolean | null;
111+
directedReadOptions?: google.spanner.v1.IDirectedReadOptions;
110112
}
111113

112114
export interface BatchUpdateError extends grpc.ServiceError {
@@ -457,6 +459,8 @@ export class Snapshot extends EventEmitter {
457459
* PartitionReadRequest message used to create this partition_token.
458460
* @property {google.spanner.v1.RequestOptions} [requestOptions]
459461
* Common options for this request.
462+
* @property {google.spanner.v1.IDirectedReadOptions} [directedReadOptions]
463+
* Indicates which replicas or regions should be used for non-transactional reads or queries.
460464
* @property {object} [gaxOptions]
461465
* Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions}
462466
* for more details.
@@ -591,6 +595,10 @@ export class Snapshot extends EventEmitter {
591595
transaction.singleUse = this._options;
592596
}
593597

598+
const directedReadOptions = this._getDirectedReadOptions(
599+
request.directedReadOptions
600+
);
601+
594602
request = Object.assign({}, request);
595603

596604
delete request.gaxOptions;
@@ -600,6 +608,7 @@ export class Snapshot extends EventEmitter {
600608
delete request.keys;
601609
delete request.ranges;
602610
delete request.requestOptions;
611+
delete request.directedReadOptions;
603612

604613
const reqOpts: spannerClient.spanner.v1.IReadRequest = Object.assign(
605614
request,
@@ -610,6 +619,7 @@ export class Snapshot extends EventEmitter {
610619
this.requestOptions?.transactionTag ?? undefined,
611620
requestOptions
612621
),
622+
directedReadOptions: directedReadOptions,
613623
transaction,
614624
table,
615625
keySet,
@@ -993,6 +1003,8 @@ export class Snapshot extends EventEmitter {
9931003
* that it is not ready for any more data. Increase this value if you
9941004
* experience 'Stream is still not ready to receive data' errors as a
9951005
* result of a slow writer in your receiving stream.
1006+
* @property {object} [directedReadOptions]
1007+
* Indicates which replicas or regions should be used for non-transactional reads or queries.
9961008
*/
9971009
/**
9981010
* Create a readable object stream to receive resulting rows from a SQL
@@ -1068,6 +1080,10 @@ export class Snapshot extends EventEmitter {
10681080
query;
10691081
let reqOpts;
10701082

1083+
const directedReadOptions = this._getDirectedReadOptions(
1084+
query.directedReadOptions
1085+
);
1086+
10711087
const sanitizeRequest = () => {
10721088
query = query as ExecuteSqlRequest;
10731089
const {params, paramTypes} = Snapshot.encodeParams(query);
@@ -1085,6 +1101,7 @@ export class Snapshot extends EventEmitter {
10851101
delete query.maxResumeRetries;
10861102
delete query.requestOptions;
10871103
delete query.types;
1104+
delete query.directedReadOptions;
10881105

10891106
reqOpts = Object.assign(query, {
10901107
session: this.session.formattedName_!,
@@ -1094,6 +1111,7 @@ export class Snapshot extends EventEmitter {
10941111
this.requestOptions?.transactionTag ?? undefined,
10951112
requestOptions
10961113
),
1114+
directedReadOptions: directedReadOptions,
10971115
transaction,
10981116
params,
10991117
paramTypes,
@@ -1288,6 +1306,28 @@ export class Snapshot extends EventEmitter {
12881306
return {params, paramTypes};
12891307
}
12901308

1309+
/**
1310+
* Get directed read options
1311+
* @private
1312+
* @param {google.spanner.v1.IDirectedReadOptions} directedReadOptions Request directedReadOptions object.
1313+
*/
1314+
protected _getDirectedReadOptions(
1315+
directedReadOptions:
1316+
| google.spanner.v1.IDirectedReadOptions
1317+
| null
1318+
| undefined
1319+
) {
1320+
if (
1321+
!directedReadOptions &&
1322+
this._getSpanner().directedReadOptions &&
1323+
this._options.readOnly
1324+
) {
1325+
return this._getSpanner().directedReadOptions;
1326+
}
1327+
1328+
return directedReadOptions;
1329+
}
1330+
12911331
/**
12921332
* Update transaction properties from the response.
12931333
*

0 commit comments

Comments
 (0)