Skip to content

Commit 23c3c58

Browse files
committed
wip: periodic check for domain change
1 parent 5dc1049 commit 23c3c58

File tree

7 files changed

+198
-143
lines changed

7 files changed

+198
-143
lines changed

src/cloud-sql-instance.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414

1515
import {IpAddressTypes, selectIpAddress} from './ip-addresses';
1616
import {InstanceConnectionInfo} from './instance-connection-info';
17-
import {resolveInstanceName} from './parse-instance-connection-name';
17+
import {
18+
isSameInstance,
19+
resolveInstanceName,
20+
} from './parse-instance-connection-name';
1821
import {InstanceMetadata} from './sqladmin-fetcher';
1922
import {generateKeys} from './crypto';
2023
import {RSAKeys} from './rsa-keys';
@@ -57,7 +60,10 @@ export class CloudSQLInstance {
5760
): Promise<CloudSQLInstance> {
5861
const instance = new CloudSQLInstance({
5962
options: options,
60-
instanceInfo: await resolveInstanceName(options.instanceConnectionName, options.domainName),
63+
instanceInfo: await resolveInstanceName(
64+
options.instanceConnectionName,
65+
options.domainName
66+
),
6167
});
6268
await instance.refresh();
6369
return instance;
@@ -312,4 +318,18 @@ export class CloudSQLInstance {
312318
isClosed(): boolean {
313319
return this.closed;
314320
}
321+
async checkDomainChanged() {
322+
if (!this.instanceInfo.domainName) {
323+
return;
324+
}
325+
326+
const newInfo = await resolveInstanceName(
327+
undefined,
328+
this.instanceInfo.domainName
329+
);
330+
if (!isSameInstance(this.instanceInfo, newInfo)) {
331+
// Domain name changed. Close and remove, then create a new map entry.
332+
this.close();
333+
}
334+
}
315335
}

src/connector.ts

Lines changed: 61 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {IpAddressTypes} from './ip-addresses';
2222
import {AuthTypes} from './auth-types';
2323
import {SQLAdminFetcher} from './sqladmin-fetcher';
2424
import {CloudSQLConnectorError} from './errors';
25-
import {isSameInstance, resolveInstanceName} from './parse-instance-connection-name';
2625

2726
// These Socket types are subsets from nodejs definitely typed repo, ref:
2827
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/ae0fe42ff0e6e820e8ae324acf4f8e944aa1b2b7/types/node/v18/net.d.ts#L437
@@ -74,11 +73,24 @@ export declare interface TediousDriverOptions {
7473
connector: PromisedStreamFunction;
7574
encrypt: boolean;
7675
}
76+
class CacheEntry {
77+
promise: Promise<CloudSQLInstance>;
78+
instance?: CloudSQLInstance;
79+
80+
constructor(promise: Promise<CloudSQLInstance>) {
81+
this.promise = promise;
82+
this.promise.then(inst => (this.instance = inst));
83+
}
84+
85+
isResolved(): boolean {
86+
return Boolean(this.instance);
87+
}
88+
}
7789

7890
// Internal mapping of the CloudSQLInstances that
7991
// adds extra logic to async initialize items.
80-
class CloudSQLInstanceMap extends Map<string,CloudSQLInstance> {
81-
private readonly sqlAdminFetcher: SQLAdminFetcher
92+
class CloudSQLInstanceMap extends Map<string, CacheEntry> {
93+
private readonly sqlAdminFetcher: SQLAdminFetcher;
8294

8395
constructor(sqlAdminFetcher: SQLAdminFetcher) {
8496
super();
@@ -91,53 +103,60 @@ class CloudSQLInstanceMap extends Map<string,CloudSQLInstance> {
91103
// https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/pull/426
92104
// then the cache key should contain both the domain name
93105
// and the resolved instance name.
94-
return (opts.instanceConnectionName || opts.domainName)+"-"+
95-
opts.authType+"-"+opts.ipType;
106+
return (
107+
(opts.instanceConnectionName || opts.domainName) +
108+
'-' +
109+
opts.authType +
110+
'-' +
111+
opts.ipType
112+
);
96113
}
97114

98115
async loadInstance(opts: ConnectionOptions): Promise<void> {
99116
// in case an instance to that connection name has already
100117
// been setup there's no need to set it up again
101-
if (this.has(this.cacheKey(opts))) {
102-
const instance = this.get(this.cacheKey(opts));
103-
let oldInfo = instance?.instanceInfo
104-
if(oldInfo && oldInfo.domainName){
105-
// configured with domain name
106-
let newInfo = await resolveInstanceName(undefined, oldInfo.domainName);
107-
if(!isSameInstance(oldInfo, newInfo) ) {
108-
// Domain name changed. Close and remove, then create a new map entry.
109-
instance?.close();
110-
this.delete(this.cacheKey(opts))
111-
} else {
112-
// Domain name resolves to the same instance, do nothing.
113-
return
114-
}
115-
} else{
116-
// Configured with instance name. Existing map entry is OK.
118+
const entry = this.get(this.cacheKey(opts));
119+
if (entry) {
120+
if (!entry.isResolved()) {
121+
// A cache entry request is in progress.
122+
await entry.promise;
117123
return;
124+
} else {
125+
// Check the domain name, then if the instance is still open
126+
// return it
127+
await entry.instance?.checkDomainChanged();
128+
if (!entry.instance?.isClosed()) {
129+
// The instance is open and the domain has not changed.
130+
// use the cached instance.
131+
return;
132+
}
118133
}
119134
}
120135

121-
const connectionInstance = await CloudSQLInstance.getCloudSQLInstance({
136+
// Start the refresh and add a cache entry immediately.
137+
const promise = CloudSQLInstance.getCloudSQLInstance({
122138
instanceConnectionName: opts.instanceConnectionName,
123139
domainName: opts.domainName,
124140
authType: opts.authType || AuthTypes.PASSWORD,
125141
ipType: opts.ipType || IpAddressTypes.PUBLIC,
126142
limitRateInterval: 30 * 1000, // 30 sec
127143
sqlAdminFetcher: this.sqlAdminFetcher,
128144
});
129-
this.set(this.cacheKey(opts), connectionInstance);
145+
this.set(this.cacheKey(opts), new CacheEntry(promise));
146+
147+
// Wait for the cache entry to resolve.
148+
await promise;
130149
}
131150

132151
getInstance(opts: ConnectionOptions): CloudSQLInstance {
133152
const connectionInstance = this.get(this.cacheKey(opts));
134-
if (!connectionInstance) {
153+
if (!connectionInstance || !connectionInstance.instance) {
135154
throw new CloudSQLConnectorError({
136155
message: `Cannot find info for instance: ${opts.instanceConnectionName}`,
137156
code: 'ENOINSTANCEINFO',
138157
});
139158
}
140-
return connectionInstance;
159+
return connectionInstance.instance;
141160
}
142161
}
143162

@@ -185,17 +204,17 @@ export class Connector {
185204
// const pool = new Pool(opts)
186205
// const res = await pool.query('SELECT * FROM pg_catalog.pg_tables;')
187206
async getOptions({
188-
authType = AuthTypes.PASSWORD,
189-
ipType = IpAddressTypes.PUBLIC,
190-
instanceConnectionName,
191-
domainName,
192-
}: ConnectionOptions): Promise<DriverOptions> {
207+
authType = AuthTypes.PASSWORD,
208+
ipType = IpAddressTypes.PUBLIC,
209+
instanceConnectionName,
210+
domainName,
211+
}: ConnectionOptions): Promise<DriverOptions> {
193212
const {instances} = this;
194213
await instances.loadInstance({
195214
ipType,
196215
authType,
197216
instanceConnectionName,
198-
domainName
217+
domainName,
199218
});
200219

201220
return {
@@ -253,10 +272,10 @@ export class Connector {
253272
}
254273

255274
async getTediousOptions({
256-
authType,
257-
ipType,
258-
instanceConnectionName,
259-
}: ConnectionOptions): Promise<TediousDriverOptions> {
275+
authType,
276+
ipType,
277+
instanceConnectionName,
278+
}: ConnectionOptions): Promise<TediousDriverOptions> {
260279
if (authType === AuthTypes.IAM) {
261280
throw new CloudSQLConnectorError({
262281
message: 'Tedious does not support Auto IAM DB Authentication',
@@ -293,11 +312,11 @@ export class Connector {
293312
// `postgresql://${user}@localhost/${database}?host=${process.cwd()}`;
294313
// const prisma = new PrismaClient({ datasourceUrl });
295314
async startLocalProxy({
296-
authType,
297-
ipType,
298-
instanceConnectionName,
299-
listenOptions,
300-
}: SocketConnectionOptions): Promise<void> {
315+
authType,
316+
ipType,
317+
instanceConnectionName,
318+
listenOptions,
319+
}: SocketConnectionOptions): Promise<void> {
301320
const {stream} = await this.getOptions({
302321
authType,
303322
ipType,
@@ -339,7 +358,7 @@ export class Connector {
339358
close(): void {
340359
this.closed = true;
341360
for (const instance of this.instances.values()) {
342-
instance.close();
361+
instance.promise.then(inst => inst.close());
343362
}
344363
for (const server of this.localProxies) {
345364
server.close();
@@ -349,7 +368,7 @@ export class Connector {
349368
}
350369
}
351370

352-
isClosed():boolean {
371+
isClosed(): boolean {
353372
return this.closed;
354373
}
355374
}

src/instance-connection-info.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,4 @@ export interface InstanceConnectionInfo {
1717
regionId: string;
1818
instanceId: string;
1919
domainName: string | undefined;
20-
21-
2220
}

src/parse-instance-connection-name.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@ import {InstanceConnectionInfo} from './instance-connection-info';
1616
import {CloudSQLConnectorError} from './errors';
1717
import {resolveTxtRecord} from './dns-lookup';
1818

19-
export function isSameInstance(a: InstanceConnectionInfo, b: InstanceConnectionInfo): boolean {
20-
return a.instanceId == b.instanceId &&
21-
a.regionId == b.regionId &&
22-
a.projectId == b.projectId &&
23-
a.domainName == b.domainName;
19+
export function isSameInstance(
20+
a: InstanceConnectionInfo,
21+
b: InstanceConnectionInfo
22+
): boolean {
23+
return (
24+
a.instanceId === b.instanceId &&
25+
a.regionId === b.regionId &&
26+
a.projectId === b.projectId &&
27+
a.domainName === b.domainName
28+
);
2429
}
2530

2631
export async function resolveInstanceName(
@@ -33,7 +38,10 @@ export async function resolveInstanceName(
3338
'Missing instance connection name, expected: "PROJECT:REGION:INSTANCE" or a valid domain name.',
3439
code: 'ENOCONNECTIONNAME',
3540
});
36-
} else if (instanceConnectionName && isInstanceConnectionName(instanceConnectionName)) {
41+
} else if (
42+
instanceConnectionName &&
43+
isInstanceConnectionName(instanceConnectionName)
44+
) {
3745
return parseInstanceConnectionName(instanceConnectionName);
3846
} else if (domainName && isValidDomainName(domainName)) {
3947
return await resolveDomainName(domainName);

system-test/pg-connect.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,7 @@ t.test(
145145
async t => {
146146
const connector = new Connector();
147147
const clientOpts = await connector.getOptions({
148-
domainName: String(
149-
process.env.POSTGRES_CUSTOMER_CAS_DOMAIN_NAME
150-
),
148+
domainName: String(process.env.POSTGRES_CUSTOMER_CAS_DOMAIN_NAME),
151149
});
152150
const client = new Client({
153151
...clientOpts,
@@ -177,9 +175,7 @@ t.test(
177175
async t => {
178176
const connector = new Connector();
179177
const clientOpts = await connector.getOptions({
180-
domainName: String(
181-
process.env.POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME
182-
),
178+
domainName: String(process.env.POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME),
183179
});
184180
const client = new Client({
185181
...clientOpts,

0 commit comments

Comments
 (0)