go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/filter/featureBreaker/featurebreaker.go (about) 1 // Copyright 2015 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 featureBreaker 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "runtime" 22 "strings" 23 "sync" 24 ) 25 26 // BreakFeatureCallback can be used to break features at the time of the call. 27 // 28 // If it return an error, this error will be returned by the corresponding API 29 // call as is. If returns nil, the call will be performed as usual. 30 // 31 // It receives a derivative of a context passed to the original API call which 32 // can be used to extract any contextual information, if necessary. 33 // 34 // The callback will be called often and concurrently. Provide your own 35 // synchronization if necessary. 36 type BreakFeatureCallback func(ctx context.Context, feature string) error 37 38 // FeatureBreaker is the state-access interface for all Filter* functions in 39 // this package. A feature is the Name of some method on the filtered service. 40 // 41 // So if you had: 42 // 43 // c, fb := FilterMC(...) 44 // mc := gae.GetMC(c) 45 // 46 // you could do: 47 // 48 // fb.BreakFeatures(memcache.ErrServerError, "Add", "Set") 49 // 50 // and then 51 // 52 // mc.Add(...) and mc.Set(...) 53 // 54 // would return the error. 55 // 56 // You may also pass nil as the error for BreakFeatures, and the fake will 57 // provide the DefaultError which you passed to the Filter function. 58 // 59 // This interface can only break features which return errors. 60 type FeatureBreaker interface { 61 // BreakFeatures allows you to set an error that should be returned by the 62 // corresponding functions. 63 // 64 // For example 65 // m.BreakFeatures(memcache.ErrServerError, "Add") 66 // 67 // would make memcache.Add return memcache.ErrServerError. You can reverse 68 // this by calling UnbreakFeatures("Add"). 69 // 70 // The only exception to this rule is two "fake" functions that can be used 71 // to simulate breaking "RunInTransaction" in a more detailed way: 72 // * Use "BeginTransaction" as a feature name to simulate breaking of a new 73 // transaction attempt. It is called before each individual retry. 74 // * Use "CommitTransaction" as a feature name to simulate breaking the 75 // transaction commit RPC. It is called after the transaction body 76 // completes. Returning datastore.ErrConcurrentTransaction here will cause 77 // a retry. 78 // 79 // "RunInTransaction" itself is not breakable. Break "BeginTransaction" or 80 // "CommitTransaction" instead. 81 BreakFeatures(err error, feature ...string) 82 83 // BreakFeaturesWithCallback is like BreakFeatures, except it allows you to 84 // decide whether to return an error or not at the time the function call is 85 // happening. 86 // 87 // The callback will be called often and concurrently. Provide your own 88 // synchronization if necessary. 89 // 90 // You may use a callback returned by flaky.Errors(...) to emulate randomly 91 // occurring errors. 92 // 93 // Note that the default error passed to Filter* functions are ignored when 94 // using callbacks. 95 BreakFeaturesWithCallback(cb BreakFeatureCallback, feature ...string) 96 97 // UnbreakFeatures is the inverse of BreakFeatures/BreakFeaturesWithCallback, 98 // and will return the named features back to their original functionality. 99 UnbreakFeatures(feature ...string) 100 } 101 102 // errUseDefault is never returned but used as an indicator to use defaultError. 103 var errUseDefault = errors.New("use default error") 104 105 type state struct { 106 l sync.RWMutex 107 broken map[string]BreakFeatureCallback 108 109 // defaultError is the default error to return when you call 110 // BreakFeatures(nil, ...). If this is unset and the user calls BreakFeatures 111 // with nil, BrokenFeatures will return a generic error. 112 defaultError error 113 } 114 115 func newState(dflt error) *state { 116 return &state{ 117 broken: map[string]BreakFeatureCallback{}, 118 defaultError: dflt, 119 } 120 } 121 122 func (s *state) BreakFeatures(err error, feature ...string) { 123 if err == nil { 124 err = errUseDefault 125 } 126 s.BreakFeaturesWithCallback( 127 func(context.Context, string) error { return err }, 128 feature...) 129 } 130 131 func (s *state) BreakFeaturesWithCallback(cb BreakFeatureCallback, feature ...string) { 132 for _, f := range feature { 133 if f == "RunInTransaction" { 134 panic("break BeginTransaction or CommitTransaction instead of RunInTransaction") 135 } 136 } 137 s.l.Lock() 138 defer s.l.Unlock() 139 for _, f := range feature { 140 s.broken[f] = cb 141 } 142 } 143 144 func (s *state) UnbreakFeatures(feature ...string) { 145 s.l.Lock() 146 defer s.l.Unlock() 147 for _, f := range feature { 148 delete(s.broken, f) 149 } 150 } 151 152 func (s *state) run(c context.Context, f func() error) error { 153 if s.noBrokenFeatures() { 154 return f() 155 } 156 157 pc, _, _, _ := runtime.Caller(1) 158 fullName := runtime.FuncForPC(pc).Name() 159 fullNameParts := strings.Split(fullName, ".") 160 name := fullNameParts[len(fullNameParts)-1] 161 162 s.l.RLock() 163 cb := s.broken[name] 164 dflt := s.defaultError 165 s.l.RUnlock() 166 167 if cb == nil { 168 return f() 169 } 170 171 switch err := cb(c, name); { 172 case err == nil: // the callback decided not to break the feature 173 return f() 174 case err == errUseDefault: 175 if dflt != nil { 176 return dflt 177 } 178 return fmt.Errorf("feature %q is broken", name) 179 default: 180 return err 181 } 182 } 183 184 func (s *state) noBrokenFeatures() bool { 185 s.l.RLock() 186 defer s.l.RUnlock() 187 return len(s.broken) == 0 188 }