Skip to content

Add Configurable Timeout and Retry Settings to GcpDatastoreAutoConfiguration #4249

@carlos-acevedo-f9

Description

@carlos-acevedo-f9

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)

// 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

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

@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

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:

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:

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:

# 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:

@Configuration
@AutoConfigureBefore(GcpDatastoreAutoConfiguration.class)
public class CustomDatastoreTimeoutConfig {
    
    @Bean
    @ConditionalOnMissingBean
    public DatastoreProvider datastoreProvider(
        GcpProjectIdProvider projectIdProvider,
        ObjectProvider<DatastoreNamespaceProvider> 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)

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)

spring:
  cloud:
    gcp:
      storage:
        project-id: my-project
        credentials:
          location: classpath:credentials.json

Spring Cloud GCP Datastore (Currently Missing Configuration)

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:

spring:
  cloud:
    gcp:
      datastore:
        timeout:
          connect-timeout-ms: 1000
          read-timeout-ms: 2000
        retry:
          max-attempts: 1

References

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions