From 10117a48e330111f291d224c3503d2c8d72b4aa7 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Sun, 25 Oct 2020 20:30:33 +0100 Subject: [PATCH 1/2] [feature] [option] adding a func as a option to randomize each request (#236) * addition of randomized func for every binary request add some tests and todo and change signature * rename type struct and add parameter * expose callData --- runner/call_template_data.go | 16 +++++----- runner/call_template_data_test.go | 10 +++--- runner/options.go | 20 ++++++++++-- runner/options_test.go | 5 +++ runner/run_test.go | 52 +++++++++++++++++++++++++++++++ runner/worker.go | 13 +++++--- 6 files changed, 97 insertions(+), 19 deletions(-) diff --git a/runner/call_template_data.go b/runner/call_template_data.go index be95f6f7..0656fe6b 100644 --- a/runner/call_template_data.go +++ b/runner/call_template_data.go @@ -18,7 +18,7 @@ var seededRand *rand.Rand = rand.New( rand.NewSource(time.Now().UnixNano())) // call template data -type callTemplateData struct { +type CallData struct { WorkerID string // unique worker ID RequestNumber int64 // unique incremented request number for each request FullyQualifiedName string // fully-qualified name of the method call @@ -42,11 +42,11 @@ var tmplFuncMap = template.FuncMap{ "randomString": randomString, } -// newCallTemplateData returns new call template data -func newCallTemplateData( +// newCallData returns new callData +func newCallData( mtd *desc.MethodDescriptor, funcs template.FuncMap, - workerID string, reqNum int64) *callTemplateData { + workerID string, reqNum int64) *CallData { now := time.Now() newUUID, _ := uuid.NewRandom() @@ -61,7 +61,7 @@ func newCallTemplateData( } } - return &callTemplateData{ + return &CallData{ WorkerID: workerID, RequestNumber: reqNum, FullyQualifiedName: mtd.GetFullyQualifiedName(), @@ -80,14 +80,14 @@ func newCallTemplateData( } } -func (td *callTemplateData) execute(data string) (*bytes.Buffer, error) { +func (td *CallData) execute(data string) (*bytes.Buffer, error) { t := template.Must(template.New("call_template_data").Funcs(td.templateFuncs).Parse(data)) var tpl bytes.Buffer err := t.Execute(&tpl, td) return &tpl, err } -func (td *callTemplateData) executeData(data string) ([]byte, error) { +func (td *CallData) executeData(data string) ([]byte, error) { if len(data) > 0 { input := []byte(data) tpl, err := td.execute(data) @@ -101,7 +101,7 @@ func (td *callTemplateData) executeData(data string) ([]byte, error) { return []byte{}, nil } -func (td *callTemplateData) executeMetadata(metadata string) (map[string]string, error) { +func (td *CallData) executeMetadata(metadata string) (map[string]string, error) { var mdMap map[string]string if len(metadata) > 0 { diff --git a/runner/call_template_data_test.go b/runner/call_template_data_test.go index 28074682..bdf2897d 100644 --- a/runner/call_template_data_test.go +++ b/runner/call_template_data_test.go @@ -15,7 +15,7 @@ func TestCallTemplateData_New(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, md) - ctd := newCallTemplateData(md, nil, "worker_id_123", 100) + ctd := newCallData(md, nil, "worker_id_123", 100) assert.NotNil(t, ctd) assert.Equal(t, "worker_id_123", ctd.WorkerID) @@ -41,7 +41,7 @@ func TestCallTemplateData_ExecuteData(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, md) - ctd := newCallTemplateData(md, nil, "worker_id_123", 200) + ctd := newCallData(md, nil, "worker_id_123", 200) assert.NotNil(t, ctd) @@ -92,7 +92,7 @@ func TestCallTemplateData_ExecuteMetadata(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, md) - ctd := newCallTemplateData(md, nil, "worker_id_123", 200) + ctd := newCallData(md, nil, "worker_id_123", 200) assert.NotNil(t, ctd) @@ -138,7 +138,7 @@ func TestCallTemplateData_ExecuteFuncs(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, md) - ctd := newCallTemplateData(md, nil, "worker_id_123", 200) + ctd := newCallData(md, nil, "worker_id_123", 200) assert.NotNil(t, ctd) @@ -249,7 +249,7 @@ func TestCallTemplateData_ExecuteFuncs(t *testing.T) { }) t.Run("custom functions", func(t *testing.T) { - ctd = newCallTemplateData(md, template.FuncMap{ + ctd = newCallData(md, template.FuncMap{ "getSKU": func() string { return "custom-sku" }, diff --git a/runner/options.go b/runner/options.go index 45706b40..9a73e77b 100644 --- a/runner/options.go +++ b/runner/options.go @@ -15,6 +15,7 @@ import ( "text/template" "time" + "github.com/jhump/protoreflect/desc" "github.com/pkg/errors" "google.golang.org/grpc/credentials" ) @@ -58,7 +59,11 @@ type RunConfig struct { streamInterval time.Duration // data - data []byte + data []byte + + // data func + dataFunc func(mtd *desc.MethodDescriptor, callData *CallData) []byte + binary bool metadata []byte rmd map[string]string @@ -300,6 +305,17 @@ func WithBinaryData(data []byte) Option { } } +// WithBinaryDataFunc specifies the binary data func which will be called on each request +// WithBinaryDataFunc(changeFunc) +func WithBinaryDataFunc(data func(mtd *desc.MethodDescriptor, callData *CallData) []byte) Option { + return func(o *RunConfig) error { + o.dataFunc = data + o.binary = true + + return nil + } +} + // WithBinaryDataFromFile specifies the binary data // WithBinaryDataFromFile("request_data.bin") func WithBinaryDataFromFile(path string) Option { @@ -557,7 +573,7 @@ func WithLogger(log Logger) Option { } } -// WithTemplateFuncs adds additional tempalte functions +// WithTemplateFuncs adds additional template functions func WithTemplateFuncs(funcMap template.FuncMap) Option { return func(o *RunConfig) error { o.funcs = funcMap diff --git a/runner/options_test.go b/runner/options_test.go index 844e581b..b4598212 100644 --- a/runner/options_test.go +++ b/runner/options_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "math" "os" + "reflect" "runtime" "testing" "time" @@ -124,6 +125,7 @@ func TestRunConfig_newRunConfig(t *testing.T) { WithDialTimeout(time.Duration(30*time.Second)), WithName("asdf"), WithCPUs(4), + WithBinaryDataFunc(changeFunc), WithBinaryData([]byte("asdf1234foobar")), WithMetadataFromFile("../testdata/metadata.json"), WithProtoset("testdata/bundle.protoset"), @@ -150,6 +152,9 @@ func TestRunConfig_newRunConfig(t *testing.T) { assert.Equal(t, 4, c.cpus) assert.Equal(t, "asdf", c.name) assert.Equal(t, []byte("asdf1234foobar"), c.data) + funcName1 := runtime.FuncForPC(reflect.ValueOf(changeFunc).Pointer()).Name() + funcName2 := runtime.FuncForPC(reflect.ValueOf(c.dataFunc).Pointer()).Name() + assert.Equal(t, funcName1, funcName2) assert.Equal(t, `{"request-id": "{{.RequestNumber}}"}`, string(c.metadata)) assert.Equal(t, "", string(c.proto)) assert.Equal(t, "testdata/bundle.protoset", string(c.protoset)) diff --git a/runner/run_test.go b/runner/run_test.go index dfca98c9..0c38bac6 100644 --- a/runner/run_test.go +++ b/runner/run_test.go @@ -10,9 +10,17 @@ import ( "github.com/bojand/ghz/internal" "github.com/bojand/ghz/internal/helloworld" "github.com/golang/protobuf/proto" + "github.com/jhump/protoreflect/desc" "github.com/stretchr/testify/assert" ) +func changeFunc(mtd *desc.MethodDescriptor, cd *CallData) []byte { + msg := &helloworld.HelloRequest{} + msg.Name = "bob" + binData, _ := proto.Marshal(msg) + return binData +} + func TestRunUnary(t *testing.T) { callType := helloworld.Unary @@ -352,6 +360,50 @@ func TestRunUnary(t *testing.T) { assert.Equal(t, 1, connCount) }) + t.Run("test binary with func", func(t *testing.T) { + + gs.ResetCounters() + + report, err := Run( + "helloworld.Greeter.SayHello", + internal.TestLocalhost, + WithProtoFile("../testdata/greeter.proto", []string{}), + WithTotalRequests(5), + WithBinaryDataFunc(changeFunc), + WithConcurrency(1), + WithTimeout(time.Duration(20*time.Second)), + WithDialTimeout(time.Duration(20*time.Second)), + WithInsecure(true), + ) + + assert.NoError(t, err) + + assert.NotNil(t, report) + + assert.Equal(t, 5, int(report.Count)) + assert.NotZero(t, report.Average) + assert.NotZero(t, report.Fastest) + assert.NotZero(t, report.Slowest) + assert.NotZero(t, report.Rps) + assert.Empty(t, report.Name) + assert.NotEmpty(t, report.Date) + assert.NotEmpty(t, report.Options) + assert.NotEmpty(t, report.Details) + assert.NotEmpty(t, report.LatencyDistribution) + assert.Equal(t, ReasonNormalEnd, report.EndReason) + assert.Empty(t, report.ErrorDist) + + assert.NotEqual(t, report.Average, report.Slowest) + assert.NotEqual(t, report.Average, report.Fastest) + assert.NotEqual(t, report.Slowest, report.Fastest) + + count := gs.GetCount(callType) + assert.Equal(t, 5, count) + + connCount := gs.GetConnectionCount() + assert.Equal(t, 1, connCount) + }) + t.Run("test connections", func(t *testing.T) { gs.ResetCounters() diff --git a/runner/worker.go b/runner/worker.go index f5a8fb6c..b581c171 100644 --- a/runner/worker.go +++ b/runner/worker.go @@ -65,7 +65,7 @@ func (w *Worker) makeRequest() error { reqNum := atomic.AddInt64(w.reqCounter, 1) - ctd := newCallTemplateData(w.mtd, w.config.funcs, w.workerID, reqNum) + ctd := newCallData(w.mtd, w.config.funcs, w.workerID, reqNum) var inputs []*dynamic.Message var err error @@ -167,7 +167,7 @@ func (w *Worker) makeRequest() error { return err } -func (w *Worker) getMessages(ctd *callTemplateData, inputData []byte) ([]*dynamic.Message, error) { +func (w *Worker) getMessages(ctd *CallData, inputData []byte) ([]*dynamic.Message, error) { var inputs []*dynamic.Message if w.cachedMessages != nil { @@ -186,12 +186,17 @@ func (w *Worker) getMessages(ctd *callTemplateData, inputData []byte) ([]*dynami // Json messages are not cached due to templating } else { var err error + if w.config.dataFunc != nil { + inputData = w.config.dataFunc(w.mtd, ctd) + } inputs, err = createPayloadsFromBin(inputData, w.mtd) if err != nil { return nil, err } - - w.cachedMessages = inputs + // We only cache in case we don't dynamically change the binary message + if w.config.dataFunc == nil { + w.cachedMessages = inputs + } } return inputs, nil From 0a1938f07d30e047de238ff7711f720be5561682 Mon Sep 17 00:00:00 2001 From: Bojan Date: Sun, 25 Oct 2020 17:02:14 -0300 Subject: [PATCH 2/2] add call data docs and clean up code --- runner/{call_template_data.go => calldata.go} | 4 +-- ...template_data_test.go => calldata_test.go} | 6 ++-- runner/options.go | 7 ++++- www/docs/calldata.md | 29 ++++++++++++++++--- 4 files changed, 36 insertions(+), 10 deletions(-) rename runner/{call_template_data.go => calldata.go} (97%) rename runner/{call_template_data_test.go => calldata_test.go} (98%) diff --git a/runner/call_template_data.go b/runner/calldata.go similarity index 97% rename from runner/call_template_data.go rename to runner/calldata.go index 0656fe6b..b7c2320a 100644 --- a/runner/call_template_data.go +++ b/runner/calldata.go @@ -17,7 +17,7 @@ const charset = "abcdefghijklmnopqrstuvwxyz" + var seededRand *rand.Rand = rand.New( rand.NewSource(time.Now().UnixNano())) -// call template data +// CallData represents contextualized data available for templating type CallData struct { WorkerID string // unique worker ID RequestNumber int64 // unique incremented request number for each request @@ -42,7 +42,7 @@ var tmplFuncMap = template.FuncMap{ "randomString": randomString, } -// newCallData returns new callData +// newCallData returns new CallData func newCallData( mtd *desc.MethodDescriptor, funcs template.FuncMap, diff --git a/runner/call_template_data_test.go b/runner/calldata_test.go similarity index 98% rename from runner/call_template_data_test.go rename to runner/calldata_test.go index bdf2897d..db416c3f 100644 --- a/runner/call_template_data_test.go +++ b/runner/calldata_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCallTemplateData_New(t *testing.T) { +func TestCallData_New(t *testing.T) { md, err := protodesc.GetMethodDescFromProto("helloworld.Greeter/SayHello", "../testdata/greeter.proto", []string{}) assert.NoError(t, err) assert.NotNil(t, md) @@ -36,7 +36,7 @@ func TestCallTemplateData_New(t *testing.T) { assert.Equal(t, 36, len(ctd.UUID)) } -func TestCallTemplateData_ExecuteData(t *testing.T) { +func TestCallData_ExecuteData(t *testing.T) { md, err := protodesc.GetMethodDescFromProto("helloworld.Greeter/SayHello", "../testdata/greeter.proto", []string{}) assert.NoError(t, err) assert.NotNil(t, md) @@ -87,7 +87,7 @@ func TestCallTemplateData_ExecuteData(t *testing.T) { } } -func TestCallTemplateData_ExecuteMetadata(t *testing.T) { +func TestCallData_ExecuteMetadata(t *testing.T) { md, err := protodesc.GetMethodDescFromProto("helloworld.Greeter/SayHello", "../testdata/greeter.proto", []string{}) assert.NoError(t, err) assert.NotNil(t, md) diff --git a/runner/options.go b/runner/options.go index 9a73e77b..1778fb8e 100644 --- a/runner/options.go +++ b/runner/options.go @@ -20,6 +20,11 @@ import ( "google.golang.org/grpc/credentials" ) +// BinaryDataFunc is a function that can be used for provide binary data for request programatically. +// MethodDescriptor of the call is passed to the data function. +// CallData for the request is passed and can be used to access worker id, request number, etc... +type BinaryDataFunc func(mtd *desc.MethodDescriptor, callData *CallData) []byte + // RunConfig represents the request Configs type RunConfig struct { // call settings @@ -62,7 +67,7 @@ type RunConfig struct { data []byte // data func - dataFunc func(mtd *desc.MethodDescriptor, callData *CallData) []byte + dataFunc BinaryDataFunc binary bool metadata []byte diff --git a/www/docs/calldata.md b/www/docs/calldata.md index 6ee7cc23..9c1e324e 100644 --- a/www/docs/calldata.md +++ b/www/docs/calldata.md @@ -1,13 +1,13 @@ --- id: calldata -title: Call Template Data +title: Call Data --- Data and metadata can specify [template actions](https://golang.org/pkg/text/template/) that will be parsed and evaluated at every request. Each request gets a new instance of the data. The available variables / actions are: ```go -// call template data -type callTemplateData struct { +// CallData represents contextualized data available for templating +type CallData struct { // unique worker ID WorkerID string @@ -53,7 +53,7 @@ type callTemplateData struct { } ``` -**Functions** +**Template Functions** There are also two template functions available: @@ -95,3 +95,24 @@ Would result in data with JSON representation: ``` See [example calls](examples.md) for some more usage examples. + +### Data Function API + +When using the `ghz/runner` package programmatically, we can dynamically create data for each request using `WithBinaryDataFunc()` API: + +```go +func dataFunc(mtd *desc.MethodDescriptor, cd *runner.CallData) []byte { + msg := &helloworld.HelloRequest{} + msg.Name = cd.WorkerID + binData, err := proto.Marshal(msg) + return binData +} + +report, err := runner.Run( + "helloworld.Greeter.SayHello", + "0.0.0.0:50051", + runner.WithProtoFile("./testdata/greeter.proto", []string{}), + runner.WithInsecure(true), + runner.WithBinaryDataFunc(dataFunc), +) +```