Skip to content

Commit

Permalink
add Compiled.Clone to make them safe for concurrent execution (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
d5 authored Mar 8, 2019
1 parent e93f6f6 commit b7977a4
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 28 deletions.
30 changes: 29 additions & 1 deletion docs/interoperability.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Type Conversion Table](#type-conversion-table)
- [User Types](#user-types)
- [Sandbox Environments](#sandbox-environments)
- [Concurrency](#concurrency)
- [Compiler and VM](#compiler-and-vm)

## Using Scripts
Expand Down Expand Up @@ -187,7 +188,34 @@ Sets the maximum byte-length of string values. This limit applies to all running

#### tengo.MaxBytesLen

Sets the maximum length of bytes values. This limit applies to all running VM instances in the process. Also it's not recommended to set or update this value while any VM is executing.
Sets the maximum length of bytes values. This limit applies to all running VM instances in the process. Also it's not recommended to set or update this value while any VM is executing.

## Concurrency

A compiled script (`script.Compiled`) can be used to run the code multiple times by a goroutine. If you want to run the compiled script by multiple goroutine, you should use `Compiled.Clone` function to make a copy of Compiled instances.

#### Compiled.Clone()

Clone creates a new copy of Compiled instance. Cloned copies are safe for concurrent use by multiple goroutines.

```golang
for i := 0; i < concurrency; i++ {
go func(compiled *script.Compiled) {
// inputs
_ = compiled.Set("a", rand.Intn(10))
_ = compiled.Set("b", rand.Intn(10))
_ = compiled.Set("c", rand.Intn(10))

if err := compiled.Run(); err != nil {
panic(err)
}

// outputs
d = compiled.Get("d").Int()
e = compiled.Get("e").Int()
}(compiled.Clone()) // Pass the cloned copy of Compiled
}
```

## Compiler and VM

Expand Down
100 changes: 75 additions & 25 deletions script/compiled.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package script
import (
"context"
"fmt"
"sync"

"github.com/d5/tengo/compiler"
"github.com/d5/tengo/objects"
Expand All @@ -12,26 +13,41 @@ import (
// Compiled is a compiled instance of the user script.
// Use Script.Compile() to create Compiled object.
type Compiled struct {
symbolTable *compiler.SymbolTable
machine *runtime.VM
globalIndexes map[string]int // global symbol name to index
bytecode *compiler.Bytecode
globals []*objects.Object
builtinFunctions []objects.Object
builtinModules map[string]*objects.Object
maxAllocs int64
lock sync.RWMutex
}

// Run executes the compiled script in the virtual machine.
func (c *Compiled) Run() error {
return c.machine.Run()
c.lock.Lock()
defer c.lock.Unlock()

v := runtime.NewVM(c.bytecode, c.globals, c.builtinFunctions, c.builtinModules, c.maxAllocs)

return v.Run()
}

// RunContext is like Run but includes a context.
func (c *Compiled) RunContext(ctx context.Context) (err error) {
c.lock.Lock()
defer c.lock.Unlock()

v := runtime.NewVM(c.bytecode, c.globals, c.builtinFunctions, c.builtinModules, c.maxAllocs)

ch := make(chan error, 1)

go func() {
ch <- c.machine.Run()
ch <- v.Run()
}()

select {
case <-ctx.Done():
c.machine.Abort()
v.Abort()
<-ch
err = ctx.Err()
case err = <-ch:
Expand All @@ -40,14 +56,42 @@ func (c *Compiled) RunContext(ctx context.Context) (err error) {
return
}

// Clone creates a new copy of Compiled.
// Cloned copies are safe for concurrent use by multiple goroutines.
func (c *Compiled) Clone() *Compiled {
c.lock.Lock()
defer c.lock.Unlock()

clone := &Compiled{
globalIndexes: c.globalIndexes,
bytecode: c.bytecode,
globals: make([]*objects.Object, len(c.globals)),
builtinFunctions: c.builtinFunctions,
builtinModules: c.builtinModules,
maxAllocs: c.maxAllocs,
}

// copy global objects
for idx, g := range c.globals {
if g != nil {
clone.globals[idx] = objectPtr((*g).Copy())
}
}

return clone
}

// IsDefined returns true if the variable name is defined (has value) before or after the execution.
func (c *Compiled) IsDefined(name string) bool {
symbol, _, ok := c.symbolTable.Resolve(name)
c.lock.RLock()
defer c.lock.RUnlock()

idx, ok := c.globalIndexes[name]
if !ok {
return false
}

v := c.machine.Globals()[symbol.Index]
v := c.globals[idx]
if v == nil {
return false
}
Expand All @@ -57,11 +101,13 @@ func (c *Compiled) IsDefined(name string) bool {

// Get returns a variable identified by the name.
func (c *Compiled) Get(name string) *Variable {
c.lock.RLock()
defer c.lock.RUnlock()

value := &objects.UndefinedValue

symbol, _, ok := c.symbolTable.Resolve(name)
if ok && symbol.Scope == compiler.ScopeGlobal {
value = c.machine.Globals()[symbol.Index]
if idx, ok := c.globalIndexes[name]; ok {
value = c.globals[idx]
if value == nil {
value = &objects.UndefinedValue
}
Expand All @@ -75,20 +121,21 @@ func (c *Compiled) Get(name string) *Variable {

// GetAll returns all the variables that are defined by the compiled script.
func (c *Compiled) GetAll() []*Variable {
c.lock.RLock()
defer c.lock.RUnlock()

var vars []*Variable
for _, name := range c.symbolTable.Names() {
symbol, _, ok := c.symbolTable.Resolve(name)
if ok && symbol.Scope == compiler.ScopeGlobal {
value := c.machine.Globals()[symbol.Index]
if value == nil {
value = &objects.UndefinedValue
}

vars = append(vars, &Variable{
name: name,
value: value,
})

for name, idx := range c.globalIndexes {
value := c.globals[idx]
if value == nil {
value = &objects.UndefinedValue
}

vars = append(vars, &Variable{
name: name,
value: value,
})
}

return vars
Expand All @@ -97,17 +144,20 @@ func (c *Compiled) GetAll() []*Variable {
// Set replaces the value of a global variable identified by the name.
// An error will be returned if the name was not defined during compilation.
func (c *Compiled) Set(name string, value interface{}) error {
c.lock.Lock()
defer c.lock.Unlock()

obj, err := objects.FromInterface(value)
if err != nil {
return err
}

symbol, _, ok := c.symbolTable.Resolve(name)
if !ok || symbol.Scope != compiler.ScopeGlobal {
idx, ok := c.globalIndexes[name]
if !ok {
return fmt.Errorf("'%s' is not defined", name)
}

c.machine.Globals()[symbol.Index] = &obj
c.globals[idx] = &obj

return nil
}
17 changes: 15 additions & 2 deletions script/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ func (s *Script) Compile() (*Compiled, error) {
// reduce globals size
globals = globals[:symbolTable.MaxSymbols()+1]

// global symbol names to indexes
globalIndexes := make(map[string]int, len(globals))
for _, name := range symbolTable.Names() {
symbol, _, _ := symbolTable.Resolve(name)
if symbol.Scope == compiler.ScopeGlobal {
globalIndexes[name] = symbol.Index
}
}

// remove duplicates from constants
bytecode := c.Bytecode()
bytecode.RemoveDuplicates()
Expand All @@ -141,8 +150,12 @@ func (s *Script) Compile() (*Compiled, error) {
}

return &Compiled{
symbolTable: symbolTable,
machine: runtime.NewVM(bytecode, globals, s.builtinFuncs, s.builtinModules, s.maxAllocs),
globalIndexes: globalIndexes,
bytecode: bytecode,
globals: globals,
builtinFunctions: s.builtinFuncs,
builtinModules: s.builtinModules,
maxAllocs: s.maxAllocs,
}, nil
}

Expand Down
110 changes: 110 additions & 0 deletions script/script_concurrency_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package script_test

import (
"math/rand"
"sync"
"testing"
"time"

"github.com/d5/tengo/assert"
"github.com/d5/tengo/objects"
"github.com/d5/tengo/script"
)

func TestScriptConcurrency(t *testing.T) {
solve := func(a, b, c int) (d, e int) {
a += 2
b += c
a += b * 2
d = a + b + c
e = 0
for i := 1; i <= d; i++ {
e += i
}
e *= 2
return
}

code := []byte(`
mod1 := import("mod1")
a += 2
b += c
a += b * 2
arr := [a, b, c]
arrstr := stringify(arr)
map := {a: a, b: b, c: c}
d := a + b + c
s := 0
for i:=1; i<=d; i++ {
s += i
}
e := mod1.double(s)
`)
mod1 := &objects.ImmutableMap{
Value: map[string]objects.Object{
"double": &objects.UserFunction{
Value: func(args ...objects.Object) (ret objects.Object, err error) {
arg0, _ := objects.ToInt64(args[0])
ret = &objects.Int{Value: arg0 * 2}
return
},
},
},
}

scr := script.New(code)
_ = scr.Add("a", 0)
_ = scr.Add("b", 0)
_ = scr.Add("c", 0)
scr.SetBuiltinModules(map[string]*objects.ImmutableMap{
"mod1": mod1,
})
scr.SetBuiltinFunctions([]*objects.BuiltinFunction{
{
Name: "stringify",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
ret = &objects.String{Value: args[0].String()}
return
},
},
})
compiled, err := scr.Compile()
assert.NoError(t, err)

executeFn := func(compiled *script.Compiled, a, b, c int) (d, e int) {
_ = compiled.Set("a", a)
_ = compiled.Set("b", b)
_ = compiled.Set("c", c)
err := compiled.Run()
assert.NoError(t, err)
d = compiled.Get("d").Int()
e = compiled.Get("e").Int()
return
}

concurrency := 500
var wg sync.WaitGroup
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func(compiled *script.Compiled) {
time.Sleep(time.Duration(rand.Int63n(50)) * time.Millisecond)
defer wg.Done()

a := rand.Intn(10)
b := rand.Intn(10)
c := rand.Intn(10)

d, e := executeFn(compiled, a, b, c)
expectedD, expectedE := solve(a, b, c)

assert.Equal(t, expectedD, d, "input: %d, %d, %d", a, b, c)
assert.Equal(t, expectedE, e, "input: %d, %d, %d", a, b, c)
}(compiled.Clone())
}
wg.Wait()
}

0 comments on commit b7977a4

Please sign in to comment.