Skip to content
Open
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
7 changes: 7 additions & 0 deletions .schema/flag-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,13 @@
"date": {
"type": "string",
"format": "date-time"
},
"strategy": {
"type": "string",
"enum": ["merge", "reset"],
"default": "merge",
"title": "strategy",
"description": "Strategy to apply when the scheduled step is triggered. 'merge' (default) merges the step configuration with the current flag. 'reset' wipes the flag configuration and applies only the step configuration."
}
},
"additionalProperties": false,
Expand Down
47 changes: 47 additions & 0 deletions examples/rollout_scheduled/flags.goff.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,50 @@ new-admin-access:
- name: rule_1
query: beta eq "false"
date: 2022-05-12T15:36:00.1+02:00

new-admin-access-override:
variations:
default: false
false_var: false
true_var: true
targeting:
- query: key eq "785a14bf-d2c5-4caa-9c70-2bbc4e3732a5"
name: rule_1
percentage:
false_var: 0
true_var: 100
defaultRule:
variation: default
scheduledRollout:
- strategy: override
targeting:
- name: rule_2
query: key eq "785a14bf-d2c5-4caa-9c70-2bbc4e3732a5"
percentage:
false_var: 100
true_var: 0
date: 2022-05-12T15:36:00.1+02:00

new-admin-access-reset:
variations:
default: false
false_var: false
true_var: true
targeting:
- query: key eq "785a14bf-d2c5-4caa-9c70-2bbc4e3732a5"
name: rule_1
percentage:
false_var: 0
true_var: 100
defaultRule:
variation: default
scheduledRollout:
- targeting:
- name: rule_1
query: key eq "785a14bf-d2c5-4caa-9c70-2bbc4e3732a5"
percentage:
false_var: 0
true_var: 100
date: 2022-05-12T15:36:00.1+02:00
- date: 2022-06-12T15:36:00.1+02:00
strategy: reset
18 changes: 17 additions & 1 deletion examples/rollout_scheduled/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ func main() {
// Call multiple time the same flag to see the change in time.
for true {
time.Sleep(1 * time.Second)
fmt.Println(ffclient.BoolVariation("new-admin-access", user, false))
newAdminAccess, err := ffclient.BoolVariationDetails("new-admin-access", user, false)
if err != nil {
log.Fatal(err)
}
fmt.Println("new-admin-access:", newAdminAccess.Value, newAdminAccess.Reason)

newAdminAccessOverride, err := ffclient.BoolVariationDetails("new-admin-access-override", user, false)
if err != nil {
log.Fatal(err)
}
fmt.Println("new-admin-access-override:", newAdminAccessOverride.Value, newAdminAccessOverride.Reason)

newAdminAccessReset, err := ffclient.BoolVariationDetails("new-admin-access-reset", user, false)
if err != nil {
log.Fatal(err)
}
fmt.Println("new-admin-access-reset:", newAdminAccessReset.Value, newAdminAccessReset.Reason)
}
}
109 changes: 79 additions & 30 deletions modules/core/flag/internal_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (f *InternalFlag) selectVariation(

// nolint: gocognit
// applyScheduledRolloutSteps is checking if the flag has a scheduled rollout configured.
// If yes we merge the changes to the current flag.
// If yes we merge the changes to the current flag or reset it based on the strategy.
func (f *InternalFlag) applyScheduledRolloutSteps(evaluationDate time.Time) (*InternalFlag, error) {
if f.Scheduled == nil {
return f, nil
Expand All @@ -223,43 +223,92 @@ func (f *InternalFlag) applyScheduledRolloutSteps(evaluationDate time.Time) (*In
for _, steps := range *f.Scheduled {
if steps.Date != nil &&
(steps.Date.Before(evaluationDate) || steps.Date.Equal(evaluationDate)) {
flagCopy.Rules = MergeSetOfRules(f.GetRules(), steps.GetRules())
if steps.Disable != nil {
flagCopy.Disable = steps.Disable
switch steps.GetStrategy() {
case ScheduledStrategyOverride:
f.applyScheduledStepOverride(flagCopy, steps)
case ScheduledStrategyReset:
f.applyScheduledStepReset(flagCopy)
case ScheduledStrategyMerge:
f.applyScheduledStepMerge(flagCopy, steps)
}
}
}
return flagCopy, nil
}

if steps.TrackEvents != nil {
flagCopy.TrackEvents = steps.TrackEvents
}
// applyScheduledStepOverride overrides the flag configuration with the scheduled step configuration.
func (f *InternalFlag) applyScheduledStepOverride(flagCopy *InternalFlag, steps ScheduledStep) {
flagCopy.Variations = coalesce(steps.Variations, flagCopy.Variations)
flagCopy.Rules = coalesce(steps.Rules, flagCopy.Rules)
flagCopy.BucketingKey = coalesce(steps.BucketingKey, flagCopy.BucketingKey)
flagCopy.DefaultRule = coalesce(steps.DefaultRule, flagCopy.DefaultRule)
flagCopy.Experimentation = coalesce(steps.Experimentation, flagCopy.Experimentation)
flagCopy.Scheduled = f.Scheduled // Always keep scheduled steps
flagCopy.TrackEvents = coalesce(steps.TrackEvents, flagCopy.TrackEvents)
flagCopy.Disable = coalesce(steps.Disable, flagCopy.Disable)
flagCopy.Version = coalesce(steps.Version, flagCopy.Version)
flagCopy.Metadata = coalesce(steps.Metadata, flagCopy.Metadata)
}

if steps.DefaultRule != nil {
flagCopy.DefaultRule.MergeRules(*steps.DefaultRule)
}
// coalesce returns override if non-nil, otherwise fallback.
func coalesce[T any](override, fallback *T) *T {
if override != nil {
return override
}
return fallback
}

if steps.Variations != nil {
for key, value := range steps.GetVariations() {
flagCopy.GetVariations()[key] = value
}
}
// applyScheduledStepReset resets the flag configuration to its initial state.
func (f *InternalFlag) applyScheduledStepReset(flagCopy *InternalFlag) {
flagCopy.Variations = f.Variations
flagCopy.Rules = f.Rules
flagCopy.BucketingKey = nil
flagCopy.DefaultRule = f.DefaultRule
flagCopy.Experimentation = f.Experimentation
flagCopy.Scheduled = f.Scheduled
flagCopy.TrackEvents = f.TrackEvents
flagCopy.Disable = f.Disable
flagCopy.Version = f.Version
flagCopy.Metadata = f.Metadata
}

if steps.Version != nil {
flagCopy.Version = steps.Version
}
// applyScheduledStepMerge merges the scheduled step configuration with the current flag configuration.
func (f *InternalFlag) applyScheduledStepMerge(flagCopy *InternalFlag, steps ScheduledStep) {
flagCopy.Rules = MergeSetOfRules(f.GetRules(), steps.GetRules())
if steps.Disable != nil {
flagCopy.Disable = steps.Disable
}

if steps.Experimentation != nil {
if flagCopy.Experimentation == nil {
flagCopy.Experimentation = &ExperimentationRollout{}
}
if steps.Experimentation.Start != nil {
flagCopy.Experimentation.Start = steps.Experimentation.Start
}
if steps.Experimentation.End != nil {
flagCopy.Experimentation.End = steps.Experimentation.End
}
}
if steps.TrackEvents != nil {
flagCopy.TrackEvents = steps.TrackEvents
}

if steps.DefaultRule != nil {
flagCopy.DefaultRule.MergeRules(*steps.DefaultRule)
}

if steps.Variations != nil {
for key, value := range steps.GetVariations() {
flagCopy.GetVariations()[key] = value
}
}
return flagCopy, nil

if steps.Version != nil {
flagCopy.Version = steps.Version
}

if steps.Experimentation != nil {
if flagCopy.Experimentation == nil {
flagCopy.Experimentation = &ExperimentationRollout{}
}
if steps.Experimentation.Start != nil {
flagCopy.Experimentation.Start = steps.Experimentation.Start
}
if steps.Experimentation.End != nil {
flagCopy.Experimentation.End = steps.Experimentation.End
}
}

}

// isExperimentationOver checks if we are in an experimentation or not
Expand Down
Loading