12
12
// See the License for the specific language governing permissions and
13
13
// limitations under the License.
14
14
15
- import { Server , Socket , createServer } from 'node:net' ;
15
+ import { createServer , Server , Socket } from 'node:net' ;
16
16
import tls from 'node:tls' ;
17
17
import { promisify } from 'node:util' ;
18
18
import { AuthClient , GoogleAuth } from 'google-auth-library' ;
@@ -43,6 +43,8 @@ export declare interface ConnectionOptions {
43
43
authType ?: AuthTypes ;
44
44
ipType ?: IpAddressTypes ;
45
45
instanceConnectionName : string ;
46
+ domainName ?: string ;
47
+ limitRateInterval ?: number ;
46
48
}
47
49
48
50
export declare interface SocketConnectionOptions extends ConnectionOptions {
@@ -72,71 +74,102 @@ export declare interface TediousDriverOptions {
72
74
connector : PromisedStreamFunction ;
73
75
encrypt : boolean ;
74
76
}
77
+ // CacheEntry holds the promise and resolved instance metadata for
78
+ // the connector's instances. The instance field will be set when
79
+ // the promise resolves.
80
+ class CacheEntry {
81
+ promise : Promise < CloudSQLInstance > ;
82
+ instance ?: CloudSQLInstance ;
83
+ err ?: Error ;
84
+
85
+ constructor ( promise : Promise < CloudSQLInstance > ) {
86
+ this . promise = promise ;
87
+ this . promise
88
+ . then ( inst => ( this . instance = inst ) )
89
+ . catch ( err => ( this . err = err ) ) ;
90
+ }
91
+
92
+ isResolved ( ) : boolean {
93
+ return Boolean ( this . instance ) ;
94
+ }
95
+ isError ( ) : boolean {
96
+ return Boolean ( this . err ) ;
97
+ }
98
+ }
75
99
76
100
// Internal mapping of the CloudSQLInstances that
77
101
// adds extra logic to async initialize items.
78
- class CloudSQLInstanceMap extends Map {
79
- async loadInstance ( {
80
- ipType,
81
- authType,
82
- instanceConnectionName,
83
- sqlAdminFetcher,
84
- } : {
85
- ipType : IpAddressTypes ;
86
- authType : AuthTypes ;
87
- instanceConnectionName : string ;
88
- sqlAdminFetcher : SQLAdminFetcher ;
89
- } ) : Promise < void > {
102
+ class CloudSQLInstanceMap extends Map < string , CacheEntry > {
103
+ private readonly sqlAdminFetcher : SQLAdminFetcher ;
104
+
105
+ constructor ( sqlAdminFetcher : SQLAdminFetcher ) {
106
+ super ( ) ;
107
+ this . sqlAdminFetcher = sqlAdminFetcher ;
108
+ }
109
+
110
+ private cacheKey ( opts : ConnectionOptions ) : string {
111
+ //TODO: for now, the cache key function must be synchronous.
112
+ // When we implement the async connection info from
113
+ // https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/pull/426
114
+ // then the cache key should contain both the domain name
115
+ // and the resolved instance name.
116
+ return (
117
+ ( opts . instanceConnectionName || opts . domainName ) +
118
+ '-' +
119
+ opts . authType +
120
+ '-' +
121
+ opts . ipType
122
+ ) ;
123
+ }
124
+
125
+ async loadInstance ( opts : ConnectionOptions ) : Promise < void > {
90
126
// in case an instance to that connection name has already
91
127
// been setup there's no need to set it up again
92
- if ( this . has ( instanceConnectionName ) ) {
93
- const instance = this . get ( instanceConnectionName ) ;
94
- if ( instance . authType && instance . authType !== authType ) {
95
- throw new CloudSQLConnectorError ( {
96
- message :
97
- `getOptions called for instance ${ instanceConnectionName } with authType ${ authType } , ` +
98
- `but was previously called with authType ${ instance . authType } . ` +
99
- 'If you require both for your use case, please use a new connector object.' ,
100
- code : 'EMISMATCHAUTHTYPE' ,
101
- } ) ;
128
+ const key = this . cacheKey ( opts ) ;
129
+ const entry = this . get ( key ) ;
130
+ if ( entry ) {
131
+ if ( entry . isResolved ( ) ) {
132
+ if ( ! entry . instance ?. isClosed ( ) ) {
133
+ // The instance is open and the domain has not changed.
134
+ // use the cached instance.
135
+ return ;
136
+ }
137
+ } else if ( entry . isError ( ) ) {
138
+ // The instance failed it's initial refresh. Remove it from the
139
+ // cache and throw the error.
140
+ this . delete ( key ) ;
141
+ throw entry . err ;
142
+ } else {
143
+ // The instance initial refresh is in progress.
144
+ await entry . promise ;
145
+ return ;
102
146
}
103
- return ;
104
147
}
105
- const connectionInstance = await CloudSQLInstance . getCloudSQLInstance ( {
106
- ipType,
107
- authType,
108
- instanceConnectionName,
109
- sqlAdminFetcher : sqlAdminFetcher ,
148
+
149
+ // Start the refresh and add a cache entry.
150
+ const promise = CloudSQLInstance . getCloudSQLInstance ( {
151
+ instanceConnectionName : opts . instanceConnectionName ,
152
+ domainName : opts . domainName ,
153
+ authType : opts . authType || AuthTypes . PASSWORD ,
154
+ ipType : opts . ipType || IpAddressTypes . PUBLIC ,
155
+ limitRateInterval : opts . limitRateInterval || 30 * 1000 , // 30 sec
156
+ sqlAdminFetcher : this . sqlAdminFetcher ,
110
157
} ) ;
111
- this . set ( instanceConnectionName , connectionInstance ) ;
158
+ this . set ( key , new CacheEntry ( promise ) ) ;
159
+
160
+ // Wait for the cache entry to resolve.
161
+ await promise ;
112
162
}
113
163
114
- getInstance ( {
115
- instanceConnectionName,
116
- authType,
117
- } : {
118
- instanceConnectionName : string ;
119
- authType : AuthTypes ;
120
- } ) : CloudSQLInstance {
121
- const connectionInstance = this . get ( instanceConnectionName ) ;
122
- if ( ! connectionInstance ) {
164
+ getInstance ( opts : ConnectionOptions ) : CloudSQLInstance {
165
+ const connectionInstance = this . get ( this . cacheKey ( opts ) ) ;
166
+ if ( ! connectionInstance || ! connectionInstance . instance ) {
123
167
throw new CloudSQLConnectorError ( {
124
- message : `Cannot find info for instance: ${ instanceConnectionName } ` ,
168
+ message : `Cannot find info for instance: ${ opts . instanceConnectionName } ` ,
125
169
code : 'ENOINSTANCEINFO' ,
126
170
} ) ;
127
- } else if (
128
- connectionInstance . authType &&
129
- connectionInstance . authType !== authType
130
- ) {
131
- throw new CloudSQLConnectorError ( {
132
- message :
133
- `getOptions called for instance ${ instanceConnectionName } with authType ${ authType } , ` +
134
- `but was previously called with authType ${ connectionInstance . authType } . ` +
135
- 'If you require both for your use case, please use a new connector object.' ,
136
- code : 'EMISMATCHAUTHTYPE' ,
137
- } ) ;
138
171
}
139
- return connectionInstance ;
172
+ return connectionInstance . instance ;
140
173
}
141
174
}
142
175
@@ -160,13 +193,13 @@ export class Connector {
160
193
private readonly sockets : Set < Socket > ;
161
194
162
195
constructor ( opts : ConnectorOptions = { } ) {
163
- this . instances = new CloudSQLInstanceMap ( ) ;
164
196
this . sqlAdminFetcher = new SQLAdminFetcher ( {
165
197
loginAuth : opts . auth ,
166
198
sqlAdminAPIEndpoint : opts . sqlAdminAPIEndpoint ,
167
199
universeDomain : opts . universeDomain ,
168
200
userAgent : opts . userAgent ,
169
201
} ) ;
202
+ this . instances = new CloudSQLInstanceMap ( this . sqlAdminFetcher ) ;
170
203
this . localProxies = new Set ( ) ;
171
204
this . sockets = new Set ( ) ;
172
205
}
@@ -182,25 +215,13 @@ export class Connector {
182
215
// });
183
216
// const pool = new Pool(opts)
184
217
// const res = await pool.query('SELECT * FROM pg_catalog.pg_tables;')
185
- async getOptions ( {
186
- authType = AuthTypes . PASSWORD ,
187
- ipType = IpAddressTypes . PUBLIC ,
188
- instanceConnectionName,
189
- } : ConnectionOptions ) : Promise < DriverOptions > {
218
+ async getOptions ( opts : ConnectionOptions ) : Promise < DriverOptions > {
190
219
const { instances} = this ;
191
- await instances . loadInstance ( {
192
- ipType,
193
- authType,
194
- instanceConnectionName,
195
- sqlAdminFetcher : this . sqlAdminFetcher ,
196
- } ) ;
220
+ await instances . loadInstance ( opts ) ;
197
221
198
222
return {
199
223
stream ( ) {
200
- const cloudSqlInstance = instances . getInstance ( {
201
- instanceConnectionName,
202
- authType,
203
- } ) ;
224
+ const cloudSqlInstance = instances . getInstance ( opts ) ;
204
225
const {
205
226
instanceInfo,
206
227
ephemeralCert,
@@ -228,7 +249,7 @@ export class Connector {
228
249
privateKey,
229
250
serverCaCert,
230
251
serverCaMode,
231
- dnsName,
252
+ dnsName : instanceInfo . domainName || dnsName , // use the configured domain name, or the instance dnsName.
232
253
} ) ;
233
254
tlsSocket . once ( 'error' , ( ) => {
234
255
cloudSqlInstance . forceRefresh ( ) ;
@@ -333,7 +354,7 @@ export class Connector {
333
354
// Also clear up any local proxy servers and socket connections.
334
355
close ( ) : void {
335
356
for ( const instance of this . instances . values ( ) ) {
336
- instance . close ( ) ;
357
+ instance . promise . then ( inst => inst . close ( ) ) ;
337
358
}
338
359
for ( const server of this . localProxies ) {
339
360
server . close ( ) ;
0 commit comments