Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions azureappconfiguration/app_configuration_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package azureappconfiguration

import (
"context"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2"
)

// appConfigClient abstracts the Azure App Configuration operations used by the provider.
type appConfigClient interface {
// Key-value operations.
NewListSettingsPager(selector azappconfig.SettingSelector, options *azappconfig.ListSettingsOptions) *runtime.Pager[azappconfig.ListSettingsPageResponse]
GetSetting(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error)
GetSnapshot(ctx context.Context, snapshotName string, options *azappconfig.GetSnapshotOptions) (azappconfig.GetSnapshotResponse, error)
NewListSettingsForSnapshotPager(snapshotName string, options *azappconfig.ListSettingsForSnapshotOptions) *runtime.Pager[azappconfig.ListSettingsForSnapshotResponse]

// Enhanced feature flag operations.
NewListFeatureFlagsPager(selector azappconfig.FeatureFlagSelector, options *azappconfig.ListFeatureFlagsOptions) *runtime.Pager[azappconfig.ListFeatureFlagsPageResponse]
}

// appConfigurationClient is the default appConfigClient implementation.
// The feature flag client is derived from the configuration client so both share the same pipeline and sync-token cache.
type appConfigurationClient struct {
configurationClient *azappconfig.Client
featureFlagClient *azappconfig.FeatureFlagClient
}

func newAppConfigurationClient(endpoint string, credential azcore.TokenCredential, options *azappconfig.ClientOptions) (appConfigClient, error) {
configurationClient, err := azappconfig.NewClient(endpoint, credential, options)
if err != nil {
return nil, err
}

return &appConfigurationClient{
configurationClient: configurationClient,
featureFlagClient: configurationClient.NewFeatureFlagClient(),
}, nil
}

func newAppConfigurationClientFromConnectionString(connectionString string, options *azappconfig.ClientOptions) (appConfigClient, error) {
configurationClient, err := azappconfig.NewClientFromConnectionString(connectionString, options)
if err != nil {
return nil, err
}

return &appConfigurationClient{
configurationClient: configurationClient,
featureFlagClient: configurationClient.NewFeatureFlagClient(),
}, nil
}

func (c *appConfigurationClient) NewListSettingsPager(selector azappconfig.SettingSelector, options *azappconfig.ListSettingsOptions) *runtime.Pager[azappconfig.ListSettingsPageResponse] {
return c.configurationClient.NewListSettingsPager(selector, options)
}

func (c *appConfigurationClient) GetSetting(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) {
return c.configurationClient.GetSetting(ctx, key, options)
}

func (c *appConfigurationClient) GetSnapshot(ctx context.Context, snapshotName string, options *azappconfig.GetSnapshotOptions) (azappconfig.GetSnapshotResponse, error) {
return c.configurationClient.GetSnapshot(ctx, snapshotName, options)
}

func (c *appConfigurationClient) NewListSettingsForSnapshotPager(snapshotName string, options *azappconfig.ListSettingsForSnapshotOptions) *runtime.Pager[azappconfig.ListSettingsForSnapshotResponse] {
return c.configurationClient.NewListSettingsForSnapshotPager(snapshotName, options)
}

