Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Added `startup-timeout` configuration option that enables automatic retry with backoff when transient failures occur during application startup. The provider will continue retrying until the timeout expires (default: 100 seconds).

### Breaking Changes

### Bugs Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ String findOriginForEndpoint(String endpoint) {
return endpoint;
}

/**
* Gets the duration in milliseconds until the next client becomes available for the specified store.
*
* @param originEndpoint the origin configuration store endpoint
* @return duration in milliseconds until next client is available, or 0 if one is available now
*/
long getMillisUntilNextClientAvailable(String originEndpoint) {
return CONNECTIONS.get(originEndpoint).getMillisUntilNextClientAvailable();
}

/**
* Updates the sync token for a specific replica endpoint.
*
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public List<AzureAppConfigDataResource> resolveProfileSpecific(

for (ConfigStore store : properties.getStores()) {
locations.add(
new AzureAppConfigDataResource(properties.isEnabled(), store, profiles, START_UP.get(), properties.getRefreshInterval()));
new AzureAppConfigDataResource(properties.isEnabled(), store, profiles, START_UP.get(), properties.getRefreshInterval(), properties.getStartupTimeout()));
}
START_UP.set(false);
return locations;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,21 @@ public class AzureAppConfigDataResource extends ConfigDataResource {
/** The interval at which configuration should be refreshed from the store. */
private final Duration refreshInterval;

/** The timeout duration for retry attempts during startup. */
private final Duration startupTimeout;

/**
* Constructs a new AzureAppConfigDataResource with the specified configuration store settings.
*
* @param appConfigEnabled true if Azure App Configuration is globally enabled
* @param configStore the configuration store settings containing endpoint, selectors, and other options
* @param profiles the Spring Boot profiles for conditional configuration loading
* @param startup true if this is a startup load operation, false if it is a refresh operation
* @param refreshInterval the interval at which configuration should be refreshed
* @param startupTimeout the timeout duration for retry attempts during startup
*/
AzureAppConfigDataResource(boolean appConfigEnabled, ConfigStore configStore, Profiles profiles, boolean startup,
Duration refreshInterval) {
Duration refreshInterval, Duration startupTimeout) {
this.configStoreEnabled = appConfigEnabled && configStore.isEnabled();
this.endpoint = configStore.getEndpoint();
this.selects = configStore.getSelects();
Expand All @@ -66,6 +71,7 @@ public class AzureAppConfigDataResource extends ConfigDataResource {
this.profiles = profiles;
this.isRefresh = !startup;
this.refreshInterval = refreshInterval;
this.startupTimeout = startupTimeout;
}

/**
Expand Down Expand Up @@ -148,4 +154,13 @@ public boolean isRefresh() {
public Duration getRefreshInterval() {
return refreshInterval;
}

/**
* Gets the timeout duration for retry attempts during startup.
*
* @return the startup timeout duration
*/
public Duration getStartupTimeout() {
return startupTimeout;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,51 @@ void backoffClient(String endpoint) {
autoFailoverClients.get(endpoint).updateBackoffEndTime(Instant.now().plusNanos(backoffTime));
}

/**
* Gets the duration in milliseconds until the next client becomes available (exits backoff).
* Returns 0 if a client is already available, or the minimum wait time if all clients are in backoff.
*
* @return duration in milliseconds until next client is available, or 0 if one is available now
*/
long getMillisUntilNextClientAvailable() {
Instant now = Instant.now();
Instant earliestAvailable = Instant.MAX;

// Check configured clients
if (clients != null) {
for (AppConfigurationReplicaClient client : clients) {
Instant backoffEnd = client.getBackoffEndTime();
if (!backoffEnd.isAfter(now)) {
return 0; // Client available now
}
if (backoffEnd.isBefore(earliestAvailable)) {
earliestAvailable = backoffEnd;
}
}
}

// Check auto-failover clients
for (AppConfigurationReplicaClient client : autoFailoverClients.values()) {
Instant backoffEnd = client.getBackoffEndTime();
if (!backoffEnd.isAfter(now)) {
return 0; // Client available now
}
if (backoffEnd.isBefore(earliestAvailable)) {
earliestAvailable = backoffEnd;
}
}

// If no clients were found or no backoff times were set, avoid calling toEpochMilli on Instant.MAX.
if (Instant.MAX.equals(earliestAvailable)) {
// No clients are currently tracked; treat as no wait required.
return 0L;
}

long millisUntilNext = earliestAvailable.toEpochMilli() - now.toEpochMilli();
// Ensure we never return a negative duration, even in the presence of clock skew.
return Math.max(millisUntilNext, 0L);
}
Comment thread
mrm9084 marked this conversation as resolved.

/**
* Updates the synchronization token for the specified client endpoint.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ public class AppConfigurationProperties {
private Duration refreshInterval;

/**
* Returns whether Azure App Configuration is enabled.
*
* @return {@code true} if enabled, {@code false} otherwise
* The timeout duration for retry attempts during startup.
*/
private Duration startupTimeout = Duration.ofSeconds(100);

/**
* @return the enabled
*/
public boolean isEnabled() {
return enabled;
Expand Down Expand Up @@ -91,10 +94,22 @@ public void setRefreshInterval(Duration refreshInterval) {
}

/**
* Validates that at least one store is configured with a valid endpoint or
* connection string, and that no duplicate endpoints exist.
*
* @throws IllegalArgumentException if validation fails or duplicate endpoints are found
* @return the startupTimeout
*/
public Duration getStartupTimeout() {
return startupTimeout;
}

/**
* @param startupTimeout the startupTimeout to set
*/
public void setStartupTimeout(Duration startupTimeout) {
this.startupTimeout = startupTimeout;
}

/**
* Validates at least one store is configured for use, and that they are valid.
* @throws IllegalArgumentException when duplicate endpoints are configured
*/
@PostConstruct
public void validateAndInit() {
Expand Down Expand Up @@ -134,5 +149,12 @@ public void validateAndInit() {
if (refreshInterval != null) {
Assert.isTrue(refreshInterval.getSeconds() >= 1, "Minimum refresh interval time is 1 Second.");
}
Comment thread
mrm9084 marked this conversation as resolved.
if (startupTimeout == null) {
throw new IllegalArgumentException("startupTimeout cannot be null.");
}
Comment thread
mrm9084 marked this conversation as resolved.
if (startupTimeout.compareTo(Duration.ofSeconds(30)) < 0
|| startupTimeout.compareTo(Duration.ofSeconds(600)) > 0) {
throw new IllegalArgumentException("startupTimeout must be between 30 and 600 seconds.");
}
Comment thread
mrm9084 marked this conversation as resolved.
Comment thread
mrm9084 marked this conversation as resolved.
}
}
Loading
Loading