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 Jun 24, 2021
1 parent 2039c56 commit 666c43f
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 21 deletions.
14 changes: 14 additions & 0 deletions js/common/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,17 @@ func Bind(rt *goja.Runtime, v interface{}, ctxPtr *context.Context) map[string]i

return exports
}

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

type Exports struct {
Default interface{}
Others map[string]interface{}
}
30 changes: 30 additions & 0 deletions js/initcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,44 @@ func (i *InitContext) Require(arg string) goja.Value {
}
}

type moduleInstanceImpl struct {
ctxPtr *context.Context
// we can technically put lib.State here
}

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

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

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(&moduleInstanceImpl{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
88 changes: 70 additions & 18 deletions js/modules/k6/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,39 @@ func checkName(name string) bool {
}

type Metric struct {
metric *stats.Metric
metric *stats.Metric
getContext func() context.Context
}

// 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 (m *MetricsModule) newMetric(call goja.ConstructorCall, t stats.MetricType) (*goja.Object, error) {
ctx := m.GetContext()
if lib.GetState(ctx) != 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

// TODO this kind of conversions can possibly be automated by the parts of common.Bind that are curently automating
// it and some wrapping
name := call.Argument(0).String()
isTime := call.Argument(1).ToBoolean()
// 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] {
if isTime {
valueType = stats.Time
}

rt := common.GetRuntime(*ctxPtr)
return common.Bind(rt, Metric{stats.New(name, t, valueType)}, ctxPtr), nil
return rt.ToValue(Metric{metric: stats.New(name, t, valueType), getContext: m.GetContext}).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.getContext()
state := lib.GetState(ctx)
if state == nil {
return false, ErrMetricsAddInInitContext
Expand Down Expand Up @@ -96,24 +103,69 @@ func (m Metric) GetName() string {
return m.metric.Name
}

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

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

func (*Metrics) XCounter(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Counter, isTime)
func New() *RootMetricsModule {
return &RootMetricsModule{}
}

func (*Metrics) XGauge(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Gauge, isTime)
func (m *MetricsModule) GetExports() common.Exports {
return common.Exports{
Default: "this will be our default export",
Others: map[string]interface{}{
"Counter": m.XCounter,
"Gauge": m.XGauge,
"Trend": m.XTrend,
"Rate": m.XRate,

"returnMetricType": m.ReturnMetricType,
},
}
}

func (*Metrics) XTrend(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Trend, 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) XRate(ctx *context.Context, name string, isTime ...bool) (interface{}, error) {
return newMetric(ctx, name, stats.Rate, isTime)
// Counter ... // NOTE we still need to use goja.ConstructorCall somewhere to have access to the
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 (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 (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
}
20 changes: 17 additions & 3 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,9 @@ func TestMetrics(t *testing.T) {

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

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 @@ -86,7 +100,7 @@ func TestMetrics(t *testing.T) {
t.Run("ExitInit", func(t *testing.T) {
*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
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.ModuleInstance) 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 666c43f

Please sign in to comment.