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) - [Type Conversion Table](#type-conversion-table)
- [User Types](#user-types) - [User Types](#user-types)
- [Sandbox Environments](#sandbox-environments) - [Sandbox Environments](#sandbox-environments)
- [Concurrency](#concurrency)
- [Compiler and VM](#compiler-and-vm) - [Compiler and VM](#compiler-and-vm)
## Using Scripts ## 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. 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 ## 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. 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 ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/d5/tengo/compiler" "github.com/d5/tengo/compiler"
"github.com/d5/tengo/objects" "github.com/d5/tengo/objects"
@ -12,26 +13,41 @@ import (
// Compiled is a compiled instance of the user script. // Compiled is a compiled instance of the user script.
// Use Script.Compile() to create Compiled object. // Use Script.Compile() to create Compiled object.
type Compiled struct { type Compiled struct {
symbolTable *compiler.SymbolTable globalIndexes map[string]int // global symbol name to index
machine *runtime.VM 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. // Run executes the compiled script in the virtual machine.
func (c *Compiled) Run() error { 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. // RunContext is like Run but includes a context.
func (c *Compiled) RunContext(ctx context.Context) (err error) { 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) ch := make(chan error, 1)
go func() { go func() {
ch <- c.machine.Run() ch <- v.Run()
}() }()
select { select {
case <-ctx.Done(): case <-ctx.Done():
c.machine.Abort() v.Abort()
<-ch <-ch
err = ctx.Err() err = ctx.Err()
case err = <-ch: case err = <-ch:
@ -40,14 +56,42 @@ func (c *Compiled) RunContext(ctx context.Context) (err error) {
return 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. // IsDefined returns true if the variable name is defined (has value) before or after the execution.
func (c *Compiled) IsDefined(name string) bool { 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 { if !ok {
return false return false
} }
v := c.machine.Globals()[symbol.Index] v := c.globals[idx]
if v == nil { if v == nil {
return false return false
} }
@ -57,11 +101,13 @@ func (c *Compiled) IsDefined(name string) bool {
// Get returns a variable identified by the name. // Get returns a variable identified by the name.
func (c *Compiled) Get(name string) *Variable { func (c *Compiled) Get(name string) *Variable {
c.lock.RLock()
defer c.lock.RUnlock()
value := &objects.UndefinedValue value := &objects.UndefinedValue
symbol, _, ok := c.symbolTable.Resolve(name) if idx, ok := c.globalIndexes[name]; ok {
if ok && symbol.Scope == compiler.ScopeGlobal { value = c.globals[idx]
value = c.machine.Globals()[symbol.Index]
if value == nil { if value == nil {
value = &objects.UndefinedValue value = &objects.UndefinedValue
} }
@ -75,11 +121,13 @@ func (c *Compiled) Get(name string) *Variable {
// GetAll returns all the variables that are defined by the compiled script. // GetAll returns all the variables that are defined by the compiled script.
func (c *Compiled) GetAll() []*Variable { func (c *Compiled) GetAll() []*Variable {
c.lock.RLock()
defer c.lock.RUnlock()
var vars []*Variable var vars []*Variable
for _, name := range c.symbolTable.Names() {
symbol, _, ok := c.symbolTable.Resolve(name) for name, idx := range c.globalIndexes {
if ok && symbol.Scope == compiler.ScopeGlobal { value := c.globals[idx]
value := c.machine.Globals()[symbol.Index]
if value == nil { if value == nil {
value = &objects.UndefinedValue value = &objects.UndefinedValue
} }
@ -89,7 +137,6 @@ func (c *Compiled) GetAll() []*Variable {
value: value, value: value,
}) })
} }
}
return vars return vars
} }
@ -97,17 +144,20 @@ func (c *Compiled) GetAll() []*Variable {
// Set replaces the value of a global variable identified by the name. // 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. // An error will be returned if the name was not defined during compilation.
func (c *Compiled) Set(name string, value interface{}) error { func (c *Compiled) Set(name string, value interface{}) error {
c.lock.Lock()
defer c.lock.Unlock()
obj, err := objects.FromInterface(value) obj, err := objects.FromInterface(value)
if err != nil { if err != nil {
return err return err
} }
symbol, _, ok := c.symbolTable.Resolve(name) idx, ok := c.globalIndexes[name]
if !ok || symbol.Scope != compiler.ScopeGlobal { if !ok {
return fmt.Errorf("'%s' is not defined", name) return fmt.Errorf("'%s' is not defined", name)
} }
c.machine.Globals()[symbol.Index] = &obj c.globals[idx] = &obj
return nil return nil
} }

View file

@ -128,6 +128,15 @@ func (s *Script) Compile() (*Compiled, error) {
// reduce globals size // reduce globals size
globals = globals[:symbolTable.MaxSymbols()+1] 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 // remove duplicates from constants
bytecode := c.Bytecode() bytecode := c.Bytecode()
bytecode.RemoveDuplicates() bytecode.RemoveDuplicates()
@ -141,8 +150,12 @@ func (s *Script) Compile() (*Compiled, error) {
} }
return &Compiled{ return &Compiled{
symbolTable: symbolTable, globalIndexes: globalIndexes,
machine: runtime.NewVM(bytecode, globals, s.builtinFuncs, s.builtinModules, s.maxAllocs), bytecode: bytecode,
globals: globals,
builtinFunctions: s.builtinFuncs,
builtinModules: s.builtinModules,
maxAllocs: s.maxAllocs,
}, nil }, 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()
}