@@ -100,6 +100,17 @@ def load(
100
100
:paramtype on_refresh_error: Optional[Callable[[Exception], None]]
101
101
:keyword on_refresh_error: Optional callback to be invoked when an error occurs while refreshing settings. If not
102
102
specified, errors will be raised.
103
+ :paramtype feature_flag_enabled: bool
104
+ :keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False.
105
+ :paramtype feature_flag_selectors: List[SettingSelector]
106
+ :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all
107
+ feature flags without a label.
108
+ :paramtype feature_flag_refresh_enabled: bool
109
+ :keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is
110
+ False.
111
+ :paramtype feature_flag_trim_prefixes: List[str]
112
+ :keyword feature_flag_trim_prefixes: After the FEATURE_FLAG_PREFIX is trimmed, the first match in
113
+ feature_flag_trim_prefixes will be trimmed, if there is one.
103
114
"""
104
115
105
116
@@ -144,6 +155,17 @@ def load(
144
155
:paramtype on_refresh_error: Optional[Callable[[Exception], None]]
145
156
:keyword on_refresh_error: Optional callback to be invoked when an error occurs while refreshing settings. If not
146
157
specified, errors will be raised.
158
+ :paramtype feature_flag_enabled: bool
159
+ :keyword feature_flag_enabled: Optional flag to enable or disable the loading of feature flags. Default is False.
160
+ :paramtype feature_flag_selectors: List[SettingSelector]
161
+ :keyword feature_flag_selectors: Optional list of selectors to filter feature flags. By default will load all
162
+ feature flags without a label.
163
+ :paramtype feature_flag_refresh_enabled: bool
164
+ :keyword feature_flag_refresh_enabled: Optional flag to enable or disable the refresh of feature flags. Default is
165
+ False.
166
+ :paramtype feature_flag_trim_prefixes: List[str]
167
+ :keyword feature_flag_trim_prefixes: After the FEATURE_FLAG_PREFIX is trimmed, the first match in
168
+ feature_flag_trim_prefixes will be trimmed, if there is one.
147
169
"""
148
170
149
171
@@ -438,12 +460,19 @@ def __init__(self, **kwargs) -> None:
438
460
or self ._keyvault_client_configs is not None
439
461
or self ._secret_resolver is not None
440
462
)
463
+ self ._feature_flag_enabled = kwargs .pop ("feature_flag_enabled" , False )
464
+ self ._feature_flag_selectors = kwargs .pop ("feature_flag_selectors" , [SettingSelector (key_filter = "*" )])
465
+ self ._refresh_on_feature_flags = None
466
+ self ._feature_flag_refresh_timer : _RefreshTimer = _RefreshTimer (** kwargs )
467
+ self ._feature_flag_refresh_enabled = kwargs .pop ("feature_flag_refresh_enabled" , False )
468
+ feature_flag_trim_prefixes = kwargs .pop ("feature_flag_trim_prefixes" , [])
469
+ self ._feature_flag_trim_prefixes : List [str ] = sorted (feature_flag_trim_prefixes , key = len , reverse = True )
441
470
self ._update_lock = Lock ()
442
471
self ._refresh_lock = Lock ()
443
472
444
473
def refresh (self , ** kwargs ) -> None :
445
- if not self ._refresh_on :
446
- logging .debug ("Refresh called but no refresh options set ." )
474
+ if not self ._refresh_on and not self . _feature_flag_refresh_enabled :
475
+ logging .debug ("Refresh called but no refresh enabled ." )
447
476
return
448
477
if not self ._refresh_timer .needs_refresh ():
449
478
logging .debug ("Refresh called but refresh interval not elapsed." )
@@ -503,7 +532,76 @@ def refresh(self, **kwargs) -> None:
503
532
elif need_refresh and self ._on_refresh_success :
504
533
self ._on_refresh_success ()
505
534
535
+ def refresh_configuration_settings (self , configuration_settings , sentinel_keys , timer , ** kwargs ) -> None :
536
+ success = False
537
+ need_refresh = False
538
+ updated_feature_flags = False
539
+ headers = _get_headers ("Watch" , uses_key_vault = self ._uses_key_vault , ** kwargs )
540
+ try :
541
+ for (key , label ), etag in sentinel_keys .items ():
542
+ etag , update , is_feature_flag , configuration_settings = self ._check_configuration_settings (
543
+ key , label , etag , headers , configuration_settings , ** kwargs
544
+ )
545
+ if update and not is_feature_flag :
546
+ need_refresh = True
547
+ elif update and is_feature_flag :
548
+ updated_feature_flags = True
549
+
550
+ sentinel_keys [(key , label )] = etag
551
+ # Need to only update once, no matter how many sentinels are updated
552
+ if need_refresh :
553
+ configuration_settings , sentinel_keys = self ._load_configuration_settings (
554
+ headers = headers , sentinel_keys = sentinel_keys , ** kwargs
555
+ )
556
+ success = True
557
+ # Even if we don't need to refresh, we should reset the timer
558
+ timer .reset ()
559
+ except (ServiceRequestError , ServiceResponseError , HttpResponseError ) as e :
560
+ # If a call back is provided, we should call it, otherwise raise the error
561
+ if not self ._on_refresh_error :
562
+ raise
563
+ self ._on_refresh_error (e )
564
+ finally :
565
+ # If we get an error we should retry sooner than the next refresh interval
566
+ if not success :
567
+ timer .backoff ()
568
+ return configuration_settings , sentinel_keys , need_refresh or updated_feature_flags
569
+
570
+ def _check_configuration_settings (self , key , label , etag , headers , configuration_settings , ** kwargs ) -> None :
571
+ is_feature_flag = False
572
+ try :
573
+ updated_config = self ._client .get_configuration_setting (
574
+ key = key , label = label , etag = etag , match_condition = MatchConditions .IfModified , headers = headers , ** kwargs
575
+ )
576
+ is_feature_flag = not isinstance (updated_config , FeatureFlagConfigurationSetting )
577
+ if updated_config is not None :
578
+ if not is_feature_flag :
579
+ logging .debug ("Refresh all triggered by key: %s label %s." , key , label )
580
+ elif is_feature_flag :
581
+ configuration_settings [self ._process_key_name (updated_config )] = updated_config .value
582
+ return updated_config .etag , True , is_feature_flag , configuration_settings
583
+ except HttpResponseError as e :
584
+ if e .status_code == 404 :
585
+ if etag is not None and not is_feature_flag :
586
+ # If the sentinel is not found, it means the key/label was deleted, so we should refresh
587
+ logging .debug ("Refresh all triggered by key: %s label %s." , key , label )
588
+ elif etag is not None and is_feature_flag :
589
+ configuration_settings [key ] = None
590
+ return None , True , is_feature_flag , configuration_settings
591
+ raise e
592
+ return etag , False , is_feature_flag , configuration_settings
593
+
506
594
def _load_all (self , ** kwargs ):
595
+ configuration_settings , sentinel_keys = self ._load_configuration_settings (** kwargs )
596
+ feature_flags , feature_flag_sentinel_keys = self ._load_feature_flags (** kwargs )
597
+ if self ._feature_flag_enabled :
598
+ configuration_settings [FEATURE_MANAGEMENT_KEY ] = feature_flags
599
+ self ._refresh_on_feature_flags = feature_flag_sentinel_keys
600
+ with self ._update_lock :
601
+ self ._refresh_on = sentinel_keys
602
+ self ._dict = configuration_settings
603
+
604
+ def _load_configuration_settings (self , ** kwargs ):
507
605
configuration_settings = {}
508
606
sentinel_keys = kwargs .pop ("sentinel_keys" , self ._refresh_on )
509
607
for select in self ._selects :
@@ -514,30 +612,51 @@ def _load_all(self, **kwargs):
514
612
key = self ._process_key_name (config )
515
613
value = self ._process_key_value (config )
516
614
if isinstance (config , FeatureFlagConfigurationSetting ):
517
- feature_management = configuration_settings .get (FEATURE_MANAGEMENT_KEY , {})
518
- feature_management [key ] = value
519
- if FEATURE_MANAGEMENT_KEY not in configuration_settings :
520
- configuration_settings [FEATURE_MANAGEMENT_KEY ] = feature_management
615
+ # Feature flags are ignored when loaded by Selects, as they are selected from
616
+ # `feature_flag_selectors`
617
+ pass
521
618
else :
522
619
configuration_settings [key ] = value
523
620
# Every time we run load_all, we should update the etag of our refresh sentinels
524
621
# so they stay up-to-date.
525
622
# Sentinel keys will have unprocessed key names, so we need to use the original key.
526
623
if (config .key , config .label ) in self ._refresh_on :
527
624
sentinel_keys [(config .key , config .label )] = config .etag
528
- self ._refresh_on = sentinel_keys
529
- with self ._update_lock :
530
- self ._dict = configuration_settings
625
+ return configuration_settings , sentinel_keys
626
+
627
+ def _load_feature_flags (self , ** kwargs ):
628
+ feature_flag_sentinel_keys = {}
629
+ loaded_feature_flags = {}
630
+ if self ._feature_flag_enabled :
631
+ for select in self ._feature_flag_selectors :
632
+ feature_flags = self ._client .list_configuration_settings (
633
+ key_filter = FEATURE_FLAG_PREFIX + select .key_filter , label_filter = select .label_filter , ** kwargs
634
+ )
635
+ for feature_flag in feature_flags :
636
+ key = self ._process_key_name (feature_flag )
637
+ loaded_feature_flags [key ] = feature_flag .value
638
+ if self ._feature_flag_refresh_enabled :
639
+ feature_flag_sentinel_keys [(feature_flag .key , feature_flag .label )] = feature_flag .etag
640
+ return loaded_feature_flags , feature_flag_sentinel_keys
531
641
532
642
def _process_key_name (self , config ):
533
643
trimmed_key = config .key
534
644
# Trim the key if it starts with one of the prefixes provided
645
+
646
+ # Feature Flags have there own prefix, so we need to trim that first
647
+ if isinstance (config , FeatureFlagConfigurationSetting ):
648
+ if trimmed_key .startswith (FEATURE_FLAG_PREFIX ):
649
+ trimmed_key = trimmed_key [len (FEATURE_FLAG_PREFIX ) :]
650
+ for trim in self ._feature_flag_trim_prefixes :
651
+ if trimmed_key .startswith (trim ):
652
+ trimmed_key = trimmed_key [len (trim ) :]
653
+ break
654
+ return trimmed_key
655
+
535
656
for trim in self ._trim_prefixes :
536
657
if config .key .startswith (trim ):
537
658
trimmed_key = config .key [len (trim ) :]
538
659
break
539
- if isinstance (config , FeatureFlagConfigurationSetting ) and trimmed_key .startswith (FEATURE_FLAG_PREFIX ):
540
- return trimmed_key [len (FEATURE_FLAG_PREFIX ) :]
541
660
return trimmed_key
542
661
543
662
def _process_key_value (self , config ):
0 commit comments