Skip to content
This repository has been archived by the owner on Aug 29, 2024. It is now read-only.

Commit

Permalink
Merge pull request #605 from mrm9084/RefreshAsync
Browse files Browse the repository at this point in the history
App Config - Refresh Async
  • Loading branch information
superrdean authored Jan 10, 2020
2 parents 8feaf91 + d5e1fe4 commit c2eafdf
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@
*/
package com.microsoft.azure.spring.cloud.config;

import static com.microsoft.azure.spring.cloud.config.Constants.CONFIGURATION_SUFFIX;
import static com.microsoft.azure.spring.cloud.config.Constants.FEATURE_STORE_WATCH_KEY;
import static com.microsoft.azure.spring.cloud.config.Constants.FEATURE_SUFFIX;

import java.time.Duration;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.endpoint.event.RefreshEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;

import com.azure.data.appconfiguration.models.ConfigurationSetting;
Expand All @@ -31,39 +33,30 @@
public class AzureCloudConfigRefresh implements ApplicationEventPublisherAware {
private static final Logger LOGGER = LoggerFactory.getLogger(AzureCloudConfigRefresh.class);

private final Map<String, String> storeEtagMap = new ConcurrentHashMap<>();

private final AtomicBoolean running = new AtomicBoolean(false);

private ApplicationEventPublisher publisher;

private final Map<String, Boolean> firstTimeMap = new ConcurrentHashMap<>();

private final List<ConfigStore> configStores;

private final Map<String, List<String>> storeContextsMap;

private static final String CONFIGURATION_SUFFIX = "_configuration";

private static final String FEATURE_SUFFIX = "_feature";

private static final String FEATURE_STORE_SUFFIX = ".appconfig";

private static final String FEATURE_STORE_WATCH_KEY = FEATURE_STORE_SUFFIX + "*";

private Duration delay;

private ClientStore clientStore;

private Date lastCheckedTime;

private String eventDataInfo;

public AzureCloudConfigRefresh(AzureCloudConfigProperties properties, Map<String, List<String>> storeContextsMap,
ClientStore clientStore) {
this.configStores = properties.getStores();
this.storeContextsMap = storeContextsMap;
this.delay = properties.getCacheExpiration();
this.lastCheckedTime = new Date();
this.clientStore = clientStore;
this.eventDataInfo = "";
}

@Override
Expand All @@ -74,31 +67,56 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv
/**
* Checks configurations to see if they are no longer cached. If they are no longer
* cached they are updated.
*
* @return Future with a boolean of if a RefreshEvent was published. If
* refreshConfigurations is currently being run elsewhere this method will return
* right away as <b>false</b>.
*/
public void refreshConfigurations() {
if (this.running.compareAndSet(false, true)) {
Boolean refreshed = false;
Date notCachedTime = DateUtils.addSeconds(lastCheckedTime, Math.toIntExact(delay.getSeconds()));
Date date = new Date();
if (date.after(notCachedTime)) {
for (ConfigStore configStore : configStores) {
String watchedKeyNames = watchedKeyNames(configStore, storeContextsMap);
refreshed = refresh(configStore, CONFIGURATION_SUFFIX, watchedKeyNames);
// Refresh Feature Flags
if (!refreshed) {
refreshed = refresh(configStore, FEATURE_SUFFIX, FEATURE_STORE_WATCH_KEY);
}
public Future<Boolean> refreshConfigurations() {
return CompletableFuture.supplyAsync(() -> refreshStores());
}

// The Refresh Event updates all config stores
if (refreshed) {
break;
/**
* Goes through each config store and checks if any of its keys need to be refreshed.
* If any store has a value that needs to be updated a refresh event is called after
* every store is checked.
* @return If a refresh event is called.
*/
private boolean refreshStores() {
boolean willRefresh = false;
if (running.compareAndSet(false, true)) {
try {
Date notCachedTime = null;

// LastCheckedTime isn't sent until refresh is run once, this forces a
// eTag set on startup
if (lastCheckedTime != null) {
notCachedTime = DateUtils.addSeconds(lastCheckedTime, Math.toIntExact(delay.getSeconds()));
}
Date date = new Date();
if (notCachedTime == null || date.after(notCachedTime)) {
for (ConfigStore configStore : configStores) {
String watchedKeyNames = clientStore.watchedKeyNames(configStore, storeContextsMap);
willRefresh = refresh(configStore, CONFIGURATION_SUFFIX, watchedKeyNames) ? true
: willRefresh;
// Refresh Feature Flags
willRefresh = refresh(configStore, FEATURE_SUFFIX, FEATURE_STORE_WATCH_KEY) ? true
: willRefresh;
}
// Resetting last Checked date to now.
lastCheckedTime = new Date();
}
if (willRefresh) {
// Only one refresh Event needs to be call to update all of the
// stores, not one for each.
RefreshEventData eventData = new RefreshEventData(eventDataInfo);
publisher.publishEvent(new RefreshEvent(this, eventData, eventData.getMessage()));
}
// Resetting last Checked date to now.
lastCheckedTime = new Date();
} finally {
running.set(false);
}
this.running.set(false);
}
return willRefresh;
}

/**
Expand All @@ -110,30 +128,32 @@ public void refreshConfigurations() {
* @param watchedKeyNames Key used to check if refresh should occur
* @return Refresh event was triggered. No other sources need to be checked.
*/
private Boolean refresh(ConfigStore store, String storeSuffix, String watchedKeyNames) {
private boolean refresh(ConfigStore store, String storeSuffix, String watchedKeyNames) {
String storeNameWithSuffix = store.getEndpoint() + storeSuffix;
SettingSelector settingSelector = new SettingSelector().setKeyFilter(watchedKeyNames)
.setLabelFilter(StringUtils.arrayToCommaDelimitedString(store.getLabels()));

List<ConfigurationSetting> items = clientStore.listSettingRevisons(settingSelector, store.getEndpoint());

if (items == null || items.isEmpty()) {
return false;
String etag = "";
// If there is no result, etag will be considered empty.
// A refresh will trigger once the selector returns a value.
if (items != null && !items.isEmpty()) {
etag = items.get(0).getETag();
}

String etag = items.get(0).getETag();
if (firstTimeMap.get(storeNameWithSuffix) == null) {
storeEtagMap.put(storeNameWithSuffix, etag);
firstTimeMap.put(storeNameWithSuffix, false);
if (StateHolder.getState(storeNameWithSuffix) == null) {
return false;
}

if (!etag.equals(storeEtagMap.get(storeNameWithSuffix))) {
if (!etag.equals(StateHolder.getState(storeNameWithSuffix).getETag())) {
LOGGER.trace("Some keys in store [{}] matching [{}] is updated, will send refresh event.",
store.getEndpoint(), watchedKeyNames);
storeEtagMap.put(storeNameWithSuffix, etag);
RefreshEventData eventData = new RefreshEventData(watchedKeyNames);
publisher.publishEvent(new RefreshEvent(this, eventData, eventData.getMessage()));
if (eventDataInfo.isEmpty()) {
eventDataInfo = watchedKeyNames;
} else {
eventDataInfo += ", " + watchedKeyNames;
}

// Don't need to refresh here will be done in Property Source
return true;
Expand All @@ -158,38 +178,4 @@ public String getMessage() {
return this.message;
}
}

/**
* Composite watched key names separated by comma, the key names is made up of:
* prefix, context and key name pattern e.g., prefix: /config, context: /application,
* watched key: my.watch.key will return: /config/application/my.watch.key
*
* The returned watched key will be one key pattern, one or multiple specific keys
* e.g., 1) * 2) /application/abc* 3) /application/abc 4) /application/abc,xyz
*
* @param store the {@code store} for which to composite watched key names
* @param storeContextsMap map storing store name and List of context key-value pair
* @return the full name of the key mapping to the configuration store
*/
private String watchedKeyNames(ConfigStore store, Map<String, List<String>> storeContextsMap) {
String watchedKey = store.getWatchedKey().trim();
List<String> contexts = storeContextsMap.get(store.getEndpoint());

String watchedKeys = contexts.stream().map(ctx -> genKey(ctx, watchedKey))
.collect(Collectors.joining(","));

if (watchedKeys.contains(",") && watchedKeys.contains("*")) {
// Multi keys including one or more key patterns is not supported by API, will
// watch all keys(*) instead
watchedKeys = "*";
}

return watchedKeys;
}

private String genKey(@NonNull String context, @Nullable String watchedKey) {
String trimmedWatchedKey = StringUtils.hasText(watchedKey) ? watchedKey.trim() : "*";

return String.format("%s%s", context, trimmedWatchedKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,12 @@ public ConnectionPool initConnectionString(AzureCloudConfigProperties properties
pool.put(store.getEndpoint(), new Connection(store.getConnectionString()));
} else if (StringUtils.hasText(store.getEndpoint())) {
AzureManagedIdentityProperties msiProps = properties.getManagedIdentity();

if (msiProps != null && msiProps.getClientId() != null) {
pool.put(store.getEndpoint(), new Connection(store.getEndpoint(), msiProps.getClientId()));
} else {
pool.put(store.getEndpoint(), new Connection(store.getEndpoint(), ""));
}

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
*/
package com.microsoft.azure.spring.cloud.config;

import static com.microsoft.azure.spring.cloud.config.Constants.CONFIGURATION_SUFFIX;
import static com.microsoft.azure.spring.cloud.config.Constants.FEATURE_FLAG_CONTENT_TYPE;
import static com.microsoft.azure.spring.cloud.config.Constants.FEATURE_FLAG_PREFIX;
import static com.microsoft.azure.spring.cloud.config.Constants.FEATURE_MANAGEMENT_KEY;
import static com.microsoft.azure.spring.cloud.config.Constants.FEATURE_STORE_WATCH_KEY;
import static com.microsoft.azure.spring.cloud.config.Constants.FEATURE_SUFFIX;
import static com.microsoft.azure.spring.cloud.config.Constants.KEY_VAULT_CONTENT_TYPE;

import java.io.IOException;
Expand All @@ -22,6 +27,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import com.azure.data.appconfiguration.ConfigurationClient;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
Expand All @@ -33,6 +39,7 @@
import com.microsoft.azure.spring.cloud.config.feature.management.entity.FeatureManagementItem;
import com.microsoft.azure.spring.cloud.config.feature.management.entity.FeatureSet;
import com.microsoft.azure.spring.cloud.config.stores.ClientStore;
import com.microsoft.azure.spring.cloud.config.stores.ConfigStore;
import com.microsoft.azure.spring.cloud.config.stores.KeyVaultClient;

public class AzureConfigPropertySource extends EnumerablePropertySource<ConfigurationClient> {
Expand All @@ -42,40 +49,39 @@ public class AzureConfigPropertySource extends EnumerablePropertySource<Configur

private Map<String, Object> properties = new LinkedHashMap<>();

private final String storeName;

private final String label;

private AzureCloudConfigProperties azureProperties;

private static ObjectMapper mapper = new ObjectMapper();

private static final String FEATURE_MANAGEMENT_KEY = "feature-management.featureManagement";

private static final String FEATURE_FLAG_PREFIX = ".appconfig.featureflag/";

private HashMap<String, KeyVaultClient> keyVaultClients;

private ClientStore clients;

private KeyVaultCredentialProvider keyVaultCredentialProvider;

private AppConfigProviderProperties appProperties;

AzureConfigPropertySource(String context, String storeName, String label,
AzureCloudConfigProperties azureProperties, ClientStore clients,
AppConfigProviderProperties appProperties, KeyVaultCredentialProvider keyVaultCredentialProvider) {
private ConfigStore configStore;

private Map<String, List<String>> storeContextsMap;

AzureConfigPropertySource(String context, ConfigStore configStore, String label,
AzureCloudConfigProperties azureProperties, ClientStore clients, AppConfigProviderProperties appProperties,
KeyVaultCredentialProvider keyVaultCredentialProvider, Map<String, List<String>> storeContextsMap) {
// The context alone does not uniquely define a PropertySource, append storeName
// and label to uniquely define a PropertySource
super(context + storeName + "/" + label);
super(context + configStore.getEndpoint() + "/" + label);
this.context = context;
this.storeName = storeName;
this.configStore = configStore;
this.label = label;
this.azureProperties = azureProperties;
this.appProperties = appProperties;
this.keyVaultClients = new HashMap<String, KeyVaultClient>();
this.clients = clients;
this.keyVaultCredentialProvider = keyVaultCredentialProvider;
this.storeContextsMap = storeContextsMap;
}

@Override
Expand Down Expand Up @@ -106,6 +112,7 @@ public Object getProperty(String name) {
* @return Updated Feature Set from Property Source
*/
FeatureSet initProperties(FeatureSet featureSet) throws IOException {
String storeName = configStore.getEndpoint();
Date date = new Date();
SettingSelector settingSelector = new SettingSelector();
if (!label.equals("%00")) {
Expand All @@ -115,6 +122,11 @@ FeatureSet initProperties(FeatureSet featureSet) throws IOException {
// * for wildcard match
settingSelector.setKeyFilter(context + "*");
List<ConfigurationSetting> settings = clients.listSettings(settingSelector, storeName);

// Reading In Features
settingSelector.setKeyFilter(".appconfig*");
List<ConfigurationSetting> features = clients.listSettings(settingSelector, storeName);

if (settings == null) {
if (!azureProperties.isFailFast()) {
return featureSet;
Expand All @@ -134,14 +146,31 @@ FeatureSet initProperties(FeatureSet featureSet) throws IOException {
} else {
properties.put(key, setting.getValue());
}
}

featureSet = addToFeatureSet(featureSet, features, date);

// Setting new ETag values for Watch
String watchedKeyNames = clients.watchedKeyNames(configStore, storeContextsMap);
settingSelector = new SettingSelector().setKeyFilter(watchedKeyNames)
.setLabelFilter(StringUtils.arrayToCommaDelimitedString(configStore.getLabels()));

List<ConfigurationSetting> configurationRevisions = clients.listSettingRevisons(settingSelector, storeName);

settingSelector = new SettingSelector().setKeyFilter(FEATURE_STORE_WATCH_KEY)
.setLabelFilter(StringUtils.arrayToCommaDelimitedString(configStore.getLabels()));

List<ConfigurationSetting> featureRevisions = clients.listSettingRevisons(settingSelector, storeName);

if (configurationRevisions != null && !configurationRevisions.isEmpty()) {
StateHolder.setState(configStore.getEndpoint() + CONFIGURATION_SUFFIX, configurationRevisions.get(0));
}

// Reading In Features
settingSelector.setKeyFilter(".appconfig*");
settings = clients.listSettings(settingSelector, storeName);
if (featureRevisions != null && !featureRevisions.isEmpty()) {
StateHolder.setState(configStore.getEndpoint() + FEATURE_SUFFIX, featureRevisions.get(0));
}

return addToFeatureSet(featureSet, settings, date);
return featureSet;
}

/**
Expand Down
Loading

0 comments on commit c2eafdf

Please sign in to comment.