add Compiled.Clone to make them safe for concurrent execution (#134)

This commit is contained in:
Daniel 2019-03-08 01:10:49 -08:00 committed by GitHub
parent e93f6f6325
commit b7977a419b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 229 additions and 28 deletions

View file

@ -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
@ -189,6 +190,33 @@ Sets the maximum byte-length of string values. This limit applies to all running
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
Although it's not recommended, you can directly create and run the Tengo [Parser](https://godoc.org/github.com/d5/tengo/compiler/parser#Parser), [Compiler](https://godoc.org/github.com/d5/tengo/compiler#Compiler), and [VM](https://godoc.org/github.com/d5/tengo/runtime#VM) for yourself instead of using Scripts and Script Variables. It's a bit more involved as you have to manage the symbol tables and global variables between them, but, basically that's what Script and Script Variable is doing internally.

View file

@ -3,6 +3,7 @@ package script
import (
"context"
"fmt"
"sync"
"github.com/d5/tengo/compiler"
"github.com/d5/tengo/objects"
@ -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:
@ -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
}
@ -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
}
@ -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 {
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
}
c.lock.RLock()
defer c.lock.RUnlock()
vars = append(vars, &Variable{
name: name,
value: value,
})
var vars []*Variable
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
@ -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
}

View file

@ -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()
@ -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
}

View file

@ -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()
}