Skip to content
Merged
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
95 changes: 80 additions & 15 deletions azureappconfiguration/azureappconfiguration.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,25 @@ type AzureAppConfiguration struct {
featureFlags map[string]any

// Settings configured from Options
kvSelectors []Selector
ffEnabled bool
ffSelectors []Selector
trimPrefixes []string
watchedSettings []WatchedSetting
kvSelectors []Selector
ffEnabled bool
ffSelectors []Selector
trimPrefixes []string
watchedSettings []WatchedSetting
loadBalancingEnabled bool

// Settings used for refresh scenarios
sentinelETags map[WatchedSetting]*azcore.ETag
watchAll bool
kvETags map[Selector][]*azcore.ETag
ffETags map[Selector][]*azcore.ETag
keyVaultRefs map[string]string // unversioned Key Vault references
kvRefreshTimer refresh.Condition
secretRefreshTimer refresh.Condition
ffRefreshTimer refresh.Condition
onRefreshSuccess []func()
tracingOptions tracing.Options
sentinelETags map[WatchedSetting]*azcore.ETag
watchAll bool
kvETags map[Selector][]*azcore.ETag
ffETags map[Selector][]*azcore.ETag
keyVaultRefs map[string]string // unversioned Key Vault references
kvRefreshTimer refresh.Condition
secretRefreshTimer refresh.Condition
ffRefreshTimer refresh.Condition
onRefreshSuccess []func()
tracingOptions tracing.Options
lastSuccessfulEndpoint string

// Clients talking to Azure App Configuration/Azure Key Vault service
clientManager clientManager
Expand Down Expand Up @@ -103,6 +105,7 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
azappcfg.featureFlags = make(map[string]any)
azappcfg.kvSelectors = deduplicateSelectors(options.Selectors)
azappcfg.ffEnabled = options.FeatureFlagOptions.Enabled
azappcfg.loadBalancingEnabled = options.LoadBalancingEnabled

azappcfg.trimPrefixes = options.TrimKeyPrefixes
azappcfg.clientManager = clientManager
Expand Down Expand Up @@ -631,6 +634,10 @@ func (azappcfg *AzureAppConfiguration) executeFailoverPolicy(ctx context.Context
azappcfg.clientManager.refreshClients(ctx)
return fmt.Errorf("no client is available to connect to the target App Configuration store")
}
// If load balancing is enabled, rotate the clients so that the next client to be used is not the last successful one
if azappcfg.loadBalancingEnabled && azappcfg.lastSuccessfulEndpoint != "" && len(clients) > 1 {
rotateClientsToNextEndpoint(clients, azappcfg.lastSuccessfulEndpoint)
}

if manager, ok := azappcfg.clientManager.(*configurationClientManager); ok {
azappcfg.tracingOptions.ReplicaCount = len(manager.dynamicClients)
Expand All @@ -651,6 +658,10 @@ func (azappcfg *AzureAppConfiguration) executeFailoverPolicy(ctx context.Context
}

clientWrapper.updateBackoffStatus(true)
// Update the last successful endpoint for load balancing
if azappcfg.loadBalancingEnabled {
azappcfg.lastSuccessfulEndpoint = clientWrapper.endpoint
}
return nil
}

Expand Down Expand Up @@ -748,6 +759,10 @@ func configureTracingOptions(options *Options) tracing.Options {
tracingOption.KeyVaultConfigured = true
}

if options.LoadBalancingEnabled {
tracingOption.IsLoadBalancingEnabled = true
}

if options.FeatureFlagOptions.Enabled {
tracingOption.FeatureFlagTracing = &tracing.FeatureFlagTracing{}
}
Expand Down Expand Up @@ -854,6 +869,56 @@ func (azappcfg *AzureAppConfiguration) updateFeatureFlagTracing(featureFlag map[
}
}

Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

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

The rotateClientsToNextEndpoint function lacks documentation. It should include a docstring explaining its purpose, parameters, and how it modifies the clients slice in-place for load balancing.

Suggested change
// rotateClientsToNextEndpoint rotates the clients slice in-place so that the client following
// the last successful endpoint becomes the first element. This is used for load balancing
// across multiple endpoints. If the last successful endpoint is found, the slice is rotated
// to start with the next client. If there is only one client or the endpoint is not found,
// the slice is not modified.
//
// Parameters:
// - clients: A slice of pointers to configurationClientWrapper. This slice is modified in-place.
// - lastSuccessfulEndpoint: The endpoint string of the last successful client.

Copilot uses AI. Check for mistakes.
func rotateClientsToNextEndpoint(clients []*configurationClientWrapper, lastSuccessfulEndpoint string) {
if len(clients) <= 1 {
return
}

// Find the index of the last successful endpoint
lastSuccessfulIndex := -1
for i, clientWrapper := range clients {
if clientWrapper.endpoint == lastSuccessfulEndpoint {
lastSuccessfulIndex = i
break
}
}

// If we found the last successful endpoint, rotate to the next one
if lastSuccessfulIndex >= 0 {
nextClientIndex := (lastSuccessfulIndex + 1) % len(clients)
if nextClientIndex > 0 {
rotateSliceInPlace(clients, nextClientIndex)
}
}
}

// rotateSliceInPlace rotates a slice left by k positions using the reverse-reverse algorithm.
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

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

The rotateSliceInPlace function lacks documentation explaining its purpose, parameters, and the reverse-reverse algorithm it implements. Consider adding a docstring that explains the algorithm and its time/space complexity.

Suggested change
// rotateSliceInPlace rotates a slice left by k positions using the reverse-reverse algorithm.
// rotateSliceInPlace rotates a slice left by k positions in place using the reverse-reverse algorithm.
//
// Parameters:
// slice: The slice to be rotated. The rotation is performed in place.
// k: The number of positions to rotate the slice to the left. If k >= len(slice), k is reduced modulo len(slice).
//
// Algorithm:
// The function uses the reverse-reverse algorithm:
// 1. Reverse the entire slice.
// 2. Reverse the first len(slice)-k elements.
// 3. Reverse the remaining k elements.
// This results in the slice being rotated left by k positions.
//
// Time Complexity: O(n), where n is the length of the slice.
// Space Complexity: O(1), as the rotation is performed in place.

Copilot uses AI. Check for mistakes.
func rotateSliceInPlace[T any](slice []T, k int) {
if len(slice) <= 1 {
return
}

k = k % len(slice)
if k == 0 {
return
}

// Reverse the entire slice
for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 {
slice[i], slice[j] = slice[j], slice[i]
}

// Reverse the first len(slice)-k elements
for i, j := 0, len(slice)-k-1; i < j; i, j = i+1, j-1 {
slice[i], slice[j] = slice[j], slice[i]
}

// Reverse the remaining elements
for i, j := len(slice)-k, len(slice)-1; i < j; i, j = i+1, j-1 {
slice[i], slice[j] = slice[j], slice[i]
}
}

func isFailoverable(err error) bool {
if err == nil {
return false
Expand Down
3 changes: 0 additions & 3 deletions azureappconfiguration/client_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,11 @@ func (manager *configurationClientManager) processSrvTargetHosts(srvTargetHosts
if strings.EqualFold(targetEndpoint, manager.endpoint) {
continue // Skip primary endpoint
}

client, err := manager.newConfigurationClient(targetEndpoint)
if err != nil {
log.Printf("failed to create client for replica %s: %v", targetEndpoint, err)
continue // Continue with other replicas instead of returning
}

newDynamicClients = append(newDynamicClients, &configurationClientWrapper{
endpoint: targetEndpoint,
client: client,
Expand Down Expand Up @@ -349,7 +347,6 @@ func (client *configurationClientWrapper) getBackoffDuration() time.Duration {
// Cap the exponent to prevent overflow
exponent := math.Min(float64(client.failedAttempts-1), float64(safeShiftLimit))
calculatedMilliseconds := float64(minBackoffDuration.Milliseconds()) * math.Pow(2, exponent)

if calculatedMilliseconds > float64(maxBackoffDuration.Milliseconds()) || calculatedMilliseconds <= 0 {
calculatedMilliseconds = float64(maxBackoffDuration.Milliseconds())
}
Expand Down
Loading
Loading