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

/**
Comment on lines +111 to 120
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The method getMillisUntilNextClientAvailable is not used anywhere in the production code. It's only called in tests. This suggests that either the method should be removed as dead code, or it should be integrated into the retry logic. Consider removing this method or using it in the startup retry implementation.

Suggested change
* 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();
}
/**

Copilot uses AI. Check for mistakes.
* 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 on lines +251 to +280
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

The method getMillisUntilNextClientAvailable is not used anywhere in the production code. It's only called in tests. This suggests that either the method should be removed as dead code, or the retry logic in AzureAppConfigDataLoader should be using this method to determine optimal wait times instead of fixed backoff intervals. Consider removing this method or integrating it into the actual retry logic.

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.");
}
Comment on lines +137 to +142
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

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

There are no tests verifying the startupTimeout validation logic that was added. Consider adding tests to verify that: 1) null startupTimeout throws IllegalArgumentException, 2) values below 30 seconds throw IllegalArgumentException, 3) values above 600 seconds throw IllegalArgumentException, and 4) values within the valid range (30-600) are accepted.

Copilot uses AI. Check for mistakes.
}
}
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