|
3 | 3 | # Licensed under the MIT License. See License.txt in the project root for
|
4 | 4 | # license information.
|
5 | 5 | # -------------------------------------------------------------------------
|
| 6 | +import copy |
6 | 7 | import os
|
7 | 8 | import json
|
8 | 9 | import random
|
@@ -438,63 +439,65 @@ def __init__(self, **kwargs) -> None:
|
438 | 439 | or self._secret_resolver is not None
|
439 | 440 | )
|
440 | 441 | self._update_lock = Lock()
|
| 442 | + self._refresh_lock = Lock() |
441 | 443 |
|
442 | 444 | def refresh(self, **kwargs) -> None:
|
443 | 445 | if not self._refresh_on:
|
444 | 446 | logging.debug("Refresh called but no refresh options set.")
|
445 | 447 | return
|
446 |
| - |
447 | 448 | if not self._refresh_timer.needs_refresh():
|
448 | 449 | logging.debug("Refresh called but refresh interval not elapsed.")
|
449 | 450 | 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 |
450 | 454 | success = False
|
451 | 455 | need_refresh = False
|
452 | 456 | 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, |
465 | 474 | )
|
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) |
472 | 483 | 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 |
491 | 493 | except (ServiceRequestError, ServiceResponseError, HttpResponseError) as e:
|
492 | 494 | # If we get an error we should retry sooner than the next refresh interval
|
493 | 495 | if self._on_refresh_error:
|
494 | 496 | self._on_refresh_error(e)
|
495 | 497 | return
|
496 | 498 | raise
|
497 | 499 | finally:
|
| 500 | + self._refresh_lock.release() |
498 | 501 | if not success:
|
499 | 502 | self._refresh_timer.backoff()
|
500 | 503 | elif need_refresh and self._on_refresh_success:
|
@@ -523,7 +526,8 @@ def _load_all(self, **kwargs):
|
523 | 526 | if (config.key, config.label) in self._refresh_on:
|
524 | 527 | sentinel_keys[(config.key, config.label)] = config.etag
|
525 | 528 | self._refresh_on = sentinel_keys
|
526 |
| - self._dict = configuration_settings |
| 529 | + with self._update_lock: |
| 530 | + self._dict = configuration_settings |
527 | 531 |
|
528 | 532 | def _process_key_name(self, config):
|
529 | 533 | trimmed_key = config.key
|
@@ -576,42 +580,43 @@ def keys(self) -> Iterable[str]:
|
576 | 580 | :rtype: Iterable[str]
|
577 | 581 | """
|
578 | 582 | with self._update_lock:
|
579 |
| - return self._dict.keys() |
| 583 | + return list(self._dict.keys()) |
580 | 584 |
|
581 |
| - def items(self) -> Iterable[Tuple[str, str]]: |
| 585 | + def items(self) -> Iterable[Tuple[str, Union[str, JSON]]]: |
582 | 586 | """
|
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. |
585 | 589 |
|
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]]] |
588 | 592 | """
|
589 | 593 | with self._update_lock:
|
590 |
| - return self._dict.items() |
| 594 | + return copy.deepcopy(self._dict.items()) |
591 | 595 |
|
592 |
| - def values(self) -> Iterable[str]: |
| 596 | + def values(self) -> Iterable[Union[str, JSON]]: |
593 | 597 | """
|
594 | 598 | Returns a list of values loaded from Azure App Configuration. Any values that are Key Vault references will be
|
595 | 599 | resolved.
|
596 | 600 |
|
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]] |
599 | 604 | """
|
600 | 605 | with self._update_lock:
|
601 |
| - return self._dict.values() |
| 606 | + return copy.deepcopy(list((self._dict.values()))) |
602 | 607 |
|
603 |
| - def get(self, key: str, default: Optional[str] = None) -> str: |
| 608 | + def get(self, key: str, default: Optional[str] = None) -> Union[str, JSON]: |
604 | 609 | """
|
605 | 610 | Returns the value of the specified key. If the key does not exist, returns the default value.
|
606 | 611 |
|
607 | 612 | :param str key: The key of the value to get.
|
608 | 613 | :param default: The default value to return.
|
609 | 614 | :type: str or None
|
610 | 615 | :return: The value of the specified key.
|
611 |
| - :rtype: str |
| 616 | + :rtype: Union[str, JSON] |
612 | 617 | """
|
613 | 618 | with self._update_lock:
|
614 |
| - return self._dict.get(key, default) |
| 619 | + return copy.deepcopy(self._dict.get(key, default)) |
615 | 620 |
|
616 | 621 | def __eq__(self, other: Any) -> bool:
|
617 | 622 | if not isinstance(other, AzureAppConfigurationProvider):
|
|
0 commit comments