Skip to content

Commit c770934

Browse files
committed
js module: Improved use of Contexts
- 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.
1 parent 88ea081 commit c770934

File tree

13 files changed

+139
-127
lines changed

13 files changed

+139
-127
lines changed

js/js_test.go

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,20 @@ import (
2626
)
2727

2828
func TestSquare(t *testing.T) {
29-
ctx := context.Background()
3029
TestWithVMs(t, func(t *testing.T, vm VM) {
30+
assert.NotNil(t, vm.Context())
3131
service := NewService(vm, "square", `function(n) {return n * n;}`)
3232
assert.Equal(t, "square", service.Name())
3333
assert.Equal(t, vm, service.Host())
3434

3535
// Test Run:
36-
result, err := service.Run(ctx, 13)
36+
result, err := service.Run(vm.Context(), 13)
3737
assert.NoError(t, err)
3838
assert.EqualValues(t, 169, result)
3939

4040
// Test WithRunner:
4141
result, err = service.WithRunner(func(runner Runner) (any, error) {
42-
assert.Nil(t, runner.Context())
43-
assert.NotNil(t, runner.ContextOrDefault())
44-
runner.SetContext(ctx)
45-
assert.Equal(t, ctx, runner.Context())
46-
assert.Equal(t, ctx, runner.ContextOrDefault())
47-
42+
assert.Equal(t, vm.Context(), runner.Context())
4843
return runner.Run(9)
4944
})
5045
assert.NoError(t, err)
@@ -53,7 +48,7 @@ func TestSquare(t *testing.T) {
5348
}
5449

5550
func TestSquareV8Args(t *testing.T) {
56-
vm := V8.NewVM()
51+
vm := V8.NewVM(testCtx(t))
5752
defer vm.Close()
5853

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

7267
func TestJSON(t *testing.T) {
73-
ctx := context.Background()
68+
ctx := testCtx(t)
7469

7570
var pool VMPool
76-
pool.Init(V8, 4)
71+
pool.Init(ctx, V8, 4)
7772
defer pool.Close()
7873

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

9287
func TestCallback(t *testing.T) {
93-
ctx := context.Background()
88+
ctx := testCtx(t)
9489

95-
vm := V8.NewVM()
90+
vm := V8.NewVM(ctx)
9691
defer vm.Close()
9792

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

124119
// Test conversion of numbers into/out of JavaScript.
125120
func TestNumbers(t *testing.T) {
126-
ctx := context.Background()
127-
128121
TestWithVMs(t, func(t *testing.T, vm VM) {
129122
service := NewService(vm, "numbers", `function(n, expectedStr) {
130123
if (typeof(n) != 'number' && typeof(n) != 'bigint') throw "Unexpectedly n is a " + typeof(n);
@@ -136,7 +129,7 @@ func TestNumbers(t *testing.T) {
136129

137130
t.Run("integers", func(t *testing.T) {
138131
testInt := func(n int64) {
139-
result, err := service.Run(ctx, n, strconv.FormatInt(n, 10))
132+
result, err := service.Run(nil, n, strconv.FormatInt(n, 10))
140133
if assert.NoError(t, err) {
141134
assert.EqualValues(t, n, result)
142135
}
@@ -159,7 +152,7 @@ func TestNumbers(t *testing.T) {
159152

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

185178
t.Run("json_Number_integer", func(t *testing.T) {
186179
hugeInt := json.Number("123456789012345")
187-
result, err := service.Run(ctx, hugeInt, string(hugeInt))
180+
result, err := service.Run(nil, hugeInt, string(hugeInt))
188181
if assert.NoError(t, err) {
189182
assert.EqualValues(t, 123456789012345, result)
190183
}
@@ -193,7 +186,7 @@ func TestNumbers(t *testing.T) {
193186
if vm.Engine().languageVersion >= 11 { // (Otto does not support BigInts)
194187
t.Run("json_Number_huge_integer", func(t *testing.T) {
195188
hugeInt := json.Number("1234567890123456789012345678901234567890")
196-
result, err := service.Run(ctx, hugeInt, string(hugeInt))
189+
result, err := service.Run(nil, hugeInt, string(hugeInt))
197190
if assert.NoError(t, err) {
198191
ibig := new(big.Int)
199192
ibig, _ = ibig.SetString(string(hugeInt), 10)
@@ -204,7 +197,7 @@ func TestNumbers(t *testing.T) {
204197

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

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

219-
vm := V8.NewVM() // Otto appears to have no way to refer to the global object...
212+
vm := V8.NewVM(ctx) // Otto appears to have no way to refer to the global object...
220213
defer vm.Close()
221214

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

238231
// Verify that ECMAScript modules can't be loaded. (The older `require` is checked in TestNoIO.)
239232
func TestNoModules(t *testing.T) {
240-
ctx := context.Background()
241-
242-
vm := V8.NewVM() // Otto doesn't support ES modules
233+
ctx := testCtx(t)
234+
vm := V8.NewVM(ctx) // Otto doesn't support ES modules
243235
defer vm.Close()
244236

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

256248
func TestTimeout(t *testing.T) {
257249
TestWithVMs(t, func(t *testing.T, vm VM) {
258-
ctx := context.Background()
250+
ctx := vm.Context()
259251

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

271263
func TestOutOfMemory(t *testing.T) {
272-
vm := V8.NewVM()
264+
ctx := testCtx(t)
265+
vm := V8.NewVM(ctx)
273266
defer vm.Close()
274-
ctx := context.Background()
275267

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

287279
func TestStackOverflow(t *testing.T) {
288-
vm := V8.NewVM()
280+
ctx := testCtx(t)
281+
vm := V8.NewVM(ctx)
289282
defer vm.Close()
290-
ctx := context.Background()
291283

292284
service := NewService(vm, "Overflow", `
293285
function() {

js/otto_runner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func newOttoRunner(vm *ottoVM, service *Service) (*OttoRunner, error) {
6060
extra += str + " "
6161
}
6262

63-
LoggingCallback(r.ContextOrDefault(), LogLevel(ilevel), "%s %s", message, extra)
63+
LoggingCallback(r.Context(), LogLevel(ilevel), "%s %s", message, extra)
6464
return otto.UndefinedValue()
6565
})
6666
if err != nil {

js/otto_vm.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ licenses/APL2.txt.
1111
package js
1212

1313
import (
14+
"context"
1415
"fmt"
1516
"time"
1617
)
@@ -27,10 +28,10 @@ const ottoVMName = "Otto"
2728
var Otto = &Engine{
2829
name: ottoVMName,
2930
languageVersion: 5, // https://github.com/robertkrimen/otto#caveat-emptor
30-
factory: func(engine *Engine, services *servicesConfiguration) VM {
31+
factory: func(ctx context.Context, engine *Engine, services *servicesConfiguration) VM {
3132
return &ottoVM{
32-
baseVM: &baseVM{engine: engine, services: services}, // "superclass"
33-
runners: []*OttoRunner{}, // Cached reusable Runners
33+
baseVM: &baseVM{ctx: ctx, engine: engine, services: services}, // "superclass"
34+
runners: []*OttoRunner{}, // Cached reusable Runners
3435
}
3536
},
3637
}
@@ -107,7 +108,7 @@ func (vm *ottoVM) withRunner(service *Service, fn func(Runner) (any, error)) (an
107108
}
108109

109110
func (vm *ottoVM) returnRunner(r *OttoRunner) {
110-
r.goContext = nil
111+
r.ctx = nil // clear any override
111112
if vm.curRunner == r {
112113
vm.curRunner = nil
113114
} else if r.vm != vm {

js/runner.go

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@ type Runner interface {
2626
// Associates a Go `Context` with this Runner.
2727
// If this Context has a deadline, JS calls will abort if it expires.
2828
SetContext(ctx context.Context)
29-
// The associated `Context`, if you've set one; else nil.
29+
// The associated `Context`. Defaults to the VM's Context.
3030
Context() context.Context
31-
// The associated `Context`, else the default `context.Background()` instance.
32-
ContextOrDefault() context.Context
3331
// Returns the remaining duration until the Context's deadline, or nil if none.
3432
Timeout() *time.Duration
3533
// Runs the Service's JavaScript function.
@@ -44,30 +42,27 @@ type Runner interface {
4442
type baseRunner struct {
4543
id serviceID // The service ID in its VM
4644
vm VM // The owning VM object
47-
goContext context.Context // context.Context value for use by Go callbacks
45+
ctx context.Context // Overrides vm.Context() if non-nil
4846
associated any
4947
}
5048

5149
func (r *baseRunner) VM() VM { return r.vm }
5250
func (r *baseRunner) AssociatedValue() any { return r.associated }
5351
func (r *baseRunner) SetAssociatedValue(obj any) { r.associated = obj }
54-
func (r *baseRunner) SetContext(ctx context.Context) { r.goContext = ctx }
55-
func (r *baseRunner) Context() context.Context { return r.goContext }
52+
func (r *baseRunner) SetContext(ctx context.Context) { r.ctx = ctx }
5653

57-
func (r *baseRunner) ContextOrDefault() context.Context {
58-
if r.goContext != nil {
59-
return r.goContext
54+
func (r *baseRunner) Context() context.Context {
55+
if r.ctx != nil {
56+
return r.ctx
6057
} else {
61-
return context.TODO()
58+
return r.vm.Context()
6259
}
6360
}
6461

6562
func (r *baseRunner) Timeout() *time.Duration {
66-
if r.goContext != nil {
67-
if deadline, hasDeadline := r.goContext.Deadline(); hasDeadline {
68-
timeout := time.Until(deadline)
69-
return &timeout
70-
}
63+
if deadline, hasDeadline := r.Context().Deadline(); hasDeadline {
64+
timeout := time.Until(deadline)
65+
return &timeout
7166
}
7267
return nil
7368
}

js/service.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type serviceID uint32 // internal ID, used as an array index in VM and VMPool.
2828
// A provider of a JavaScript runtime for Services. VM and VMPool implement this.
2929
type ServiceHost interface {
3030
Engine() *Engine
31+
Context() context.Context
3132
Close()
3233
FindService(name string) *Service
3334
registerService(*Service)
@@ -49,7 +50,7 @@ type TemplateFactory func(base *V8BasicTemplate) (V8Template, error)
4950
// The source code should be of the form `function(arg1,arg2…) {…body…; return result;}`.
5051
// If you have a more complex script, like one that defines several functions, use NewCustomService.
5152
func NewService(host ServiceHost, name string, jsFunctionSource string) *Service {
52-
debug(context.Background(), "Creating JavaScript service %q", name)
53+
debug(host.Context(), "Creating JavaScript service %q", name)
5354
service := &Service{
5455
host: host,
5556
name: name,
@@ -82,7 +83,7 @@ func (service *Service) Host() ServiceHost { return service.host }
8283
// - If the host is a VM, this call will fail if there is another Runner in use belonging to any
8384
// Service hosted by that VM.
8485
func (service *Service) GetRunner() (Runner, error) {
85-
debug(context.Background(), "Running JavaScript service %q", service.name)
86+
debug(service.host.Context(), "Running JavaScript service %q", service.name)
8687
return service.host.getRunner(service)
8788
}
8889

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

9596
// A high-level method that runs a service in a VM without your needing to interact with a Runner.
96-
// The arguments can be Go types or V8 Values; any types supported by VM.NewValue.
97+
// The `ctx` parameter may be nil, to use the VM's Context.
98+
// The arguments can be Go types or V8 Values: any types supported by VM.NewValue.
9799
// The result is converted back to a Go type.
98100
// If the function throws a JavaScript exception it's converted to a Go `error`.
99101
func (service *Service) Run(ctx context.Context, args ...any) (any, error) {
100102
return service.WithRunner(func(runner Runner) (any, error) {
101-
runner.SetContext(ctx)
103+
if ctx != nil {
104+
runner.SetContext(ctx)
105+
}
102106
return runner.Run(args...)
103107
})
104108
}

js/test_utils.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,27 @@ licenses/APL2.txt.
1010

1111
package js
1212

13-
import "testing"
13+
import (
14+
"context"
15+
"testing"
16+
)
17+
18+
// testCtx creates a context for the given test which is also cancelled once the test has completed.
19+
func testCtx(t testing.TB) context.Context {
20+
ctx, cancelCtx := context.WithCancel(context.TODO())
21+
t.Cleanup(cancelCtx)
22+
return ctx
23+
}
1424

1525
// Unit-test utility. Calls the function with each supported type of VM (Otto and V8).
1626
func TestWithVMs(t *testing.T, fn func(t *testing.T, vm VM)) {
1727
for _, engine := range testEngines {
1828
t.Run(engine.String(), func(t *testing.T) {
19-
vm := engine.NewVM()
20-
defer vm.Close()
29+
ctx, cancelCtx := context.WithCancel(context.TODO())
30+
vm := engine.NewVM(ctx)
2131
fn(t, vm)
32+
vm.Close()
33+
cancelCtx()
2234
})
2335
}
2436
}
@@ -28,9 +40,11 @@ func TestWithVMs(t *testing.T, fn func(t *testing.T, vm VM)) {
2840
func TestWithVMPools(t *testing.T, maxVMs int, fn func(t *testing.T, pool *VMPool)) {
2941
for _, engine := range testEngines {
3042
t.Run(engine.String(), func(t *testing.T) {
31-
pool := NewVMPool(engine, maxVMs)
32-
defer pool.Close()
43+
ctx, cancelCtx := context.WithCancel(context.TODO())
44+
pool := NewVMPool(ctx, engine, maxVMs)
3345
fn(t, pool)
46+
pool.Close()
47+
cancelCtx()
3448
})
3549
}
3650
}

0 commit comments

Comments
 (0)