Skip to content

Commit d84d082

Browse files
authoredMay 29, 2024··
feat: Add lazy refresh strategy to the connector. Fixes #992.
The lazy refresh strategy only refreshes credentials and certificate information when the application attempts to establish a new database connection. On Cloud Run and other serverless runtimes, this is more reliable than the default background refresh strategy. Fixes #992
1 parent d97a93b commit d84d082

File tree

7 files changed

+195
-18
lines changed

7 files changed

+195
-18
lines changed
 

‎core/src/main/java/com/google/cloud/sql/core/Connector.java

+16-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.cloud.sql.ConnectorConfig;
2020
import com.google.cloud.sql.CredentialFactory;
21+
import com.google.cloud.sql.RefreshStrategy;
2122
import com.google.common.util.concurrent.ListenableFuture;
2223
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
2324
import java.io.File;
@@ -26,6 +27,7 @@
2627
import java.net.Socket;
2728
import java.security.KeyPair;
2829
import java.util.concurrent.ConcurrentHashMap;
30+
import java.util.concurrent.ExecutionException;
2931
import javax.net.ssl.SSLSocket;
3032
import jnr.unixsocket.UnixSocketAddress;
3133
import jnr.unixsocket.UnixSocketChannel;
@@ -154,9 +156,21 @@ ConnectionInfoCache getConnection(ConnectionConfig config) {
154156
private ConnectionInfoCache createConnectionInfo(ConnectionConfig config) {
155157
logger.debug(
156158
String.format("[%s] Connection info added to cache.", config.getCloudSqlInstance()));
159+
if (config.getConnectorConfig().getRefreshStrategy() == RefreshStrategy.LAZY) {
160+
// Resolve the key operation immediately.
161+
KeyPair keyPair = null;
162+
try {
163+
keyPair = localKeyPair.get();
164+
} catch (InterruptedException | ExecutionException e) {
165+
throw new RuntimeException(e);
166+
}
167+
return new LazyRefreshConnectionInfoCache(
168+
config, adminApi, instanceCredentialFactory, keyPair);
157169

158-
return new RefreshAheadConnectionInfoCache(
159-
config, adminApi, instanceCredentialFactory, executor, localKeyPair, minRefreshDelayMs);
170+
} else {
171+
return new RefreshAheadConnectionInfoCache(
172+
config, adminApi, instanceCredentialFactory, executor, localKeyPair, minRefreshDelayMs);
173+
}
160174
}
161175

162176
public void close() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.sql.core;
18+
19+
import com.google.cloud.sql.CredentialFactory;
20+
import java.security.KeyPair;
21+
22+
/**
23+
* Implements the lazy refresh cache strategy, which loads the new certificate as needed during a
24+
* request for a new connection.
25+
*/
26+
class LazyRefreshConnectionInfoCache implements ConnectionInfoCache {
27+
private final ConnectionConfig config;
28+
private final CloudSqlInstanceName instanceName;
29+
30+
private final LazyRefreshStrategy refreshStrategy;
31+
32+
/**
33+
* Initializes a new Cloud SQL instance based on the given connection name using the lazy refresh
34+
* strategy.
35+
*
36+
* @param config instance connection name in the format "PROJECT_ID:REGION_ID:INSTANCE_ID"
37+
* @param connectionInfoRepository Service class for interacting with the Cloud SQL Admin API
38+
* @param keyPair public/private key pair used to authenticate connections
39+
*/
40+
public LazyRefreshConnectionInfoCache(
41+
ConnectionConfig config,
42+
ConnectionInfoRepository connectionInfoRepository,
43+
CredentialFactory tokenSourceFactory,
44+
KeyPair keyPair) {
45+
this.config = config;
46+
this.instanceName = new CloudSqlInstanceName(config.getCloudSqlInstance());
47+
48+
AccessTokenSupplier accessTokenSupplier =
49+
DefaultAccessTokenSupplier.newInstance(config.getAuthType(), tokenSourceFactory);
50+
CloudSqlInstanceName instanceName = new CloudSqlInstanceName(config.getCloudSqlInstance());
51+
52+
this.refreshStrategy =
53+
new LazyRefreshStrategy(
54+
config.getCloudSqlInstance(),
55+
() ->
56+
connectionInfoRepository.getConnectionInfoSync(
57+
instanceName, accessTokenSupplier, config.getAuthType(), keyPair));
58+
}
59+
60+
@Override
61+
public ConnectionMetadata getConnectionMetadata(long timeoutMs) {
62+
return refreshStrategy.getConnectionInfo(timeoutMs).toConnectionMetadata(config, instanceName);
63+
}
64+
65+
@Override
66+
public void forceRefresh() {
67+
refreshStrategy.forceRefresh();
68+
}
69+
70+
@Override
71+
public void refreshIfExpired() {
72+
refreshStrategy.refreshIfExpired();
73+
}
74+
75+
@Override
76+
public void close() {
77+
refreshStrategy.close();
78+
}
79+
}

‎core/src/main/java/com/google/cloud/sql/core/LazyRefreshStrategy.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,18 @@ private void fetchConnectionInfo() {
7676
logger.debug(String.format("[%s] Lazy Refresh Operation: Starting refresh operation.", name));
7777
try {
7878
this.connectionInfo = this.refreshOperation.get();
79+
logger.debug(
80+
String.format(
81+
"[%s] Lazy Refresh Operation: Completed refresh with new certificate "
82+
+ "expiration at %s.",
83+
name, connectionInfo.getExpiration().toString()));
84+
7985
} catch (TerminalException e) {
8086
logger.debug(String.format("[%s] Lazy Refresh Operation: Failed! No retry.", name), e);
8187
throw e;
8288
} catch (Exception e) {
8389
throw new RuntimeException(String.format("[%s] Refresh Operation: Failed!", name), e);
8490
}
85-
86-
logger.debug(
87-
String.format(
88-
"[%s] Lazy Refresh Operation: Completed refresh with new certificate "
89-
+ "expiration at %s.",
90-
name, connectionInfo.getExpiration().toString()));
9191
}
9292
}
9393

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.sql.core;
17+
18+
import static com.google.common.truth.Truth.assertThat;
19+
20+
import com.google.common.util.concurrent.Futures;
21+
import com.google.common.util.concurrent.ListenableFuture;
22+
import java.security.KeyPair;
23+
import java.util.concurrent.ExecutionException;
24+
import org.junit.Before;
25+
import org.junit.Test;
26+
27+
public class LazyRefreshConnectionInfoCacheTest {
28+
private ListenableFuture<KeyPair> keyPairFuture;
29+
private final StubCredentialFactory stubCredentialFactory =
30+
new StubCredentialFactory("my-token", System.currentTimeMillis() + 3600L);
31+
32+
@Before
33+
public void setup() throws Exception {
34+
MockAdminApi mockAdminApi = new MockAdminApi();
35+
this.keyPairFuture = Futures.immediateFuture(mockAdminApi.getClientKeyPair());
36+
}
37+
38+
@Test
39+
public void testCloudSqlInstanceDataLazyStrategyRetrievedSuccessfully()
40+
throws ExecutionException, InterruptedException {
41+
KeyPair kp = keyPairFuture.get();
42+
TestDataSupplier instanceDataSupplier = new TestDataSupplier(false);
43+
44+
// initialize connectionInfoCache after mocks are set up
45+
LazyRefreshConnectionInfoCache connectionInfoCache =
46+
new LazyRefreshConnectionInfoCache(
47+
new ConnectionConfig.Builder().withCloudSqlInstance("project:region:instance").build(),
48+
instanceDataSupplier,
49+
stubCredentialFactory,
50+
kp);
51+
52+
ConnectionMetadata gotMetadata = connectionInfoCache.getConnectionMetadata(300);
53+
ConnectionMetadata gotMetadata2 = connectionInfoCache.getConnectionMetadata(300);
54+
55+
// Assert that the underlying ConnectionInfo was retrieved exactly once.
56+
assertThat(instanceDataSupplier.counter.get()).isEqualTo(1);
57+
58+
// Assert that the ConnectionInfo fields are added to ConnectionMetadata
59+
assertThat(gotMetadata.getKeyManagerFactory())
60+
.isSameInstanceAs(instanceDataSupplier.response.getSslData().getKeyManagerFactory());
61+
assertThat(gotMetadata.getKeyManagerFactory())
62+
.isSameInstanceAs(gotMetadata2.getKeyManagerFactory());
63+
}
64+
}