func (c *appConfigurationClient) NewListFeatureFlagsPager(selector azappconfig.FeatureFlagSelector, options *azappconfig.ListFeatureFlagsOptions) *runtime.Pager[azappconfig.ListFeatureFlagsPageResponse] {
return c.featureFlagClient.NewListFeatureFlagsPager(selector, options)
}
90 changes: 48 additions & 42 deletions azureappconfiguration/azureappconfiguration.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type AzureAppConfiguration struct {
kvSelectors []Selector
ffEnabled bool
ffSelectors []Selector
enhancedFFSelectors []Selector
trimPrefixes []string
watchedSettings []WatchedSetting
loadBalancingEnabled bool
Expand All @@ -57,6 +58,7 @@ type AzureAppConfiguration struct {
watchAll bool
kvETags map[comparableSelector][]*azcore.ETag
ffETags map[comparableSelector][]*azcore.ETag
enhancedFFETags map[comparableSelector][]*azcore.ETag
keyVaultRefs map[string]string // unversioned Key Vault references
kvRefreshTimer refresh.Condition
secretRefreshTimer refresh.Condition
Expand Down Expand Up @@ -135,9 +137,11 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op

if azappcfg.ffEnabled {
azappcfg.ffSelectors = getFeatureFlagSelectors(deduplicateSelectors(options.FeatureFlagOptions.Selectors))
azappcfg.enhancedFFSelectors = deduplicateSelectors(options.FeatureFlagOptions.Selectors)
if options.FeatureFlagOptions.RefreshOptions.Enabled {
azappcfg.ffRefreshTimer = refresh.NewTimer(options.FeatureFlagOptions.RefreshOptions.Interval)
azappcfg.ffETags = make(map[comparableSelector][]*azcore.ETag)
azappcfg.enhancedFFETags = make(map[comparableSelector][]*azcore.ETag)
}
}

Expand Down Expand Up @@ -245,7 +249,7 @@ func (azappcfg *AzureAppConfiguration) Refresh(ctx context.Context) error {
defer azappcfg.refreshInProgress.Store(false)

var keyValueRefreshed, featureFlagRefreshed bool
refreshTask := func(client *azappconfig.Client) error {
refreshTask := func(client appConfigClient) error {
var kvRefreshed, ffRefreshed bool
eg, egCtx := errgroup.WithContext(ctx)
eg.Go(func() error {
Expand Down Expand Up @@ -320,7 +324,7 @@ func (azappcfg *AzureAppConfiguration) OnRefreshSuccess(callback func()) {
}

func (azappcfg *AzureAppConfiguration) load(ctx context.Context) error {
loadTask := func(client *azappconfig.Client) error {
loadTask := func(client appConfigClient) error {
eg, egCtx := errgroup.WithContext(ctx)
eg.Go(func() error {
keyValuesClient := &selectorSettingsClient{
Expand Down Expand Up @@ -349,7 +353,12 @@ func (azappcfg *AzureAppConfiguration) load(ctx context.Context) error {
client: client,
tracingOptions: azappcfg.tracingOptions,
}
return azappcfg.loadFeatureFlags(egCtx, ffClient)
enhancedFFClient := &enhFFSettingsClient{
selectors: azappcfg.enhancedFFSelectors,
client: client,
tracingOptions: azappcfg.tracingOptions,
}
return azappcfg.loadFeatureFlags(egCtx, ffClient, enhancedFFClient)
})
}

Expand Down Expand Up @@ -575,34 +584,19 @@ func (azappcfg *AzureAppConfiguration) loadKeyVaultSecrets(ctx context.Context,
return secrets, nil
}

func (azappcfg *AzureAppConfiguration) loadFeatureFlags(ctx context.Context, settingsClient settingsClient) error {
settingsResponse, err := settingsClient.getSettings(ctx)
func (azappcfg *AzureAppConfiguration) loadFeatureFlags(ctx context.Context, ffClient settingsClient, enhancedFFClient settingsClient) error {
ffRsp, err := ffClient.getSettings(ctx)
if err != nil {
return err
}

dedupFeatureFlags := make(map[string]any, len(settingsResponse.settings))
for _, setting := range settingsResponse.settings {
// Skip non-feature flag settings
if setting.ContentType == nil || *setting.ContentType != featureFlagContentType {
continue
}

if setting.Key != nil {
var v map[string]any
if err := json.Unmarshal([]byte(*setting.Value), &v); err != nil {
log.Printf("Invalid feature flag setting: key=%s, error=%s, just ignore", *setting.Key, err.Error())
continue
}
azappcfg.updateFeatureFlagTracing(v)
dedupFeatureFlags[*setting.Key] = v
}
enhFFRsp, err := enhancedFFClient.getSettings(ctx)
if err != nil {
return err
}

featureFlags := make([]any, 0, len(dedupFeatureFlags))
for _, v := range dedupFeatureFlags {
featureFlags = append(featureFlags, v)
}
azappcfg.tracingOptions.UseEnhancedFeatureFlag = len(enhFFRsp.featureFlags) > 0
featureFlags := azappcfg.processFeatureFlags(ffRsp.settings, enhFFRsp.featureFlags)

// "feature_management": {"feature_flags": [{...}, {...}]}
ffSettings := map[string]any{
Expand All @@ -611,7 +605,8 @@ func (azappcfg *AzureAppConfiguration) loadFeatureFlags(ctx context.Context, set
},
}

azappcfg.ffETags = settingsResponse.pageETags
azappcfg.ffETags = ffRsp.pageETags
azappcfg.enhancedFFETags = enhFFRsp.pageETags
azappcfg.featureFlags = ffSettings

return nil
Expand Down Expand Up @@ -707,27 +702,28 @@ func (azappcfg *AzureAppConfiguration) refreshFeatureFlags(ctx context.Context,
return false, nil
}

// Check if any ETags have changed
eTagChanged, err := refreshClient.monitor.checkIfETagChanged(ctx)
ffChanged, err := refreshClient.monitor.checkIfETagChanged(ctx)
if err != nil {
log.Printf("Failed to check if feature flag settings have changed: %s", err.Error())
return false, err
}

if !eTagChanged {
if !ffChanged {

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 am wondering if the refresh logic here is an intentional decision: it currently reloads both flags when either is changed, which means more network calls if only one flag is changed. If the flag merge logic is correct, we should not need to reload both flags every time?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's intentional, I remember it was discussed before that we always make call for both flags.

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.

Yuan Qu (@yuanqu72)

I am wondering if the refresh logic here is an intentional decision: it currently reloads both flags when either is changed, which means more network calls if only one flag is changed.

If we detect that some classic feature flags are missing or have been deleted, it could mean that the customer has adopted or migrated to the new enhanced feature flags. Therefore, we should reload both types of feature flags.

enhFFChanged, err := refreshClient.enhFFMonitor.checkIfETagChanged(ctx)
if err != nil {
log.Printf("Failed to check if enhanced feature flags have changed: %s", err.Error())
return false, err
}
ffChanged = enhFFChanged
}

if !ffChanged {
// No changes detected, reset timer and return
azappcfg.ffRefreshTimer.Reset()
return false, nil
}

// Reload feature flags
eg, egCtx := errgroup.WithContext(ctx)
eg.Go(func() error {
settingsClient := refreshClient.loader
return azappcfg.loadFeatureFlags(egCtx, settingsClient)
})

if err := eg.Wait(); err != nil {
if err := azappcfg.loadFeatureFlags(ctx, refreshClient.loader, refreshClient.enhFFLoader); err != nil {
log.Printf("Failed to reload feature flag configuration: %s", err.Error())
// Don't reset the timer if reload failed
return false, err
Expand All @@ -738,7 +734,7 @@ func (azappcfg *AzureAppConfiguration) refreshFeatureFlags(ctx context.Context,
return true, nil
}

func (azappcfg *AzureAppConfiguration) executeFailoverPolicy(ctx context.Context, operation func(*azappconfig.Client) error) error {
func (azappcfg *AzureAppConfiguration) executeFailoverPolicy(ctx context.Context, operation func(appConfigClient) error) error {
clients, err := azappcfg.clientManager.getClients(ctx)
if err != nil {
return err
Expand All @@ -753,7 +749,7 @@ func (azappcfg *AzureAppConfiguration) executeFailoverPolicy(ctx context.Context
rotateClientsToNextEndpoint(clients, azappcfg.lastSuccessfulEndpoint)
}

if manager, ok := azappcfg.clientManager.(*configurationClientManager); ok {
if manager, ok := azappcfg.clientManager.(*appConfigClientManager); ok {
azappcfg.tracingOptions.ReplicaCount = len(manager.dynamicClients)
}

Expand Down Expand Up @@ -956,7 +952,7 @@ func normalizedWatchedSettings(s []WatchedSetting) []WatchedSetting {
return result
}

func (azappcfg *AzureAppConfiguration) newKeyValueRefreshClient(client *azappconfig.Client) refreshClient {
func (azappcfg *AzureAppConfiguration) newKeyValueRefreshClient(client appConfigClient) refreshClient {
var monitor eTagsClient
if azappcfg.watchAll {
monitor = &pageETagsClient{
Expand Down Expand Up @@ -987,7 +983,7 @@ func (azappcfg *AzureAppConfiguration) newKeyValueRefreshClient(client *azappcon
}
}

func (azappcfg *AzureAppConfiguration) newFeatureFlagRefreshClient(client *azappconfig.Client) refreshClient {
func (azappcfg *AzureAppConfiguration) newFeatureFlagRefreshClient(client appConfigClient) refreshClient {
return refreshClient{
loader: &selectorSettingsClient{
selectors: azappcfg.ffSelectors,
Expand All @@ -999,6 +995,16 @@ func (azappcfg *AzureAppConfiguration) newFeatureFlagRefreshClient(client *azapp
tracingOptions: azappcfg.tracingOptions,
pageETags: azappcfg.ffETags,
},
enhFFLoader: &enhFFSettingsClient{
selectors: azappcfg.enhancedFFSelectors,
client: client,
tracingOptions: azappcfg.tracingOptions,
},
enhFFMonitor: &enhFFETagsClient{
client: client,
tracingOptions: azappcfg.tracingOptions,
pageETags: azappcfg.enhancedFFETags,
},
}
}

Expand Down Expand Up @@ -1040,7 +1046,7 @@ func (azappcfg *AzureAppConfiguration) updateFeatureFlagTracing(featureFlag map[
}
}

func rotateClientsToNextEndpoint(clients []*configurationClientWrapper, lastSuccessfulEndpoint string) {
func rotateClientsToNextEndpoint(clients []*appConfigClientWrapper, lastSuccessfulEndpoint string) {

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.

A note for future consideration: this file is very long and contains all the core logic for setup/load/refresh + helper methods. Maybe some helper methods can be separated out and only leave the high level orchestration logic in this file.

if len(clients) <= 1 {
return
}
Expand Down
Loading
Loading