Skip to content

Commit

Permalink
js module: Improved use of Contexts
Browse files Browse the repository at this point in the history
- VM and VMPool now have an associated Context, which must be given when
  creating one. (VMs created by VMPool inherit its Context.)
- VM and VMPool now have a Context() method.
- Runner.Context() now returns the VM's Context by default, but calling
  Runner.SetContext() overrides it until the Runner is returned.
- Removed Runner.ContextOrDefault(); use Context() instead.
- The `ctx` parameter to Service.Run() may be nil, in which case the default
  Context (the VM's) is used.
- Tests use a new testCtx() function that's similar to the one in SG.base.
  It creates a Context derived from context.TODO that cancels when the test
  exits.

Some renaming to avoid confusion:
- Renamed v8Runner.ctx to v8ctx.
- Renamed baseRunner.goContext to ctx.
- Renamed other variables of type *v8.Context to v8ctx.
  • Loading branch information
snej committed Jun 20, 2023
1 parent 88ea081 commit c770934
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 127 deletions.
52 changes: 22 additions & 30 deletions js/js_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,20 @@ import (
)

func TestSquare(t *testing.T) {
ctx := context.Background()
TestWithVMs(t, func(t *testing.T, vm VM) {
assert.NotNil(t, vm.Context())
service := NewService(vm, "square", `function(n) {return n * n;}`)
assert.Equal(t, "square", service.Name())
assert.Equal(t, vm, service.Host())

// Test Run:
result, err := service.Run(ctx, 13)
result, err := service.Run(vm.Context(), 13)
assert.NoError(t, err)
assert.EqualValues(t, 169, result)

// Test WithRunner:
result, err = service.WithRunner(func(runner Runner) (any, error) {
assert.Nil(t, runner.Context())
assert.NotNil(t, runner.ContextOrDefault())
runner.SetContext(ctx)
assert.Equal(t, ctx, runner.Context())
assert.Equal(t, ctx, runner.ContextOrDefault())

assert.Equal(t, vm.Context(), runner.Context())
return runner.Run(9)
})
assert.NoError(t, err)
Expand All @@ -53,7 +48,7 @@ func TestSquare(t *testing.T) {
}

func TestSquareV8Args(t *testing.T) {
vm := V8.NewVM()
vm := V8.NewVM(testCtx(t))
defer vm.Close()

service := NewService(vm, "square", `function(n) {return n * n;}`)
Expand All @@ -70,10 +65,10 @@ func TestSquareV8Args(t *testing.T) {
}

func TestJSON(t *testing.T) {
ctx := context.Background()
ctx := testCtx(t)

var pool VMPool
pool.Init(V8, 4)
pool.Init(ctx, V8, 4)
defer pool.Close()

service := NewService(&pool, "length", `function(v) {return v.length;}`)
Expand All @@ -90,9 +85,9 @@ func TestJSON(t *testing.T) {
}

func TestCallback(t *testing.T) {
ctx := context.Background()
ctx := testCtx(t)

vm := V8.NewVM()
vm := V8.NewVM(ctx)
defer vm.Close()

src := `(function() {
Expand Down Expand Up @@ -123,8 +118,6 @@ func TestCallback(t *testing.T) {

// Test conversion of numbers into/out of JavaScript.
func TestNumbers(t *testing.T) {
ctx := context.Background()

TestWithVMs(t, func(t *testing.T, vm VM) {
service := NewService(vm, "numbers", `function(n, expectedStr) {
if (typeof(n) != 'number' && typeof(n) != 'bigint') throw "Unexpectedly n is a " + typeof(n);
Expand All @@ -136,7 +129,7 @@ func TestNumbers(t *testing.T) {

t.Run("integers", func(t *testing.T) {
testInt := func(n int64) {
result, err := service.Run(ctx, n, strconv.FormatInt(n, 10))
result, err := service.Run(nil, n, strconv.FormatInt(n, 10))
if assert.NoError(t, err) {
assert.EqualValues(t, n, result)
}
Expand All @@ -159,7 +152,7 @@ func TestNumbers(t *testing.T) {

t.Run("floats", func(t *testing.T) {
testFloat := func(n float64) {
result, err := service.Run(ctx, n, strconv.FormatFloat(n, 'f', -1, 64))
result, err := service.Run(nil, n, strconv.FormatFloat(n, 'f', -1, 64))
if assert.NoError(t, err) {
assert.EqualValues(t, n, result)
}
Expand All @@ -184,7 +177,7 @@ func TestNumbers(t *testing.T) {

t.Run("json_Number_integer", func(t *testing.T) {
hugeInt := json.Number("123456789012345")
result, err := service.Run(ctx, hugeInt, string(hugeInt))
result, err := service.Run(nil, hugeInt, string(hugeInt))
if assert.NoError(t, err) {
assert.EqualValues(t, 123456789012345, result)
}
Expand All @@ -193,7 +186,7 @@ func TestNumbers(t *testing.T) {
if vm.Engine().languageVersion >= 11 { // (Otto does not support BigInts)
t.Run("json_Number_huge_integer", func(t *testing.T) {
hugeInt := json.Number("1234567890123456789012345678901234567890")
result, err := service.Run(ctx, hugeInt, string(hugeInt))
result, err := service.Run(nil, hugeInt, string(hugeInt))
if assert.NoError(t, err) {
ibig := new(big.Int)
ibig, _ = ibig.SetString(string(hugeInt), 10)
Expand All @@ -204,7 +197,7 @@ func TestNumbers(t *testing.T) {

t.Run("json_Number_float", func(t *testing.T) {
floatStr := json.Number("1234567890.123")
result, err := service.Run(ctx, floatStr, string(floatStr))
result, err := service.Run(nil, floatStr, string(floatStr))
if assert.NoError(t, err) {
assert.EqualValues(t, 1234567890.123, result)
}
Expand All @@ -214,9 +207,9 @@ func TestNumbers(t *testing.T) {

// For security purposes, verify that JS APIs to do network or file I/O are not present:
func TestNoIO(t *testing.T) {
ctx := context.Background()
ctx := testCtx(t)

vm := V8.NewVM() // Otto appears to have no way to refer to the global object...
vm := V8.NewVM(ctx) // Otto appears to have no way to refer to the global object...
defer vm.Close()

service := NewService(vm, "check", `function() {
Expand All @@ -237,9 +230,8 @@ func TestNoIO(t *testing.T) {

// Verify that ECMAScript modules can't be loaded. (The older `require` is checked in TestNoIO.)
func TestNoModules(t *testing.T) {
ctx := context.Background()

vm := V8.NewVM() // Otto doesn't support ES modules
ctx := testCtx(t)
vm := V8.NewVM(ctx) // Otto doesn't support ES modules
defer vm.Close()

src := `import foo from 'foo';
Expand All @@ -255,7 +247,7 @@ func TestNoModules(t *testing.T) {

func TestTimeout(t *testing.T) {
TestWithVMs(t, func(t *testing.T, vm VM) {
ctx := context.Background()
ctx := vm.Context()

ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
Expand All @@ -269,9 +261,9 @@ func TestTimeout(t *testing.T) {
}

func TestOutOfMemory(t *testing.T) {
vm := V8.NewVM()
ctx := testCtx(t)
vm := V8.NewVM(ctx)
defer vm.Close()
ctx := context.Background()

service := NewService(vm, "OOM", `
function() {
Expand All @@ -285,9 +277,9 @@ func TestOutOfMemory(t *testing.T) {
}

func TestStackOverflow(t *testing.T) {
vm := V8.NewVM()
ctx := testCtx(t)
vm := V8.NewVM(ctx)
defer vm.Close()
ctx := context.Background()

service := NewService(vm, "Overflow", `
function() {
Expand Down
2 changes: 1 addition & 1 deletion js/otto_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func newOttoRunner(vm *ottoVM, service *Service) (*OttoRunner, error) {
extra += str + " "
}

LoggingCallback(r.ContextOrDefault(), LogLevel(ilevel), "%s %s", message, extra)
LoggingCallback(r.Context(), LogLevel(ilevel), "%s %s", message, extra)
return otto.UndefinedValue()
})
if err != nil {
Expand Down
9 changes: 5 additions & 4 deletions js/otto_vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ licenses/APL2.txt.
package js

import (
"context"
"fmt"
"time"
)
Expand All @@ -27,10 +28,10 @@ const ottoVMName = "Otto"
var Otto = &Engine{
name: ottoVMName,
languageVersion: 5, // https://github.com/robertkrimen/otto#caveat-emptor
factory: func(engine *Engine, services *servicesConfiguration) VM {
factory: func(ctx context.Context, engine *Engine, services *servicesConfiguration) VM {
return &ottoVM{
baseVM: &baseVM{engine: engine, services: services}, // "superclass"
runners: []*OttoRunner{}, // Cached reusable Runners
baseVM: &baseVM{ctx: ctx, engine: engine, services: services}, // "superclass"
runners: []*OttoRunner{}, // Cached reusable Runners
}
},
}
Expand Down Expand Up @@ -107,7 +108,7 @@ func (vm *ottoVM) withRunner(service *Service, fn func(Runner) (any, error)) (an
}

func (vm *ottoVM) returnRunner(r *OttoRunner) {
r.goContext = nil
r.ctx = nil // clear any override
if vm.curRunner == r {
vm.curRunner = nil
} else if r.vm != vm {
Expand Down
25 changes: 10 additions & 15 deletions js/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ type Runner interface {
// Associates a Go `Context` with this Runner.
// If this Context has a deadline, JS calls will abort if it expires.
SetContext(ctx context.Context)
// The associated `Context`, if you've set one; else nil.
// The associated `Context`. Defaults to the VM's Context.
Context() context.Context
// The associated `Context`, else the default `context.Background()` instance.
ContextOrDefault() context.Context
// Returns the remaining duration until the Context's deadline, or nil if none.
Timeout() *time.Duration
// Runs the Service's JavaScript function.
Expand All @@ -44,30 +42,27 @@ type Runner interface {
type baseRunner struct {
id serviceID // The service ID in its VM
vm VM // The owning VM object
goContext context.Context // context.Context value for use by Go callbacks
ctx context.Context // Overrides vm.Context() if non-nil
associated any
}

func (r *baseRunner) VM() VM { return r.vm }
func (r *baseRunner) AssociatedValue() any { return r.associated }
func (r *baseRunner) SetAssociatedValue(obj any) { r.associated = obj }
func (r *baseRunner) SetContext(ctx context.Context) { r.goContext = ctx }
func (r *baseRunner) Context() context.Context { return r.goContext }
func (r *baseRunner) SetContext(ctx context.Context) { r.ctx = ctx }

func (r *baseRunner) ContextOrDefault() context.Context {
if r.goContext != nil {
return r.goContext
func (r *baseRunner) Context() context.Context {
if r.ctx != nil {
return r.ctx
} else {
return context.TODO()
return r.vm.Context()
}
}

func (r *baseRunner) Timeout() *time.Duration {
if r.goContext != nil {
if deadline, hasDeadline := r.goContext.Deadline(); hasDeadline {
timeout := time.Until(deadline)
return &timeout
}
if deadline, hasDeadline := r.Context().Deadline(); hasDeadline {
timeout := time.Until(deadline)
return &timeout
}
return nil
}
Expand Down
12 changes: 8 additions & 4 deletions js/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type serviceID uint32 // internal ID, used as an array index in VM and VMPool.
// A provider of a JavaScript runtime for Services. VM and VMPool implement this.
type ServiceHost interface {
Engine() *Engine
Context() context.Context
Close()
FindService(name string) *Service
registerService(*Service)
Expand All @@ -49,7 +50,7 @@ type TemplateFactory func(base *V8BasicTemplate) (V8Template, error)
// The source code should be of the form `function(arg1,arg2…) {…body…; return result;}`.
// If you have a more complex script, like one that defines several functions, use NewCustomService.
func NewService(host ServiceHost, name string, jsFunctionSource string) *Service {
debug(context.Background(), "Creating JavaScript service %q", name)
debug(host.Context(), "Creating JavaScript service %q", name)
service := &Service{
host: host,
name: name,
Expand Down Expand Up @@ -82,7 +83,7 @@ func (service *Service) Host() ServiceHost { return service.host }
// - If the host is a VM, this call will fail if there is another Runner in use belonging to any
// Service hosted by that VM.
func (service *Service) GetRunner() (Runner, error) {
debug(context.Background(), "Running JavaScript service %q", service.name)
debug(service.host.Context(), "Running JavaScript service %q", service.name)
return service.host.getRunner(service)
}

Expand All @@ -93,12 +94,15 @@ func (service *Service) WithRunner(fn func(Runner) (any, error)) (any, error) {
}

// A high-level method that runs a service in a VM without your needing to interact with a Runner.
// The arguments can be Go types or V8 Values; any types supported by VM.NewValue.
// The `ctx` parameter may be nil, to use the VM's Context.
// The arguments can be Go types or V8 Values: any types supported by VM.NewValue.
// The result is converted back to a Go type.
// If the function throws a JavaScript exception it's converted to a Go `error`.
func (service *Service) Run(ctx context.Context, args ...any) (any, error) {
return service.WithRunner(func(runner Runner) (any, error) {
runner.SetContext(ctx)
if ctx != nil {
runner.SetContext(ctx)
}
return runner.Run(args...)
})
}
24 changes: 19 additions & 5 deletions js/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,27 @@ licenses/APL2.txt.

package js

import "testing"
import (
"context"
"testing"
)

// testCtx creates a context for the given test which is also cancelled once the test has completed.
func testCtx(t testing.TB) context.Context {
ctx, cancelCtx := context.WithCancel(context.TODO())
t.Cleanup(cancelCtx)
return ctx
}

// Unit-test utility. Calls the function with each supported type of VM (Otto and V8).
func TestWithVMs(t *testing.T, fn func(t *testing.T, vm VM)) {
for _, engine := range testEngines {
t.Run(engine.String(), func(t *testing.T) {
vm := engine.NewVM()
defer vm.Close()
ctx, cancelCtx := context.WithCancel(context.TODO())
vm := engine.NewVM(ctx)
fn(t, vm)
vm.Close()
cancelCtx()
})
}
}
Expand All @@ -28,9 +40,11 @@ func TestWithVMs(t *testing.T, fn func(t *testing.T, vm VM)) {
func TestWithVMPools(t *testing.T, maxVMs int, fn func(t *testing.T, pool *VMPool)) {
for _, engine := range testEngines {
t.Run(engine.String(), func(t *testing.T) {
pool := NewVMPool(engine, maxVMs)
defer pool.Close()
ctx, cancelCtx := context.WithCancel(context.TODO())
pool := NewVMPool(ctx, engine, maxVMs)
fn(t, pool)
pool.Close()
cancelCtx()
})
}
}
Loading

0 comments on commit c770934

Please sign in to comment.