@@ -62,6 +62,7 @@ def load(
6262 key_vault_options : Optional [AzureAppConfigurationKeyVaultOptions ] = None ,
6363 refresh_on : Optional [List [Tuple [str , str ]]] = None ,
6464 refresh_interval : int = 30 ,
65+ on_refresh_success : Optional [Callable ] = None ,
6566 on_refresh_error : Optional [Callable [[Exception ], None ]] = None ,
6667 ** kwargs
6768) -> "AzureAppConfigurationProvider" :
@@ -89,6 +90,9 @@ def load(
8990 :paramtype refresh_on: List[Tuple[str, str]]
9091 :keyword int refresh_interval: The minimum time in seconds between when a call to `refresh` will actually trigger a
9192 service call to update the settings. Default value is 30 seconds.
93+ :paramtype on_refresh_success: Optional[Callable]
94+ :keyword on_refresh_success: Optional callback to be invoked when a change is found and a successful refresh has
95+ happened.
9296 :paramtype on_refresh_error: Optional[Callable[[Exception], None]]
9397 :keyword on_refresh_error: Optional callback to be invoked when an error occurs while refreshing settings. If not
9498 specified, errors will be raised.
@@ -104,6 +108,7 @@ def load(
104108 key_vault_options : Optional [AzureAppConfigurationKeyVaultOptions ] = None ,
105109 refresh_on : Optional [List [Tuple [str , str ]]] = None ,
106110 refresh_interval : int = 30 ,
111+ on_refresh_success : Optional [Callable ] = None ,
107112 on_refresh_error : Optional [Callable [[Exception ], None ]] = None ,
108113 ** kwargs
109114) -> "AzureAppConfigurationProvider" :
@@ -129,6 +134,9 @@ def load(
129134 :paramtype refresh_on: List[Tuple[str, str]]
130135 :keyword int refresh_interval: The minimum time in seconds between when a call to `refresh` will actually trigger a
131136 service call to update the settings. Default value is 30 seconds.
137+ :paramtype on_refresh_success: Optional[Callable]
138+ :keyword on_refresh_success: Optional callback to be invoked when a change is found and a successful refresh has
139+ happened.
132140 :paramtype on_refresh_error: Optional[Callable[[Exception], None]]
133141 :keyword on_refresh_error: Optional callback to be invoked when an error occurs while refreshing settings. If not
134142 specified, errors will be raised.
@@ -183,8 +191,19 @@ def load(*args, **kwargs) -> "AzureAppConfigurationProvider":
183191 # Refresh-All sentinels are not updated on load_all, as they are not necessarily included in the provider.
184192 for (key , label ), etag in provider ._refresh_on .items ():
185193 if not etag :
186- sentinel = provider ._client .get_configuration_setting (key , label , headers = headers )
187- provider ._refresh_on [(key , label )] = sentinel .etag
194+ try :
195+ sentinel = provider ._client .get_configuration_setting (key , label , headers = headers )
196+ provider ._refresh_on [(key , label )] = sentinel .etag
197+ except HttpResponseError as e :
198+ if e .status_code == 404 :
199+ # If the sentinel is not found a refresh should be triggered when it is created.
200+ logging .debug (
201+ "WatchKey key: %s label %s was configured but not found. Refresh will be triggered if created." ,
202+ key ,
203+ label ,
204+ )
205+ else :
206+ raise e
188207 return provider
189208
190209
@@ -320,21 +339,6 @@ def _build_sentinel(setting: Union[str, Tuple[str, str]]) -> Tuple[str, str]:
320339 return key , label
321340
322341
323- def _is_retryable_error (error : HttpResponseError ) -> bool :
324- """Determine whether the service error should be silently retried after a backoff period, or raised.
325- Don't know what errors this applies to yet, so just always raising for now.
326- :param error: The http error to check.
327- :type error: ~azure.core.exceptions.HttpResponseError
328- :return: Whether the error should be retried.
329- :rtype: bool
330- """
331- # 408: Request Timeout
332- # 429: Too Many Requests
333- # 500: Internal Server Error
334- # 504: Gateway Timeout
335- return error .status_code in [408 , 429 , 500 , 504 ]
336-
337-
338342class _RefreshTimer :
339343 """
340344 A timer that tracks the next refresh time and the number of attempts.
@@ -353,7 +357,7 @@ def reset(self) -> None:
353357 self ._next_refresh_time = time .time () + self ._interval
354358 self ._attempts = 1
355359
356- def retry (self ) -> None :
360+ def backoff (self ) -> None :
357361 self ._next_refresh_time = time .time () + self ._calculate_backoff () / 1000
358362 self ._attempts += 1
359363
@@ -402,6 +406,7 @@ def __init__(self, **kwargs) -> None:
402406 refresh_on : List [Tuple [str , str ]] = kwargs .pop ("refresh_on" , None ) or []
403407 self ._refresh_on : Mapping [Tuple [str , str ] : Optional [str ]] = {_build_sentinel (s ): None for s in refresh_on }
404408 self ._refresh_timer : _RefreshTimer = _RefreshTimer (** kwargs )
409+ self ._on_refresh_success : Optional [Callable ] = kwargs .pop ("on_refresh_success" , None )
405410 self ._on_refresh_error : Optional [Callable [[Exception ], None ]] = kwargs .pop ("on_refresh_error" , None )
406411 self ._keyvault_credential = kwargs .pop ("keyvault_credential" , None )
407412 self ._secret_resolver = kwargs .pop ("secret_resolver" , None )
@@ -421,47 +426,62 @@ def refresh(self, **kwargs) -> None:
421426 if not self ._refresh_timer .needs_refresh ():
422427 logging .debug ("Refresh called but refresh interval not elapsed." )
423428 return
429+ success = False
424430 try :
425431 with self ._update_lock :
426- for (key , label ), etag in self ._refresh_on .items ():
427- updated_sentinel = self ._client .get_configuration_setting (
428- key = key ,
429- label = label ,
430- etag = etag ,
431- match_condition = MatchConditions .IfModified ,
432- headers = _get_headers ("Watch" , uses_key_vault = self ._uses_key_vault , ** kwargs ),
433- ** kwargs
434- )
435- if updated_sentinel is not None :
436- logging .debug (
437- "Refresh all triggered by key: %s label %s." ,
438- key ,
439- label ,
432+ need_refresh = False
433+ updated_sentinel_keys = dict (self ._refresh_on )
434+ headers = _get_headers ("Watch" , uses_key_vault = self ._uses_key_vault , ** kwargs )
435+ for (key , label ), etag in updated_sentinel_keys .items ():
436+ try :
437+ updated_sentinel = self ._client .get_configuration_setting (
438+ key = key ,
439+ label = label ,
440+ etag = etag ,
441+ match_condition = MatchConditions .IfModified ,
442+ headers = headers ,
443+ ** kwargs
440444 )
441- self ._load_all (headers = _get_headers ("Watch" , uses_key_vault = self ._uses_key_vault , ** kwargs ))
442- self ._refresh_on [(key , label )] = updated_sentinel .etag
443- self ._refresh_timer .reset ()
444- return
445- except (ServiceRequestError , ServiceResponseError ) as e :
446- logging .debug ("Failed to refresh, retrying: %r" , e )
447- self ._refresh_timer .retry ()
448- except HttpResponseError as e :
449- # If we get an error we should retry sooner than the next refresh interval
450- self ._refresh_timer .retry ()
451- if _is_retryable_error (e ):
452- return
453- if self ._on_refresh_error :
454- self ._on_refresh_error (e )
445+ if updated_sentinel is not None :
446+ logging .debug (
447+ "Refresh all triggered by key: %s label %s." ,
448+ key ,
449+ label ,
450+ )
451+ need_refresh = True
452+
453+ updated_sentinel_keys [(key , label )] = updated_sentinel .etag
454+ except HttpResponseError as e :
455+ if e .status_code == 404 :
456+ if etag is not None :
457+ # If the sentinel is not found, it means the key/label was deleted, so we should refresh
458+ logging .debug ("Refresh all triggered by key: %s label %s." , key , label )
459+ need_refresh = True
460+ updated_sentinel_keys [(key , label )] = None
461+ else :
462+ raise e
463+ # Need to only update once, no matter how many sentinels are updated
464+ if need_refresh :
465+ self ._load_all (headers = headers , sentinel_keys = updated_sentinel_keys , ** kwargs )
466+ if self ._on_refresh_success :
467+ self ._on_refresh_success ()
468+ # Even if we don't need to refresh, we should reset the timer
469+ self ._refresh_timer .reset ()
470+ success = True
455471 return
456- raise
457- except Exception as e :
472+ except ( ServiceRequestError , ServiceResponseError , HttpResponseError ) as e :
473+ # If we get an error we should retry sooner than the next refresh interval
458474 if self ._on_refresh_error :
459475 self ._on_refresh_error (e )
460476 return
461477 raise
478+ finally :
479+ if not success :
480+ self ._refresh_timer .backoff ()
462481
463482 def _load_all (self , ** kwargs ):
464483 configuration_settings = {}
484+ sentinel_keys = kwargs .pop ("sentinel_keys" , self ._refresh_on )
465485 for select in self ._selects :
466486 configurations = self ._client .list_configuration_settings (
467487 key_filter = select .key_filter , label_filter = select .label_filter , ** kwargs
@@ -481,7 +501,8 @@ def _load_all(self, **kwargs):
481501 # so they stay up-to-date.
482502 # Sentinel keys will have unprocessed key names, so we need to use the original key.
483503 if (config .key , config .label ) in self ._refresh_on :
484- self ._refresh_on [(config .key , config .label )] = config .etag
504+ sentinel_keys [(config .key , config .label )] = config .etag
505+ self ._refresh_on = sentinel_keys
485506 self ._dict = configuration_settings
486507
487508 def _process_key_name (self , config ):
0 commit comments