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  }