diff --git a/README.md b/README.md index ad368218..26e59e37 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,9 @@ Flags: -D, --data-file= File path for call data JSON file. Examples: /home/user/file.json or ./file.json. -b, --binary The call data comes as serialized binary message or multiple count-prefixed messages read from stdin. -B, --binary-file= File path for the call data as serialized binary message or multiple count-prefixed messages. - -m, --metadata= Request metadata as stringified JSON. + -m, --metadata= Request metadata as stringified JSON. Either as an object or an array of objects. -M, --metadata-file= File path for call metadata JSON file. Examples: /home/user/metadata.json or ./metadata.json. + --plaintext-metadata True to not try to templatize the metadata object. This should be used when using large metadata obejects when template functionality is not used since it speeds things up. --stream-interval=0 Interval for stream requests between message sends. --reflect-metadata= Reflect metadata as stringified JSON used only for reflection request. -o, --output= Output path. If none provided stdout is used. diff --git a/cmd/ghz/main.go b/cmd/ghz/main.go index b5445a16..2978e997 100644 --- a/cmd/ghz/main.go +++ b/cmd/ghz/main.go @@ -123,13 +123,17 @@ var ( Short('B').PlaceHolder(" ").IsSetByUser(&isBinDataPathSet).String() isMDSet = false - md = kingpin.Flag("metadata", "Request metadata as stringified JSON."). + md = kingpin.Flag("metadata", "Request metadata as stringified JSON. Either as an object or an array of objects."). Short('m').PlaceHolder(" ").IsSetByUser(&isMDSet).String() isMDPathSet = false mdPath = kingpin.Flag("metadata-file", "File path for call metadata JSON file. Examples: /home/user/metadata.json or ./metadata.json."). Short('M').PlaceHolder(" ").IsSetByUser(&isMDPathSet).String() + isPlainTextMetadataSet = false + plaintextMetadata = kingpin.Flag("plaintext-metadata", "Don't try to templatize metadata string for each request and used cached value for all the requests."). + Default("false").IsSetByUser(&isPlainTextMetadataSet).Bool() + isSISet = false si = kingpin.Flag("stream-interval", "Interval for stream requests between message sends."). Default("0").IsSetByUser(&isSISet).Duration() @@ -315,11 +319,34 @@ func createConfigFromArgs(cfg *runner.Config) error { binaryData = b } - var metadata map[string]string + var metadataArray []map[string]string + var metadataMap map[string]string + *md = strings.TrimSpace(*md) if *md != "" { - if err := json.Unmarshal([]byte(*md), &metadata); err != nil { - return fmt.Errorf("Error unmarshaling metadata '%v': %v", *md, err.Error()) + // For backward compatibility reasons we support both approaches - specifying an array + // with multiple object items and specifying a single object + + // 1. First try de-serializing it into an object + if err := json.Unmarshal([]byte(*md), &metadataMap); err != nil { + if !strings.Contains(err.Error(), "cannot unmarshal array into Go value of type map") { + // Some other fatal error which we should immediately propagate instead of try to + // de-serializing input into an array of maps (e.g. unexpected end of JSON input, + //etc.) + return fmt.Errorf("Error unmarshaling metadata '%v': %v", *md, err.Error()) + } + + // 2. If that fails, try to de-serialize it into an array + // NOTE: We could also simply check if string begins with [ or {, but that approach is + // not 100% robust + + if err := json.Unmarshal([]byte(*md), &metadataArray); err != nil { + return fmt.Errorf("Error unmarshaling metadata '%v': %v", *md, err.Error()) + } + } + + if metadataMap != nil { + metadataArray = append(metadataArray, metadataMap) } } @@ -369,8 +396,9 @@ func createConfigFromArgs(cfg *runner.Config) error { cfg.DataPath = *dataPath cfg.BinData = binaryData cfg.BinDataPath = *binPath - cfg.Metadata = metadata + cfg.Metadata = metadataArray cfg.MetadataPath = *mdPath + cfg.PlaintextMetadata = *plaintextMetadata cfg.SI = runner.Duration(*si) cfg.Output = *output cfg.Format = *format @@ -489,6 +517,10 @@ func mergeConfig(dest *runner.Config, src *runner.Config) error { dest.MetadataPath = src.MetadataPath } + if isPlainTextMetadataSet { + dest.PlaintextMetadata = src.PlaintextMetadata + } + if isSISet { dest.SI = src.SI } diff --git a/internal/helloworld/greeter_server.go b/internal/helloworld/greeter_server.go index 7b09323a..ac153abb 100644 --- a/internal/helloworld/greeter_server.go +++ b/internal/helloworld/greeter_server.go @@ -8,6 +8,7 @@ import ( "time" context "golang.org/x/net/context" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/stats" ) @@ -36,6 +37,7 @@ type Greeter struct { mutex *sync.RWMutex callCounts map[CallType]int calls map[CallType][][]*HelloRequest + metadata map[CallType][][]metadata.MD } func randomSleep() { @@ -49,22 +51,32 @@ func (s *Greeter) recordCall(ct CallType) int { s.callCounts[ct]++ var messages []*HelloRequest + var metadataItems []metadata.MD s.calls[ct] = append(s.calls[ct], messages) + s.metadata[ct] = append(s.metadata[ct], metadataItems) return len(s.calls[ct]) - 1 } -func (s *Greeter) recordMessage(ct CallType, callIdx int, msg *HelloRequest) { +func (s *Greeter) recordMessageAndMetadata(ct CallType, callIdx int, msg *HelloRequest, ctx context.Context) { s.mutex.Lock() defer s.mutex.Unlock() s.calls[ct][callIdx] = append(s.calls[ct][callIdx], msg) + + var md metadata.MD + + if ctx != nil { + md, _ = metadata.FromIncomingContext(ctx) + } + + s.metadata[ct][callIdx] = append(s.metadata[ct][callIdx], md) } // SayHello implements helloworld.GreeterServer func (s *Greeter) SayHello(ctx context.Context, in *HelloRequest) (*HelloReply, error) { callIdx := s.recordCall(Unary) - s.recordMessage(Unary, callIdx, in) + s.recordMessageAndMetadata(Unary, callIdx, in, ctx) randomSleep() @@ -74,7 +86,7 @@ func (s *Greeter) SayHello(ctx context.Context, in *HelloRequest) (*HelloReply, // SayHellos lists all hellos func (s *Greeter) SayHellos(req *HelloRequest, stream Greeter_SayHellosServer) error { callIdx := s.recordCall(ServerStream) - s.recordMessage(ServerStream, callIdx, req) + s.recordMessageAndMetadata(ServerStream, callIdx, req, nil) randomSleep() @@ -104,7 +116,7 @@ func (s *Greeter) SayHelloCS(stream Greeter_SayHelloCSServer) error { if err != nil { return err } - s.recordMessage(ClientStream, callIdx, in) + s.recordMessageAndMetadata(ClientStream, callIdx, in, nil) msgCount++ } } @@ -124,7 +136,7 @@ func (s *Greeter) SayHelloBidi(stream Greeter_SayHelloBidiServer) error { return err } - s.recordMessage(Bidi, callIdx, in) + s.recordMessageAndMetadata(Bidi, callIdx, in, nil) msg := "Hello " + in.Name if err := stream.Send(&HelloReply{Message: msg}); err != nil { return err @@ -148,6 +160,12 @@ func (s *Greeter) ResetCounters() { s.calls[ClientStream] = make([][]*HelloRequest, 0) s.calls[Bidi] = make([][]*HelloRequest, 0) + s.metadata = make(map[CallType][][]metadata.MD) + s.metadata[Unary] = make([][]metadata.MD, 0) + s.metadata[ServerStream] = make([][]metadata.MD, 0) + s.metadata[ClientStream] = make([][]metadata.MD, 0) + s.metadata[Bidi] = make([][]metadata.MD, 0) + s.mutex.Unlock() if s.Stats != nil { @@ -180,6 +198,18 @@ func (s *Greeter) GetCalls(key CallType) [][]*HelloRequest { return nil } +// GetMetadata gets the received metadata for the specific call type +func (s *Greeter) GetMetadata(key CallType) [][]metadata.MD { + s.mutex.Lock() + val, ok := s.metadata[key] + s.mutex.Unlock() + + if ok { + return val + } + return nil +} + // GetConnectionCount gets the connection count func (s *Greeter) GetConnectionCount() int { return s.Stats.GetConnectionCount() diff --git a/runner/call_template_data.go b/runner/call_template_data.go index be95f6f7..93324de2 100644 --- a/runner/call_template_data.go +++ b/runner/call_template_data.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "math/rand" + "strings" "text/template" "time" @@ -102,7 +103,7 @@ func (td *callTemplateData) executeData(data string) ([]byte, error) { } func (td *callTemplateData) executeMetadata(metadata string) (map[string]string, error) { - var mdMap map[string]string + var md map[string]string if len(metadata) > 0 { input := []byte(metadata) @@ -111,13 +112,40 @@ func (td *callTemplateData) executeMetadata(metadata string) (map[string]string, input = tpl.Bytes() } - err = json.Unmarshal(input, &mdMap) + err = json.Unmarshal(input, &md) if err != nil { return nil, err } } - return mdMap, nil + return md, nil +} + +// Same as executeMetadata, but this method ensures that the input metadata JSON string is always +// an array. If the input is an object, but not an array, it's converted to an array. +func (td *callTemplateData) executeMetadataArray(metadata string) ([]map[string]string, error) { + var mdArray []map[string]string + var metadataSanitized = strings.TrimSpace(metadata) + + // If the input is an object and not an array, we ensure we always work with an array + if !strings.HasPrefix(metadataSanitized, "[") && !strings.HasSuffix(metadataSanitized, "]") { + metadata = "[" + metadataSanitized + "]" + } + + if len(metadata) > 0 { + input := []byte(metadata) + tpl, err := td.execute(metadata) + if err == nil { + input = tpl.Bytes() + } + + err = json.Unmarshal(input, &mdArray) + if err != nil { + return nil, err + } + } + + return mdArray, nil } func newUUID() string { diff --git a/runner/config.go b/runner/config.go index 8de2f981..27d2edfb 100644 --- a/runner/config.go +++ b/runner/config.go @@ -3,6 +3,7 @@ package runner import ( "errors" "path" + "reflect" "strings" "time" @@ -79,8 +80,9 @@ type Config struct { DataPath string `json:"data-file" toml:"data-file" yaml:"data-file"` BinData []byte `json:"-" toml:"-" yaml:"-"` BinDataPath string `json:"binary-file" toml:"binary-file" yaml:"binary-file"` - Metadata map[string]string `json:"metadata,omitempty" toml:"metadata,omitempty" yaml:"metadata,omitempty"` + Metadata interface{} `json:"metadata,omitempty" toml:"metadata,omitempty" yaml:"metadata,omitempty"` MetadataPath string `json:"metadata-file" toml:"metadata-file" yaml:"metadata-file"` + PlaintextMetadata bool `json:"plaintextMetadata" toml:"plaintextMetadata" yaml:"PlaintextMetadata"` SI Duration `json:"stream-interval" toml:"stream-interval" yaml:"stream-interval"` Output string `json:"output" toml:"output" yaml:"output"` Format string `json:"format" toml:"format" yaml:"format" default:"summary"` @@ -96,6 +98,7 @@ type Config struct { EnableCompression bool `json:"enable-compression,omitempty" toml:"enable-compression,omitempty" yaml:"enable-compression,omitempty"` } +// Ensure that the data field value is either a map or an array of map items func checkData(data interface{}) error { _, isObjData := data.(map[string]interface{}) if !isObjData { @@ -118,6 +121,28 @@ func checkData(data interface{}) error { return nil } +// Ensure that the metadata field value is either a map or an array of map items +func checkMetadata(metadata interface{}) error { + _, isObjData := metadata.(map[string]interface{}) + if !isObjData { + arrData, isArrData := metadata.([]map[string]interface{}) + if !isArrData { + return errors.New("Unsupported type for Metadata") + } + if len(arrData) == 0 { + return errors.New("Metadata array must not be empty") + } + for _, elem := range arrData { + elemType := reflect.ValueOf(elem).Kind() + if elemType != reflect.Map { + return errors.New("Metadata array contains unsupported type") + } + } + } + + return nil +} + // LoadConfig loads the config from a file func LoadConfig(p string, c *Config) error { err := configor.Load(c, p) @@ -125,6 +150,8 @@ func LoadConfig(p string, c *Config) error { return err } + // Process data field - we support two notations for this field - either an object or + // an array of objects so we do the conversion here if c.Data != nil { ext := path.Ext(p) if strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") { @@ -151,6 +178,62 @@ func LoadConfig(p string, c *Config) error { } } + // Process metadata field - we support two notations for this field - either an object or + // an array of objects so we do the conversion here + if c.Metadata != nil { + ext := path.Ext(p) + if strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") { + // Ensure that keys are of a string type and cast them + objData, isObjData2 := c.Metadata.(map[interface{}]interface{}) + if isObjData2 { + nd := make(map[string]interface{}) + for k, v := range objData { + sk, isString := k.(string) + if !isString { + return errors.New("Data key must string") + } + if len(sk) > 0 { + nd[sk] = v + } + } + + c.Metadata = nd + } else { + // TODO: Refactor this into utility function + arrData, isArray := c.Metadata.([]interface{}) + + if isArray { + var array []map[string]interface{} + for _, item := range arrData { + objData3, isObjData3 := item.(map[interface{}]interface{}) + newItem := make(map[string]interface{}) + + if isObjData3 { + for k, v := range objData3 { + sk, isString := k.(string) + if !isString { + return errors.New("Data key must string") + } + if len(sk) > 0 { + newItem[sk] = v + } + } + + array = append(array, newItem) + } + } + + c.Metadata = array + } + } + } + + err := checkMetadata(c.Metadata) + if err != nil { + return err + } + } + c.ZStop = strings.ToLower(c.ZStop) if c.ZStop != "close" && c.ZStop != "ignore" && c.ZStop != "wait" { c.ZStop = "close" diff --git a/runner/config_test.go b/runner/config_test.go index 6b24351f..2b8532d5 100644 --- a/runner/config_test.go +++ b/runner/config_test.go @@ -58,6 +58,34 @@ func TestConfig_Load(t *testing.T) { Data: map[string]interface{}{ "f_strings": []interface{}{"123", "456"}, }, + Metadata: map[string]interface{}{ + "key_one": "value 1", + }, + Format: "summary", + DialTimeout: Duration(10 * time.Second), + }, + true, + }, + { + "valid metadata is array", + &Config{ + Insecure: true, + ImportPaths: []string{"/home/user/pb/grpcbin"}, + Proto: "grpcbin.proto", + Call: "grpcbin.GRPCBin.DummyUnary", + Host: "127.0.0.1:9000", + Z: Duration(20 * time.Second), + X: Duration(60 * time.Second), + SI: Duration(25 * time.Second), + Timeout: Duration(30 * time.Second), + N: 200, + C: 50, + Connections: 1, + ZStop: "close", + Data: map[string]interface{}{ + "f_strings": []interface{}{"123", "456"}, + }, + Metadata: []map[string]interface{}{{"key_one": "value 1"}, {"key_two": "value 2"}}, Format: "summary", DialTimeout: Duration(10 * time.Second), }, diff --git a/runner/options.go b/runner/options.go index 45706b40..d0ba2a1c 100644 --- a/runner/options.go +++ b/runner/options.go @@ -58,10 +58,11 @@ type RunConfig struct { streamInterval time.Duration // data - data []byte - binary bool - metadata []byte - rmd map[string]string + data []byte + binary bool + metadata []byte + PlaintextMetadata bool + rmd map[string]string // debug hasLog bool @@ -386,14 +387,30 @@ func WithMetadataFromJSON(md string) Option { } } -// WithMetadata specifies the metadata to be used as a map +// WithMetadata specifies metadata as generic data which can either be an object +// or an array of objects +// +// For example: +// // md := make(map[string]string) // md["token"] = "foobar" // md["request-id"] = "123" // WithMetadata(&md) -func WithMetadata(md map[string]string) Option { +// +// Or: +// +// var mdArray []map[string]string +// +// md := make(map[string]string) +// md["token"] = "foobar" +// md["request-id"] = "123" +// +// mdArray = append(mdArray, md) +// +func WithMetadata(md interface{}) Option { return func(o *RunConfig) error { mdJSON, err := json.Marshal(md) + if err != nil { return err } @@ -419,6 +436,15 @@ func WithMetadataFromFile(path string) Option { } } +// WithPlaintextMetadata to not utilize metadata templating functionality +func WithPlaintextMetadata(value bool) Option { + return func(o *RunConfig) error { + o.PlaintextMetadata = value + + return nil + } +} + // WithName sets the name of the test run // WithName("greeter service test") func WithName(name string) Option { @@ -709,6 +735,7 @@ func fromConfig(cfg *Config) []Option { WithName(cfg.Name), WithCPUs(cfg.CPUs), WithMetadata(cfg.Metadata), + WithPlaintextMetadata(cfg.PlaintextMetadata), WithTags(cfg.Tags), WithStreamInterval(time.Duration(cfg.SI)), WithReflectionMetadata(cfg.ReflectMetadata), diff --git a/runner/options_test.go b/runner/options_test.go index 844e581b..1b4f6c0e 100644 --- a/runner/options_test.go +++ b/runner/options_test.go @@ -109,6 +109,50 @@ func TestRunConfig_newRunConfig(t *testing.T) { assert.Equal(t, c.enableCompression, false) }) + t.Run("with JSON array as a metadata value", func(t *testing.T) { + c, err := NewConfig( + "call", "localhost:50050", + WithInsecure(true), + WithTotalRequests(100), + WithConcurrency(20), + WithQPS(5), + WithSkipFirst(5), + WithRunDuration(time.Duration(5*time.Minute)), + WithKeepalive(time.Duration(60*time.Second)), + WithTimeout(time.Duration(10*time.Second)), + WithDialTimeout(time.Duration(30*time.Second)), + WithName("asdf"), + WithCPUs(4), + WithDataFromJSON(`{"name":"bob"}`), + WithMetadataFromJSON(`[{"request-id":"123"}, {"request-id":"456"}]`), + WithProtoFile("testdata/data.proto", []string{"/home/protos"}), + ) + + assert.NoError(t, err) + + assert.Equal(t, "call", c.call) + assert.Equal(t, "localhost:50050", c.host) + assert.Equal(t, true, c.insecure) + assert.Equal(t, math.MaxInt32, c.n) + assert.Equal(t, 20, c.c) + assert.Equal(t, 5, c.qps) + assert.Equal(t, 5, c.skipFirst) + assert.Equal(t, false, c.binary) + assert.Equal(t, time.Duration(5*time.Minute), c.z) + assert.Equal(t, time.Duration(60*time.Second), c.keepaliveTime) + assert.Equal(t, time.Duration(10*time.Second), c.timeout) + assert.Equal(t, time.Duration(30*time.Second), c.dialTimeout) + assert.Equal(t, 4, c.cpus) + assert.False(t, c.binary) + assert.Equal(t, "asdf", c.name) + assert.Equal(t, `{"name":"bob"}`, string(c.data)) + assert.Equal(t, `[{"request-id":"123"}, {"request-id":"456"}]`, string(c.metadata)) + assert.Equal(t, "testdata/data.proto", string(c.proto)) + assert.Equal(t, "", string(c.protoset)) + assert.Equal(t, []string{"testdata", ".", "/home/protos"}, c.importPaths) + assert.Equal(t, c.enableCompression, false) + }) + t.Run("with binary data, protoset and metadata file", func(t *testing.T) { c, err := NewConfig( "call", "localhost:50050", @@ -169,10 +213,14 @@ func TestRunConfig_newRunConfig(t *testing.T) { Age: 11, Fruits: []string{"apple", "peach", "pear"}} + var mdArray []map[string]string + md := make(map[string]string) md["token"] = "foobar" md["request-id"] = "123" + mdArray = append(mdArray, md) + tags := make(map[string]string) tags["env"] = "staging" tags["created by"] = "joe developer" @@ -195,7 +243,7 @@ func TestRunConfig_newRunConfig(t *testing.T) { WithName("asdf"), WithCPUs(4), WithData(d), - WithMetadata(md), + WithMetadata(mdArray), WithTags(tags), WithReflectionMetadata(rmd), ) @@ -219,7 +267,7 @@ func TestRunConfig_newRunConfig(t *testing.T) { assert.Equal(t, 4, c.cpus) assert.Equal(t, "asdf", c.name) assert.Equal(t, `{"name":"bob","age":11,"fruits":["apple","peach","pear"]}`, string(c.data)) - assert.Equal(t, `{"request-id":"123","token":"foobar"}`, string(c.metadata)) + assert.Equal(t, `[{"request-id":"123","token":"foobar"}]`, string(c.metadata)) assert.Equal(t, `{"created by":"joe developer","env":"staging"}`, string(c.tags)) assert.Equal(t, "testdata/data.proto", string(c.proto)) assert.Equal(t, "", string(c.protoset)) diff --git a/runner/run_test.go b/runner/run_test.go index dfca98c9..188004be 100644 --- a/runner/run_test.go +++ b/runner/run_test.go @@ -402,7 +402,7 @@ func TestRunUnary(t *testing.T) { assert.Equal(t, 5, connCount) }) - t.Run("test round-robin c = 2", func(t *testing.T) { + t.Run("test data and metadata round-robin c = 2", func(t *testing.T) { gs.ResetCounters() data := make([]map[string]interface{}, 3) @@ -421,6 +421,7 @@ func TestRunUnary(t *testing.T) { WithDialTimeout(time.Duration(20*time.Second)), WithInsecure(true), WithData(data), + WithMetadataFromJSON(`[{"index": "1 one"}, {"index": "2 two"}, {"index": "3 three"}]`), ) assert.NoError(t, err) @@ -429,6 +430,24 @@ func TestRunUnary(t *testing.T) { count := gs.GetCount(callType) assert.Equal(t, 6, count) + // Verify metadata + + // We specify 3 unique metadata items over which the requester should round-robin + // for all of the 6 requests. This means we should see each unique item twice. + metadata := gs.GetMetadata(callType) + assert.Equal(t, len(metadata), 6) + + seenMetadataIndexValues := make([]string, 0) + + for _, metadataItem := range metadata { + seenMetadataIndexValues = append(seenMetadataIndexValues, metadataItem[0]["index"][0]) + } + + // we don't expect to have the same order of elements since requests are concurrent + assert.ElementsMatch(t, []string{"1 one", "2 two", "3 three", "1 one", "2 two", "3 three"}, + seenMetadataIndexValues) + + // Verify actual payload / messages calls := gs.GetCalls(callType) assert.NotNil(t, calls) assert.Len(t, calls, 6) @@ -462,6 +481,7 @@ func TestRunUnary(t *testing.T) { WithDialTimeout(time.Duration(20*time.Second)), WithInsecure(true), WithData(data), + WithMetadataFromJSON(`{"index": "1 one"}`), ) assert.NoError(t, err) @@ -470,6 +490,21 @@ func TestRunUnary(t *testing.T) { count := gs.GetCount(callType) assert.Equal(t, 6, count) + // Verify metadata + // We specify a single item for metadata which should be used for all the requests + metadata := gs.GetMetadata(callType) + assert.Equal(t, len(metadata), 6) + + seenMetadataIndexValues := make([]string, 0) + + for _, metadataItem := range metadata { + seenMetadataIndexValues = append(seenMetadataIndexValues, metadataItem[0]["index"][0]) + } + + assert.ElementsMatch(t, []string{"1 one", "1 one", "1 one", "1 one", "1 one", "1 one"}, + seenMetadataIndexValues) + + // Verify actual payload / messages calls := gs.GetCalls(callType) assert.NotNil(t, calls) assert.Len(t, calls, 6) diff --git a/runner/worker.go b/runner/worker.go index f5a8fb6c..9f17f8e0 100644 --- a/runner/worker.go +++ b/runner/worker.go @@ -32,6 +32,10 @@ type Worker struct { // cached messages only for binary cachedMessages []*dynamic.Message + // cached metadata array for situations where metadata templating + // functionality is not used + cachedReqMDs []metadata.MD + // non-binary json optimization arrayJSONData []string } @@ -82,19 +86,34 @@ func (w *Worker) makeRequest() error { } } - mdMap, err := ctd.executeMetadata(string(w.config.metadata)) - if err != nil { - return err - } + var reqMDs []metadata.MD - var reqMD *metadata.MD - if len(mdMap) > 0 { - md := metadata.New(mdMap) - reqMD = &md + if w.cachedReqMDs != nil { + // If templating is not used and cached metadata is available, we use this + // array. This way we avoid rendering and json loading potentially very + // large metadata object for every single request + reqMDs = w.cachedReqMDs } else { - reqMD = &metadata.MD{} + mdArray, err := ctd.executeMetadataArray(string(w.config.metadata)) + if err != nil { + return err + } + + if len(mdArray) > 0 { + for _, mdItem := range mdArray { + reqMDs = append(reqMDs, metadata.New(mdItem)) + } + } else { + reqMDs = append(reqMDs, metadata.MD{}) + } + + w.cachedReqMDs = reqMDs } + metadatasLen := len(reqMDs) + metadataIdx := int((reqNum - 1) % int64(metadatasLen)) + reqMD := &reqMDs[metadataIdx] + if w.config.enableCompression { reqMD.Append("grpc-accept-encoding", gzip.Name) } @@ -158,7 +177,7 @@ func (w *Worker) makeRequest() error { if w.config.hasLog { w.config.log.Debugw("Received response", "workerID", w.workerID, "call type", callType, "call", w.mtd.GetFullyQualifiedName(), - "input", inputs, "metadata", reqMD, + "input", inputs[inputIdx], "metadata", reqMD, "response", res, "error", resErr) } } diff --git a/testdata/config/config5.json b/testdata/config/config5.json index 960c6c0f..e0a02628 100644 --- a/testdata/config/config5.json +++ b/testdata/config/config5.json @@ -10,10 +10,13 @@ "max-duration":"60s", "stream-interval":"25s", "timeout":"30s", + "metadata": { + "key_one": "value 1" + }, "data": { "f_strings": [ "123", "456" ] } -} \ No newline at end of file +} diff --git a/testdata/config/config5.toml b/testdata/config/config5.toml index 8d5c4743..64c54219 100644 --- a/testdata/config/config5.toml +++ b/testdata/config/config5.toml @@ -10,8 +10,11 @@ max-duration = "60s" stream-interval = "25s" timeout = "30s" +[metadata] +key_one = "value 1" + [data] f_strings = [ "123", "456" -] \ No newline at end of file +] diff --git a/testdata/config/config5.yaml b/testdata/config/config5.yaml index 96a2c5ce..db564069 100644 --- a/testdata/config/config5.yaml +++ b/testdata/config/config5.yaml @@ -9,6 +9,8 @@ duration: 20s max-duration: 60s stream-interval: 25s timeout: 30s +metadata: + key_one: value 1 data: f_strings: - '123' diff --git a/testdata/config/config6.json b/testdata/config/config6.json new file mode 100644 index 00000000..ce331ba8 --- /dev/null +++ b/testdata/config/config6.json @@ -0,0 +1,27 @@ +{ + "insecure": true, + "import-paths": [ + "/home/user/pb/grpcbin" + ], + "proto": "grpcbin.proto", + "call": "grpcbin.GRPCBin.DummyUnary", + "host": "127.0.0.1:9000", + "duration":"20s", + "max-duration":"60s", + "stream-interval":"25s", + "timeout":"30s", + "metadata": [ + { + "key_one": "value 1" + }, + { + "key_two": "value 2" + } + ], + "data": {, + "f_strings": [ + "123", + "456" + ] + } +} diff --git a/testdata/config/config6.toml b/testdata/config/config6.toml new file mode 100644 index 00000000..b59bdd14 --- /dev/null +++ b/testdata/config/config6.toml @@ -0,0 +1,23 @@ +insecure = true +import-paths = [ + "/home/user/pb/grpcbin" +] +proto = "grpcbin.proto" +call = "grpcbin.GRPCBin.DummyUnary" +host = "127.0.0.1:9000" +duration = "20s" +max-duration = "60s" +stream-interval = "25s" +timeout = "30s" + +[[metadata]] +key_one = "value 1" + +[[metadata]] +key_two = "value 2" + +[data] +f_strings = [ + "123", + "456" +] diff --git a/testdata/config/config6.yaml b/testdata/config/config6.yaml new file mode 100644 index 00000000..8002b7ba --- /dev/null +++ b/testdata/config/config6.yaml @@ -0,0 +1,20 @@ +--- +insecure: true +import-paths: +- "/home/user/pb/grpcbin" +proto: grpcbin.proto +call: grpcbin.GRPCBin.DummyUnary +host: 127.0.0.1:9000 +duration: 20s +max-duration: 60s +stream-interval: 25s +timeout: 30s +metadata: + - + key_one: value 1 + - + key_two: value 2 +data: + f_strings: + - '123' + - '456' diff --git a/testdata/metadata_array.json b/testdata/metadata_array.json new file mode 100644 index 00000000..564fea33 --- /dev/null +++ b/testdata/metadata_array.json @@ -0,0 +1 @@ +[{"request-id": "1"}, {"request-number": "2"}] diff --git a/www/docs/examples.md b/www/docs/examples.md index 13237725..8eaf0241 100644 --- a/www/docs/examples.md +++ b/www/docs/examples.md @@ -87,6 +87,24 @@ ghz --insecure \ 0.0.0.0:50051 ``` +Round-robin of messages and metadata for unary call: + +```sh +ghz --insecure \ + --proto ./greeter.proto \ + --call helloworld.Greeter.SayHello \ + -d '[{"name":"Joe"},{"name":"Bob"}]' \ + -m '[{"item one":"value 1"},{"item two":"value 2"}]' \ + --plaintext-metadata \ + 0.0.0.0:50051 +``` + +If you are using large metadata array and don't rely on template functionality in metadata +JSON string, you should also use ``--plaintext-metadata`` as shown above. + +This will cause the code to skip rendering the metadata item as a template for every single +RPC request and will speed things up. + ### Custom parameters