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();
}

/**
Comment thread
mrm9084 marked this conversation as resolved.
Outdated
* Sets the current active replica for a configuration store.
*
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 @@ -242,6 +242,43 @@ 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;
}
}

return earliestAvailable.toEpochMilli() - now.toEpochMilli();
Comment thread
mrm9084 marked this conversation as resolved.
Outdated
Comment thread
mrm9084 marked this conversation as resolved.
Outdated
}
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 @@ -36,6 +36,11 @@ public class AppConfigurationProperties {

private Duration refreshInterval;

/**
* The timeout duration for retry attempts during startup.
*/
private Duration startupTimeout = Duration.ofSeconds(100);

/**
* @return the enabled
*/
Expand Down Expand Up @@ -78,6 +83,20 @@ public void setRefreshInterval(Duration refreshInterval) {
this.refreshInterval = refreshInterval;
}

/**
* @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
Expand Down Expand Up @@ -115,5 +134,11 @@ 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.getSeconds() < 30 || startupTimeout.getSeconds() > 600) {
Comment thread
mrm9084 marked this conversation as resolved.
Outdated
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.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -264,29 +264,28 @@ public void validateAndInit() {
}

if (StringUtils.hasText(connectionString)) {
String endpoint = (AppConfigurationReplicaClientsBuilder.getEndpointFromConnectionString(connectionString));
String parsedEndpoint = AppConfigurationReplicaClientsBuilder.getEndpointFromConnectionString(connectionString);
try {
// new URI is used to validate the endpoint as a valid URI
new URI(endpoint);
this.endpoint = endpoint;
new URI(parsedEndpoint);
this.endpoint = parsedEndpoint;
} catch (URISyntaxException e) {
throw new IllegalStateException("Endpoint in connection string is not a valid URI.", e);
}
} else if (connectionStrings.size() > 0) {
} else if (!connectionStrings.isEmpty()) {
for (String connection : connectionStrings) {

String endpoint = (AppConfigurationReplicaClientsBuilder.getEndpointFromConnectionString(connection));
String parsedEndpoint = AppConfigurationReplicaClientsBuilder.getEndpointFromConnectionString(connection);
try {
// new URI is used to validate the endpoint as a valid URI
new URI(endpoint).toURL();
new URI(parsedEndpoint).toURL();
if (!StringUtils.hasText(this.endpoint)) {
this.endpoint = endpoint;
this.endpoint = parsedEndpoint;
}
} catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) {
throw new IllegalStateException("Endpoint in connection string is not a valid URI.", e);
}
}
} else if (endpoints.size() > 0) {
} else if (!endpoints.isEmpty()) {
endpoint = endpoints.get(0);
}

Expand Down
Loading
Loading