1212// See the License for the specific language governing permissions and
1313// limitations under the License.
1414
15- import { Server , Socket , createServer } from 'node:net' ;
15+ import { createServer , Server , Socket } from 'node:net' ;
1616import tls from 'node:tls' ;
1717import { promisify } from 'node:util' ;
1818import { AuthClient , GoogleAuth } from 'google-auth-library' ;
@@ -43,6 +43,8 @@ export declare interface ConnectionOptions {
4343 authType ?: AuthTypes ;
4444 ipType ?: IpAddressTypes ;
4545 instanceConnectionName : string ;
46+ domainName ?: string ;
47+ limitRateInterval ?: number ;
4648}
4749
4850export declare interface SocketConnectionOptions extends ConnectionOptions {
@@ -72,71 +74,102 @@ export declare interface TediousDriverOptions {
7274 connector : PromisedStreamFunction ;
7375 encrypt : boolean ;
7476}
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+ }
7599
76100// Internal mapping of the CloudSQLInstances that
77101// 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 > {
90126 // in case an instance to that connection name has already
91127 // 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 ;
102146 }
103- return ;
104147 }
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 ,
110157 } ) ;
111- this . set ( instanceConnectionName , connectionInstance ) ;
158+ this . set ( key , new CacheEntry ( promise ) ) ;
159+
160+ // Wait for the cache entry to resolve.
161+ await promise ;
112162 }
113163
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 ) {
123167 throw new CloudSQLConnectorError ( {
124- message : `Cannot find info for instance: ${ instanceConnectionName } ` ,
168+ message : `Cannot find info for instance: ${ opts . instanceConnectionName } ` ,
125169 code : 'ENOINSTANCEINFO' ,
126170 } ) ;
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- } ) ;
138171 }
139- return connectionInstance ;
172+ return connectionInstance . instance ;
140173 }
141174}
142175
@@ -160,13 +193,13 @@ export class Connector {
160193 private readonly sockets : Set < Socket > ;
161194
162195 constructor ( opts : ConnectorOptions = { } ) {
163- this . instances = new CloudSQLInstanceMap ( ) ;
164196 this . sqlAdminFetcher = new SQLAdminFetcher ( {
165197 loginAuth : opts . auth ,
166198 sqlAdminAPIEndpoint : opts . sqlAdminAPIEndpoint ,
167199 universeDomain : opts . universeDomain ,
168200 userAgent : opts . userAgent ,
169201 } ) ;
202+ this . instances = new CloudSQLInstanceMap ( this . sqlAdminFetcher ) ;
170203 this . localProxies = new Set ( ) ;
171204 this . sockets = new Set ( ) ;
172205 }
@@ -182,25 +215,13 @@ export class Connector {
182215 // });
183216 // const pool = new Pool(opts)
184217 // 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 > {
190219 const { instances} = this ;
191- await instances . loadInstance ( {
192- ipType,
193- authType,
194- instanceConnectionName,
195- sqlAdminFetcher : this . sqlAdminFetcher ,
196- } ) ;
220+ await instances . loadInstance ( opts ) ;
197221
198222 return {
199223 stream ( ) {
200- const cloudSqlInstance = instances . getInstance ( {
201- instanceConnectionName,
202- authType,
203- } ) ;
224+ const cloudSqlInstance = instances . getInstance ( opts ) ;
204225 const {
205226 instanceInfo,
206227 ephemeralCert,
@@ -228,7 +249,7 @@ export class Connector {
228249 privateKey,
229250 serverCaCert,
230251 serverCaMode,
231- dnsName,
252+ dnsName : instanceInfo . domainName || dnsName , // use the configured domain name, or the instance dnsName.
232253 } ) ;
233254 tlsSocket . once ( 'error' , ( ) => {
234255 cloudSqlInstance . forceRefresh ( ) ;
@@ -333,7 +354,7 @@ export class Connector {
333354 // Also clear up any local proxy servers and socket connections.
334355 close ( ) : void {
335356 for ( const instance of this . instances . values ( ) ) {
336- instance . close ( ) ;
357+ instance . promise . then ( inst => inst . close ( ) ) ;
337358 }
338359 for ( const server of this . localProxies ) {
339360 server . close ( ) ;
0 commit comments