Skip to content

Load feature flags from the dedicated feature flag resource endpoint#48222

Open
yuanqu72 wants to merge 1 commit into
Azure:AppConfig-FFEndpointfrom
yuanqu72:yuan-provider-on-ffendpoint
Open

Load feature flags from the dedicated feature flag resource endpoint#48222
yuanqu72 wants to merge 1 commit into
Azure:AppConfig-FFEndpointfrom
yuanqu72:yuan-provider-on-ffendpoint

Conversation

@yuanqu72

@yuanqu72 yuanqu72 commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

Loads feature flags from the new dedicated Feature Flag resource endpoint (FeatureFlagClient/FeatureFlag) in azure-appconfiguration-provider, in addition to the existing key-value based feature flags.

  • When feature_flag_enabled=True, feature flags created via the feature flag resource endpoint are now loaded automatically alongside key-value based feature flags.
  • Both kinds are merged into the same feature_management.feature_flags list. When a resource-based feature flag and a key-value based feature flag share the same name, the resource-based one takes precedence.
  • No new load() options are required to opt in; existing feature_flag_selectors filter both kinds.
  • Bumped minimum dependency on azure-appconfiguration to >=1.10.0b1 for FeatureFlagClient/FeatureFlag support.

Dependency on #47620

⚠️ This PR is stacked on top of #47620 (AppConfig-FFEndpoint branch), which introduces the FeatureFlagClient APIs in azure-appconfiguration that this change depends on. Because of that, this PR targets AppConfig-FFEndpoint instead of main — the diff here is scoped only to sdk/appconfiguration/azure-appconfiguration-provider.

This PR should be reviewed/merged after (or together with) #47620, once that lands in main.

Changes

  • _azureappconfigurationprovider.py / aio/_azureappconfigurationproviderasync.py: load and merge feature flags from the feature flag resource endpoint
  • _client_manager.py / aio/_async_client_manager.py: new client manager support for feature flag resource clients
  • _constants.py, _request_tracing_context.py: new constants and request tracing updates for the feature flag resource path
  • New samples: samples/feature_flag_resource_sample.py, samples/async_feature_flag_resource_sample.py
  • New tests: tests/test_provider_feature_flag_resources.py, tests/test_async_provider_feature_flag_resources.py, tests/test_azureappconfigurationproviderbase.py, tests/test_configuration_client_manager.py
  • CHANGELOG.md, README.md, samples/README.md updated

Testing

  • Added unit tests and integ tests covering feature flag resource loading, merging/precedence behavior, and client manager creation (sync + async). All of them passed locally:
✅ All 360 tests passed (7m52s, 0 failures, 0 skips)

…in the provider

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
10 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@github-actions github-actions Bot added the App Configuration Azure.ApplicationModel.Configuration label Jul 22, 2026
@mrm9084

mrm9084 commented Jul 22, 2026

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
You have several pipelines (over 10) configured to build pull requests in this repository. Specify which pipelines you would like to run by using /azp run [pipelines] command. You can specify multiple pipelines using a comma separated list.

@mrm9084

mrm9084 commented Jul 22, 2026

Copy link
Copy Markdown
Member

/azp run appconfiguration

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@mrm9084

mrm9084 commented Jul 22, 2026

Copy link
Copy Markdown
Member

/azp run python - appconfiguration

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@yuanqu72 yuanqu72 changed the title Load feature flags from the dedicated feature flag resource endpoint … Load feature flags from the dedicated feature flag resource endpoint Jul 22, 2026
return False

@distributed_trace
@distributed_trace

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there two of these?

loaded_feature_flags: List[FeatureFlag] = []
page_etags: List[List[str]] = []
# Needs to be removed unknown keyword argument for the feature flag client
kwargs.pop("sentinel_keys", None)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is in the other methods, but we shouldn't need it anymore. We have a utils method that validates the header.

return loaded_feature_flags, [[] for _ in feature_flag_selectors]
for select in feature_flag_selectors:
selector_etags: List[str] = []
if select.snapshot_name is not None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems weird, I know that we use the same selects as "key value feature flags".

selector_etags: List[str] = []
if select.snapshot_name is not None:
# Snapshots are not supported by the feature flag resource endpoint
page_etags.append(selector_etags)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to not have this, it seems weird having an empty selector for something that wasn't loaded. Typically we do this to support selecting an empty set, and that having the item appear live.

async for page in iterator:
async for ff in page:
loaded_feature_flags.append(ff)
selector_etags.append(iterator.etag) # type: ignore[attr-defined]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that we ignore this seems wrong. Either we have something wrong above, or the sdk isn't correct.

if self._feature_flag_client is None:
return False
for i, select in enumerate(feature_flag_selectors):
if select.snapshot_name is not None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if instead of skipping the snapshots every time, we just filter them out at startup. We can then remove all of the snapshot logic from the 'new feature flags`.

