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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go-version: ["1.21", "1.22", "1.23", "1.24"]
go-version: ["1.22", "1.23", "1.24", "1.25"]

steps:
- name: Check out code
Expand Down
66 changes: 64 additions & 2 deletions azureappconfiguration/azureappconfiguration.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"strings"
"sync"
"sync/atomic"
"time"

"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/jsonc"
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/refresh"
Expand Down Expand Up @@ -140,7 +141,7 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
}
}

if err := azappcfg.load(ctx); err != nil {
if err := azappcfg.startupWithRetry(ctx, options.StartupOptions.Timeout, azappcfg.load); err != nil {
return nil, err
}
// Set the initial load finished flag
Expand Down Expand Up @@ -480,6 +481,11 @@ func (azappcfg *AzureAppConfiguration) loadFeatureFlags(ctx context.Context, set

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 {
Expand Down Expand Up @@ -676,6 +682,59 @@ func (azappcfg *AzureAppConfiguration) executeFailoverPolicy(ctx context.Context
return fmt.Errorf("failed to get settings from all clients: %v", errors)
}

// startupWithRetry implements retry logic for startup loading with timeout and exponential backoff
func (azappcfg *AzureAppConfiguration) startupWithRetry(ctx context.Context, timeout time.Duration, operation func(context.Context) error) error {
// If no timeout is specified, use the default startup timeout
if timeout <= 0 {
timeout = defaultStartupTimeout
}

// Create a context with timeout for the entire startup process
startupCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

attempt := 0
startTime := time.Now()

for {
attempt++

// Try to load with the current context
err := operation(startupCtx)
if err == nil {
return nil
}

// Check if the error is retriable
if !(isFailoverable(err) ||
strings.Contains(err.Error(), "no client is available") ||
strings.Contains(err.Error(), "failed to get settings from all clients")) {
return fmt.Errorf("load from Azure App Configuration failed with non-retriable error: %w", err)
}

// Calculate backoff duration
timeElapsed := time.Since(startTime)
backoffDuration := getFixedBackoffDuration(timeElapsed)
if backoffDuration == 0 {
backoffDuration = calculateBackoffDuration(attempt)
}

// Check if we have enough time left to wait and retry
timeRemaining := timeout - timeElapsed
if timeRemaining <= backoffDuration {
return fmt.Errorf("load from Azure App Configuration failed after %d attempts within timeout %v: %w", attempt, timeout, err)
}

// Wait for the backoff duration before retrying
select {
case <-startupCtx.Done():
return fmt.Errorf("load from Azure App Configuration timed out: %w", startupCtx.Err())
case <-time.After(backoffDuration):
// Continue to next retry attempt
}
}
}

func (azappcfg *AzureAppConfiguration) trimPrefix(key string) string {
result := key
for _, prefix := range azappcfg.trimPrefixes {
Expand Down Expand Up @@ -725,7 +784,9 @@ func deduplicateSelectors(selectors []Selector) []Selector {

func getFeatureFlagSelectors(selectors []Selector) []Selector {
for i := range selectors {
selectors[i].KeyFilter = featureFlagKeyPrefix + selectors[i].KeyFilter
if selectors[i].SnapshotName == "" {
selectors[i].KeyFilter = featureFlagKeyPrefix + selectors[i].KeyFilter
}
}

return selectors
Expand Down Expand Up @@ -760,6 +821,7 @@ func configureTracingOptions(options *Options) tracing.Options {
}

tracingOption.Host = tracing.GetHostType()
tracingOption.FMVersion = tracing.GetFeatureManagementVersion()

if !(options.KeyVaultOptions.SecretResolver == nil && options.KeyVaultOptions.Credential == nil) {
tracingOption.KeyVaultConfigured = true
Expand Down
23 changes: 19 additions & 4 deletions azureappconfiguration/client_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,17 +335,17 @@ func (client *configurationClientWrapper) updateBackoffStatus(success bool) {
client.backOffEndTime = time.Time{}
} else {
client.failedAttempts++
client.backOffEndTime = time.Now().Add(client.getBackoffDuration())
client.backOffEndTime = time.Now().Add(calculateBackoffDuration(client.failedAttempts))
}
}

func (client *configurationClientWrapper) getBackoffDuration() time.Duration {
if client.failedAttempts <= 1 {
func calculateBackoffDuration(failedAttempts int) time.Duration {
if failedAttempts <= 1 {
return minBackoffDuration
}

// Cap the exponent to prevent overflow
exponent := math.Min(float64(client.failedAttempts-1), float64(safeShiftLimit))
exponent := math.Min(float64(failedAttempts-1), float64(safeShiftLimit))
calculatedMilliseconds := float64(minBackoffDuration.Milliseconds()) * math.Pow(2, exponent)
if calculatedMilliseconds > float64(maxBackoffDuration.Milliseconds()) || calculatedMilliseconds <= 0 {
calculatedMilliseconds = float64(maxBackoffDuration.Milliseconds())
Expand All @@ -355,6 +355,21 @@ func (client *configurationClientWrapper) getBackoffDuration() time.Duration {
return jitter(calculatedDuration)
}

func getFixedBackoffDuration(timeElapsed time.Duration) time.Duration {
if timeElapsed < time.Second*100 {
return time.Second * 5
}
if timeElapsed < time.Second*200 {
return time.Second * 10
}

if timeElapsed < time.Second*600 {
return minBackoffDuration
}

return 0
}

func jitter(duration time.Duration) time.Duration {
// Calculate the amount of jitter to add to the duration
jitter := float64(duration) * jitterRatio
Expand Down
5 changes: 5 additions & 0 deletions azureappconfiguration/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,8 @@ const (
jitterRatio float64 = 0.25
safeShiftLimit int = 63
)

// Startup constants
const (
defaultStartupTimeout time.Duration = 100 * time.Second
)
Loading
Loading