github.com/yandex/pandora@v0.5.32/components/guns/grpc/scenario/core.go (about)

     1  package scenario
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"math/rand"
     8  	"time"
     9  
    10  	"github.com/jhump/protoreflect/dynamic"
    11  	grpcgun "github.com/yandex/pandora/components/guns/grpc"
    12  	"github.com/yandex/pandora/core"
    13  	"github.com/yandex/pandora/core/aggregator/netsample"
    14  	"github.com/yandex/pandora/core/warmup"
    15  	"github.com/yandex/pandora/lib/answlog"
    16  	"go.uber.org/zap"
    17  	"golang.org/x/exp/maps"
    18  	"google.golang.org/grpc/metadata"
    19  )
    20  
    21  const defaultTimeout = time.Second * 15
    22  
    23  type GunConfig struct {
    24  	Target          string            `validate:"required"`
    25  	ReflectPort     int64             `config:"reflect_port"`
    26  	ReflectMetadata map[string]string `config:"reflect_metadata"`
    27  	Timeout         time.Duration     `config:"timeout"` // grpc request timeout
    28  	TLS             bool              `config:"tls"`
    29  	DialOptions     GrpcDialOptions   `config:"dial_options"`
    30  	AnswLog         AnswLogConfig     `config:"answlog"`
    31  }
    32  
    33  type GrpcDialOptions struct {
    34  	Authority string        `config:"authority"`
    35  	Timeout   time.Duration `config:"timeout"`
    36  }
    37  
    38  type AnswLogConfig struct {
    39  	Enabled bool   `config:"enabled"`
    40  	Path    string `config:"path"`
    41  	Filter  string `config:"filter" valid:"oneof=all warning error"`
    42  }
    43  
    44  func DefaultGunConfig() GunConfig {
    45  	return GunConfig{
    46  		Target: "default target",
    47  		AnswLog: AnswLogConfig{
    48  			Enabled: false,
    49  			Path:    "answ.log",
    50  			Filter:  "all",
    51  		},
    52  	}
    53  }
    54  
    55  func NewGun(conf GunConfig) *Gun {
    56  	answLog := answlog.Init(conf.AnswLog.Path, conf.AnswLog.Enabled)
    57  	r := rand.New(rand.NewSource(0)) //TODO: use real random
    58  	return &Gun{
    59  		templ: NewTextTemplater(),
    60  		gun: &grpcgun.Gun{Conf: grpcgun.GunConfig{
    61  			Target:          conf.Target,
    62  			ReflectPort:     conf.ReflectPort,
    63  			ReflectMetadata: conf.ReflectMetadata,
    64  			Timeout:         conf.Timeout,
    65  			TLS:             conf.TLS,
    66  			DialOptions: grpcgun.GrpcDialOptions{
    67  				Authority: conf.DialOptions.Authority,
    68  				Timeout:   conf.DialOptions.Timeout,
    69  			},
    70  			AnswLog: grpcgun.AnswLogConfig{
    71  				Enabled: conf.AnswLog.Enabled,
    72  				Path:    conf.AnswLog.Path,
    73  				Filter:  conf.AnswLog.Filter,
    74  			},
    75  		},
    76  			AnswLog: answLog},
    77  		rand: r,
    78  	}
    79  }
    80  
    81  type Gun struct {
    82  	gun   *grpcgun.Gun
    83  	rand  *rand.Rand
    84  	templ Templater
    85  }
    86  
    87  func (g *Gun) WarmUp(opts *warmup.Options) (interface{}, error) {
    88  	return g.gun.WarmUp(opts)
    89  }
    90  
    91  func (g *Gun) Bind(aggr core.Aggregator, deps core.GunDeps) error {
    92  	return g.gun.Bind(aggr, deps)
    93  }
    94  
    95  func (g *Gun) Shoot(am core.Ammo) {
    96  	scen := am.(*Scenario)
    97  
    98  	templateVars := map[string]any{}
    99  	if scen.VariableStorage != nil {
   100  		templateVars["source"] = scen.VariableStorage.Variables()
   101  	} else {
   102  		templateVars["source"] = map[string]any{}
   103  	}
   104  
   105  	err := g.shoot(scen, templateVars)
   106  	if err != nil {
   107  		g.gun.Log.Warn("Invalid ammo", zap.Uint64("request", scen.id), zap.Error(err))
   108  		return
   109  	} else {
   110  		g.gun.Log.Debug("Valid ammo", zap.Uint64("request", scen.id))
   111  	}
   112  }
   113  
   114  func (g *Gun) shoot(ammo *Scenario, templateVars map[string]any) error {
   115  	if templateVars == nil {
   116  		templateVars = map[string]any{}
   117  	}
   118  
   119  	requestVars := map[string]any{}
   120  	templateVars["request"] = requestVars
   121  	if g.gun.DebugLog {
   122  		g.gun.GunDeps.Log.Debug("Source variables", zap.Any("variables", templateVars))
   123  	}
   124  
   125  	startAt := time.Now()
   126  	for _, call := range ammo.Calls {
   127  		tag := ammo.Name + "." + call.Tag
   128  		sample := netsample.Acquire(tag)
   129  
   130  		err := g.shootStep(&call, sample, ammo.Name, templateVars, requestVars)
   131  		if err != nil {
   132  			return err
   133  		}
   134  	}
   135  	spent := time.Since(startAt)
   136  	if ammo.MinWaitingTime > spent {
   137  		time.Sleep(ammo.MinWaitingTime - spent)
   138  	}
   139  	return nil
   140  }
   141  
   142  func (g *Gun) shootStep(step *Call, sample *netsample.Sample, ammoName string, templateVars map[string]any, requestVars map[string]any) error {
   143  	const op = "base_gun.shootStep"
   144  	code := 0
   145  	defer func() {
   146  		sample.SetProtoCode(code)
   147  		g.gun.Aggr.Report(sample)
   148  	}()
   149  
   150  	stepVars := map[string]any{}
   151  	requestVars[step.Name] = stepVars
   152  
   153  	// Preprocessor
   154  	preprocVars := map[string]any{}
   155  	for _, preProcessor := range step.Preprocessors {
   156  		pp, err := preProcessor.Process(step, templateVars)
   157  		if err != nil {
   158  			return fmt.Errorf("%s preProcessor %w", op, err)
   159  		}
   160  		preprocVars = mergeMaps(preprocVars, pp)
   161  		if g.gun.DebugLog {
   162  			g.gun.GunDeps.Log.Debug("PreparePreprocessor variables", zap.Any(fmt.Sprintf(".request.%s.preprocessor", step.Name), pp))
   163  		}
   164  	}
   165  	stepVars["preprocessor"] = preprocVars
   166  
   167  	// Template
   168  	payloadJSON, err := g.templ.Apply(step.Payload, step.Metadata, templateVars, ammoName, step.Name)
   169  	if err != nil {
   170  		return fmt.Errorf("%s templater.Apply %w", op, err)
   171  	}
   172  
   173  	// Method
   174  	method, ok := g.gun.Services[step.Call]
   175  	if !ok {
   176  		g.gun.GunDeps.Log.Error("invalid step.Call", zap.String("method", step.Call),
   177  			zap.Strings("allowed_methods", maps.Keys(g.gun.Services)))
   178  		return fmt.Errorf("%s invalid step.Call", op)
   179  	}
   180  
   181  	md := method.GetInputType()
   182  	message := dynamic.NewMessage(md)
   183  	err = message.UnmarshalJSON(payloadJSON)
   184  	if err != nil {
   185  		code = 400
   186  		g.gun.GunDeps.Log.Error("invalid payload. Cant unmarshal gRPC", zap.Error(err))
   187  		return fmt.Errorf("%s invalid payload. Cant unmarshal gRPC", op)
   188  	}
   189  
   190  	timeout := defaultTimeout
   191  	if g.gun.Conf.Timeout != 0 {
   192  		timeout = g.gun.Conf.Timeout
   193  	}
   194  
   195  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
   196  	defer cancel()
   197  	ctx = metadata.NewOutgoingContext(ctx, metadata.New(step.Metadata))
   198  	out, grpcErr := g.gun.Stub.InvokeRpc(ctx, &method, message)
   199  	code = grpcgun.ConvertGrpcStatus(grpcErr)
   200  	sample.SetProtoCode(code) // for setRTT inside
   201  
   202  	if grpcErr != nil {
   203  		g.gun.GunDeps.Log.Error("response error", zap.Error(err))
   204  	}
   205  
   206  	g.gun.Answ(&method, message, step.Metadata, out, grpcErr, code)
   207  
   208  	for _, postProcessor := range step.Postprocessors {
   209  		pp, err := postProcessor.Process(out, code)
   210  		if err != nil {
   211  			return fmt.Errorf("%s postProcessor %w", op, err)
   212  		}
   213  		stepVars = mergeMaps(stepVars, pp)
   214  		if g.gun.DebugLog {
   215  			g.gun.GunDeps.Log.Debug("Postprocessor variables", zap.Any(fmt.Sprintf(".request.%s.postprocessor", step.Name), pp))
   216  		}
   217  	}
   218  	if out != nil {
   219  		// Postprocessor
   220  		// if it is nessesary
   221  		md = method.GetOutputType()
   222  		message = dynamic.NewMessage(md)
   223  		err = message.ConvertFrom(out)
   224  		if err != nil {
   225  			// unexpected result
   226  			return fmt.Errorf("%s message.ConvertFrom `%s`; err: %w", op, out.String(), err)
   227  
   228  		}
   229  		b, err := message.MarshalJSON()
   230  		if err != nil {
   231  			// unexpected result
   232  			return fmt.Errorf("%s message.MarshalJSON %w", op, err)
   233  		}
   234  		var outMap map[string]any
   235  		err = json.Unmarshal(b, &outMap)
   236  		if err != nil {
   237  			// unexpected result
   238  			return fmt.Errorf("%s json.Unmarshal %w", op, err)
   239  		}
   240  		stepVars["postprocessor"] = outMap
   241  
   242  		if g.gun.DebugLog {
   243  			g.gun.GunDeps.Log.Debug("Postprocessor variables", zap.String(fmt.Sprintf(".resuest.%s.postprocessor", step.Name), out.String()))
   244  		}
   245  	}
   246  
   247  	if step.Sleep > 0 {
   248  		time.Sleep(step.Sleep)
   249  	}
   250  
   251  	return nil
   252  }
   253  
   254  // mergeMaps merges newvars into previous
   255  // if key exists in previous, it will be skipped
   256  func mergeMaps(previous map[string]any, newvars map[string]any) map[string]any {
   257  	for k, v := range newvars {
   258  		if _, ok := previous[k]; !ok {
   259  			previous[k] = v
   260  		}
   261  	}
   262  	return previous
   263  }
   264  
   265  func (g *Gun) reportErr(sample *netsample.Sample, err error) {
   266  	if err == nil {
   267  		return
   268  	}
   269  	sample.AddTag("__EMPTY__")
   270  	sample.SetProtoCode(0)
   271  	sample.SetErr(err)
   272  	g.gun.Aggr.Report(sample)
   273  }
   274  
   275  var _ warmup.WarmedUp = (*Gun)(nil)