Skip to content

Commit

Permalink
Basic implementation of variant 3 from #1802
Browse files Browse the repository at this point in the history
This goes all the way and tries (unfortunately not very well, I will try
again) to make the user embed the ModuleInstance it gets in the return
module so that it always has access to the Context and w/e else we
decide to add to it.

I also decided to force some esm along it (this is not required) to
test out some ideas.
  • Loading branch information
mstoykov committed Aug 5, 2021
1 parent a733b30 commit 717cdda
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 38 deletions.
45 changes: 45 additions & 0 deletions js/common/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,48 @@ func Bind(rt *goja.Runtime, v interface{}, ctxPtr *context.Context) map[string]i

return exports
}

// TODO move this
// ModuleInstance is what a module needs to return
type ModuleInstance interface {
ModuleInstanceCore
GetExports() Exports
}

// ModuleInstanceCore is something that will be provided to modules and they need to embed it in ModuleInstance
type ModuleInstanceCore interface {
// we can add other methods here
// sealing field will help probably with pointing users that they just need to embed this in the
GetContext() context.Context
}

type Exports struct {
Default interface{}
Others map[string]interface{}
}

func GenerateExports(v interface{}) Exports {
exports := make(map[string]interface{})
val := reflect.ValueOf(v)
typ := val.Type()
for i := 0; i < typ.NumMethod(); i++ {
meth := typ.Method(i)
name := MethodName(typ, meth)
fn := val.Method(i)
exports[name] = fn.Interface()
}

// If v is a pointer, we need to indirect it to access fields.
if typ.Kind() == reflect.Ptr {
val = val.Elem()
typ = val.Type()
}
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
name := FieldName(typ, field)
if name != "" {
exports[name] = val.Field(i).Interface()
}
}
return Exports{Default: exports, Others: exports}
}
26 changes: 26 additions & 0 deletions js/initcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,40 @@ func (i *InitContext) Require(arg string) goja.Value {
}
}

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

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

func toEsModuleexports(exp common.Exports) map[string]interface{} {
result := make(map[string]interface{}, len(exp.Others)+2)

for k, v := range exp.Others {
result[k] = v
}
// Maybe check that those weren't set
result["default"] = exp.Default
result["__esModule"] = true // this so babel works with
return result
}

func (i *InitContext) requireModule(name string) (goja.Value, error) {
mod, ok := i.modules[name]
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 perInstance, ok := mod.(modules.HasModuleInstancePerVU); ok {
mod = perInstance.NewModuleInstancePerVU()
}

return i.runtime.ToValue(common.Bind(i.runtime, mod, i.ctxPtr)), nil
}

Expand Down
111 changes: 79 additions & 32 deletions js/modules/k6/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
package metrics

