Skip to content

Commit 45bb3c7

Browse files
committed
wip: periodic check for domain change
1 parent cdd646b commit 45bb3c7

File tree

9 files changed

+203
-155
lines changed

9 files changed

+203
-155
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;
@@ -314,4 +320,18 @@ export class CloudSQLInstance {
314320
isClosed(): boolean {
315321
return this.closed;
316322
}
323+
async checkDomainChanged() {
324+
if (!this.instanceInfo.domainName) {
325+
return;
326+
}
327+
328+
const newInfo = await resolveInstanceName(
329+
undefined,
330+
this.instanceInfo.domainName
331+
);
332+
if (!isSameInstance(this.instanceInfo, newInfo)) {
333+
// Domain name changed. Close and remove, then create a new map entry.
334+
this.close();
335+
}
336+
}
317337
}

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

@@ -184,17 +203,17 @@ export class Connector {
184203
// const pool = new Pool(opts)
185204
// const res = await pool.query('SELECT * FROM pg_catalog.pg_tables;')
186205
async getOptions({
187-
authType = AuthTypes.PASSWORD,
188-
ipType = IpAddressTypes.PUBLIC,
189-
instanceConnectionName,
190-
domainName,
191-
}: ConnectionOptions): Promise<DriverOptions> {
206+
authType = AuthTypes.PASSWORD,
207+
ipType = IpAddressTypes.PUBLIC,
208+
instanceConnectionName,
209+
domainName,
210+
}: ConnectionOptions): Promise<DriverOptions> {
192211
const {instances} = this;
193212
await instances.loadInstance({
194213
ipType,
195214
authType,
196215
instanceConnectionName,
197-
domainName
216+
domainName,
198217
});
199218

200219
return {
@@ -252,10 +271,10 @@ export class Connector {
252271
}
253272

254273
async getTediousOptions({
255-
authType,
256-
ipType,
257-
instanceConnectionName,
258-
}: ConnectionOptions): Promise<TediousDriverOptions> {
274+
authType,
275+
ipType,
276+
instanceConnectionName,
277+
}: ConnectionOptions): Promise<TediousDriverOptions> {
259278
if (authType === AuthTypes.IAM) {
260279
throw new CloudSQLConnectorError({
261280
message: 'Tedious does not support Auto IAM DB Authentication',
@@ -292,11 +311,11 @@ export class Connector {
292311
// `postgresql://${user}@localhost/${database}?host=${process.cwd()}`;
293312
// const prisma = new PrismaClient({ datasourceUrl });
294313
async startLocalProxy({
295-
authType,
296-
ipType,
297-
instanceConnectionName,
298-
listenOptions,
299-
}: SocketConnectionOptions): Promise<void> {
314+
authType,
315+
ipType,
316+
instanceConnectionName,
317+
listenOptions,
318+
}: SocketConnectionOptions): Promise<void> {
300319
const {stream} = await this.getOptions({
301320
authType,
302321
ipType,
@@ -337,7 +356,7 @@ export class Connector {
337356
// Also clear up any local proxy servers and socket connections.
338357
close(): void {
339358
for (const instance of this.instances.values()) {
340-
instance.close();
359+
instance.promise.then(inst => inst.close());
341360
}
342361
for (const server of this.localProxies) {
343362
server.close();
@@ -347,7 +366,7 @@ export class Connector {
347366
}
348367
}
349368

350-
isClosed():boolean {
369+
isClosed(): boolean {
351370
return this.closed;
352371
}
353372
}

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.cjs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,7 @@ t.test(
141141
async t => {
142142
const connector = new Connector();
143143
const clientOpts = await connector.getOptions({
144-
instanceConnectionName: String(
145-
process.env.POSTGRES_CUSTOMER_CAS_DOMAIN_NAME
146-
),
144+
domainName: String(process.env.POSTGRES_CUSTOMER_CAS_DOMAIN_NAME),
147145
});
148146
const client = new Client({
149147
...clientOpts,
@@ -173,9 +171,7 @@ t.test(
173171
async t => {
174172
const connector = new Connector();
175173
const clientOpts = await connector.getOptions({
176-
instanceConnectionName: String(
177-
process.env.POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME
178-
),
174+
domainName: String(process.env.POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME),
179175
});
180176
const client = new Client({
181177
...clientOpts,
@@ -202,3 +198,4 @@ t.test(
202198
}
203199
}
204200
);
201+

system-test/pg-connect.mjs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,7 @@ t.test(
142142
async t => {
143143
const connector = new Connector();
144144
const clientOpts = await connector.getOptions({
145-
instanceConnectionName: String(
146-
process.env.POSTGRES_CUSTOMER_CAS_DOMAIN_NAME
147-
),
145+
domainName: String(process.env.POSTGRES_CUSTOMER_CAS_DOMAIN_NAME),
148146
});
149147
const client = new Client({
150148
...clientOpts,
@@ -174,9 +172,7 @@ t.test(
174172
async t => {
175173
const connector = new Connector();
176174
const clientOpts = await connector.getOptions({
177-
instanceConnectionName: String(
178-
process.env.POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME
179-
),
175+
domainName: String(process.env.POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME),
180176
});
181177
const client = new Client({
182178
...clientOpts,

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)