add Compiled.Clone to make them safe for concurrent execution (#134)
This commit is contained in:
parent
e93f6f6325
commit
b7977a419b
4 changed files with 229 additions and 28 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
110
script/script_concurrency_test.go
Normal file
110
script/script_concurrency_test.go
Normal 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()
|
||||
}
|
Loading…
Reference in a new issue