Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}

/**
* 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();
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no clients exist or all have Instant.MAX as backoffEnd, earliestAvailable will remain Instant.MAX, and the calculation 'earliestAvailable.toEpochMilli() - now.toEpochMilli()' will produce an extremely large positive value or potentially overflow. Consider adding a guard to return a sensible maximum value or Long.MAX_VALUE when earliestAvailable is still Instant.MAX.

Suggested change
return earliestAvailable.toEpochMilli() - now.toEpochMilli();
// If no clients exist or all have Instant.MAX as backoffEnd, avoid overflowing the duration.
if (Instant.MAX.equals(earliestAvailable)) {
return Long.MAX_VALUE;
}
long millisUntilAvailable = earliestAvailable.toEpochMilli() - now.toEpochMilli();
return millisUntilAvailable > 0 ? millisUntilAvailable : 0;

Copilot uses AI. Check for mistakes.
}

/**
* 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.");
}
if (startupTimeout == null) {
throw new IllegalArgumentException("startupTimeout cannot be null.");
}
if (startupTimeout.getSeconds() < 30 || startupTimeout.getSeconds() > 600) {
throw new IllegalArgumentException("startupTimeout must be between 30 and 600 seconds.");
}
}
}
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