retry_backoff_max=retry_backoff_max,
**kwargs,
),
FeatureFlagClient(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably only create a feature flag client if feature flag loading is enabled.

Comment on lines +123 to +130
# Per-selector collection ETags for feature flags loaded from the feature flag resource endpoint. This is
# independent of the key-value based feature_flag_page_etags, since the resource endpoint is a separate
# resource type with its own change-detection mechanism.
self._feature_flag_resource_etags: List[List[str]] = []
# Feature flags are loaded from two independent sources: the classic key-value store, and the newer
# dedicated feature flag resource endpoint. Each source's processed output is cached separately so that a
# refresh of one source does not require re-processing or discarding the other source's data. The two are
# merged (resource-based feature flags take precedence on identifier collision) whenever either changes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These and a few others sort of seem like copilot comments about the properties. This seems more like copilot direction than property descriptions.

Comment on lines +313 to +314
# Key-value based feature flags store the variant value under "configuration_value". Feature
# flags loaded from the feature flag resource endpoint store it under "value" instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional verified.

Comment on lines 315 to +320
if "configuration_value" in v:
allocation_id += (
f"{json.dumps(v.get('configuration_value', ''), separators=(',', ':'), sort_keys=True)}"
)
elif "value" in v:
allocation_id += f"{json.dumps(v.get('value', ''), separators=(',', ':'), sort_keys=True)}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we want to swap this around. FF endpoint feature flags have a server-side enforced schema, while ff value feature flags don't.

Either way we should use reference_path_segment so we don't guess.

ALLOCATION_ID_KEY = "AllocationId"
ETAG_KEY = "ETag"

# Identifier field used by feature flags loaded from the classic key-value store.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update everywhere with the new terms. "Feature Flags" and "Enhanced Feature Flags"

FEATURE_FLAG_NAME_FIELD: feature_flag.name,
"enabled": feature_flag.enabled,
}
if feature_flag.label and not feature_flag.label.isspace():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we do this isspace check anywhere else. I just checked it isn't against our test api, but it isn't allowed in the portal.

# Feature flag value is not a valid JSON
return {}

def _process_feature_flag_resource(self, feature_flag: FeatureFlag) -> Dict[str, Any]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't make all kv feature flags FeatureFlag maybe make a helper method for this. I'd rather not have two copies of this logic.

merged[identifier] = ff
for ff in resource_feature_flags:
identifier = ff.get(FEATURE_FLAG_NAME_FIELD)
merged[identifier] = ff

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized this is also going to be an issue. We need to convert these to JSON, then change the name field to be 'id' to match the schema.

Can you validate that this new provider works with the feature flag library with zero changes on the feature flag libraries part.

* **Configuration not refreshing** — Make sure you are calling `config.refresh()` periodically (e.g., before each request in a web app). The provider does not auto-refresh in the background.
* **Startup failures** — If the store is unreachable during startup, the provider will retry until `startup_timeout` (default 100 seconds) is exceeded. Increase this value if your store is expected to have high latency.

## Testing

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move this to it's own file, or a readme inside the testing folder.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds dedicated feature-flag resource loading to the App Configuration provider, alongside existing key-value feature flags.

Changes:

  • Adds synchronous and asynchronous loading, merging, refresh, and telemetry support.
  • Adds integration/unit tests and samples.
  • Updates dependency, version, changelog, documentation, and recordings.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/testcase.py Adds resource test helpers.
tests/test_provider_feature_flag_resources.py Tests synchronous resource loading.
tests/test_configuration_client_manager.py Tests resource paging and ETags.
tests/test_azureappconfigurationproviderbase.py Tests conversion and merging.
tests/asynctestcase.py Adds asynchronous test helpers.
tests/aio/test_async_provider_feature_flag_resources.py Tests asynchronous resource loading.
setup.py Raises the App Configuration dependency.
samples/README.md Lists the new samples.
samples/feature_flag_resource_sample.py Demonstrates synchronous usage.
samples/async_feature_flag_resource_sample.py Demonstrates asynchronous usage.
README.md Documents resource flags and testing.
CHANGELOG.md Records the feature and dependency update.
aio/_azureappconfigurationproviderasync.py Integrates asynchronous loading and refresh.
aio/_async_client_manager.py Adds asynchronous resource-client support.
_version.py Sets version 2.6.0b1.
_request_tracing_context.py Tracks filters by name.
_constants.py Adds identifier and reference constants.
_client_manager.py Adds synchronous resource-client support.
_azureappconfigurationproviderbase.py Converts, caches, and merges resource flags.
_azureappconfigurationprovider.py Integrates synchronous loading and refresh.
assets.json Updates test recording assets.
Comments suppressed due to low confidence (1)

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py:455

  • An empty list is a valid refreshed result when the last resource flag is deleted, but this truthiness check leaves _processed_resource_feature_flags unchanged; the subsequent merge therefore keeps serving the deleted flag. Treat None as “not refreshed” and an empty list as “refreshed to empty” for both sources and for the merge condition.
        if feature_flag_resources:

Comment on lines +448 to 450
if feature_flags or feature_flag_resources:
# Reset feature flag usage
self._tracing_context.reset_feature_filter_usage()
Comment on lines +516 to +518
feature_flag_value: Dict[str, Any] = {
FEATURE_FLAG_NAME_FIELD: feature_flag.name,
"enabled": feature_flag.enabled,
Comment on lines +304 to +305
@distributed_trace
@distributed_trace
Comment on lines +190 to 191
if (settings_refreshed or feature_flags or feature_flag_resources) and self._on_refresh_success:
self._on_refresh_success()
Comment on lines +202 to 203
if (settings_refreshed or feature_flags or feature_flag_resources) and self._on_refresh_success:
self._on_refresh_success()
Comment on lines +157 to +160
if not self._feature_flag_resource_etags or client.check_feature_flag_resource_etags(
self._feature_flag_selectors, self._feature_flag_resource_etags, headers=headers, **kwargs
):
feature_flag_resources, feature_flag_resource_etags = client.load_feature_flag_resources(
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

App Configuration Azure.ApplicationModel.Configuration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants