diff --git a/runner/call_template_data.go b/runner/calldata.go similarity index 88% rename from runner/call_template_data.go rename to runner/calldata.go index be95f6f7..b7c2320a 100644 --- a/runner/call_template_data.go +++ b/runner/calldata.go @@ -17,8 +17,8 @@ const charset = "abcdefghijklmnopqrstuvwxyz" + var seededRand *rand.Rand = rand.New( rand.NewSource(time.Now().UnixNano())) -// call template data -type callTemplateData struct { +// CallData represents contextualized data available for templating +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/calldata_test.go similarity index 94% rename from runner/call_template_data_test.go rename to runner/calldata_test.go index 28074682..db416c3f 100644 --- a/runner/call_template_data_test.go +++ b/runner/calldata_test.go @@ -10,12 +10,12 @@ 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) - 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) @@ -36,12 +36,12 @@ 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) - ctd := newCallTemplateData(md, nil, "worker_id_123", 200) + ctd := newCallData(md, nil, "worker_id_123", 200) assert.NotNil(t, ctd) @@ -87,12 +87,12 @@ 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) - 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 a201b56d..ca6aeac2 100644 --- a/runner/options.go +++ b/runner/options.go @@ -15,10 +15,16 @@ import ( "text/template" "time" + "github.com/jhump/protoreflect/desc" "github.com/pkg/errors" "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 + // ScheduleConst is a constant load schedule const ScheduleConst = "const" @@ -84,7 +90,11 @@ type RunConfig struct { streamInterval time.Duration // data - data []byte + data []byte + + // data func + dataFunc BinaryDataFunc + binary bool metadata []byte rmd map[string]string @@ -326,6 +336,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 { @@ -583,7 +604,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 f057da83..76f9f43a 100644 --- a/runner/options_test.go +++ b/runner/options_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "math" "os" + "reflect" "runtime" "testing" "time" @@ -133,6 +134,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"), @@ -159,6 +161,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 0cedce2e..e376f716 100644 --- a/runner/run_test.go +++ b/runner/run_test.go @@ -9,9 +9,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 @@ -337,6 +345,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 1e85b097..47d9aadd 100644 --- a/runner/worker.go +++ b/runner/worker.go @@ -80,7 +80,7 @@ func (w *Worker) Stop() { func (w *Worker) makeRequest(tv TickValue) error { reqNum := int64(tv.reqNumber) - 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 @@ -166,7 +166,7 @@ func (w *Worker) makeRequest(tv TickValue) 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 { @@ -185,12 +185,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 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), +) +```