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 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions js/initcontext.go
Original file line number Diff line number Diff line change
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
Contributor 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 @@ -201,7 +202,7 @@ func (i *InitContext) requireModule(name string) (goja.Value, error) {
return nil, fmt.Errorf("unknown module: %s", name)
}
if m, ok := mod.(modules.Module); ok {
instance := m.NewModuleInstance(&moduleInstanceCoreImpl{ctxPtr: i.ctxPtr})
instance := m.NewModuleInstance(&moduleVUImpl{ctxPtr: i.ctxPtr})
return i.runtime.ToValue(toESModuleExports(instance.Exports())), nil
}
if perInstance, ok := mod.(modules.HasModuleInstancePerVU); ok {
Expand Down
18 changes: 9 additions & 9 deletions js/modules/k6/execution/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type (

// ModuleInstance represents an instance of the execution module.
ModuleInstance struct {
modules.InstanceCore
vu modules.VU
obj *goja.Object
}
)
Expand All @@ -57,9 +57,9 @@ func New() *RootModule {

// 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 Down Expand Up @@ -90,17 +90,17 @@ func (mi *ModuleInstance) Exports() modules.Exports {
// 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
14 changes: 7 additions & 7 deletions js/modules/k6/execution/execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ 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)
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
27 changes: 17 additions & 10 deletions js/modules/k6/metrics/metrics.go
Original file line number Diff line number Diff line change
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,7 +139,7 @@ type (
RootModule struct{}
// ModuleInstance represents an instance of the metrics module
ModuleInstance struct {
modules.InstanceCore
vu modules.VU
}
)

Expand All @@ -149,8 +149,8 @@ var (
)

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

// New returns a new RootModule.
Expand All @@ -160,7 +160,14 @@ func New() *RootModule {

// Exports returns the exports of the metrics module
func (mi *ModuleInstance) Exports() modules.Exports {
return modules.GenerateExports(mi)
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
30 changes: 15 additions & 15 deletions js/modules/k6/metrics/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ 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)
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,10 +186,10 @@ 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)
Expand All @@ -214,10 +214,10 @@ 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)
Expand Down
73 changes: 14 additions & 59 deletions js/modules/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,11 @@ type HasModuleInstancePerVU interface {
NewModuleInstancePerVU() interface{}
}

// Module is the interface js modules should implement in order to get access to the InstanceCore
// Module is the interface js modules should implement in order to get access to the VU
type Module interface {
// NewModuleInstance will get InstanceCore that should provide the module with *everything* it needs and return an
// Instance implementation (embedding the InstanceCore).
// This method will be called for *each* require/import and return an object for VUs.
NewModuleInstance(InstanceCore) Instance
// NewModuleInstance will get modules.VU that should provide the module with a way to interact with the VU
// This method will be called for *each* require/import and should return an unique instance for each call
NewModuleInstance(VU) Instance
}

// checks that modules implement HasModuleInstancePerVU
Expand All @@ -92,7 +91,6 @@ func GetJSModules() map[string]interface{} {

// Instance is what a module needs to return
type Instance interface {
InstanceCore
Exports() Exports
}

Expand All @@ -108,21 +106,19 @@ func getInterfaceMethods() []string {
return result
}

// InstanceCore is something that will be provided to modules and they need to embed it in ModuleInstance
type InstanceCore interface {
GetContext() context.Context
// VU gives access to the currently executing VU to a module Instance
type VU interface {
// Context return the context.Context about the current VU
Context() context.Context

// GetInitEnv returns common.InitEnvironment instance if present
GetInitEnv() *common.InitEnvironment
// InitEnv returns common.InitEnvironment instance if present
InitEnv() *common.InitEnvironment

// GetState returns lib.State if any is present
GetState() *lib.State
// State returns lib.State if any is present
State() *lib.State

// GetRuntime returns the goja.Runtime for the current VU
GetRuntime() *goja.Runtime

// sealing field will help probably with pointing users that they just need to embed this in their Instance
// implementations
// Runtime returns the goja.Runtime for the current VU
Runtime() *goja.Runtime
}

// Exports is representation of ESM exports of a module
Expand All @@ -132,44 +128,3 @@ type Exports struct {
// Named is the named exports of a module
Named map[string]interface{}
}

// GenerateExports generates an Exports from a module akin to how common.Bind does now.
// it also skips anything that is expected will not want to be exported such as methods and fields coming from
// interfaces defined in this package.
func GenerateExports(v interface{}) Exports {
exports := make(map[string]interface{})
val := reflect.ValueOf(v)
typ := val.Type()
badNames := getInterfaceMethods()
outer:
for i := 0; i < typ.NumMethod(); i++ {
meth := typ.Method(i)
for _, badname := range badNames {
if meth.Name == badname {
continue outer
}
}
name := common.MethodName(typ, meth)

fn := val.Method(i)
exports[name] = fn.Interface()
}

// If v is a pointer, we need to indirect it to access its fields.
if typ.Kind() == reflect.Ptr {
val = val.Elem()
typ = val.Type()
}
var mic InstanceCore // TODO move this out
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if field.Type == reflect.TypeOf(&mic).Elem() {
continue
}
name := common.FieldName(typ, field)
if name != "" {
exports[name] = val.Field(i).Interface()
}
}
return Exports{Default: exports, Named: exports}
}
Loading