Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit 3468cd9

Browse files
danieljbrucegcf-owl-bot[bot]Benjamin E. Coe
authored
feat: Multi cluster routing (#1007)
* Add third option in app profile config. * First test * Fix a test case that broke * test for error when list is not clusters * Fix linter errors * Add a create app profile for an array of clusters * linting fixes * Working system test * Remove unused imports * linting fix * Add to license * Update copyright year * PR fix 1 * linter fix * Use a set instead of an array * Refactor that will make test writing easier * Second refactor * Another refactor to shorten the test * before function is successful * create instance with clusters * Make individual test as short as possible * single and multiple cluster tests * Added a third test * test for creating a profile and modifying it * change a test case message * Reformatting test cases * linter fixes * First PR correction * PR fixes * Test for single cluster routing * A set of clusters * change test description * Added capability for an array of strings passed in * Checks if every element is a string * Error message update * linter fix * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * message change * Fixed regular expression * Removed unnecessary escape characters. * Fixed indent * update a description * grpc-gpc upgrade * Use the refactored generateId * fake cluster and types update * Changed generate id code fragment * Revert "Changed generate id code fragment" This reverts commit 7b862ef. Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com> Co-authored-by: Benjamin E. Coe <[email protected]>
1 parent f7fc934 commit 3468cd9

6 files changed

Lines changed: 272 additions & 20 deletions

File tree

src/app-profile.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export interface AppProfileOptions {
2929
* value is required when creating the app profile and optional when setting
3030
* the metadata.
3131
*/
32-
routing?: 'any' | Cluster;
32+
routing?: 'any' | Cluster | Set<Cluster> | Set<string>;
3333
/**
3434
* Whether or not CheckAndMutateRow and ReadModifyWriteRow requests are
3535
* allowed by this app profile. It is unsafe to send these requests to the
@@ -200,9 +200,26 @@ Please use the format 'my-app-profile' or '${instance.name}/appProfiles/my-app-p
200200
): google.bigtable.admin.v2.IAppProfile {
201201
const appProfile: google.bigtable.admin.v2.IAppProfile = {};
202202

203+
const errMessage =
204+
'An app profile routing policy can only contain "any" for multi cluster routing, a `Cluster` for single routing, or a set of clusterIds as strings or `Clusters` for multi cluster routing.';
203205
if (options.routing) {
204206
if (options.routing === 'any') {
205207
appProfile.multiClusterRoutingUseAny = {};
208+
} else if (options.routing instanceof Set) {
209+
const routingAsArray = [...options.routing];
210+
if (isClusterArray(routingAsArray)) {
211+
// Runs if routing is a set and every element in it is a cluster
212+
appProfile.multiClusterRoutingUseAny = {
213+
clusterIds: routingAsArray.map(cluster => cluster.id),
214+
};
215+
} else if (isStringArray(routingAsArray)) {
216+
// Runs if routing is a set and every element in it is a string
217+
appProfile.multiClusterRoutingUseAny = {
218+
clusterIds: routingAsArray,
219+
};
220+
} else {
221+
throw new Error(errMessage);
222+
}
206223
} else if (options.routing instanceof Cluster) {
207224
appProfile.singleClusterRouting = {
208225
clusterId: options.routing.id,
@@ -212,9 +229,7 @@ Please use the format 'my-app-profile' or '${instance.name}/appProfiles/my-app-p
212229
options.allowTransactionalWrites;
213230
}
214231
} else {
215-
throw new Error(
216-
'An app profile routing policy can only contain "any" or a `Cluster`.'
217-
);
232+
throw new Error(errMessage);
218233
}
219234
}
220235

@@ -498,6 +513,27 @@ Please use the format 'my-app-profile' or '${instance.name}/appProfiles/my-app-p
498513
}
499514
}
500515

516+
function isStringArray(array: any): array is string[] {
517+
return array.every((cluster: any) => {
518+
return typeof cluster === 'string';
519+
});
520+
}
521+
522+
function isClusterArray(array: any): array is Cluster[] {
523+
return array.every((cluster: any) => {
524+
return isCluster(cluster);
525+
});
526+
}
527+
528+
function isCluster(cluster: any): cluster is Cluster {
529+
return (
530+
(cluster as Cluster).bigtable !== undefined &&
531+
(cluster as Cluster).instance !== undefined &&
532+
(cluster as Cluster).id !== undefined &&
533+
(cluster as Cluster).name !== undefined
534+
);
535+
}
536+
501537
/*! Developer Documentation
502538
*
503539
* All async methods (except for streams) will return a Promise in the event

system-test/app-profile.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2022 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+
// https://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+
import {describe, it, before, after} from 'mocha';
16+
import {generateId} from './common';
17+
import {AppProfileOptions, Bigtable, Instance} from '../src';
18+
import {AppProfile} from '../src';
19+
import assert = require('assert');
20+
21+
describe('📦 App Profile', () => {
22+
const bigtable = new Bigtable();
23+
24+
describe('📦 Create a profile', () => {
25+
let instance: Instance;
26+
let clusterIds: string[];
27+
28+
// Creates an app profile and returns information containing the app profile response.
29+
async function createProfile(
30+
instance: Instance,
31+
options: AppProfileOptions
32+
): Promise<AppProfile> {
33+
const appProfileId = generateId('app-profile');
34+
await instance.createAppProfile(appProfileId, options);
35+
const appProfile = instance.appProfile(appProfileId);
36+
const getAppProfileResponse = await appProfile.get();
37+
return getAppProfileResponse[0];
38+
}
39+
40+
before(async () => {
41+
// Creates an instance with clusters
42+
const instanceClusters = [
43+
'us-east1-c',
44+
'us-central1-b',
45+
'us-west1-b',
46+
].map(location => {
47+
return {
48+
id: generateId('cluster'),
49+
location,
50+
};
51+
});
52+
clusterIds = instanceClusters.map(cluster => cluster.id);
53+
const instanceId = generateId('instance');
54+
instance = bigtable.instance(instanceId);
55+
const [, operation] = await instance.create({
56+
clusters: instanceClusters.map(cluster => {
57+
return {
58+
...cluster,
59+
nodes: 1,
60+
};
61+
}),
62+
labels: {
63+
time_created: Date.now(),
64+
},
65+
});
66+
await operation.promise();
67+
});
68+
69+
after(async () => {
70+
await instance.delete();
71+
});
72+
73+
it('should create a profile with a single cluster', async () => {
74+
const options = {
75+
routing: instance.cluster(clusterIds[1]),
76+
};
77+
const appProfile = await createProfile(instance, options);
78+
assert.deepStrictEqual(
79+
appProfile.metadata?.singleClusterRouting?.clusterId,
80+
options.routing.id
81+
);
82+
});
83+
84+
it('should create a profile with multiple clusters', async () => {
85+
const options = {
86+
routing: new Set([
87+
instance.cluster(clusterIds[1]),
88+
instance.cluster(clusterIds[2]),
89+
]),
90+
};
91+
const appProfile = await createProfile(instance, options);
92+
assert.deepStrictEqual(
93+
new Set(appProfile.metadata?.multiClusterRoutingUseAny?.clusterIds),
94+
new Set([...options.routing].map(cluster => cluster.id))
95+
);
96+
});
97+
98+
it('should create a profile with multiple clusters using strings', async () => {
99+
const options = {
100+
routing: new Set([clusterIds[1], clusterIds[2]]),
101+
};
102+
const appProfile = await createProfile(instance, options);
103+
assert.deepStrictEqual(
104+
new Set(appProfile.metadata?.multiClusterRoutingUseAny?.clusterIds),
105+
new Set([...options.routing])
106+
);
107+
});
108+
109+
it('should create a profile with no clusters', async () => {
110+
const options: {routing: 'any'} = {
111+
routing: 'any',
112+
};
113+
const appProfile = await createProfile(instance, options);
114+
assert.deepStrictEqual(
115+
appProfile.metadata?.multiClusterRoutingUseAny?.clusterIds,
116+
[]
117+
);
118+
});
119+
120+
it('should ensure clusters match an updated profile', async () => {
121+
const options = {
122+
routing: instance.cluster(clusterIds[1]),
123+
};
124+
const appProfile = await createProfile(instance, options);
125+
assert.deepStrictEqual(
126+
appProfile.metadata?.singleClusterRouting?.clusterId,
127+
clusterIds[1]
128+
);
129+
const newOptions = {
130+
routing: new Set([
131+
instance.cluster(clusterIds[1]),
132+
instance.cluster(clusterIds[2]),
133+
]),
134+
};
135+
await appProfile.setMetadata(newOptions);
136+
const appProfileAfterUpdate = (await appProfile.get())[0];
137+
assert.deepStrictEqual(
138+
new Set(
139+
appProfileAfterUpdate.metadata?.multiClusterRoutingUseAny?.clusterIds
140+
),
141+
new Set([...newOptions.routing].map(cluster => cluster.id))
142+
);
143+
});
144+
});
145+
});

system-test/bigtable.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {PreciseDate} from '@google-cloud/precise-date';
1717
import * as assert from 'assert';
1818
import {beforeEach, afterEach, describe, it, before, after} from 'mocha';
1919
import Q from 'p-queue';
20-
import * as uuid from 'uuid';
2120

2221
import {Backup, Bigtable, Instance} from '../src';
2322
import {AppProfile} from '../src/app-profile.js';
@@ -26,6 +25,7 @@ import {Family} from '../src/family.js';
2625
import {Row} from '../src/row.js';
2726
import {Table} from '../src/table.js';
2827
import {RawFilter} from '../src/filter';
28+
import {generateId} from './common';
2929

3030
const PREFIX = 'gcloud-tests-';
3131

@@ -1416,9 +1416,6 @@ describe('Bigtable', () => {
14161416
});
14171417
});
14181418

1419-
function generateId(resourceType: string) {
1420-
return PREFIX + resourceType + '-' + uuid.v1().substr(0, 8);
1421-
}
14221419
function createInstanceConfig(
14231420
clusterId: string,
14241421
location: string,

system-test/common.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2022 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+
// https://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+
import * as uuid from 'uuid';
16+
import {Cluster} from '../src/cluster';
17+
import * as inst from '../src/instance';
18+
19+
export const PREFIX = 'gcloud-tests-';
20+
21+
export function generateId(resourceType: string) {
22+
return PREFIX + resourceType + '-' + uuid.v1().substr(0, 8);
23+
}
24+
25+
export class FakeCluster extends Cluster {
26+
calledWith_: Array<{}>;
27+
constructor(...args: [inst.Instance, string]) {
28+
super(args[0], args[1]);
29+
this.calledWith_ = args;
30+
}
31+
}

test/app-profile.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,14 @@ describe('Bigtable/AppProfile', () => {
4747
instance: any;
4848
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4949
id: any;
50+
name: string;
51+
bigtable: any;
5052
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5153
constructor(instance: any, id: any) {
5254
this.instance = instance;
5355
this.id = id;
56+
this.name = 'cluster-name';
57+
this.bigtable = instance.bigtable;
5458
}
5559
}
5660

@@ -106,6 +110,9 @@ describe('Bigtable/AppProfile', () => {
106110
});
107111

108112
describe('formatAppProfile_', () => {
113+
const errorReg =
114+
/An app profile routing policy can only contain "any" for multi cluster routing, a `Cluster` for single routing, or a set of clusterIds as strings or `Clusters` for multi cluster routing\./;
115+
109116
it("should accept an 'any' cluster routing policy", () => {
110117
const formattedAppProfile = AppProfile.formatAppProfile_({
111118
routing: 'any',
@@ -125,6 +132,7 @@ describe('Bigtable/AppProfile', () => {
125132
clusterId,
126133
});
127134
});
135+
128136
it('should accept allowTransactionalWrites', () => {
129137
const formattedAppProfile = AppProfile.formatAppProfile_({
130138
routing: cluster,
@@ -145,9 +153,6 @@ describe('Bigtable/AppProfile', () => {
145153
});
146154

147155
it('should throw for an invalid routing policy', () => {
148-
const errorReg =
149-
/An app profile routing policy can only contain "any" or a `Cluster`\./;
150-
151156
assert.throws(
152157
AppProfile.formatAppProfile_.bind(null, {
153158
routing: 'not-any',
@@ -156,6 +161,34 @@ describe('Bigtable/AppProfile', () => {
156161
);
157162
});
158163
});
164+
165+
describe('with a multi cluster routing policy', () => {
166+
it('should use multi cluster routing when providing an array of clusters', () => {
167+
const clusterIds = ['clusterId1', 'clusterId2'];
168+
const clusters = clusterIds.map(
169+
clusterId => new FakeCluster(INSTANCE, clusterId)
170+
);
171+
const formattedAppProfile = AppProfile.formatAppProfile_({
172+
routing: new Set(clusters),
173+
});
174+
assert.deepStrictEqual(
175+
new Set(formattedAppProfile.multiClusterRoutingUseAny.clusterIds),
176+
new Set(clusterIds)
177+
);
178+
});
179+
it('should ensure elements in the array are clusters', () => {
180+
const notAllClusters = [
181+
new FakeCluster(INSTANCE, 'clusterId'),
182+
'not a cluster',
183+
];
184+
assert.throws(
185+
AppProfile.formatAppProfile_.bind(null, {
186+
routing: notAllClusters,
187+
}),
188+
errorReg
189+
);
190+
});
191+
});
159192
});
160193

161194
describe('create', () => {

0 commit comments

Comments
 (0)