Skip to content

Commit 73079c5

Browse files
committed
Added fallback treatment config
1 parent b56f0e2 commit 73079c5

File tree

5 files changed

+157
-51
lines changed

5 files changed

+157
-51
lines changed

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ module github.com/splitio/go-client/v6
33
go 1.18
44

55
require (
6-
github.com/splitio/go-split-commons/v8 v8.0.0
6+
github.com/splitio/go-split-commons/v8 v8.0.1-0.20251120161359-a7eb977ebb64
77
github.com/splitio/go-toolkit/v5 v5.4.1
88
github.com/stretchr/testify v1.11.1
99
)
1010

11+
replace github.com/splitio/go-split-commons/v8 => /Users/nadiamayor/go/src/github.com/splitio/go-split-commons
12+
1113
require (
1214
github.com/bits-and-blooms/bitset v1.3.1 // indirect
1315
github.com/bits-and-blooms/bloom/v3 v3.3.1 // indirect

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
1818
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1919
github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc=
2020
github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
21-
github.com/splitio/go-split-commons/v8 v8.0.0 h1:wLk5eT6WU2LfxtaWG3ZHlTbNMGWP2eYsZTb1o+tFpkI=
22-
github.com/splitio/go-split-commons/v8 v8.0.0/go.mod h1:vgRGPn0s4RC9/zp1nIn4KeeIEj/K3iXE2fxYQbCk/WI=
2321
github.com/splitio/go-toolkit/v5 v5.4.1 h1:srTyvDBJZMUcJ/KiiQDMyjCuELVgTBh2TGRVn0sOXEE=
2422
github.com/splitio/go-toolkit/v5 v5.4.1/go.mod h1:SifzysrOVDbzMcOE8zjX02+FG5az4FrR3Us/i5SeStw=
2523
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=

splitio/client/client_test.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,22 @@ func TestClient(t *testing.T) {
13221322
cfg.LabelsEnabled = true
13231323
logger := logging.NewLogger(nil)
13241324

1325+
stringConfig := "flag1_config"
1326+
globalTreatment := "global_treatment"
1327+
flag1Treatment := "flag1_treatment"
1328+
config := &dtos.FallbackTreatmentConfig{
1329+
GlobalFallbackTreatment: &dtos.FallbackTreatment{
1330+
Treatment: &globalTreatment,
1331+
},
1332+
ByFlagFallbackTreatment: map[string]dtos.FallbackTreatment{
1333+
"flag1": {
1334+
Treatment: &flag1Treatment,
1335+
Config: &stringConfig,
1336+
},
1337+
},
1338+
}
1339+
fallbackTreatmentCalculator := dtos.NewFallbackTreatmentCalculatorImp(config)
1340+
13251341
evaluator := evaluator.NewEvaluator(
13261342
mocks.MockSplitStorage{
13271343
SplitCall: func(splitName string) *dtos.SplitDTO {
@@ -1374,6 +1390,7 @@ func TestClient(t *testing.T) {
13741390
logger,
13751391
cfg.Advanced.FeatureFlagRules,
13761392
cfg.Advanced.RuleBasedSegmentRules,
1393+
fallbackTreatmentCalculator,
13771394
)
13781395

13791396
mockedTelemetryStorage := mocks.MockTelemetryStorage{
@@ -1409,19 +1426,19 @@ func TestClient(t *testing.T) {
14091426
t.Error("Wrong impression saved")
14101427
}
14111428

1412-
expectedTreatment(client.Treatment("invalid", "invalid", nil), "control", t)
1413-
if client.Treatment("invalid", "invalid", nil) != "control" {
1429+
expectedTreatment(client.Treatment("invalid", "invalid", nil), "global_treatment", t)
1430+
if client.Treatment("invalid", "invalid", nil) != "global_treatment" {
14141431
t.Error("Unexpected Treatment Result")
14151432
}
14161433

14171434
expectedTreatment(client.Treatment("invalid", "killed", nil), "defTreatment", t)
1418-
if isInvalidImpression(client, "invalid", "killed", "defTreatment") {
1435+
if isInvalidImpression(client, "invalid", "invalid", "global_treatment") {
14191436
t.Error("Wrong impression saved")
14201437
}
14211438

14221439
// Assertion Treatments
14231440
treatments := client.Treatments("user1", []string{"valid", "invalid", "killed"}, nil)
1424-
expectedTreatment(treatments["invalid"], "control", t)
1441+
expectedTreatment(treatments["invalid"], "global_treatment", t)
14251442
expectedTreatment(treatments["killed"], "defTreatment", t)
14261443
expectedTreatment(treatments["valid"], "on", t)
14271444
client.impressions.(storage.ImpressionStorage).PopN(cfg.Advanced.ImpressionsBulkSize)
@@ -1437,15 +1454,15 @@ func TestClient(t *testing.T) {
14371454
t.Error("Wrong impression saved")
14381455
}
14391456

1440-
expectedTreatmentAndConfig(client.TreatmentWithConfig("invalid", "invalid", nil), "control", "", t)
1457+
expectedTreatmentAndConfig(client.TreatmentWithConfig("invalid", "invalid", nil), "global_treatment", "", t)
14411458
expectedTreatmentAndConfig(client.TreatmentWithConfig("invalid", "killed", nil), "defTreatment", "{\"color\": \"orange\",\"size\": 15}", t)
1442-
if isInvalidImpression(client, "invalid", "killed", "defTreatment") {
1459+
if isInvalidImpression(client, "invalid", "invalid", "global_treatment") {
14431460
t.Error("Wrong impression saved")
14441461
}
14451462

14461463
// Assertion TreatmentsWithConfig
14471464
treatmentsWithConfigs := client.TreatmentsWithConfig("user1", []string{"valid", "invalid", "killed"}, nil)
1448-
expectedTreatmentAndConfig(treatmentsWithConfigs["invalid"], "control", "", t)
1465+
expectedTreatmentAndConfig(treatmentsWithConfigs["invalid"], "global_treatment", "", t)
14491466
expectedTreatmentAndConfig(treatmentsWithConfigs["killed"], "defTreatment", "{\"color\": \"orange\",\"size\": 15}", t)
14501467
expectedTreatmentAndConfig(treatmentsWithConfigs["valid"], "on", "{\"color\": \"blue\",\"size\": 13}", t)
14511468
}

splitio/client/factory.go

Lines changed: 67 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -65,27 +65,28 @@ type sdkStorages struct {
6565

6666
// SplitFactory struct is responsible for instantiating and storing instances of client and manager.
6767
type SplitFactory struct {
68-
startTime time.Time // Tracking startTime
69-
metadata dtos.Metadata
70-
storages sdkStorages
71-
apikey string
72-
status atomic.Value
73-
readinessSubscriptors map[int]chan int
74-
operationMode string
75-
mutex sync.Mutex
76-
cfg *conf.SplitSdkConfig
77-
impressionListener *impressionlistener.WrapperImpressionListener
78-
logger logging.LoggerInterface
79-
syncManager synchronizer.Manager
80-
telemetrySync telemetry.TelemetrySynchronizer // To execute SynchronizeInit
81-
impressionManager provisional.ImpressionManager
68+
startTime time.Time // Tracking startTime
69+
metadata dtos.Metadata
70+
storages sdkStorages
71+
apikey string
72+
status atomic.Value
73+
readinessSubscriptors map[int]chan int
74+
operationMode string
75+
mutex sync.Mutex
76+
cfg *conf.SplitSdkConfig
77+
impressionListener *impressionlistener.WrapperImpressionListener
78+
logger logging.LoggerInterface
79+
syncManager synchronizer.Manager
80+
telemetrySync telemetry.TelemetrySynchronizer // To execute SynchronizeInit
81+
impressionManager provisional.ImpressionManager
82+
fallbackTreatmentCalculator dtos.FallbackTreatmentCalculator
8283
}
8384

8485
// Client returns the split client instantiated by the factory
8586
func (f *SplitFactory) Client() *SplitClient {
8687
return &SplitClient{
8788
logger: f.logger,
88-
evaluator: evaluator.NewEvaluator(f.storages.splits, f.storages.segments, f.storages.ruleBasedSegments, nil, engine.NewEngine(f.logger), f.logger, f.cfg.Advanced.FeatureFlagRules, f.cfg.Advanced.RuleBasedSegmentRules),
89+
evaluator: evaluator.NewEvaluator(f.storages.splits, f.storages.segments, f.storages.ruleBasedSegments, nil, engine.NewEngine(f.logger), f.logger, f.cfg.Advanced.FeatureFlagRules, f.cfg.Advanced.RuleBasedSegmentRules, f.fallbackTreatmentCalculator),
8990
impressions: f.storages.impressions,
9091
events: f.storages.events,
9192
validator: inputValidation{
@@ -297,7 +298,15 @@ func setupInMemoryFactory(
297298
splitAPI := api.NewSplitAPI(apikey, advanced, logger, metadata)
298299

299300
isProxy := splitAPI.SplitFetcher.IsProxy()
300-
evaluator := evaluator.NewEvaluator(splitsStorage, segmentsStorage, ruleBasedSegmentStorage, nil, engine.NewEngine(logger), logger, cfg.Advanced.FeatureFlagRules, cfg.Advanced.RuleBasedSegmentRules)
301+
302+
fallbackTreatmentConf := dtos.FallbackTreatmentConfig{}
303+
if cfg.Advanced.FallbackTreatment != nil {
304+
fallbackTreatmentConf.GlobalFallbackTreatment = conf.SanitizeGlobalFallbackTreatment(cfg.Advanced.FallbackTreatment.GlobalFallbackTreatment, logger)
305+
fallbackTreatmentConf.ByFlagFallbackTreatment = conf.SanitizeByFlagFallBackTreatment(cfg.Advanced.FallbackTreatment.ByFlagFallbackTreatment, logger)
306+
}
307+
fallbackTreatmentCalculator := dtos.NewFallbackTreatmentCalculatorImp(&fallbackTreatmentConf)
308+
309+
evaluator := evaluator.NewEvaluator(splitsStorage, segmentsStorage, ruleBasedSegmentStorage, nil, engine.NewEngine(logger), logger, cfg.Advanced.FeatureFlagRules, cfg.Advanced.RuleBasedSegmentRules, fallbackTreatmentCalculator)
301310
ruleBuilder := grammar.NewRuleBuilder(segmentsStorage, ruleBasedSegmentStorage, nil, cfg.Advanced.FeatureFlagRules, cfg.Advanced.RuleBasedSegmentRules, logger, evaluator)
302311
workers := synchronizer.Workers{
303312
SplitUpdater: split.NewSplitUpdater(splitsStorage, ruleBasedSegmentStorage, splitAPI.SplitFetcher, logger, telemetryStorage, dummyHC, flagSetFilter, ruleBuilder, isProxy, advanced.FlagsSpecVersion),
@@ -360,17 +369,18 @@ func setupInMemoryFactory(
360369
}
361370

362371
splitFactory := SplitFactory{
363-
startTime: time.Now().UTC(),
364-
apikey: apikey,
365-
cfg: cfg,
366-
metadata: metadata,
367-
logger: logger,
368-
operationMode: conf.InMemoryStandAlone,
369-
storages: storages,
370-
readinessSubscriptors: make(map[int]chan int),
371-
syncManager: syncManager,
372-
telemetrySync: workers.TelemetryRecorder,
373-
impressionManager: impressionManager,
372+
startTime: time.Now().UTC(),
373+
apikey: apikey,
374+
cfg: cfg,
375+
metadata: metadata,
376+
logger: logger,
377+
operationMode: conf.InMemoryStandAlone,
378+
storages: storages,
379+
readinessSubscriptors: make(map[int]chan int),
380+
syncManager: syncManager,
381+
telemetrySync: workers.TelemetryRecorder,
382+
impressionManager: impressionManager,
383+
fallbackTreatmentCalculator: fallbackTreatmentCalculator,
374384
}
375385
splitFactory.status.Store(sdkStatusInitializing)
376386
setFactory(splitFactory.apikey, splitFactory.logger)
@@ -438,18 +448,26 @@ func setupRedisFactory(apikey string, cfg *conf.SplitSdkConfig, logger logging.L
438448

439449
syncManager := synchronizer.NewSynchronizerManagerRedis(syncImpl, logger)
440450

451+
fallbackTreatmentConf := dtos.FallbackTreatmentConfig{}
452+
if cfg.Advanced.FallbackTreatment != nil {
453+
fallbackTreatmentConf.GlobalFallbackTreatment = conf.SanitizeGlobalFallbackTreatment(cfg.Advanced.FallbackTreatment.GlobalFallbackTreatment, logger)
454+
fallbackTreatmentConf.ByFlagFallbackTreatment = conf.SanitizeByFlagFallBackTreatment(cfg.Advanced.FallbackTreatment.ByFlagFallbackTreatment, logger)
455+
}
456+
fallbackTreatmentCalculator := dtos.NewFallbackTreatmentCalculatorImp(&fallbackTreatmentConf)
457+
441458
factory := &SplitFactory{
442-
startTime: time.Now().UTC(),
443-
apikey: apikey,
444-
cfg: cfg,
445-
metadata: metadata,
446-
logger: logger,
447-
operationMode: conf.RedisConsumer,
448-
storages: storages,
449-
readinessSubscriptors: make(map[int]chan int),
450-
telemetrySync: telemetry.NewSynchronizerRedis(telemetryStorage, logger),
451-
impressionManager: impressionManager,
452-
syncManager: syncManager,
459+
startTime: time.Now().UTC(),
460+
apikey: apikey,
461+
cfg: cfg,
462+
metadata: metadata,
463+
logger: logger,
464+
operationMode: conf.RedisConsumer,
465+
storages: storages,
466+
readinessSubscriptors: make(map[int]chan int),
467+
telemetrySync: telemetry.NewSynchronizerRedis(telemetryStorage, logger),
468+
impressionManager: impressionManager,
469+
syncManager: syncManager,
470+
fallbackTreatmentCalculator: fallbackTreatmentCalculator,
453471
}
454472
factory.status.Store(sdkStatusInitializing)
455473
setFactory(factory.apikey, factory.logger)
@@ -514,6 +532,13 @@ func setupLocalhostFactory(
514532
return nil, err
515533
}
516534

535+
fallbackTreatmentConf := dtos.FallbackTreatmentConfig{}
536+
if cfg.Advanced.FallbackTreatment != nil {
537+
fallbackTreatmentConf.GlobalFallbackTreatment = conf.SanitizeGlobalFallbackTreatment(cfg.Advanced.FallbackTreatment.GlobalFallbackTreatment, logger)
538+
fallbackTreatmentConf.ByFlagFallbackTreatment = conf.SanitizeByFlagFallBackTreatment(cfg.Advanced.FallbackTreatment.ByFlagFallbackTreatment, logger)
539+
}
540+
fallbackTreatmentCalculator := dtos.NewFallbackTreatmentCalculatorImp(&fallbackTreatmentConf)
541+
517542
splitFactory := &SplitFactory{
518543
startTime: time.Now().UTC(),
519544
apikey: apikey,
@@ -530,9 +555,10 @@ func setupLocalhostFactory(
530555
evaluationTelemetry: telemetryStorage,
531556
runtimeTelemetry: telemetryStorage,
532557
},
533-
readinessSubscriptors: make(map[int]chan int),
534-
syncManager: syncManager,
535-
telemetrySync: &telemetry.NoOp{},
558+
readinessSubscriptors: make(map[int]chan int),
559+
syncManager: syncManager,
560+
telemetrySync: &telemetry.NoOp{},
561+
fallbackTreatmentCalculator: fallbackTreatmentCalculator,
536562
}
537563
splitFactory.status.Store(sdkStatusInitializing)
538564

splitio/conf/sdkconf.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import (
77
"math"
88
"os/user"
99
"path"
10+
"regexp"
1011
"strings"
1112

1213
impressionlistener "github.com/splitio/go-client/v6/splitio/impressionListener"
1314
"github.com/splitio/go-split-commons/v8/conf"
15+
"github.com/splitio/go-split-commons/v8/dtos"
1416
"github.com/splitio/go-split-commons/v8/engine/grammar/constants"
1517
"github.com/splitio/go-toolkit/v5/datastructures/set"
1618
"github.com/splitio/go-toolkit/v5/logging"
@@ -24,6 +26,13 @@ const (
2426
Localhost = "localhost"
2527
// InMemoryStandAlone mode
2628
InMemoryStandAlone = "inmemory-standalone"
29+
30+
// Max Flag name length
31+
MaxFlagNameLength = 100
32+
// Max Treatment length
33+
MaxTreatmentLength = 100
34+
// Treatment regexp
35+
TreatmentRegexp = "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$"
2736
)
2837

2938
var featureFlagsRules = []string{constants.MatcherTypeAllKeys, constants.MatcherTypeInSegment, constants.MatcherTypeWhitelist, constants.MatcherTypeEqualTo, constants.MatcherTypeGreaterThanOrEqualTo, constants.MatcherTypeLessThanOrEqualTo, constants.MatcherTypeBetween,
@@ -108,6 +117,7 @@ type AdvancedConfig struct {
108117
FeatureFlagRules []string
109118
RuleBasedSegmentRules []string
110119
RetryEnabled bool
120+
FallbackTreatment *conf.FallbackTreatmentConf
111121
}
112122

113123
// Default returns a config struct with all the default values
@@ -174,6 +184,7 @@ func Default() *SplitSdkConfig {
174184
FeatureFlagRules: featureFlagsRules,
175185
RuleBasedSegmentRules: ruleBasedSegmentRules,
176186
RetryEnabled: true,
187+
FallbackTreatment: nil,
177188
},
178189
}
179190
}
@@ -280,3 +291,55 @@ func Normalize(apikey string, cfg *SplitSdkConfig) error {
280291

281292
return validConfigRates(cfg)
282293
}
294+
295+
func SanitizeGlobalFallbackTreatment(global *conf.FallbackTreatmentForConf, logger logging.LoggerInterface) *dtos.FallbackTreatment {
296+
if global == nil {
297+
return nil
298+
}
299+
if !isValidTreatment(global) {
300+
logger.Error(fmt.Sprintf("Fallback treatments - Discarded global fallback: Invalid treatment (max %d chars and comply with %s)", MaxTreatmentLength, TreatmentRegexp))
301+
return nil
302+
}
303+
return &dtos.FallbackTreatment{
304+
Treatment: global.Treatment,
305+
Config: global.Config,
306+
}
307+
}
308+
309+
func isValidTreatment(fallbackTreatment *conf.FallbackTreatmentForConf) bool {
310+
if fallbackTreatment == nil || fallbackTreatment.Treatment == nil {
311+
return false
312+
}
313+
value := *fallbackTreatment.Treatment
314+
pattern := regexp.MustCompile(TreatmentRegexp)
315+
return len(value) <= MaxTreatmentLength && pattern.MatchString(value)
316+
}
317+
318+
func SanitizeByFlagFallBackTreatment(byFlag map[string]conf.FallbackTreatmentForConf, logger logging.LoggerInterface) map[string]dtos.FallbackTreatment {
319+
sanitized := map[string]dtos.FallbackTreatment{}
320+
if len(byFlag) == 0 {
321+
return sanitized
322+
}
323+
for flagName, treatment := range byFlag {
324+
if !isValidFlagName(&flagName) {
325+
logger.Error(fmt.Sprintf("Fallback treatments - Discarded flag: Invalid flag name (max %d chars, no spaces)", MaxFlagNameLength))
326+
continue
327+
}
328+
if !isValidTreatment(&treatment) {
329+
logger.Error(fmt.Sprintf("Fallback treatments - Discarded global fallback: Invalid treatment (max %d chars and comply with %s)", MaxTreatmentLength, TreatmentRegexp))
330+
continue
331+
}
332+
sanitized[flagName] = dtos.FallbackTreatment{
333+
Treatment: treatment.Treatment,
334+
Config: treatment.Config,
335+
}
336+
}
337+
return sanitized
338+
}
339+
340+
func isValidFlagName(flagName *string) bool {
341+
if flagName == nil {
342+
return false
343+
}
344+
return len(*flagName) <= MaxFlagNameLength && !strings.Contains(*flagName, " ")
345+
}

0 commit comments

Comments
 (0)