Skip to content
Merged
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 @@ -116,11 +116,11 @@ private void handleKeyVaultReference(String key, SecretReferenceConfigurationSet
KeyVaultSecret secret = keyVaultClientFactory.getClient("https://" + uri.getHost()).getSecret(uri);
properties.put(key, secret.getValue());
} catch (URISyntaxException e) {
LOGGER.error(String.format("Error Retrieving Key Vault Entry for key %s.", key));
LOGGER.error("Error Retrieving Key Vault Entry for key {}.", key);
throw new InvalidConfigurationPropertyValueException(key, "<Redacted>",
"Invalid URI found in JSON property field 'uri' unable to parse.");
} catch (RuntimeException e) {
LOGGER.error(String.format("Error Retrieving Key Vault Entry for key %s.", key));
LOGGER.error("Error Retrieving Key Vault Entry for key {}.", key);
throw e;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class AppConfigurationFeatureManagementPropertySource extends EnumerableProperty

@Override
public String[] getPropertyNames() {
if (featureFlagLoader != null && !featureFlagLoader.getFeatureFlags().isEmpty()) {
if (featureFlagLoader != null && featureFlagLoader.getFeatureFlags() != null
&& !featureFlagLoader.getFeatureFlags().isEmpty()) {
return new String[] { FEATURE_FLAG_KEY };
}
return new String[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
* Vault host.
*/
class AppConfigurationKeyVaultClientFactory {

/**
* Cache of secret client managers by Key Vault host.
*/
private final Map<String, AppConfigurationSecretClientManager> keyVaultClients;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.core.env.EnumerablePropertySource;
Expand All @@ -23,6 +22,8 @@
abstract class AppConfigurationPropertySource extends EnumerablePropertySource<AppConfigurationReplicaClient> {

/**
* Cache for storing configuration properties retrieved from Azure App Configuration.
*/
protected final Map<String, Object> properties = new LinkedHashMap<>();

/**
Expand All @@ -39,7 +40,7 @@ abstract class AppConfigurationPropertySource extends EnumerablePropertySource<A
AppConfigurationPropertySource(String name, AppConfigurationReplicaClient replicaClient) {
// The context alone does not uniquely define a PropertySource, append storeName
// and label to uniquely define a PropertySource
super(name);
super(name, replicaClient);
this.replicaClient = replicaClient;
}

Expand All @@ -50,13 +51,14 @@ abstract class AppConfigurationPropertySource extends EnumerablePropertySource<A
*/
@Override
public String[] getPropertyNames() {
Set<String> keySet = properties.keySet();
return keySet.toArray(new String[keySet.size()]);
return properties.keySet().toArray(String[]::new);
}

/**
* Returns the value of the specified property.
*
* @param name the name of the property to retrieve
* @return the value of the property, or null if not found
*/
@Override
public Object getProperty(String name) {
Expand All @@ -70,11 +72,19 @@ public Object getProperty(String name) {
* @return comma-separated string of labels, or empty string if null/empty
*/
protected static String getLabelName(String[] labelFilters) {
if (labelFilters == null) {
if (labelFilters == null || labelFilters.length == 0) {
return "";
}
return String.join(",", labelFilters);
}

protected abstract void initProperties(List<String> trim, Context context) throws InvalidConfigurationPropertyValueException;
/**
* Initializes the properties for this property source by loading them from Azure App Configuration.
*
* @param trim list of key prefixes to trim from configuration keys
* @param context the context for loading properties, may contain additional metadata
* @throws InvalidConfigurationPropertyValueException if there are issues with the configuration properties
*/
protected abstract void initProperties(List<String> trim, Context context)
throws InvalidConfigurationPropertyValueException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class AppConfigurationReplicaClient {
/**
* Holds Configuration Client and info needed to manage backoff.
* @param endpoint client endpoint
* @param originClient origin client identifier
* @param client Configuration Client to App Configuration store
*/
AppConfigurationReplicaClient(String endpoint, String originClient, ConfigurationClient client) {
Expand Down Expand Up @@ -105,6 +106,7 @@ String getOriginClient() {
* @param label String value of the watch key, use \0 for null.
* @param context Azure SDK context for request correlation
* @return The first returned configuration.
* @throws HttpResponseException if the request fails
*/
ConfigurationSetting getWatchKey(String key, String label, Context context)
throws HttpResponseException {
Expand Down Expand Up @@ -212,8 +214,9 @@ List<ConfigurationSetting> listSettingSnapshot(String snapshotName, Context cont
}

boolean checkWatchKeys(SettingSelector settingSelector, Context context) {
List<PagedResponse<ConfigurationSetting>> results = client.listConfigurationSettings(settingSelector, context)
.streamByPage().filter(pagedResponse -> pagedResponse.getStatusCode() != 304).toList();
List<PagedResponse<ConfigurationSetting>> results = client
.listConfigurationSettings(settingSelector, context)
.streamByPage().filter(pagedResponse -> pagedResponse.getStatusCode() != HTTP_NOT_MODIFIED).toList();
return results.size() > 0;
}

Expand All @@ -227,7 +230,14 @@ void updateSyncToken(String syncToken) {
}
}

private HttpResponseException hanndleHttpResponseException(HttpResponseException e) {
/**
* Handles HTTP response exceptions by determining if they are retryable.
*
* @param e the HTTP response exception to handle
* @return an AppConfigurationStatusException for retryable errors, or the original exception for non-retryable
* errors
*/
private HttpResponseException handleHttpResponseException(HttpResponseException e) {
if (e.getResponse() != null) {
int statusCode = e.getResponse().getStatusCode();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class AppConfigurationReplicaClientFactory {
AppConfigurationReplicaClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder,
List<ConfigStore> configStores, ReplicaLookUp replicaLookUp) {
this.configStores = configStores;
if (CONNECTIONS.size() == 0) {
if (CONNECTIONS.isEmpty()) {
for (ConfigStore store : configStores) {
ConnectionManager manager = new ConnectionManager(clientBuilder, store, replicaLookUp);
CONNECTIONS.put(manager.getMainEndpoint(), manager);
Expand All @@ -51,9 +51,11 @@ public Map<String, ConnectionManager> getConnections() {
/**
* Returns available replica clients for a given configuration store.
*
* @param originEndpoint identifier of the store (primary endpoint)
* @return list of available replica clients for the store
*/
List<AppConfigurationReplicaClient> getAvailableClients(String originEndpoint) {
return CONNECTIONS.get(originEndpoint).getAvailableClients();
return getAvailableClients(originEndpoint, false);
}

/**
Expand All @@ -73,7 +75,7 @@ List<AppConfigurationReplicaClient> getAvailableClients(String originEndpoint, B
* @param originEndpoint identifier of the store (primary endpoint)
* @param endpoint the specific replica endpoint that failed
*/
void backoffClientClient(String originEndpoint, String endpoint) {
void backoffClient(String originEndpoint, String endpoint) {
CONNECTIONS.get(originEndpoint).backoffClient(endpoint);
}

Expand All @@ -98,10 +100,9 @@ Map<String, AppConfigurationStoreHealth> getHealth() {
*/
String findOriginForEndpoint(String endpoint) {
for (ConfigStore store : configStores) {
for (String replica : store.getEndpoints()) {
if (replica.equals(endpoint)) {
return store.getEndpoint();
}
List<String> replicas = store.getEndpoints();
if (replicas != null && replicas.contains(endpoint)) {
return store.getEndpoint();
}
}
return endpoint;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.logging.Log;
Expand Down Expand Up @@ -125,20 +126,21 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour

boolean reloadFailed = false;
boolean pushRefresh = false;
Exception lastException = null;
PushNotification notification = resource.getMonitoring().getPushNotification();
if ((notification.getPrimaryToken() != null
&& StringUtils.hasText(notification.getPrimaryToken().getName()))
|| (notification.getSecondaryToken() != null
&& StringUtils.hasText(notification.getPrimaryToken().getName()))) {
pushRefresh = true;
}
requestContext = new Context("refresh", resource.isRefresh()).addData(PUSH_REFRESH,
pushRefresh);

// Feature Management needs to be set in the last config store.
requestContext = new Context("refresh", resource.isRefresh()).addData(PUSH_REFRESH, pushRefresh);

Iterator<AppConfigurationReplicaClient> clientIterator = clients.iterator();

for (AppConfigurationReplicaClient client : clients) {
sourceList = new ArrayList<>();
while (clientIterator.hasNext()) {
AppConfigurationReplicaClient client = clientIterator.next();

if (reloadFailed
&& !AppConfigurationRefreshUtil.refreshStoreCheck(client,
Expand Down Expand Up @@ -167,19 +169,31 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour

storeState.setState(resource.getEndpoint(), watchKeysSettings, monitoring.getRefreshInterval());
}
storeState.setLoadState(resource.getEndpoint(), true);
storeState.setLoadState(resource.getEndpoint(), true); // Success - configuration loaded, exit loop
lastException = null;
// Break out of the loop since we have successfully loaded configuration
break;
} catch (AppConfigurationStatusException e) {
reloadFailed = true;
replicaClientFactory.backoffClientClient(resource.getEndpoint(), client.getEndpoint());
replicaClientFactory.backoffClient(resource.getEndpoint(), client.getEndpoint());
lastException = e;
// Log the specific replica failure with context
logReplicaFailure(client, "status exception", clientIterator.hasNext(), e);
} catch (Exception e) {
failedToGeneratePropertySource(e);

// Not a retriable error
break;
}
if (sourceList.size() > 0) {
break;
// Store the exception to potentially use if all replicas fail
lastException = e; // Log the specific replica failure with context
logReplicaFailure(client, "exception", clientIterator.hasNext(), e);
}
} // Check if all replicas failed
if (lastException != null && !resource.isRefresh()) {
// During startup, if all replicas failed, fail the application
logger.error("Azure App Configuration failed to load configuration during startup for store: "
+ resource.getEndpoint() + ". Application cannot start without required configuration.");
failedToGeneratePropertySource(lastException);
} else if (lastException != null && resource.isRefresh()) {
// During refresh, log warning but don't fail the application
logger.warn("Azure App Configuration failed during refresh for store: "
+ resource.getEndpoint() + ". Continuing with existing configuration.");
}
}

Expand Down Expand Up @@ -256,15 +270,36 @@ private List<FeatureFlags> createFeatureFlags(AppConfigurationReplicaClient clie
return featureFlagWatchKeys;
}

/**
* Logs a replica failure with contextual information about the failure scenario and available replicas.
*
* @param client the replica client that failed
* @param exceptionType a brief description of the exception type (e.g., "status exception", "exception")
* @param hasMoreReplicas whether there are additional replicas available to try
* @param exception the exception that caused the failure
*/
private void logReplicaFailure(AppConfigurationReplicaClient client, String exceptionType,
boolean hasMoreReplicas, Exception exception) {
String scenario = resource.isRefresh() ? "refresh" : "startup";
String nextAction = hasMoreReplicas ? "Trying next replica." : "No more replicas available.";

logger.warn("Azure App Configuration replica " + client.getEndpoint()
+ " failed during " + scenario + " with " + exceptionType + ". "
+ nextAction + " Store: " + resource.getEndpoint(), exception);
}

/**
* Introduces a delay before throwing exceptions during startup to prevent fast crash loops.
*/
private void delayException() {
Instant currentDate = Instant.now();
Instant preKillTIme = START_DATE.plusSeconds(PREKILL_TIME);
if (currentDate.isBefore(preKillTIme)) {
long diffInMillies = Math.abs(preKillTIme.toEpochMilli() - currentDate.toEpochMilli());
Instant preKillTime = START_DATE.plusSeconds(PREKILL_TIME);
if (currentDate.isBefore(preKillTime)) {
long diffInMillies = Math.abs(preKillTime.toEpochMilli() - currentDate.toEpochMilli());
try {
Thread.sleep(diffInMillies);
} catch (InterruptedException e) {
logger.error("Failed to wait before fast fail.");
Thread.currentThread().interrupt(); // Restore interrupted status
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,29 @@ public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDat
if (!location.hasPrefix(PREFIX)) {
return false;
}
Boolean hasEndpoint = StringUtils.hasText(context.getBinder()
.bind(AppConfigurationProperties.CONFIG_PREFIX + ".stores[0].endpoint", String.class)
.orElse(""));
Boolean hasConnectionString = StringUtils.hasText(context.getBinder()
.bind(AppConfigurationProperties.CONFIG_PREFIX + ".stores[0].connection-string", String.class)
.orElse(""));
Boolean hasEndpoints = StringUtils.hasText(context.getBinder()
.bind(AppConfigurationProperties.CONFIG_PREFIX + ".stores[0].endpoints", String.class)
.orElse(""));
Boolean hasConnectionStrings = StringUtils.hasText(context.getBinder()
.bind(AppConfigurationProperties.CONFIG_PREFIX + ".stores[0].connection-strings", String.class)
.orElse(""));

return (hasEndpoint || hasConnectionString || hasEndpoints || hasConnectionStrings);

// Check if the configuration properties for Azure App Configuration are present
return hasValidStoreConfiguration(context.getBinder());
}

/**
* Checks if the required configuration properties for Azure App Configuration are present.
*
* @param binder the binder to check for properties
* @return true if at least one of the required properties is present, false otherwise
*/
private boolean hasValidStoreConfiguration(Binder binder) {
// Check if any of the required properties for Azure App Configuration stores are present
String configPrefix = AppConfigurationProperties.CONFIG_PREFIX + ".stores[0].";

return hasNonEmptyProperty(binder, configPrefix + "endpoint")
|| hasNonEmptyProperty(binder, configPrefix + "connection-string")
|| hasNonEmptyProperty(binder, configPrefix + "endpoints")
|| hasNonEmptyProperty(binder, configPrefix + "connection-strings");
}

private boolean hasNonEmptyProperty(Binder binder, String propertyPath) {
return StringUtils.hasText(binder.bind(propertyPath, String.class).orElse(""));
}

/**
Expand Down Expand Up @@ -105,6 +112,12 @@ public List<AzureAppConfigDataResource> resolveProfileSpecific(
AppConfigurationProperties properties = loadProperties(resolverContext);
List<AzureAppConfigDataResource> locations = new ArrayList<>();

if (properties.getStores() == null || properties.getStores().isEmpty()) {
throw new ConfigDataLocationNotFoundException(location,
"No Azure App Configuration stores are configured. Please check your application properties.",
new IllegalStateException("No stores configured"));
}

for (ConfigStore store : properties.getStores()) {
locations.add(
new AzureAppConfigDataResource(store, profiles, START_UP.get(), properties.getRefreshInterval()));
Expand Down
Loading