Skip to content

Commit bcd3840

Browse files
authored
App Config Provider - Update Locking Logic (#33433)
* Update to locking logic * Updated from comments * Formatting * Fixing Line Length * Update _azureappconfigurationproviderasync.py * Update _azureappconfigurationproviderasync.py * Updating locking * fix await * Fixing lock check * Reverting * Update _azureappconfigurationprovider.py * Update _azureappconfigurationproviderasync.py * pylint * Update _azureappconfigurationproviderasync.py * ): # pylint: disable= consider-using-with * Update _azureappconfigurationprovider.py * formatting * Removed redundant return * Fixed needs refresh, removed old prop * missing word * also missing
1 parent 723d47c commit bcd3840

File tree

2 files changed

+112
-102
lines changed

2 files changed

+112
-102
lines changed

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py

Lines changed: 57 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Licensed under the MIT License. See License.txt in the project root for
44
# license information.
55
# -------------------------------------------------------------------------
6+
import copy
67
import os
78
import json
89
import random
@@ -438,63 +439,65 @@ def __init__(self, **kwargs) -> None:
438439
or self._secret_resolver is not None
439440
)
440441
self._update_lock = Lock()
442+
self._refresh_lock = Lock()
441443

442444
def refresh(self, **kwargs) -> None:
443445
if not self._refresh_on:
444446
logging.debug("Refresh called but no refresh options set.")
445447
return
446-
447448
if not self._refresh_timer.needs_refresh():
448449
logging.debug("Refresh called but refresh interval not elapsed.")
449450
return
451+
if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with
452+
logging.debug("Refresh called but refresh already in progress.")
453+
return
450454
success = False
451455
need_refresh = False
452456
try:
453-
with self._update_lock:
454-
updated_sentinel_keys = dict(self._refresh_on)
455-
headers = _get_headers("Watch", uses_key_vault=self._uses_key_vault, **kwargs)
456-
for (key, label), etag in updated_sentinel_keys.items():
457-
try:
458-
updated_sentinel = self._client.get_configuration_setting(
459-
key=key,
460-
label=label,
461-
etag=etag,
462-
match_condition=MatchConditions.IfModified,
463-
headers=headers,
464-
**kwargs
457+
updated_sentinel_keys = dict(self._refresh_on)
458+
headers = _get_headers("Watch", uses_key_vault=self._uses_key_vault, **kwargs)
459+
for (key, label), etag in updated_sentinel_keys.items():
460+
try:
461+
updated_sentinel = self._client.get_configuration_setting(
462+
key=key,
463+
label=label,
464+
etag=etag,
465+
match_condition=MatchConditions.IfModified,
466+
headers=headers,
467+
**kwargs
468+
)
469+
if updated_sentinel is not None:
470+
logging.debug(
471+
"Refresh all triggered by key: %s label %s.",
472+
key,
473+
label,
465474
)
466-
if updated_sentinel is not None:
467-
logging.debug(
468-
"Refresh all triggered by key: %s label %s.",
469-
key,
470-
label,
471-
)
475+
need_refresh = True
476+
477+
updated_sentinel_keys[(key, label)] = updated_sentinel.etag
478+
except HttpResponseError as e:
479+
if e.status_code == 404:
480+
if etag is not None:
481+
# If the sentinel is not found, it means the key/label was deleted, so we should refresh
482+
logging.debug("Refresh all triggered by key: %s label %s.", key, label)
472483
need_refresh = True
473-
474-
updated_sentinel_keys[(key, label)] = updated_sentinel.etag
475-
except HttpResponseError as e:
476-
if e.status_code == 404:
477-
if etag is not None:
478-
# If the sentinel is not found, it means the key/label was deleted, so we should refresh
479-
logging.debug("Refresh all triggered by key: %s label %s.", key, label)
480-
need_refresh = True
481-
updated_sentinel_keys[(key, label)] = None
482-
else:
483-
raise e
484-
# Need to only update once, no matter how many sentinels are updated
485-
if need_refresh:
486-
self._load_all(headers=headers, sentinel_keys=updated_sentinel_keys, **kwargs)
487-
# Even if we don't need to refresh, we should reset the timer
488-
self._refresh_timer.reset()
489-
success = True
490-
return
484+
updated_sentinel_keys[(key, label)] = None
485+
else:
486+
raise e
487+
# Need to only update once, no matter how many sentinels are updated
488+
if need_refresh:
489+
self._load_all(headers=headers, sentinel_keys=updated_sentinel_keys, **kwargs)
490+
# Even if we don't need to refresh, we should reset the timer
491+
self._refresh_timer.reset()
492+
success = True
491493
except (ServiceRequestError, ServiceResponseError, HttpResponseError) as e:
492494
# If we get an error we should retry sooner than the next refresh interval
493495
if self._on_refresh_error:
494496
self._on_refresh_error(e)
495497
return
496498
raise
497499
finally:
500+
self._refresh_lock.release()
498501
if not success:
499502
self._refresh_timer.backoff()
500503
elif need_refresh and self._on_refresh_success:
@@ -523,7 +526,8 @@ def _load_all(self, **kwargs):
523526
if (config.key, config.label) in self._refresh_on:
524527
sentinel_keys[(config.key, config.label)] = config.etag
525528
self._refresh_on = sentinel_keys
526-
self._dict = configuration_settings
529+
with self._update_lock:
530+
self._dict = configuration_settings
527531

528532
def _process_key_name(self, config):
529533
trimmed_key = config.key
@@ -576,42 +580,43 @@ def keys(self) -> Iterable[str]:
576580
:rtype: Iterable[str]
577581
"""
578582
with self._update_lock:
579-
return self._dict.keys()
583+
return list(self._dict.keys())
580584

581-
def items(self) -> Iterable[Tuple[str, str]]:
585+
def items(self) -> Iterable[Tuple[str, Union[str, JSON]]]:
582586
"""
583-
Returns a list of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault references
584-
will be resolved.
587+
Returns a set-like object of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault
588+
references will be resolved.
585589
586-
:return: A list of key-value pairs loaded from Azure App Configuration.
587-
:rtype: Iterable[Tuple[str, str]]
590+
:return: A set-like object of key-value pairs loaded from Azure App Configuration.
591+
:rtype: Iterable[Tuple[str, Union[str, JSON]]]
588592
"""
589593
with self._update_lock:
590-
return self._dict.items()
594+
return copy.deepcopy(self._dict.items())
591595

592-
def values(self) -> Iterable[str]:
596+
def values(self) -> Iterable[Union[str, JSON]]:
593597
"""
594598
Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be
595599
resolved.
596600
597-
:return: A list of values loaded from Azure App Configuration.
598-
:rtype: Iterable[str]
601+
:return: A list of values loaded from Azure App Configuration. The values are either Strings or JSON objects,
602+
based on there content type.
603+
:rtype: Iterable[[str], [JSON]]
599604
"""
600605
with self._update_lock:
601-
return self._dict.values()
606+
return copy.deepcopy(list((self._dict.values())))
602607

603-
def get(self, key: str, default: Optional[str] = None) -> str:
608+
def get(self, key: str, default: Optional[str] = None) -> Union[str, JSON]:
604609
"""
605610
Returns the value of the specified key. If the key does not exist, returns the default value.
606611
607612
:param str key: The key of the value to get.
608613
:param default: The default value to return.
609614
:type: str or None
610615
:return: The value of the specified key.
611-
:rtype: str
616+
:rtype: Union[str, JSON]
612617
"""
613618
with self._update_lock:
614-
return self._dict.get(key, default)
619+
return copy.deepcopy(self._dict.get(key, default))
615620

616621
def __eq__(self, other: Any) -> bool:
617622
if not isinstance(other, AzureAppConfigurationProvider):

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py

Lines changed: 55 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
# license information.
55
# -------------------------------------------------------------------------
66
import json
7+
import copy
8+
from threading import Lock
79
import datetime
8-
from asyncio.locks import Lock
910
import logging
1011
from typing import (
1112
Any,
@@ -330,58 +331,61 @@ def __init__(self, **kwargs) -> None:
330331
or self._secret_resolver is not None
331332
)
332333
self._update_lock = Lock()
334+
self._refresh_lock = Lock()
333335

334336
async def refresh(self, **kwargs) -> None:
335337
if not self._refresh_on:
336338
logging.debug("Refresh called but no refresh options set.")
337339
return
340+
if not self._refresh_timer.needs_refresh():
341+
logging.debug("Refresh called but refresh interval not elapsed.")
342+
return
343+
if not self._refresh_lock.acquire(blocking=False): # pylint: disable= consider-using-with
344+
logging.debug("Refresh called but refresh already in progress.")
345+
return
338346
success = False
339347
need_refresh = False
340348
try:
341-
async with self._update_lock:
342-
if not self._refresh_timer.needs_refresh():
343-
logging.debug("Refresh called but refresh interval not elapsed.")
344-
return
345-
updated_sentinel_keys = dict(self._refresh_on)
346-
headers = _get_headers("Watch", uses_key_vault=self._uses_key_vault, **kwargs)
347-
for (key, label), etag in updated_sentinel_keys.items():
348-
try:
349-
updated_sentinel = await self._client.get_configuration_setting(
350-
key=key,
351-
label=label,
352-
etag=etag,
353-
match_condition=MatchConditions.IfModified,
354-
headers=headers,
355-
**kwargs
356-
)
357-
if updated_sentinel is not None:
349+
updated_sentinel_keys = dict(self._refresh_on)
350+
headers = _get_headers("Watch", uses_key_vault=self._uses_key_vault, **kwargs)
351+
for (key, label), etag in updated_sentinel_keys.items():
352+
try:
353+
updated_sentinel = await self._client.get_configuration_setting(
354+
key=key,
355+
label=label,
356+
etag=etag,
357+
match_condition=MatchConditions.IfModified,
358+
headers=headers,
359+
**kwargs
360+
)
361+
if updated_sentinel is not None:
362+
logging.debug("Refresh all triggered by key: %s label %s.", key, label)
363+
need_refresh = True
364+
365+
updated_sentinel_keys[(key, label)] = updated_sentinel.etag
366+
except HttpResponseError as e:
367+
if e.status_code == 404:
368+
if etag is not None:
369+
# If the sentinel is not found, it means the key/label was deleted, so we should refresh
358370
logging.debug("Refresh all triggered by key: %s label %s.", key, label)
359371
need_refresh = True
360-
361-
updated_sentinel_keys[(key, label)] = updated_sentinel.etag
362-
except HttpResponseError as e:
363-
if e.status_code == 404:
364-
if etag is not None:
365-
# If the sentinel is not found, it means the key/label was deleted, so we should refresh
366-
logging.debug("Refresh all triggered by key: %s label %s.", key, label)
367-
need_refresh = True
368-
updated_sentinel_keys[(key, label)] = None
369-
else:
370-
raise e
371-
# Need to only update once, no matter how many sentinels are updated
372-
if need_refresh:
373-
await self._load_all(headers=headers, sentinel_keys=updated_sentinel_keys, **kwargs)
374-
# Even if we don't need to refresh, we should reset the timer
375-
self._refresh_timer.reset()
376-
success = True
377-
return
372+
updated_sentinel_keys[(key, label)] = None
373+
else:
374+
raise e
375+
# Need to only update once, no matter how many sentinels are updated
376+
if need_refresh:
377+
await self._load_all(headers=headers, sentinel_keys=updated_sentinel_keys, **kwargs)
378+
# Even if we don't need to refresh, we should reset the timer
379+
self._refresh_timer.reset()
380+
success = True
378381
except (ServiceRequestError, ServiceResponseError, HttpResponseError) as e:
379382
# If we get an error we should retry sooner than the next refresh interval
380383
if self._on_refresh_error:
381384
await self._on_refresh_error(e)
382385
return
383386
raise
384387
finally:
388+
self._refresh_lock.release()
385389
if not success:
386390
self._refresh_timer.backoff()
387391
elif need_refresh and self._on_refresh_success:
@@ -463,39 +467,40 @@ def keys(self) -> Iterable[str]:
463467
:return: A list of keys loaded from Azure App Configuration.
464468
:rtype: Iterable[str]
465469
"""
466-
return self._dict.keys()
470+
return list(self._dict.keys())
467471

468-
def items(self) -> Iterable[Tuple[str, str]]:
472+
def items(self) -> Iterable[Tuple[str, Union[str, JSON]]]:
469473
"""
470-
Returns a list of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault references
471-
will be resolved.
474+
Returns a set-like object of key-value pairs loaded from Azure App Configuration. Any values that are Key Vault
475+
references will be resolved.
472476
473-
:return: A list of key-value pairs loaded from Azure App Configuration.
474-
:rtype: Iterable[Tuple[str, str]]
477+
:return: A set-like object of key-value pairs loaded from Azure App Configuration.
478+
:rtype: Iterable[Tuple[str, Union[str, JSON]]]
475479
"""
476-
return self._dict.items()
480+
return copy.deepcopy(self._dict.items())
477481

478-
def values(self) -> Iterable[str]:
482+
def values(self) -> Iterable[Union[str, JSON]]:
479483
"""
480484
Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be
481485
resolved.
482486
483-
:return: A list of values loaded from Azure App Configuration.
484-
:rtype: Iterable[str]
487+
:return: A list of values loaded from Azure App Configuration. The values are either Strings or JSON objects,
488+
based on there content type.
489+
:rtype: Iterable[[str], [JSON]]
485490
"""
486-
return self._dict.values()
491+
return copy.deepcopy(list((self._dict.values())))
487492

488-
def get(self, key: str, default: Optional[str] = None) -> str:
493+
def get(self, key: str, default: Optional[str] = None) -> Union[str, JSON]:
489494
"""
490495
Returns the value of the specified key. If the key does not exist, returns the default value.
491496
492497
:param str key: The key of the value to get.
493498
:param default: The default value to return.
494499
:type: str or None
495500
:return: The value of the specified key.
496-
:rtype: str
501+
:rtype: Union[str, JSON]
497502
"""
498-
return self._dict.get(key, default)
503+
return copy.deepcopy(self._dict.get(key, default))
499504

500505
def __eq__(self, other: Any) -> bool:
501506
if not isinstance(other, AzureAppConfigurationProvider):

0 commit comments

Comments
 (0)