‎core/src/test/java/com/google/cloud/sql/core/TestDataSupplier.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,6 @@ public ConnectionInfo getConnectionInfoSync(
9898
throw new RuntimeException("Flaky");
9999
}
100100
successCounter.incrementAndGet();
101-
return null;
101+
return response;
102102
}
103103
}

‎docs/configuration.md

+11-9
Original file line numberDiff line numberDiff line change
@@ -273,20 +273,22 @@ registered with `ConnectorRegistry.register()`.
273273
These properties configure the connector which loads Cloud SQL instance
274274
configuration using the Cloud SQL Admin API.
275275

276-
| JDBC Connection Property | R2DBC Property Name | Description | Example |
277-
|-------------------------------|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
278-
| cloudSqlTargetPrincipal | TARGET_PRINCIPAL | The service account to impersonate when connecting to the database and database admin API. | `db-user@my-project.iam.gserviceaccount.com` |
279-
| cloudSqlDelegates | DELEGATES | A comma-separated list of service accounts delegates. See [Delegated Service Account Impersonation](jdbc.md#delegated-service-account-impersonation) | `application@my-project.iam.gserviceaccount.com,services@my-project.iam.gserviceaccount.com` |
280-
| cloudSqlGoogleCredentialsPath | GOOGLE_CREDENTIALS_PATH | A file path to a JSON file containing a GoogleCredentials oauth token. | `/home/alice/secrets/my-credentials.json` |
281-
| cloudSqlAdminRootUrl | ADMIN_ROOT_URL | An alternate root url for the Cloud SQL admin API. Must end in '/' See [rootUrl](java-api-root-url) | `https://googleapis.example.com/` |
282-
| cloudSqlAdminServicePath | ADMIN_SERVICE_PATH | An alternate path to the SQL Admin API endpoint. Must not begin with '/'. Must end with '/'. See [servicePath](java-api-service-path) | `sqladmin/v1beta1/` |
283-
| cloudSqlAdminQuotaProject | ADMIN_QUOTA_PROJECT | A project ID for quota and billing. See [Quota Project][quota-project] | `my-project` |
284-
| cloudSqlUniverseDomain | UNIVERSE_DOMAIN | A universe domain for the TPC environment (default is googleapis.com). See [TPC][tpc] | test-universe.test
276+
| JDBC Connection Property | R2DBC Property Name | Description | Example |
277+
|-------------------------------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
278+
| cloudSqlTargetPrincipal | TARGET_PRINCIPAL | The service account to impersonate when connecting to the database and database admin API. | `db-user@my-project.iam.gserviceaccount.com` |
279+
| cloudSqlDelegates | DELEGATES | A comma-separated list of service accounts delegates. See [Delegated Service Account Impersonation](jdbc.md#delegated-service-account-impersonation) | `application@my-project.iam.gserviceaccount.com,services@my-project.iam.gserviceaccount.com` |
280+
| cloudSqlGoogleCredentialsPath | GOOGLE_CREDENTIALS_PATH | A file path to a JSON file containing a GoogleCredentials oauth token. | `/home/alice/secrets/my-credentials.json` |
281+
| cloudSqlAdminRootUrl | ADMIN_ROOT_URL | An alternate root url for the Cloud SQL admin API. Must end in '/' See [rootUrl](java-api-root-url) | `https://googleapis.example.com/` |
282+
| cloudSqlAdminServicePath | ADMIN_SERVICE_PATH | An alternate path to the SQL Admin API endpoint. Must not begin with '/'. Must end with '/'. See [servicePath](java-api-service-path) | `sqladmin/v1beta1/` |
283+
| cloudSqlAdminQuotaProject | ADMIN_QUOTA_PROJECT | A project ID for quota and billing. See [Quota Project][quota-project] | `my-project` |
284+
| cloudSqlUniverseDomain | UNIVERSE_DOMAIN | A universe domain for the TPC environment (default is googleapis.com). See [TPC][tpc] | test-universe.test |
285+
| cloudSqlRefreshStrategy | REFRESH_STRATEGY | The strategy used to refresh the Google Cloud SQL authentication tokens. Valid values: `background` - refresh credentials using a background thread, `lazy` - refresh credentials during connection attempts. [Refresh Strategy][refresh-strategy] | `lazy` |
285286

286287
[java-api-root-url]: https://github.com/googleapis/google-api-java-client/blob/main/google-api-client/src/main/java/com/google/api/client/googleapis/services/AbstractGoogleClient.java#L49
287288
[java-api-service-path]: https://github.com/googleapis/google-api-java-client/blob/main/google-api-client/src/main/java/com/google/api/client/googleapis/services/AbstractGoogleClient.java#L52
288289
[quota-project]: jdbc.md#quota-project
289290
[tpc]: jdbc.md#trusted-partner-cloud-tpc-support
291+
[refresh-strategy]: jdbc.md#refresh-strategy
290292

291293
### Connection Configuration Properties
292294

‎docs/jdbc.md

+18
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,24 @@ Properties connProps = new Properties();
545545
connProps.setProperty("cloudSqlUniverseDomain", "test-universe.test");
546546
```
547547

548+
549+
### Refresh Strategy for Serverless Compute
550+
551+
When the connector runs in Cloud Run, App Engine Flex, or other serverless
552+
compute platforms, the connector should be configured to use the `lazy` refresh
553+
strategy instead of the default `background` strategy.
554+
555+
Cloud Run, Flex, and other serverless compute platforms throttle application CPU
556+
in a way that interferes with the default `background` strategy used to refresh
557+
the client certificate and authentication token.
558+
559+
#### Example
560+
561+
```java
562+
Properties connProps = new Properties();
563+
connProps.setProperty("cloudSqlRefreshStrategy", "lazy");
564+
```
565+
548566
## Configuration Reference
549567

550568
- See [Configuration Reference](configuration.md)

0 commit comments

Comments
 (0)
Please sign in to comment.