Skip to content

Commit

Permalink
Instantiate a HTTP module per VU
Browse files Browse the repository at this point in the history
This is dones so we can now have per VU settign for the HTTP module such
as the ResponseCallback

This is part of #1858
  • Loading branch information
mstoykov committed Feb 19, 2021
1 parent ad8c94d commit 7e0b787
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 50 deletions.
8 changes: 7 additions & 1 deletion js/internal/modules/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ var (
func Get(name string) interface{} {
mx.RLock()
defer mx.RUnlock()
return modules[name]
mod := modules[name]
if i, ok := mod.(interface {
NewGlobalModule() interface{ NewModuleInstance() interface{} }
}); ok {
return i.NewGlobalModule().NewModuleInstance()
}
return mod
}

// Register the given mod as a JavaScript module, available
Expand Down
46 changes: 44 additions & 2 deletions js/modules/k6/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
)

func init() {
modules.Register("k6/http", New())
modules.Register("k6/http", new(RootModule))
}

const (
Expand All @@ -46,6 +46,44 @@ const (
// ErrJarForbiddenInInitContext is used when a cookie jar was made in the init context
var ErrJarForbiddenInInitContext = common.NewInitContextError("Making cookie jars in the init context is not supported")

// RootModule is only used to be registered and then to create a global (per k6 instance) module instance
type RootModule struct{}

// NewGlobalModule return a new GlobalHTTP
func (r RootModule) NewGlobalModule() interface{ NewModuleInstance() interface{} } {
return &GlobalHTTP{}
}

// GlobalHTTP is a global HTTP module for a k6 instance/test run
type GlobalHTTP struct{}

// NewModuleInstance returns an HTTP instance for each VU
func (g *GlobalHTTP) NewModuleInstance() interface{} {
return &HTTP{ // change the below fields to be not writable or not fields
SSL_3_0: netext.SSL_3_0,
TLS_1_0: netext.TLS_1_0,
TLS_1_1: netext.TLS_1_1,
TLS_1_2: netext.TLS_1_2,
TLS_1_3: netext.TLS_1_3,
OCSP_STATUS_GOOD: netext.OCSP_STATUS_GOOD,
OCSP_STATUS_REVOKED: netext.OCSP_STATUS_REVOKED,
OCSP_STATUS_SERVER_FAILED: netext.OCSP_STATUS_SERVER_FAILED,
OCSP_STATUS_UNKNOWN: netext.OCSP_STATUS_UNKNOWN,
OCSP_REASON_UNSPECIFIED: netext.OCSP_REASON_UNSPECIFIED,
OCSP_REASON_KEY_COMPROMISE: netext.OCSP_REASON_KEY_COMPROMISE,
OCSP_REASON_CA_COMPROMISE: netext.OCSP_REASON_CA_COMPROMISE,
OCSP_REASON_AFFILIATION_CHANGED: netext.OCSP_REASON_AFFILIATION_CHANGED,
OCSP_REASON_SUPERSEDED: netext.OCSP_REASON_SUPERSEDED,
OCSP_REASON_CESSATION_OF_OPERATION: netext.OCSP_REASON_CESSATION_OF_OPERATION,
OCSP_REASON_CERTIFICATE_HOLD: netext.OCSP_REASON_CERTIFICATE_HOLD,
OCSP_REASON_REMOVE_FROM_CRL: netext.OCSP_REASON_REMOVE_FROM_CRL,
OCSP_REASON_PRIVILEGE_WITHDRAWN: netext.OCSP_REASON_PRIVILEGE_WITHDRAWN,
OCSP_REASON_AA_COMPROMISE: netext.OCSP_REASON_AA_COMPROMISE,

responseCallback: defaultExpectedStatuses.match,
}
}

//nolint: golint
type HTTP struct {
SSL_3_0 string `js:"SSL_3_0"`
Expand All @@ -67,10 +105,14 @@ type HTTP struct {
OCSP_REASON_REMOVE_FROM_CRL string `js:"OCSP_REASON_REMOVE_FROM_CRL"`
OCSP_REASON_PRIVILEGE_WITHDRAWN string `js:"OCSP_REASON_PRIVILEGE_WITHDRAWN"`
OCSP_REASON_AA_COMPROMISE string `js:"OCSP_REASON_AA_COMPROMISE"`

responseCallback func(int) bool
}

// New ...
// TODO deprecate this method
func New() *HTTP {
//TODO: move this as an anonymous struct somewhere...
// TODO: move this as an anonymous struct somewhere...
return &HTTP{
SSL_3_0: netext.SSL_3_0,
TLS_1_0: netext.TLS_1_0,
Expand Down
8 changes: 4 additions & 4 deletions js/modules/k6/http/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (h *HTTP) Request(ctx context.Context, method string, url goja.Value, args
if err != nil {
return nil, err
}
return responseFromHttpext(resp), nil
return h.responseFromHttpext(resp), nil
}

//TODO break this function up
Expand All @@ -139,7 +139,7 @@ func (h *HTTP) parseRequest(
Redirects: state.Options.MaxRedirects,
Cookies: make(map[string]*httpext.HTTPRequestCookie),
Tags: make(map[string]string),
ResponseCallback: state.HTTPResponseCallback,
ResponseCallback: h.responseCallback,
}

if state.Options.DiscardResponseBodies.Bool {
Expand Down Expand Up @@ -388,7 +388,7 @@ func (h *HTTP) prepareBatchArray(
ParsedHTTPRequest: parsedReq,
Response: response,
}
results[i] = &Response{response}
results[i] = h.responseFromHttpext(response)
}

return batchReqs, results, nil
Expand All @@ -412,7 +412,7 @@ func (h *HTTP) prepareBatchObject(
ParsedHTTPRequest: parsedReq,
Response: response,
}
results[key] = &Response{response}
results[key] = h.responseFromHttpext(response)
i++
}

Expand Down
12 changes: 6 additions & 6 deletions js/modules/k6/http/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ import (
// Response is a representation of an HTTP response to be returned to the goja VM
type Response struct {
*httpext.Response `js:"-"`
h *HTTP
}

func responseFromHttpext(resp *httpext.Response) *Response {
res := Response{resp}
return &res
func (h *HTTP) responseFromHttpext(resp *httpext.Response) *Response {
return &Response{Response: resp, h: h}
}

// JSON parses the body of a response as json and returns it to the goja VM
Expand Down Expand Up @@ -159,9 +159,9 @@ func (res *Response) SubmitForm(args ...goja.Value) (*Response, error) {
q.Add(k, v.String())
}
requestURL.RawQuery = q.Encode()
return New().Request(res.GetCtx(), requestMethod, rt.ToValue(requestURL.String()), goja.Null(), requestParams)
return res.h.Request(res.GetCtx(), requestMethod, rt.ToValue(requestURL.String()), goja.Null(), requestParams)
}
return New().Request(res.GetCtx(), requestMethod, rt.ToValue(requestURL.String()), rt.ToValue(values), requestParams)
return res.h.Request(res.GetCtx(), requestMethod, rt.ToValue(requestURL.String()), rt.ToValue(values), requestParams)
}

// ClickLink parses the body as an html, looks for a link and than makes a request as if the link was
Expand Down Expand Up @@ -202,5 +202,5 @@ func (res *Response) ClickLink(args ...goja.Value) (*Response, error) {
}
requestURL := responseURL.ResolveReference(hrefURL)

return New().Get(res.GetCtx(), rt.ToValue(requestURL.String()), requestParams)
return res.h.Get(res.GetCtx(), rt.ToValue(requestURL.String()), requestParams)
}
19 changes: 9 additions & 10 deletions js/modules/k6/http/response_callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,13 @@ import (

"github.com/dop251/goja"
"github.com/loadimpact/k6/js/common"
"github.com/loadimpact/k6/lib"
)

//nolint:gochecknoglobals
var defaultExpectedStatuses = expectedStatuses{
minmax: [][2]int{{200, 399}},
}

// DefaultHTTPResponseCallback ...
func DefaultHTTPResponseCallback() func(int) bool {
return defaultExpectedStatuses.match
}

type expectedStatuses struct {
minmax [][2]int
exact []int // this can be done with the above and vice versa
Expand Down Expand Up @@ -102,10 +96,15 @@ func checkNumber(a goja.Value, rt *goja.Runtime) bool {
}

// SetResponseCallback ..
func (h HTTP) SetResponseCallback(ctx context.Context, es *expectedStatuses) {
if es != nil {
lib.GetState(ctx).HTTPResponseCallback = es.match
func (h *HTTP) SetResponseCallback(ctx context.Context, val goja.Value) {
if val != nil && !goja.IsNull(val) {
if es, ok := val.Export().(*expectedStatuses); ok {
h.responseCallback = es.match
} else {
//nolint:golint
common.Throw(common.GetRuntime(ctx), fmt.Errorf("unsupported argument, expected http.expectedStatuses"))
}
} else {
lib.GetState(ctx).HTTPResponseCallback = nil
h.responseCallback = nil
}
}
19 changes: 11 additions & 8 deletions js/modules/k6/http/response_callback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestExpectedStatuses(t *testing.T) {
ctx := context.Background()

ctx = common.WithRuntime(ctx, rt)
rt.Set("http", common.Bind(rt, New(), &ctx))
rt.Set("http", common.Bind(rt, new(RootModule).NewGlobalModule().NewModuleInstance(), &ctx))
cases := map[string]struct {
code, err string
expected expectedStatuses
Expand Down Expand Up @@ -107,9 +107,11 @@ type expectedSample struct {

func TestResponseCallbackInAction(t *testing.T) {
t.Parallel()
tb, state, samples, rt, _ := newRuntime(t)
tb, _, samples, rt, ctx := newRuntime(t)
defer tb.Cleanup()
sr := tb.Replacer.Replace
httpModule := new(RootModule).NewGlobalModule().NewModuleInstance().(*HTTP)
rt.Set("http", common.Bind(rt, httpModule, ctx))

HTTPMetricsWithoutFailed := []*stats.Metric{
metrics.HTTPReqs,
Expand Down Expand Up @@ -280,7 +282,7 @@ func TestResponseCallbackInAction(t *testing.T) {
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
state.HTTPResponseCallback = DefaultHTTPResponseCallback()
httpModule.responseCallback = defaultExpectedStatuses.match

_, err := rt.RunString(sr(testCase.code))
assert.NoError(t, err)
Expand All @@ -306,7 +308,7 @@ func TestResponseCallbackInAction(t *testing.T) {

func TestResponseCallbackInActionWithoutPassedTag(t *testing.T) {
t.Parallel()
tb, state, samples, rt, _ := newRuntime(t)
tb, state, samples, rt, ctx := newRuntime(t)
defer tb.Cleanup()
sr := tb.Replacer.Replace
allHTTPMetrics := []*stats.Metric{
Expand All @@ -321,8 +323,8 @@ func TestResponseCallbackInActionWithoutPassedTag(t *testing.T) {
metrics.HTTPReqTLSHandshaking,
}
deleteSystemTag(state, stats.TagPassed.String())

state.HTTPResponseCallback = DefaultHTTPResponseCallback()
httpModule := new(RootModule).NewGlobalModule().NewModuleInstance().(*HTTP)
rt.Set("http", common.Bind(rt, httpModule, ctx))

_, err := rt.RunString(sr(`http.request("GET", "HTTPBIN_URL/redirect/1", null, {responseCallback: http.expectedStatuses(200)});`))
assert.NoError(t, err)
Expand Down Expand Up @@ -364,10 +366,11 @@ func TestResponseCallbackInActionWithoutPassedTag(t *testing.T) {

func TestDigestWithResponseCallback(t *testing.T) {
t.Parallel()
tb, state, samples, rt, _ := newRuntime(t)
tb, _, samples, rt, ctx := newRuntime(t)
defer tb.Cleanup()

state.HTTPResponseCallback = DefaultHTTPResponseCallback()
httpModule := new(RootModule).NewGlobalModule().NewModuleInstance().(*HTTP)
rt.Set("http", common.Bind(rt, httpModule, ctx))

urlWithCreds := tb.Replacer.Replace(
"http://testuser:testpwd@HTTPBIN_IP:HTTPBIN_PORT/digest-auth/auth/testuser/testpwd",
Expand Down
5 changes: 3 additions & 2 deletions js/modules/k6/http/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const testGetFormHTML = `
</form>
</body>
`

const jsonData = `{"glossary": {
"friends": [
{"first": "Dale", "last": "Murphy", "age": 44},
Expand Down Expand Up @@ -413,15 +414,15 @@ func BenchmarkResponseJson(b *testing.B) {
tc := tc
b.Run(fmt.Sprintf("Selector %s ", tc.selector), func(b *testing.B) {
for n := 0; n < b.N; n++ {
resp := responseFromHttpext(&httpext.Response{Body: jsonData})
resp := new(HTTP).responseFromHttpext(&httpext.Response{Body: jsonData})
resp.JSON(tc.selector)
}
})
}

b.Run("Without selector", func(b *testing.B) {
for n := 0; n < b.N; n++ {
resp := responseFromHttpext(&httpext.Response{Body: jsonData})
resp := new(HTTP).responseFromHttpext(&httpext.Response{Body: jsonData})
resp.JSON()
}
})
Expand Down
28 changes: 13 additions & 15 deletions js/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import (
"golang.org/x/time/rate"

"github.com/loadimpact/k6/js/common"
k6http "github.com/loadimpact/k6/js/modules/k6/http"
"github.com/loadimpact/k6/lib"
"github.com/loadimpact/k6/lib/consts"
"github.com/loadimpact/k6/lib/netext"
Expand Down Expand Up @@ -218,20 +217,19 @@ func (r *Runner) newVU(id int64, samplesOut chan<- stats.SampleContainer) (*VU,
}

vu.state = &lib.State{
Logger: vu.Runner.Logger,
Options: vu.Runner.Bundle.Options,
Transport: vu.Transport,
Dialer: vu.Dialer,
TLSConfig: vu.TLSConfig,
CookieJar: cookieJar,
RPSLimit: vu.Runner.RPSLimit,
BPool: vu.BPool,
Vu: vu.ID,
Samples: vu.Samples,
Iteration: vu.Iteration,
Tags: vu.Runner.Bundle.Options.RunTags.CloneTags(),
Group: r.defaultGroup,
HTTPResponseCallback: k6http.DefaultHTTPResponseCallback(), // TODO maybe move it to lib after all :sign:
Logger: vu.Runner.Logger,
Options: vu.Runner.Bundle.Options,
Transport: vu.Transport,
Dialer: vu.Dialer,
TLSConfig: vu.TLSConfig,
CookieJar: cookieJar,
RPSLimit: vu.Runner.RPSLimit,
BPool: vu.BPool,
Vu: vu.ID,
Samples: vu.Samples,
Iteration: vu.Iteration,
Tags: vu.Runner.Bundle.Options.RunTags.CloneTags(),
Group: r.defaultGroup,
}
vu.Runtime.Set("console", common.Bind(vu.Runtime, vu.Console, vu.Context))

Expand Down
2 changes: 0 additions & 2 deletions lib/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ type State struct {

Vu, Iteration int64
Tags map[string]string

HTTPResponseCallback func(int) bool
}

// CloneTags makes a copy of the tags map and returns it.
Expand Down

0 comments on commit 7e0b787

Please sign in to comment.