Skip to content

Commit 1c7a4f3

Browse files
authored
[FSSDK-11857] go-sdk CMAB bug fixes from bug bash (#416)
* fix bug bash bugs * format * fix bucketer service to skip cmab experiment * fix linting
1 parent 907917e commit 1c7a4f3

File tree

4 files changed

+79
-1
lines changed

4 files changed

+79
-1
lines changed

pkg/cmab/service.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/json"
2222
"fmt"
2323
"strconv"
24+
"sync"
2425

2526
"github.com/google/uuid"
2627
"github.com/optimizely/go-sdk/v2/pkg/cache"
@@ -31,11 +32,26 @@ import (
3132
"github.com/twmb/murmur3"
3233
)
3334

35+
const (
36+
// NumLockStripes defines the number of mutexes for lock striping to reduce contention
37+
NumLockStripes = 1000
38+
)
39+
3440
// DefaultCmabService implements the CmabService interface
3541
type DefaultCmabService struct {
3642
cmabCache cache.CacheWithRemove
3743
cmabClient Client
3844
logger logging.OptimizelyLogProducer
45+
// Lock striping to prevent race conditions in concurrent CMAB requests
46+
locks [NumLockStripes]sync.Mutex
47+
}
48+
49+
// getLockIndex calculates the lock index for a given user and rule combination
50+
func (s *DefaultCmabService) getLockIndex(userID, ruleID string) int {
51+
// Create a hash of userID + ruleID for consistent lock selection
52+
hasher := murmur3.New32()
53+
_, _ = hasher.Write([]byte(userID + ruleID)) // murmur3 Write never returns an error
54+
return int(hasher.Sum32() % NumLockStripes)
3955
}
4056

4157
// ServiceOptions defines options for creating a CMAB service
@@ -66,6 +82,11 @@ func (s *DefaultCmabService) GetDecision(
6682
ruleID string,
6783
options *decide.Options,
6884
) (Decision, error) {
85+
// Use lock striping to prevent race conditions in concurrent requests
86+
lockIndex := s.getLockIndex(userContext.ID, ruleID)
87+
s.locks[lockIndex].Lock()
88+
defer s.locks[lockIndex].Unlock()
89+
6990
// Initialize reasons slice for decision
7091
reasons := []string{}
7192

pkg/cmab/service_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/optimizely/go-sdk/v2/pkg/decide"
2828
"github.com/optimizely/go-sdk/v2/pkg/entities"
2929
"github.com/optimizely/go-sdk/v2/pkg/logging"
30+
"github.com/stretchr/testify/assert"
3031
"github.com/stretchr/testify/mock"
3132
"github.com/stretchr/testify/suite"
3233
"github.com/twmb/murmur3"
@@ -843,3 +844,54 @@ func (s *CmabServiceTestSuite) TestGetDecisionApiError() {
843844
s.mockCache.AssertExpectations(s.T())
844845
s.mockClient.AssertExpectations(s.T())
845846
}
847+
848+
849+
// TestLockStripingDistribution verifies that different user/rule combinations
850+
// use different locks to allow for better concurrency
851+
func TestLockStripingDistribution(t *testing.T) {
852+
service := &DefaultCmabService{}
853+
854+
// Test different combinations to ensure they get different lock indices
855+
testCases := []struct {
856+
userID string
857+
ruleID string
858+
}{
859+
{"user1", "rule1"},
860+
{"user2", "rule1"},
861+
{"user1", "rule2"},
862+
{"user3", "rule3"},
863+
{"user4", "rule4"},
864+
}
865+
866+
lockIndices := make(map[int]bool)
867+
for _, tc := range testCases {
868+
index := service.getLockIndex(tc.userID, tc.ruleID)
869+
870+
// Verify index is within expected range
871+
assert.GreaterOrEqual(t, index, 0, "Lock index should be non-negative")
872+
assert.Less(t, index, NumLockStripes, "Lock index should be less than NumLockStripes")
873+
874+
lockIndices[index] = true
875+
}
876+
877+
// We should have multiple different lock indices (though not necessarily all unique due to hash collisions)
878+
assert.Greater(t, len(lockIndices), 1, "Different user/rule combinations should generally use different locks")
879+
}
880+
881+
// TestSameUserRuleCombinationUsesConsistentLock verifies that the same user/rule combination
882+
// always uses the same lock index
883+
func TestSameUserRuleCombinationUsesConsistentLock(t *testing.T) {
884+
service := &DefaultCmabService{}
885+
886+
userID := "test_user"
887+
ruleID := "test_rule"
888+
889+
// Get lock index multiple times
890+
index1 := service.getLockIndex(userID, ruleID)
891+
index2 := service.getLockIndex(userID, ruleID)
892+
index3 := service.getLockIndex(userID, ruleID)
893+
894+
// All should be the same
895+
assert.Equal(t, index1, index2, "Same user/rule should always use same lock")
896+
assert.Equal(t, index2, index3, "Same user/rule should always use same lock")
897+
}

pkg/decision/experiment_bucketer_service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ func (s ExperimentBucketerService) GetDecision(decisionContext ExperimentDecisio
5151
experiment := decisionContext.Experiment
5252
reasons := decide.NewDecisionReasons(options)
5353

54+
// Skip CMAB experiments - they should be handled by CMAB service only
55+
if experiment.Cmab != nil {
56+
return experimentDecision, reasons, nil
57+
}
58+
5459
// Audience evaluation using common function
5560
inAudience, audienceReasons := evaluator.CheckIfUserInAudience(experiment, userContext, decisionContext.ProjectConfig, s.audienceTreeEvaluator, options, s.logger)
5661
reasons.Append(audienceReasons)

pkg/decision/reasons/reason.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,5 @@ const (
6060
// OverrideVariationAssignmentFound - A valid override variation was found for the given user and experiment
6161
OverrideVariationAssignmentFound Reason = "Override variation assignment found"
6262
// CmabVariationAssigned is the reason when a variation is assigned by the CMAB service
63-
CmabVariationAssigned Reason = "cmab_variation_assigned"
63+
CmabVariationAssigned Reason = "cmab variation assigned"
6464
)

0 commit comments

Comments
 (0)