Skip to content

[FSSDK-11635] remove duplicated fetch method, add cache wrapper #409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 24, 2025
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
53 changes: 15 additions & 38 deletions pkg/cmab/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"encoding/json"
"fmt"
"strconv"
"time"

"github.com/google/uuid"
"github.com/optimizely/go-sdk/v2/pkg/cache"
Expand Down Expand Up @@ -76,7 +75,7 @@ func (s *DefaultCmabService) GetDecision(
// Check if we should ignore the cache
if options != nil && hasOption(options, decide.IgnoreCMABCache) {
reasons = append(reasons, "Ignoring CMAB cache as requested")
decision, err := s.fetchDecisionWithRetry(ruleID, userContext.ID, filteredAttributes)
decision, err := s.fetchDecision(ruleID, userContext.ID, filteredAttributes)
if err != nil {
return Decision{Reasons: reasons}, err
}
Expand Down Expand Up @@ -136,7 +135,7 @@ func (s *DefaultCmabService) GetDecision(
}

// Fetch new decision
decision, err := s.fetchDecisionWithRetry(ruleID, userContext.ID, filteredAttributes)
decision, err := s.fetchDecision(ruleID, userContext.ID, filteredAttributes)
if err != nil {
decision.Reasons = append(reasons, decision.Reasons...)
return decision, fmt.Errorf("CMAB API error: %w", err)
Expand All @@ -156,51 +155,29 @@ func (s *DefaultCmabService) GetDecision(
return decision, nil
}

// fetchDecisionWithRetry fetches a decision from the CMAB API with retry logic
func (s *DefaultCmabService) fetchDecisionWithRetry(
// fetchDecision fetches a decision from the CMAB API
func (s *DefaultCmabService) fetchDecision(
ruleID string,
userID string,
attributes map[string]interface{},
) (Decision, error) {
cmabUUID := uuid.New().String()
reasons := []string{}

// Retry configuration
maxRetries := 3
backoffFactor := 2
initialBackoff := 100 * time.Millisecond
s.logger.Debug(fmt.Sprintf("Fetching CMAB decision for rule %s and user %s", ruleID, userID))

var lastErr error

for attempt := 0; attempt < maxRetries; attempt++ {
// Exponential backoff if this is a retry
if attempt > 0 {
backoffDuration := initialBackoff * time.Duration(backoffFactor^attempt)
time.Sleep(backoffDuration)
reasons = append(reasons, fmt.Sprintf("Retry attempt %d/%d after backoff", attempt+1, maxRetries))
}

s.logger.Debug(fmt.Sprintf("Fetching CMAB decision for rule %s and user %s (attempt %d/%d)",
ruleID, userID, attempt+1, maxRetries))

variationID, err := s.cmabClient.FetchDecision(ruleID, userID, attributes, cmabUUID)
if err == nil {
reasons = append(reasons, fmt.Sprintf("Successfully fetched CMAB decision on attempt %d/%d", attempt+1, maxRetries))
return Decision{
VariationID: variationID,
CmabUUID: cmabUUID,
Reasons: reasons,
}, nil
}

lastErr = err
s.logger.Warning(fmt.Sprintf("CMAB API request failed (attempt %d/%d): %v",
attempt+1, maxRetries, err))
variationID, err := s.cmabClient.FetchDecision(ruleID, userID, attributes, cmabUUID)
if err != nil {
reasons = append(reasons, "Failed to fetch CMAB decision")
return Decision{Reasons: reasons}, fmt.Errorf("CMAB API error: %w", err)
}

reasons = append(reasons, fmt.Sprintf("Failed to fetch CMAB decision after %d attempts", maxRetries))
return Decision{Reasons: reasons}, fmt.Errorf("failed to fetch CMAB decision after %d attempts: %w",
maxRetries, lastErr)
reasons = append(reasons, "Successfully fetched CMAB decision")
return Decision{
VariationID: variationID,
CmabUUID: cmabUUID,
Reasons: reasons,
}, nil
}

// filterAttributes filters user attributes based on CMAB configuration
Expand Down
40 changes: 40 additions & 0 deletions pkg/odp/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/****************************************************************************
* Copyright 2022-2025, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* https://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
***************************************************************************/

// Package odp provides compatibility with the previously located cache package.
// This file exists to maintain backward compatibility with code that imports
// cache from the odp package. New code should import from pkg/cache directly.
package odp

import (
"time"

"github.com/optimizely/go-sdk/v2/pkg/cache"
)

// LRUCache wraps the cache.LRUCache to maintain backward compatibility
type LRUCache struct {
*cache.LRUCache
}

// NewLRUCache returns a new instance of Least Recently Used in-memory cache
// Deprecated: For new code, use pkg/cache directly instead.
// This function exists for backward compatibility with code that imports from pkg/odp
func NewLRUCache(size int, timeout time.Duration) *LRUCache {
return &LRUCache{
LRUCache: cache.NewLRUCache(size, timeout),
}
}