go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/experiments.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package lucicfg 16 17 import ( 18 "fmt" 19 "sort" 20 "strings" 21 22 "go.chromium.org/luci/common/data/stringset" 23 "go.chromium.org/luci/common/logging" 24 25 "go.starlark.net/starlark" 26 "go.starlark.net/syntax" 27 ) 28 29 // experiments holds a set of registered experiment IDs and enabled ones. 30 type experiments struct { 31 all map[string]version // name => lucicfg version to auto-enable on 32 enabled stringset.Set 33 lucicfgVer version // max of versions passed to lucicfg.check_version(...) 34 } 35 36 // Register adds an experiment ID to the set of known experiments. 37 func (exp *experiments) Register(id string, minVer starlark.Tuple) { 38 if exp.all == nil { 39 exp.all = make(map[string]version, 1) 40 } 41 42 ver := version{minVer} 43 exp.all[id] = ver 44 45 // Auto-enable based on version passed to lucicfg.check_version(...). 46 if exp.lucicfgVer.isSet() && ver.isSet() && exp.lucicfgVer.greaterOrEq(ver) { 47 exp.Enable(id) 48 } 49 } 50 51 // Registered returns a sorted list of all registered experiments. 52 func (exp *experiments) Registered() []string { 53 names := make([]string, 0, len(exp.all)) 54 for name := range exp.all { 55 names = append(names, name) 56 } 57 sort.Strings(names) 58 return names 59 } 60 61 // Enable adds an experiment ID to the set of enabled experiments. 62 // 63 // Always succeeds, but returns false if the experiment ID hasn't been 64 // registered before. 65 func (exp *experiments) Enable(id string) bool { 66 if exp.enabled == nil { 67 exp.enabled = stringset.New(1) 68 } 69 exp.enabled.Add(id) 70 _, known := exp.all[id] 71 return known 72 } 73 74 // IsEnabled returns true if an experiment has been enabled already. 75 func (exp *experiments) IsEnabled(id string) bool { 76 return exp.enabled.Has(id) 77 } 78 79 // setMinVersion is called from lucicfg.check_version(...). 80 // 81 // Auto-enables eligible experiments. 82 func (exp *experiments) setMinVersion(minVer starlark.Tuple) { 83 ver := version{minVer} 84 if !ver.isSet() { 85 panic(fmt.Sprintf("empty version passed to setMinVersion: %v", minVer)) 86 } 87 if exp.lucicfgVer.isSet() && exp.lucicfgVer.greaterOrEq(ver) { 88 return // already checked with more recent version 89 } 90 exp.lucicfgVer = ver 91 92 // Auto-enable experiments based on their enable_on_min_version. 93 for id, min := range exp.all { 94 if min.isSet() && exp.lucicfgVer.greaterOrEq(min) { 95 exp.Enable(id) 96 } 97 } 98 } 99 100 type version struct { 101 tup starlark.Tuple // either () or (major, minor, revision) 102 } 103 104 func (v version) isSet() bool { 105 return len(v.tup) != 0 106 } 107 108 func (v version) greaterOrEq(another version) bool { 109 yes, err := starlark.Compare(syntax.GE, v.tup, another.tup) 110 if err != nil { 111 panic(fmt.Sprintf("comparing tuples should succeed, got: %s", err)) 112 } 113 return yes 114 } 115 116 func init() { 117 // set_min_version_for_experiments is called from lucicfg.check_version. 118 declNative("set_min_version_for_experiments", func(call nativeCall) (starlark.Value, error) { 119 var ver starlark.Tuple 120 if err := call.unpack(1, &ver); err != nil { 121 return nil, err 122 } 123 call.State.experiments.setMinVersion(ver) 124 return starlark.None, nil 125 }) 126 127 // enable_experiment is used by lucicfg.enable_experiment in lucicfg.star. 128 declNative("enable_experiment", func(call nativeCall) (starlark.Value, error) { 129 var id starlark.String 130 if err := call.unpack(1, &id); err != nil { 131 return nil, err 132 } 133 if expID := id.GoString(); !call.State.experiments.Enable(expID) { 134 help := "there are no experiments available" 135 if all := call.State.experiments.Registered(); len(all) != 0 { 136 quoted := make([]string, len(all)) 137 for i, s := range all { 138 quoted[i] = fmt.Sprintf("%q", s) 139 } 140 help = "available experiments: " + strings.Join(quoted, ", ") 141 } 142 logging.Warningf(call.Ctx, "enable_experiment: unknown experiment %q (%s). "+ 143 "It is possible the experiment was retired already, consider removing this call to stop the warning.", expID, help) 144 } 145 return starlark.None, nil 146 }) 147 148 // register_experiment is used in experiments.star. 149 declNative("register_experiment", func(call nativeCall) (starlark.Value, error) { 150 var id starlark.String 151 var minVer starlark.Tuple 152 if err := call.unpack(1, &id, &minVer); err != nil { 153 return nil, err 154 } 155 call.State.experiments.Register(id.GoString(), minVer) 156 return starlark.None, nil 157 }) 158 159 // is_experiment_enabled is used in experiments.star. 160 declNative("is_experiment_enabled", func(call nativeCall) (starlark.Value, error) { 161 var id starlark.String 162 if err := call.unpack(1, &id); err != nil { 163 return nil, err 164 } 165 return starlark.Bool(call.State.experiments.IsEnabled(id.GoString())), nil 166 }) 167 168 // list_enabled_experiments lists experiments enabled via enable_experiment. 169 // 170 // Lists all experiments passed to enable_experiment(...), even ones that 171 // aren't registered anymore. Also includes all experiments auto-enabled via 172 // `enable_on_min_version` mechanism. 173 // 174 // This list ends up in `lucicfg {...}` section of project.cfg. Listing *all* 175 // experiments there is useful to figure out what LUCI projects enable retired 176 // experiments. 177 declNative("list_enabled_experiments", func(call nativeCall) (starlark.Value, error) { 178 if err := call.unpack(0); err != nil { 179 return nil, err 180 } 181 exps := make([]starlark.Value, 0, call.State.experiments.enabled.Len()) 182 for _, exp := range call.State.experiments.enabled.ToSortedSlice() { 183 exps = append(exps, starlark.String(exp)) 184 } 185 return starlark.NewList(exps), nil 186 }) 187 }