import (
"context"
"errors"
"fmt"
"regexp"
Expand All @@ -44,40 +43,52 @@ func checkName(name string) bool {

type Metric struct {
metric *stats.Metric
core common.ModuleInstanceCore
}

// 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 newMetric(ctxPtr *context.Context, name string, t stats.MetricType, isTime []bool) (interface{}, error) {
if lib.GetState(*ctxPtr) != nil {
func (mm *MetricsModule) newMetric(call goja.ConstructorCall, t stats.MetricType) (*goja.Object, error) {
ctx := mm.GetContext()
initEnv := common.GetInitEnv(ctx)
if initEnv == nil {
return nil, errors.New("metrics must be declared in the init context")
}
rt := common.GetRuntime(ctx) // NOTE we can get this differently as well
c, _ := goja.AssertFunction(rt.ToValue(func(name string, isTime ...bool) (*goja.Object, error) {
// TODO: move verification outside the JS
if !checkName(name) {
return nil, common.NewInitContextError(fmt.Sprintf("Invalid metric name: '%s'", name))
}

// TODO: move verification outside the JS
if !checkName(name) {
return nil, common.NewInitContextError(fmt.Sprintf("Invalid metric name: '%s'", name))
}

valueType := stats.Default
if len(isTime) > 0 && isTime[0] {
valueType = stats.Time
}
valueType := stats.Default
if len(isTime) > 0 && isTime[0] {
valueType = stats.Time
}
m := stats.New(name, t, valueType)

rt := common.GetRuntime(*ctxPtr)
bound := common.Bind(rt, Metric{stats.New(name, t, valueType)}, ctxPtr)
o := rt.NewObject()
err := o.DefineDataProperty("name", rt.ToValue(name), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE)
metric := &Metric{metric: m, core: mm.ModuleInstanceCore}
o := rt.NewObject()
err := o.DefineDataProperty("name", rt.ToValue(name), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE)
if err != nil {
return nil, err
}
if err = o.Set("add", rt.ToValue(metric.Add)); err != nil {
return nil, err
}
return o, nil
}))
v, err := c(call.This, call.Arguments...)
if err != nil {
return nil, err
}
if err = o.Set("add", rt.ToValue(bound["add"])); err != nil {
return nil, err
}
return o, nil

return v.ToObject(rt), nil
}

func (m Metric) Add(ctx context.Context, v goja.Value, addTags ...map[string]string) (bool, error) {
func (m Metric) Add(v goja.Value, addTags ...map[string]string) (bool, error) {
ctx := m.core.GetContext()
state := lib.GetState(ctx)
if state == nil {
return false, ErrMetricsAddInInitContext
Expand All @@ -100,24 +111,60 @@ func (m Metric) Add(ctx context.Context, v goja.Value, addTags ...map[string]str
return true, nil
}

type Metrics struct{}
type (
RootMetricsModule struct{}
MetricsModule struct {
common.ModuleInstanceCore
}
)

func (*RootMetricsModule) NewModuleInstance(m common.ModuleInstanceCore) common.ModuleInstance {
return &MetricsModule{ModuleInstanceCore: m}
}

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

func New() *Metrics {
return &Metrics{}
func (m *MetricsModule) GetExports() common.Exports {
return common.GenerateExports(m)
}

func (*Metrics) XCounter(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Counter, isTime)
// This is not possible after common.Bind as it wraps the object and doesn't return the original one.
func (m *MetricsModule) ReturnMetricType(metric Metric) string {
return metric.metric.Type.String()
}

func (*Metrics) XGauge(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Gauge, isTime)
// Counter ... // NOTE we still need to use goja.ConstructorCall somewhere to have automatic constructor support by
// goja
func (m *MetricsModule) XCounter(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object {
v, err := m.newMetric(call, stats.Counter)
if err != nil {
common.Throw(rt, err)
}
return v
}

func (*Metrics) XTrend(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Trend, isTime)
func (m *MetricsModule) XGauge(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object {
v, err := m.newMetric(call, stats.Gauge)
if err != nil {
common.Throw(rt, err)
}
return v
}

func (*Metrics) XRate(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Rate, isTime)
func (m *MetricsModule) XTrend(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object {
v, err := m.newMetric(call, stats.Trend)
if err != nil {
common.Throw(rt, err)
}
return v
}

func (m *MetricsModule) XRate(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object {
v, err := m.newMetric(call, stats.Rate)
if err != nil {
common.Throw(rt, err)
}
return v
}
30 changes: 24 additions & 6 deletions js/modules/k6/metrics/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ import (
"go.k6.io/k6/stats"
)

// this probably should be done through some test package
type moduleInstanceImpl struct {
ctxPtr *context.Context
}

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

func (m *moduleInstanceImpl) GetExports() common.Exports {
panic("this needs to be implemented by the module")
}

func TestMetrics(t *testing.T) {
t.Parallel()
types := map[string]stats.MetricType{
Expand Down Expand Up @@ -64,8 +77,10 @@ func TestMetrics(t *testing.T) {

ctxPtr := new(context.Context)
*ctxPtr = common.WithRuntime(context.Background(), rt)
rt.Set("metrics", common.Bind(rt, New(), ctxPtr))

*ctxPtr = common.WithInitEnv(*ctxPtr, &common.InitEnvironment{})
m, ok := New().NewModuleInstance(&moduleInstanceImpl{ctxPtr: ctxPtr}).(*MetricsModule)
require.True(t, ok)
rt.Set("metrics", m.GetExports().Others) // This also should probably be done by some test package
root, _ := lib.NewGroup("", nil)
child, _ := root.Group("child")
samples := make(chan stats.SampleContainer, 1000)
Expand All @@ -84,9 +99,10 @@ func TestMetrics(t *testing.T) {
require.NoError(t, err)

t.Run("ExitInit", func(t *testing.T) {
*ctxPtr = common.WithRuntime(context.Background(), rt)
*ctxPtr = lib.WithState(*ctxPtr, state)
_, err := rt.RunString(fmt.Sprintf(`new metrics.%s("my_metric")`, fn))
assert.EqualError(t, err, "metrics must be declared in the init context at apply (native)")
assert.Contains(t, err.Error(), "metrics must be declared in the init context")
})

groups := map[string]*lib.Group{
Expand Down Expand Up @@ -175,9 +191,11 @@ func TestMetricGetName(t *testing.T) {
rt := goja.New()
rt.SetFieldNameMapper(common.FieldNameMapper{})

ctxPtr := new(context.Context)
*ctxPtr = common.WithRuntime(context.Background(), rt)
require.NoError(t, rt.Set("metrics", common.Bind(rt, New(), ctxPtr)))
ctx := common.WithRuntime(context.Background(), rt)
ctx = common.WithInitEnv(ctx, &common.InitEnvironment{})
m, ok := New().NewModuleInstance(&moduleInstanceImpl{ctxPtr: &ctx}).(*MetricsModule)
require.True(t, ok)
rt.Set("metrics", m.GetExports().Others) // This also should probably be done by some test package
v, err := rt.RunString(`
var m = new metrics.Counter("my_metric")
m.name
Expand Down
6 changes: 6 additions & 0 deletions js/modules/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"strings"
"sync"

"go.k6.io/k6/js/common"
"go.k6.io/k6/js/modules/k6"
"go.k6.io/k6/js/modules/k6/crypto"
"go.k6.io/k6/js/modules/k6/crypto/x509"
Expand Down Expand Up @@ -69,6 +70,11 @@ type HasModuleInstancePerVU interface {
NewModuleInstancePerVU() interface{}
}

// IsModuleV2 ... TODO better name
type IsModuleV2 interface { // TODO rename?
NewModuleInstance(common.ModuleInstanceCore) common.ModuleInstance
}

// checks that modules implement HasModuleInstancePerVU
// this is done here as otherwise there will be a loop if the module imports this package
var _ HasModuleInstancePerVU = http.New()
Expand Down

0 comments on commit 717cdda

Please sign in to comment.