Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
89c195d
Sync refresh changes
mrm9084 Jun 26, 2025
961efda
Key Vault Refresh
mrm9084 Jun 30, 2025
be0ffa7
adding tests and fixing sync refresh
mrm9084 Jul 1, 2025
1b0f554
Updating Async
mrm9084 Jul 2, 2025
5f54837
Fixed Async Tests
mrm9084 Jul 2, 2025
d5e8b87
Updated tests and change log
mrm9084 Jul 2, 2025
3f838d7
Apply suggestions from code review
mrm9084 Jul 2, 2025
8af8c0e
Merge branch 'main' into KeyVaultRefresh
mrm9084 Jul 2, 2025
251b825
Fixing merge issue
mrm9084 Jul 2, 2025
382dbba
Updating comments
mrm9084 Jul 2, 2025
00985a1
Merge remote-tracking branch 'upstream/main' into KeyVaultRefresh
mrm9084 Aug 8, 2025
ef6620a
Merge branch 'main' into KeyVaultRefresh
mrm9084 Aug 8, 2025
6d7a8f8
Updating secret refresh
mrm9084 Aug 11, 2025
d4a8d71
Merge branch 'KeyVaultRefresh' of https://github.com/mrm9084/azure-sd…
mrm9084 Aug 11, 2025
7ccb2a0
Update _azureappconfigurationproviderasync.py
mrm9084 Aug 11, 2025
2663cb3
Fixing Optional Endpoint
mrm9084 Aug 11, 2025
12fbdda
fix mypy issue
mrm9084 Aug 11, 2025
2758caf
fixing async test
mrm9084 Aug 12, 2025
5d4d6fd
Merge remote-tracking branch 'upstream/main' into KeyVaultRefresh
mrm9084 Aug 21, 2025
f2f279f
mixing merge
mrm9084 Aug 22, 2025
4b7f2c5
fixing test after merge
mrm9084 Aug 22, 2025
7d95da6
Update testcase.py
mrm9084 Aug 22, 2025
bfaaa28
Secret Provider Base
mrm9084 Aug 27, 2025
0ab5e66
removing unused imports
mrm9084 Aug 27, 2025
6d68ba3
updating exception
mrm9084 Aug 27, 2025
15af7f3
updating resolve key vault references
mrm9084 Aug 27, 2025
b160963
Review comments
mrm9084 Aug 28, 2025
2cdff6d
fixing tests
mrm9084 Aug 29, 2025
f49340f
tox updates
mrm9084 Aug 29, 2025
4fb9532
Updating Tests
mrm9084 Aug 29, 2025
6f55701
Updating Async to be the same as sync
mrm9084 Aug 29, 2025
12dc565
Fixing formatting
mrm9084 Aug 29, 2025
2072e76
fixing tox and unneeded ""
mrm9084 Aug 29, 2025
4656f83
fixing tox items
mrm9084 Aug 29, 2025
08e5ada
fix cspell + tests recording
mrm9084 Aug 29, 2025
0599f68
Update test_async_secret_provider.py
mrm9084 Sep 2, 2025
78a0b0d
Merge remote-tracking branch 'upstream/main' into KeyVaultRefresh
mrm9084 Sep 30, 2025
35a05bf
Post Merge updates
mrm9084 Sep 30, 2025
8e04133
Move cache to shared code
mrm9084 Sep 30, 2025
f7ffe3f
removed unneeded disabled
mrm9084 Sep 30, 2025
3ebcf45
Update Secret Provider
mrm9084 Oct 2, 2025
8c2637d
Updating usage
mrm9084 Oct 7, 2025
cfad924
Merge remote-tracking branch 'upstream/main' into KeyVaultRefresh
mrm9084 Oct 8, 2025
8e14697
Update assets.json
mrm9084 Oct 8, 2025
53a0eeb
Updated to make secret refresh update dictionary
mrm9084 Oct 10, 2025
abddfd9
removing _secret_version_cache
mrm9084 Oct 17, 2025
0379631
Merge branch 'main' into KeyVaultRefresh
mrm9084 Oct 17, 2025
d5add3b
Update assets.json
mrm9084 Oct 17, 2025
f10365c
Update _secret_provider_base.py
mrm9084 Oct 20, 2025
1955caf
Update _secret_provider_base.py
mrm9084 Oct 20, 2025
b329284
Update _secret_provider_base.py
mrm9084 Oct 20, 2025
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,10 +4,15 @@

### Features Added

* Added support for forced refresh of configurations when using Key Vault references. Adds `secret_refresh_interval` to the `AzureAppConfigurationProvider.load` method. This allows the provider to refresh Key Vault secrets at a specified interval. Is set to 60 seconds by default, and can only be set if using Key Vault references.
* Added support for async `on_refresh_success`.

