Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modules fixes #2234

Merged
merged 3 commits into from Nov 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 9 additions & 8 deletions js/initcontext.go
Expand Up @@ -151,24 +151,25 @@ func (i *InitContext) Require(arg string) goja.Value {
}
}

type moduleInstanceCoreImpl struct {
// TODO this likely should just be part of the initialized VU or at least to take stuff directly from it.
type moduleVUImpl struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just use moduleVU instead of moduleVUImpl?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some hopes I will be working on the TODO above this cycle, and it will just get completely removed

ctxPtr *context.Context
// we can technically put lib.State here as well as anything else
}

func (m *moduleInstanceCoreImpl) GetContext() context.Context {
func (m *moduleVUImpl) Context() context.Context {
return *m.ctxPtr
}

func (m *moduleInstanceCoreImpl) GetInitEnv() *common.InitEnvironment {
func (m *moduleVUImpl) InitEnv() *common.InitEnvironment {
return common.GetInitEnv(*m.ctxPtr) // TODO thread it correctly instead
}

func (m *moduleInstanceCoreImpl) GetState() *lib.State {
func (m *moduleVUImpl) State() *lib.State {
return lib.GetState(*m.ctxPtr) // TODO thread it correctly instead
}

func (m *moduleInstanceCoreImpl) GetRuntime() *goja.Runtime {
func (m *moduleVUImpl) Runtime() *goja.Runtime {
return common.GetRuntime(*m.ctxPtr) // TODO thread it correctly instead
}

Expand Down Expand Up @@ -200,9 +201,9 @@ func (i *InitContext) requireModule(name string) (goja.Value, error) {
if !ok {
return nil, fmt.Errorf("unknown module: %s", name)
}
if modV2, ok := mod.(modules.IsModuleV2); ok {
instance := modV2.NewModuleInstance(&moduleInstanceCoreImpl{ctxPtr: i.ctxPtr})
return i.runtime.ToValue(toESModuleExports(instance.GetExports())), nil
if m, ok := mod.(modules.Module); ok {
instance := m.NewModuleInstance(&moduleVUImpl{ctxPtr: i.ctxPtr})
return i.runtime.ToValue(toESModuleExports(instance.Exports())), nil
}
if perInstance, ok := mod.(modules.HasModuleInstancePerVU); ok {
mod = perInstance.NewModuleInstancePerVU()
Expand Down
28 changes: 14 additions & 14 deletions js/modules/k6/execution/execution.go
Expand Up @@ -40,26 +40,26 @@ type (

// ModuleInstance represents an instance of the execution module.
ModuleInstance struct {
modules.InstanceCore
vu modules.VU
obj *goja.Object
}
)

var (
_ modules.IsModuleV2 = &RootModule{}
_ modules.Instance = &ModuleInstance{}
_ modules.Module = &RootModule{}
_ modules.Instance = &ModuleInstance{}
)

// New returns a pointer to a new RootModule instance.
func New() *RootModule {
return &RootModule{}
}
Comment on lines 54 to 56
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I'm going to say would probably be involved and may not be an easy task. And, I'm not deeply knowledgeable about the system and our goals, so this can be a fruitless suggestion, and it might not even work 🤷

Why don't we register in an extension without giving the k6 an instance of the module, like this:

func init() {
	k6modules.Register("k6/x/browser")
}

With this, we won't have to stutter in tests, and we can more natural type something like this:

New(mii)

Instead Of:

New().NewModuleInstance(mii)

So the interface would look like this:

type IsModuleV2 interface {
    New(InstanceCore) Instance
}

In an extension we can say:

type RootModule struct {
    modules.InstanceCore
}

var _ modules.IsModuleV2 = &RootModule{}

func New(m modules.InstanceCore) *RootModule {
	return &RootModule{InstanceCore: m}
}

Instead of:

type (
	RootModule struct{}
	ModuleInstance struct {
		modules.InstanceCore
	}
)

var (
	_ modules.IsModuleV2 = &RootModule{}
	_ modules.Instance   = &ModuleInstance{}
)

func (*RootModule) NewModuleInstance(m modules.InstanceCore) modules.Instance {
	return &ModuleInstance{InstanceCore: m}
}

func New() *RootModule {
	return &RootModule{}
}

Of course, with this, we need to create a struct (RootModule) of a module ourselves. But then again, we can standardize it like RootModule and instantiate it using reflection.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest (and most obvious problem) is that this all si also used for extensions. And while for internal we can have it listed the way it currently works (and likely will still need to work) is that extensions register themselves in a init function and add the RootModule so that we can call the NewModuleInstance from it ... in reality we can just register the New function, but for historical reasons previous to this interface (which is not used by any non-internal extension) you just registered interface{} and then k6 did a bunch of reflect "magic" see #1802, which had a list of problems, the major one of which is ... nobody could understand how it works or why it doesn't work for something or ... why it was needed in some cases.

Hope this explains the problem enough, but I would recommend reading the provided issue.


// NewModuleInstance implements the modules.IsModuleV2 interface to return
// NewModuleInstance implements the modules.Module interface to return
// a new instance for each VU.
func (*RootModule) NewModuleInstance(m modules.InstanceCore) modules.Instance {
mi := &ModuleInstance{InstanceCore: m}
rt := m.GetRuntime()
func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
mi := &ModuleInstance{vu: vu}
rt := vu.Runtime()
o := rt.NewObject()
defProp := func(name string, newInfo func() (*goja.Object, error)) {
err := o.DefineAccessorProperty(name, rt.ToValue(func() goja.Value {
Expand All @@ -82,25 +82,25 @@ func (*RootModule) NewModuleInstance(m modules.InstanceCore) modules.Instance {
return mi
}

// GetExports returns the exports of the execution module.
func (mi *ModuleInstance) GetExports() modules.Exports {
// Exports returns the exports of the execution module.
func (mi *ModuleInstance) Exports() modules.Exports {
return modules.Exports{Default: mi.obj}
}

// newScenarioInfo returns a goja.Object with property accessors to retrieve
// information about the scenario the current VU is running in.
func (mi *ModuleInstance) newScenarioInfo() (*goja.Object, error) {
ctx := mi.GetContext()
ctx := mi.vu.Context()
rt := common.GetRuntime(ctx)
vuState := mi.GetState()
vuState := mi.vu.State()
if vuState == nil {
return nil, errors.New("getting scenario information in the init context is not supported")
}
if rt == nil {
return nil, errors.New("goja runtime is nil in context")
}
getScenarioState := func() *lib.ScenarioState {
ss := lib.GetScenarioState(mi.GetContext())
ss := lib.GetScenarioState(mi.vu.Context())
if ss == nil {
common.Throw(rt, errors.New("getting scenario information in the init context is not supported"))
}
Expand Down Expand Up @@ -140,7 +140,7 @@ func (mi *ModuleInstance) newScenarioInfo() (*goja.Object, error) {
// newInstanceInfo returns a goja.Object with property accessors to retrieve
// information about the local instance stats.
func (mi *ModuleInstance) newInstanceInfo() (*goja.Object, error) {
ctx := mi.GetContext()
ctx := mi.vu.Context()
es := lib.GetExecutionState(ctx)
if es == nil {
return nil, errors.New("getting instance information in the init context is not supported")
Expand Down Expand Up @@ -175,7 +175,7 @@ func (mi *ModuleInstance) newInstanceInfo() (*goja.Object, error) {
// newVUInfo returns a goja.Object with property accessors to retrieve
// information about the currently executing VU.
func (mi *ModuleInstance) newVUInfo() (*goja.Object, error) {
ctx := mi.GetContext()
ctx := mi.vu.Context()
vuState := lib.GetState(ctx)
if vuState == nil {
return nil, errors.New("getting VU information in the init context is not supported")
Expand Down
16 changes: 8 additions & 8 deletions js/modules/k6/execution/execution_test.go
Expand Up @@ -64,15 +64,15 @@ func setupTagsExecEnv(t *testing.T) execEnv {
ctx := common.WithRuntime(context.Background(), rt)
ctx = lib.WithState(ctx, state)
m, ok := New().NewModuleInstance(
&modulestest.InstanceCore{
Runtime: rt,
InitEnv: &common.InitEnvironment{},
Ctx: ctx,
State: state,
&modulestest.VU{
RuntimeField: rt,
InitEnvField: &common.InitEnvironment{},
CtxField: ctx,
StateField: state,
},
).(*ModuleInstance)
require.True(t, ok)
require.NoError(t, rt.Set("exec", m.GetExports().Default))
require.NoError(t, rt.Set("exec", m.Exports().Default))

return execEnv{
Module: m,
Expand Down Expand Up @@ -102,7 +102,7 @@ func TestVUTags(t *testing.T) {
t.Parallel()

tenv := setupTagsExecEnv(t)
state := tenv.Module.GetState()
state := tenv.Module.vu.State()
state.Tags.Set("custom-tag", "mytag1")

encoded, err := tenv.Runtime.RunString(`JSON.stringify(exec.vu.tags)`)
Expand Down Expand Up @@ -157,7 +157,7 @@ func TestVUTags(t *testing.T) {
t.Parallel()

tenv := setupTagsExecEnv(t)
state := tenv.Module.GetState()
state := tenv.Module.vu.State()
state.Options.Throw = null.BoolFrom(true)
require.NotNil(t, state)

Expand Down
37 changes: 22 additions & 15 deletions js/modules/k6/metrics/metrics.go
Expand Up @@ -37,18 +37,18 @@ import (

type Metric struct {
metric *stats.Metric
core modules.InstanceCore
vu modules.VU
}

// ErrMetricsAddInInitContext is error returned when adding to metric is done in the init context
var ErrMetricsAddInInitContext = common.NewInitContextError("Adding to metrics in the init context is not supported")

func (mi *ModuleInstance) newMetric(call goja.ConstructorCall, t stats.MetricType) (*goja.Object, error) {
initEnv := mi.GetInitEnv()
initEnv := mi.vu.InitEnv()
if initEnv == nil {
return nil, errors.New("metrics must be declared in the init context")
}
rt := mi.GetRuntime()
rt := mi.vu.Runtime()
c, _ := goja.AssertFunction(rt.ToValue(func(name string, isTime ...bool) (*goja.Object, error) {
valueType := stats.Default
if len(isTime) > 0 && isTime[0] {
Expand All @@ -58,7 +58,7 @@ func (mi *ModuleInstance) newMetric(call goja.ConstructorCall, t stats.MetricTyp
if err != nil {
return nil, err
}
metric := &Metric{metric: m, core: mi.InstanceCore}
metric := &Metric{metric: m, vu: mi.vu}
o := rt.NewObject()
err = o.DefineDataProperty("name", rt.ToValue(name), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE)
if err != nil {
Expand Down Expand Up @@ -93,7 +93,7 @@ func limitValue(v string) string {
}

func (m Metric) add(v goja.Value, addTags ...map[string]string) (bool, error) {
state := m.core.GetState()
state := m.vu.State()
if state == nil {
return false, ErrMetricsAddInInitContext
}
Expand Down Expand Up @@ -130,7 +130,7 @@ func (m Metric) add(v goja.Value, addTags ...map[string]string) (bool, error) {
}

sample := stats.Sample{Time: time.Now(), Metric: m.metric, Value: vfloat, Tags: stats.IntoSampleTags(&tags)}
stats.PushIfNotDone(m.core.GetContext(), state.Samples, sample)
stats.PushIfNotDone(m.vu.Context(), state.Samples, sample)
return true, nil
}

Expand All @@ -139,28 +139,35 @@ type (
RootModule struct{}
// ModuleInstance represents an instance of the metrics module
ModuleInstance struct {
modules.InstanceCore
vu modules.VU
}
)

var (
_ modules.IsModuleV2 = &RootModule{}
_ modules.Instance = &ModuleInstance{}
_ modules.Module = &RootModule{}
_ modules.Instance = &ModuleInstance{}
)

// NewModuleInstance implements modules.IsModuleV2 interface
func (*RootModule) NewModuleInstance(m modules.InstanceCore) modules.Instance {
return &ModuleInstance{InstanceCore: m}
// NewModuleInstance implements modules.Module interface
func (*RootModule) NewModuleInstance(m modules.VU) modules.Instance {
return &ModuleInstance{vu: m}
}

// New returns a new RootModule.
func New() *RootModule {
return &RootModule{}
}

// GetExports returns the exports of the metrics module
func (mi *ModuleInstance) GetExports() modules.Exports {
return modules.GenerateExports(mi)
// Exports returns the exports of the metrics module
func (mi *ModuleInstance) Exports() modules.Exports {
return modules.Exports{
Named: map[string]interface{}{
"Counter": mi.XCounter,
"Gauge": mi.XGauge,
"Trend": mi.XTrend,
"Rate": mi.XRate,
},
}
}

// XCounter is a counter constructor
Expand Down
36 changes: 18 additions & 18 deletions js/modules/k6/metrics/metrics_test.go
Expand Up @@ -121,14 +121,14 @@ func TestMetrics(t *testing.T) {
}
test.rt = goja.New()
test.rt.SetFieldNameMapper(common.FieldNameMapper{})
mii := &modulestest.InstanceCore{
Runtime: test.rt,
InitEnv: &common.InitEnvironment{Registry: metrics.NewRegistry()},
Ctx: context.Background(),
mii := &modulestest.VU{
RuntimeField: test.rt,
InitEnvField: &common.InitEnvironment{Registry: metrics.NewRegistry()},
CtxField: context.Background(),
}
m, ok := New().NewModuleInstance(mii).(*ModuleInstance)
require.True(t, ok)
require.NoError(t, test.rt.Set("metrics", m.GetExports().Named))
require.NoError(t, test.rt.Set("metrics", m.Exports().Named))
test.samples = make(chan stats.SampleContainer, 1000)
state := &lib.State{
Options: lib.Options{},
Expand All @@ -146,12 +146,12 @@ func TestMetrics(t *testing.T) {
require.NoError(t, err)

t.Run("ExitInit", func(t *testing.T) {
mii.State = state
mii.InitEnv = nil
mii.StateField = state
mii.InitEnvField = nil
_, err := test.rt.RunString(fmt.Sprintf(`new metrics.%s("my_metric")`, fn))
assert.Contains(t, err.Error(), "metrics must be declared in the init context")
})
mii.State = state
mii.StateField = state
logger := logrus.New()
logger.Out = ioutil.Discard
test.hook = &testutils.SimpleLogrusHook{HookedLevels: logrus.AllLevels}
Expand Down Expand Up @@ -186,14 +186,14 @@ func TestMetricGetName(t *testing.T) {
rt := goja.New()
rt.SetFieldNameMapper(common.FieldNameMapper{})

mii := &modulestest.InstanceCore{
Runtime: rt,
InitEnv: &common.InitEnvironment{Registry: metrics.NewRegistry()},
Ctx: context.Background(),
mii := &modulestest.VU{
RuntimeField: rt,
InitEnvField: &common.InitEnvironment{Registry: metrics.NewRegistry()},
CtxField: context.Background(),
}
m, ok := New().NewModuleInstance(mii).(*ModuleInstance)
require.True(t, ok)
require.NoError(t, rt.Set("metrics", m.GetExports().Named))
require.NoError(t, rt.Set("metrics", m.Exports().Named))
v, err := rt.RunString(`
var m = new metrics.Counter("my_metric")
m.name
Expand All @@ -214,14 +214,14 @@ func TestMetricDuplicates(t *testing.T) {
rt := goja.New()
rt.SetFieldNameMapper(common.FieldNameMapper{})

mii := &modulestest.InstanceCore{
Runtime: rt,
InitEnv: &common.InitEnvironment{Registry: metrics.NewRegistry()},
Ctx: context.Background(),
mii := &modulestest.VU{
RuntimeField: rt,
InitEnvField: &common.InitEnvironment{Registry: metrics.NewRegistry()},
CtxField: context.Background(),
}
m, ok := New().NewModuleInstance(mii).(*ModuleInstance)
require.True(t, ok)
require.NoError(t, rt.Set("metrics", m.GetExports().Named))
require.NoError(t, rt.Set("metrics", m.Exports().Named))
_, err := rt.RunString(`
var m = new metrics.Counter("my_metric")
`)
Expand Down