GoogleCloudPlatform / GoogleCloudPlatform/spring-cloud-gcp
Add Configurable Timeout and Retry Settings to GcpDatastoreAutoConfiguration
- Dominant language
- Java
- Stars
- 551
- Forks
- 349
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 14
Description
# Summary
The Spring Cloud GCP `GcpDatastoreAutoConfiguration` should support configurable timeout and retry settings through Spring Boot properties, similar to how other Spring Cloud GCP autoconfiguration classes (like Pub/Sub) expose configuration options.
## Current Limitation
Currently, `GcpDatastoreAutoConfiguration` creates `Datastore` clients with default timeout and retry settings that cannot be customized without completely replacing the autoconfiguration. This forces developers to:
1. Create custom `DatastoreProvider` or `Datastore` beans
2. Manually replicate the autoconfiguration logic
3. Potentially break compatibility with future Spring Cloud GCP updates
### Current Code (Spring Cloud GCP 5.x)
```java
// From com.google.cloud.spring.autoconfigure.datastore.GcpDatastoreAutoConfiguration
private Datastore getDatastore(String namespace) {
DatastoreOptions.Builder builder = DatastoreOptions.newBuilder()
.setProjectId(this.projectId)
.setHeaderProvider(new UserAgentHeaderProvider(this.getClass()))
.setCredentials(this.credentials);
if (namespace != null) {
builder.setNamespace(namespace);
}
if (databaseId != null) {
builder.setDatabaseId(databaseId);
}
if (this.host != null) {
builder.setHost(this.host);
}
// ❌ NO WAY TO SET CUSTOM TIMEOUTS OR RETRY SETTINGS!
return builder.build().getService();
}
```
### What's Missing
The `DatastoreOptions.Builder` supports these configuration methods that are not exposed:
- `.setTransportOptions(HttpTransportOptions)` - for connect and read timeouts
- `.setRetrySettings(RetrySettings)` - for retry behavior (max attempts, delays, multipliers)
## Proposed Solution
Add properties to `GcpDatastoreProperties` similar to how `GcpPubSubProperties` handles configuration:
### Proposed Property Structure
```yaml
spring:
cloud:
gcp:
datastore:
# Existing properties
project-id: my-project
namespace: my-namespace
database-id: my-database
host: localhost:8081
# NEW: Timeout properties
timeout:
connect-timeout-ms: 1000 # HTTP connect timeout
read-timeout-ms: 2000 # HTTP read timeout
# NEW: Retry properties
retry:
total-timeout-ms: 3000 # Total time including retries
initial-rpc-timeout-ms: 1000 # First attempt timeout
max-rpc-timeout-ms: 2000 # Max timeout for any attempt
rpc-timeout-multiplier: 1.5 # Timeout increase per retry
max-attempts: 3 # Maximum retry attempts
initial-retry-delay-ms: 100 # Initial delay between retries
max-retry-delay-ms: 5000 # Max delay between retries
retry-delay-multiplier: 2.0 # Delay increase per retry
```
### Proposed Code Changes
#### 1. Update `GcpDatastoreProperties`
```java
@ConfigurationProperties("spring.cloud.gcp.datastore")
public class GcpDatastoreProperties implements CredentialsSupplier {
// ...existing properties...
/** Timeout configuration for Datastore operations. */
private TimeoutProperties timeout = new TimeoutProperties();
/** Retry configuration for Datastore operations. */
private RetryProperties retry = new RetryProperties();
public TimeoutProperties getTimeout() {
return timeout;
}
public void setTimeout(TimeoutProperties timeout) {
this.timeout = timeout;
}
public RetryProperties getRetry() {
return retry;
}
public void setRetry(RetryProperties retry) {
this.retry = retry;
}
/** Timeout configuration properties. */
public static class TimeoutProperties {
/** HTTP connection timeout in milliseconds. Default: 20000ms (20s) */
private int connectTimeoutMs = 20000;
/** HTTP read timeout in milliseconds. Default: 20000ms (20s) */
private int readTimeoutMs = 20000;
// getters and setters...
}
/** Retry configuration properties. */
public static class RetryProperties {
/** Total timeout including retries in milliseconds. Default: 60000ms (60s) */
private long totalTimeoutMs = 60000;
/** Initial RPC timeout in milliseconds. Default: 5000ms (5s) */
private long initialRpcTimeoutMs = 5000;
/** Maximum RPC timeout in milliseconds. Default: 30000ms (30s) */
private long maxRpcTimeoutMs = 30000;
/** RPC timeout multiplier. Default: 1.3 */
private double rpcTimeoutMultiplier = 1.3;
/** Maximum number of attempts. Default: 6 */
private int maxAttempts = 6;
/** Initial retry delay in milliseconds. Default: 100ms */
private long initialRetryDelayMs = 100;
/** Maximum retry delay in milliseconds. Default: 5000ms (5s) */
private long maxRetryDelayMs = 5000;
/** Retry delay multiplier. Default: 1.3 */
private double retryDelayMultiplier = 1.3;
// getters and setters...
}
}
```
#### 2. Update `GcpDatastoreAutoConfiguration`
```java
private Datastore getDatastore(String namespace) {
DatastoreOptions.Builder builder = DatastoreOptions.newBuilder()
.setProjectId(this.projectId)
.setHeaderProvider(new UserAgentHeaderProvider(this.getClass()))
.setCredentials(this.credentials);
// Apply timeout configuration
TimeoutProperties timeoutProps = gcpDatastoreProperties.getTimeout();
if (timeoutProps != null) {
HttpTransportOptions transportOptions = HttpTransportOptions.newBuilder()
.setConnectTimeout(timeoutProps.getConnectTimeoutMs())
.setReadTimeout(timeoutProps.getReadTimeoutMs())
.build();
builder.setTransportOptions(transportOptions);
}
// Apply retry configuration
RetryProperties retryProps = gcpDatastoreProperties.getRetry();
if (retryProps != null) {
RetrySettings retrySettings = RetrySettings.newBuilder()
.setTotalTimeout(Duration.ofMillis(retryProps.getTotalTimeoutMs()))
.setInitialRpcTimeout(Duration.ofMillis(retryProps.getInitialRpcTimeoutMs()))
.setMaxRpcTimeout(Duration.ofMillis(retryProps.getMaxRpcTimeoutMs()))
.setRpcTimeoutMultiplier(retryProps.getRpcTimeoutMultiplier())
.setMaxAttempts(retryProps.getMaxAttempts())
.setInitialRetryDelay(Duration.ofMillis(retryProps.getInitialRetryDelayMs()))
.setMaxRetryDelay(Duration.ofMillis(retryProps.getMaxRetryDelayMs()))
.setRetryDelayMultiplier(retryProps.getRetryDelayMultiplier())
.build();
builder.setRetrySettings(retrySettings);
}
if (namespace != null) {
builder.setNamespace(namespace);
}
if (databaseId != null) {
builder.setDatabaseId(databaseId);
}
if (this.host != null) {
builder.setHost(this.host);
}
return builder.build().getService();
}
```
## Use Cases
### Use Case 1: Low-Latency Requirements
Applications with strict latency requirements need to fail fast rather than wait for default timeouts:
```yaml
spring:
cloud:
gcp:
datastore:
timeout:
connect-timeout-ms: 500
read-timeout-ms: 1000
retry:
max-attempts: 1 # No retries - fail fast
```
### Use Case 2: High-Reliability Requirements
Applications that prioritize reliability over latency need more aggressive retry strategies:
```yaml
spring:
cloud:
gcp:
datastore:
timeout:
connect-timeout-ms: 5000
read-timeout-ms: 10000
retry:
max-attempts: 5
total-timeout-ms: 30000
initial-retry-delay-ms: 500
retry-delay-multiplier: 2.0
```
### Use Case 3: Environment-Specific Configuration
Different environments (dev, staging, production) require different timeout profiles:
```yaml
# application-prod.yaml
spring:
cloud:
gcp:
datastore:
timeout:
connect-timeout-ms: 2000
read-timeout-ms: 5000
retry:
max-attempts: 3
# application-dev.yaml
spring:
cloud:
gcp:
datastore:
timeout:
connect-timeout-ms: 10000
read-timeout-ms: 30000
retry:
max-attempts: 1 # Fail fast in dev for quicker feedback
```
## Current Workaround
Developers currently must create custom configuration classes that replicate Spring Cloud GCP's logic:
```java
@Configuration
@AutoConfigureBefore(GcpDatastoreAutoConfiguration.class)
public class CustomDatastoreTimeoutConfig {
@Bean
@ConditionalOnMissingBean
public DatastoreProvider datastoreProvider(
GcpProjectIdProvider projectIdProvider,
ObjectProvider namespaceProvider,
CustomTimeoutProperties timeoutProperties) {
// Must manually replicate Spring Cloud GCP's namespace handling
// Must manually replicate credential handling
// Must manually replicate project ID resolution
// Brittle - breaks if Spring Cloud GCP changes its implementation
// ... custom implementation ...
}
}
```
**Problems with this approach:**
- ❌ Code duplication
- ❌ Maintenance burden
- ❌ Risk of incompatibility with Spring Cloud GCP updates
- ❌ Loss of automatic features (namespace providers, credential providers)
- ❌ Requires deep understanding of Spring Cloud GCP internals
## Benefits of This Feature
1. **Consistency**: Aligns with how other Spring Cloud GCP components (Pub/Sub, Storage) expose configuration
2. **Simplicity**: Developers can configure timeouts through standard Spring Boot properties
3. **Flexibility**: Different environments can have different timeout profiles
4. **Maintainability**: No need for custom autoconfiguration classes
5. **Best Practices**: Encourages proper timeout configuration rather than using defaults
6. **Production Ready**: Essential for production deployments with specific SLA requirements
## Comparison with Other Spring Cloud GCP Components
### Spring Cloud GCP Pub/Sub (Already Supports Configuration)
```yaml
spring:
cloud:
gcp:
pubsub:
subscriber:
max-ack-extension-period: 0
parallel-pull-count: 1
pull-endpoint: localhost:8085
publisher:
batching:
element-count-threshold: 1
request-byte-threshold: 1000
```
### Spring Cloud GCP Storage (Already Supports Configuration)
```yaml
spring:
cloud:
gcp:
storage:
project-id: my-project
credentials:
location: classpath:credentials.json
```
### Spring Cloud GCP Datastore (Currently Missing Configuration)
```yaml
spring:
cloud:
gcp:
datastore:
project-id: my-project
namespace: my-namespace
# ❌ NO timeout configuration
# ❌ NO retry configuration
```
## Implementation Considerations
### Backward Compatibility
- All new properties should have sensible defaults matching current behavior
- Existing applications without these properties should work unchanged
- Properties should be optional
### Documentation
- Add to Spring Cloud GCP reference documentation
- Provide examples for common use cases
- Include migration guide for developers using custom workarounds
### Testing
- Add integration tests with custom timeout configurations
- Add tests verifying backward compatibility
- Add tests for environment-specific configuration
## Example Real-World Impact
Our production application requires:
- **Connect timeout**: 1000ms (must connect quickly or fail)
- **Read timeout**: 2000ms (must read quickly or fail)
- **Max attempts**: 1 (fail fast for load balancer to retry on different pod)
Without this feature, we had to:
1. Create a custom `DatastoreProvider` bean
2. Manually replicate namespace resolution logic
3. Cache Datastore instances per namespace
4. Maintain 150+ lines of configuration code
5. Risk breakage with Spring Cloud GCP updates
**With this feature**, we could simply configure:
```yaml
spring:
cloud:
gcp:
datastore:
timeout:
connect-timeout-ms: 1000
read-timeout-ms: 2000
retry:
max-attempts: 1
```
## References
- Google Cloud Datastore Client Library: https://cloud.google.com/java/docs/reference/google-cloud-datastore/latest
- `DatastoreOptions.Builder` API: https://cloud.google.com/java/docs/reference/google-cloud-datastore/latest/com.google.cloud.datastore.DatastoreOptions.Builder
- `RetrySettings` API: https://googleapis.dev/java/gax/latest/com/google/api/gax/retrying/RetrySettings.html
- `HttpTransportOptions` API: https://googleapis.dev/java/google-http-client/latest/com/google/api/client/http/HttpTransport.html
## Proposed Timeline
1. **Phase 1**: Add timeout configuration properties
2. **Phase 2**: Add retry configuration properties
3. **Phase 3**: Add documentation and examples
4. **Phase 4**: Add integration tests
Contributor guide
Assessment
This issue has not been assessed yet.