### Breaking Changes

### Bugs Fixed

* Fixed a bug where feature flags were using the configuration refresh timer instead of the feature flag refresh timer.

### Other Changes

## 2.1.0 (2025-04-28)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "python",
"TagPrefix": "python/appconfiguration/azure-appconfiguration-provider",
"Tag": "python/appconfiguration/azure-appconfiguration-provider_6f35ca6dc7"
"Tag": "python/appconfiguration/azure-appconfiguration-provider_a1d88c0647"
}
Original file line number Diff line number Diff line change
Expand Up @@ -323,84 +323,159 @@ def __init__(self, **kwargs: Any) -> None:
self._on_refresh_success: Optional[Callable] = kwargs.pop("on_refresh_success", None)
self._on_refresh_error: Optional[Callable[[Exception], None]] = kwargs.pop("on_refresh_error", None)

def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements
if not self._refresh_on and not self._feature_flag_refresh_enabled:
def _common_refresh(
self,
refresh_operation: Callable,
error_log_message: str,
timer, # Type is _RefreshTimer but avoiding import
refresh_condition: bool,
Comment thread
mrm9084 marked this conversation as resolved.
Outdated
**kwargs,
) -> None:
if not refresh_condition:
logger.debug("Refresh called but no refresh enabled.")
return
if not self._refresh_timer.needs_refresh():
logger.debug("Refresh called but refresh interval not elapsed.")
return
if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with
logger.debug("Refresh called but refresh already in progress.")
return

success = False
need_refresh = False
error_message = """
Failed to refresh configuration settings from Azure App Configuration.
"""
exception: Exception = RuntimeError(error_message)
Comment thread
mrm9084 marked this conversation as resolved.
Outdated
is_failover_request = False
try:
self._replica_client_manager.refresh_clients()
self._replica_client_manager.find_active_clients()
replica_count = self._replica_client_manager.get_client_count() - 1

while client := self._replica_client_manager.get_next_active_client():
headers = update_correlation_context_header(
kwargs.pop("headers", {}),
"Watch",
replica_count,
self._feature_flag_enabled,
self._feature_filter_usage,
self._uses_key_vault,
self._uses_load_balancing,
is_failover_request,
self._uses_ai_configuration,
self._uses_aicc_configuration,
)
self._replica_client_manager.refresh_clients()
self._replica_client_manager.find_active_clients()
replica_count = self._replica_client_manager.get_client_count() - 1

while client := self._replica_client_manager.get_next_active_client():
headers = update_correlation_context_header(
kwargs.pop("headers", {}),
"Watch",
replica_count,
self._feature_flag_enabled,
self._feature_filter_usage,
self._uses_key_vault,
self._uses_load_balancing,
is_failover_request,
self._uses_ai_configuration,
self._uses_aicc_configuration,
)

try:
if self._refresh_on:
need_refresh, self._refresh_on, configuration_settings = client.refresh_configuration_settings(
self._selects, self._refresh_on, headers=headers, **kwargs
)
configuration_settings_processed = self._process_configurations(configuration_settings)
if need_refresh:
self._dict = configuration_settings_processed
if self._feature_flag_refresh_enabled:
need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = (
client.refresh_feature_flags(
self._refresh_on_feature_flags,
self._feature_flag_selectors,
headers,
self._origin_endpoint,
**kwargs,
)
)
if refresh_on_feature_flags:
self._refresh_on_feature_flags = refresh_on_feature_flags
self._feature_filter_usage = filters_used

if need_refresh or need_ff_refresh:
self._dict[FEATURE_MANAGEMENT_KEY] = {}
self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags
# Even if we don't need to refresh, we should reset the timer
self._refresh_timer.reset()
success = True
try:
# Execute the specific refresh operation
success = refresh_operation(client, headers, **kwargs)
if success:
break
except AzureError as e:
exception = e
logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint)
self._replica_client_manager.backoff(client)
is_failover_request = True
if not success:
self._refresh_timer.backoff()
if self._on_refresh_error:
self._on_refresh_error(exception)
return
raise exception
if self._on_refresh_success:
self._on_refresh_success()
except AzureError as e:
exception = e
logger.warning(error_log_message, client.endpoint)
self._replica_client_manager.backoff(client)
is_failover_request = True

if not success:
timer.backoff()
if self._on_refresh_error:
self._on_refresh_error(exception)
return
raise exception

if self._on_refresh_success:
self._on_refresh_success()

def _refresh_configuration_settings(self, force: bool = False, **kwargs: Any) -> None:

def refresh_operation(client, headers, **inner_kwargs):
configuration_settings: Optional[List[ConfigurationSetting]] = None
need_refresh = False
force = inner_kwargs.pop("force", False)
if not force:
need_refresh, self._refresh_on, configuration_settings = client.refresh_configuration_settings(
self._selects, self._refresh_on, headers=headers, **inner_kwargs
)
else:
# Force a refresh to make sure secrets are up to date
configuration_settings = client.load_configuration_settings(
self._selects, self._refresh_on, headers=headers, **inner_kwargs
)[0]
need_refresh = True

configuration_settings_processed: Dict[str, Any] = {}
if configuration_settings is not None:
configuration_settings_processed = self._process_configurations(configuration_settings)

if need_refresh:
feature_flags = []
uses_feature_flags = False
if self._dict.get(FEATURE_MANAGEMENT_KEY, {}).get(FEATURE_FLAG_KEY):
uses_feature_flags = True
feature_flags = self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY]
self._dict = configuration_settings_processed
if uses_feature_flags:
# If feature flags were already loaded, we need to keep them
self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags

# Even if we don't need to refresh, we should reset the timer
self._refresh_timer.reset()
if self._secret_refresh_timer:
self._secret_refresh_timer.reset()

return True

self._common_refresh(
refresh_operation=refresh_operation,
error_log_message="Failed to refresh configurations from endpoint %s",
timer=self._refresh_timer,
refresh_condition=bool(self._refresh_on or force),
force=force,
**kwargs,
)

def _refresh_feature_flags(self, **kwargs) -> None: # pylint: disable=too-many-statements
"""Refresh feature flags from Azure App Configuration."""

def refresh_operation(client, headers, **inner_kwargs):
need_ff_refresh, refresh_on_feature_flags, feature_flags, filters_used = client.refresh_feature_flags(
self._refresh_on_feature_flags,
self._feature_flag_selectors,
headers,
self._origin_endpoint or "",
**inner_kwargs,
)

if refresh_on_feature_flags:
self._refresh_on_feature_flags = refresh_on_feature_flags
self._feature_filter_usage = filters_used

if need_ff_refresh:
self._dict[FEATURE_MANAGEMENT_KEY] = {}
self._dict[FEATURE_MANAGEMENT_KEY][FEATURE_FLAG_KEY] = feature_flags

# Even if we don't need to refresh, we should reset the timer
self._feature_flag_refresh_timer.reset()
return True

self._common_refresh(
refresh_operation=refresh_operation,
error_log_message="Failed to refresh feature flags from endpoint %s",
timer=self._feature_flag_refresh_timer,
refresh_condition=self._feature_flag_refresh_enabled,
**kwargs,
)

def refresh(self, **kwargs) -> None: # pylint: disable=too-many-statements
if not self._refresh_on and not self._feature_flag_refresh_enabled and not self._secret_refresh_timer:
logger.debug("Refresh called but no refresh enabled.")
return
if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with
logger.debug("Refresh called but refresh already in progress.")
return
try:
if self._secret_refresh_timer and self._secret_refresh_timer.needs_refresh():
self._refresh_configuration_settings(force=True, **kwargs)
elif self._refresh_timer and self._refresh_timer.needs_refresh():
self._refresh_configuration_settings(**kwargs)
if self._feature_flag_refresh_enabled and (
self._feature_flag_refresh_timer and self._feature_flag_refresh_timer.needs_refresh()
):
self._refresh_feature_flags(**kwargs)
finally:
self._refresh_lock.release()

Expand Down Expand Up @@ -437,7 +512,7 @@ def _load_all(self, **kwargs):
feature_flags, feature_flag_sentinel_keys, used_filters = client.load_feature_flags(
self._feature_flag_selectors,
self._feature_flag_refresh_enabled,
self._origin_endpoint,
self._origin_endpoint or "",
Comment thread
mrm9084 marked this conversation as resolved.
Outdated
headers=headers,
**kwargs,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ def __init__(self, **kwargs: Any) -> None:
or (self._keyvault_client_configs is not None and len(self._keyvault_client_configs) > 0)
or self._secret_resolver is not None
)
self._secret_refresh_timer: Optional[_RefreshTimer] = (
_RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60))
if self._uses_key_vault and "secret_refresh_interval" in kwargs
else None
)
self._feature_flag_enabled = kwargs.pop("feature_flag_enabled", False)
self._feature_flag_selectors = kwargs.pop("feature_flag_selectors", [SettingSelector(key_filter="*")])
self._refresh_on_feature_flags: Mapping[Tuple[str, str], Optional[str]] = {}
Expand Down
Loading