Skip to content

Commit

Permalink
Add context support
Browse files Browse the repository at this point in the history
This commit contains an API breaking change.
LState.NewThread now also returns a child context.Context.
  • Loading branch information
yuin committed Jan 18, 2017
1 parent 33ebc07 commit 0bbb393
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 14 deletions.
66 changes: 66 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,72 @@ You can extend GopherLua with new types written in Go.
}
}
+++++++++++++++++++++++++++++++++++++++++
Terminating a running LState
+++++++++++++++++++++++++++++++++++++++++
GopherLua supports the `Go Concurrency Patterns: Context <https://blog.golang.org/context>`_ .


.. code-block:: go
L := lua.NewState()
defer L.Close()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// set the context to our LState
L.SetContext(ctx)
err := L.DoString(`
local clock = os.clock
function sleep(n) -- seconds
local t0 = clock()
while clock() - t0 <= n do end
end
sleep(3)
`)
// err.Error() contains "context deadline exceeded"
With coroutines

.. code-block:: go
L := lua.NewState()
defer L.Close()
ctx, cancel := context.WithCancel(context.Background())
L.SetContext(ctx)
defer cancel()
L.DoString(`
function coro()
local i = 0
while true do
coroutine.yield(i)
i = i+1
end
return i
end
`)
co, cocancel := L.NewThread()
defer cocancel()
fn := L.GetGlobal("coro").(*LFunction)
_, err, values := L.Resume(co, fn) // err is nil
cancel() // cancel the parent context
_, err, values = L.Resume(co, fn) // err is NOT nil : child context was canceled
**Note that using a context causes performance degradation.**

.. code-block::
time ./glua-with-context.exe fib.lua
9227465
0.01s user 0.11s system 1% cpu 7.505 total
time ./glua-without-context.exe fib.lua
9227465
0.01s user 0.01s system 0% cpu 5.306 total
+++++++++++++++++++++++++++++++++++++++++
Goroutines
+++++++++++++++++++++++++++++++++++++++++
Expand Down
37 changes: 33 additions & 4 deletions _state.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package lua
import (
"fmt"
"github.com/yuin/gopher-lua/parse"
"golang.org/x/net/context"
"io"
"math"
"os"
Expand Down Expand Up @@ -331,6 +332,8 @@ func newLState(options Options) *LState {
wrapped: false,
uvcache: nil,
hasErrorFunc: false,
mainLoop: mainLoop,
ctx: nil,
}
ls.Env = ls.G.Global
return ls
Expand Down Expand Up @@ -783,9 +786,9 @@ func (ls *LState) callR(nargs, nret, rbase int) {
if ls.G.MainThread == nil {
ls.G.MainThread = ls
ls.G.CurrentThread = ls
mainLoop(ls, nil)
ls.mainLoop(ls, nil)
} else {
mainLoop(ls, ls.currentFrame)
ls.mainLoop(ls, ls.currentFrame)
}
if nret != MultRet {
ls.reg.SetTop(rbase + nret)
Expand Down Expand Up @@ -1115,11 +1118,18 @@ func (ls *LState) CreateTable(acap, hcap int) *LTable {
return newLTable(acap, hcap)
}

func (ls *LState) NewThread() *LState {
// NewThread returns a new LState that shares with the original state all global objects.
// If the original state has context.Context, the new state has a new child context of the original state and this function returns its cancel function.
func (ls *LState) NewThread() (*LState, context.CancelFunc) {
thread := newLState(ls.Options)
thread.G = ls.G
thread.Env = ls.Env
return thread
var f context.CancelFunc = nil
if ls.ctx != nil {
thread.mainLoop = mainLoopWithContext
thread.ctx, f = context.WithCancel(ls.ctx)
}
return thread, f
}

func (ls *LState) NewUserData() *LUserData {
Expand Down Expand Up @@ -1742,6 +1752,25 @@ func (ls *LState) SetMx(mx int) {
}()
}

// SetContext set a context ctx to this LState. The provided ctx must be non-nil.
func (ls *LState) SetContext(ctx context.Context) {
ls.mainLoop = mainLoopWithContext
ls.ctx = ctx
}

// Context returns the LState's context. To change the context, use WithContext.
func (ls *LState) Context() context.Context {
return ls.ctx
}

// RemoveContext removes the context associated with this LState and returns this context.
func (ls *LState) RemoveContext() context.Context {
oldctx := ls.ctx
ls.mainLoop = mainLoop
ls.ctx = nil
return oldctx
}

// Converts the Lua value at the given acceptable index to the chan LValue.
func (ls *LState) ToChannel(n int) chan LValue {
if lv, ok := ls.Get(n).(LChannel); ok {
Expand Down
32 changes: 31 additions & 1 deletion _vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@ func mainLoop(L *LState, baseframe *callFrame) {
}
}

func mainLoopWithContext(L *LState, baseframe *callFrame) {
var inst uint32
var cf *callFrame

if L.stack.IsEmpty() {
return
}

L.currentFrame = L.stack.Last()
if L.currentFrame.Fn.IsG {
callGFunction(L, false)
return
}

for {
cf = L.currentFrame
inst = cf.Fn.Proto.Code[cf.Pc]
cf.Pc++
select {
case <-L.ctx.Done():
L.RaiseError(L.ctx.Err().Error())
return
default:
if jumpTable[int(inst>>26)](L, inst, baseframe) == 1 {
return
}
}
}
}

func copyReturnValues(L *LState, regv, start, n, b int) { // +inline-start
if b == 1 {
// +inline-call L.reg.FillNil regv n
Expand Down Expand Up @@ -118,7 +148,7 @@ func threadRun(L *LState) {
}
}
}()
mainLoop(L, nil)
L.mainLoop(L, nil)
}

type instFunc func(*LState, uint32, *callFrame) int
Expand Down
2 changes: 1 addition & 1 deletion auxlib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func TestCheckThread(t *testing.T) {
L := NewState()
defer L.Close()
errorIfGFuncNotFail(t, L, func(L *LState) int {
th := L.NewThread()
th, _ := L.NewThread()
L.Push(th)
errorIfNotEqual(t, th, L.CheckThread(2))
L.Push(LNumber(10))
Expand Down
2 changes: 1 addition & 1 deletion coroutinelib.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var coFuncs = map[string]LGFunction{

func coCreate(L *LState) int {
fn := L.CheckFunction(1)
newthread := L.NewThread()
newthread, _ := L.NewThread()
base := 0
newthread.stack.Push(callFrame{
Fn: fn,
Expand Down
37 changes: 33 additions & 4 deletions state.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package lua
import (
"fmt"
"github.com/yuin/gopher-lua/parse"
"golang.org/x/net/context"
"io"
"math"
"os"
Expand Down Expand Up @@ -335,6 +336,8 @@ func newLState(options Options) *LState {
wrapped: false,
uvcache: nil,
hasErrorFunc: false,
mainLoop: mainLoop,
ctx: nil,
}
ls.Env = ls.G.Global
return ls
Expand Down Expand Up @@ -868,9 +871,9 @@ func (ls *LState) callR(nargs, nret, rbase int) {
if ls.G.MainThread == nil {
ls.G.MainThread = ls
ls.G.CurrentThread = ls
mainLoop(ls, nil)
ls.mainLoop(ls, nil)
} else {
mainLoop(ls, ls.currentFrame)
ls.mainLoop(ls, ls.currentFrame)
}
if nret != MultRet {
ls.reg.SetTop(rbase + nret)
Expand Down Expand Up @@ -1200,11 +1203,18 @@ func (ls *LState) CreateTable(acap, hcap int) *LTable {
return newLTable(acap, hcap)
}

func (ls *LState) NewThread() *LState {
// NewThread returns a new LState that shares with the original state all global objects.
// If the original state has context.Context, the new state has a new child context of the original state and this function returns its cancel function.
func (ls *LState) NewThread() (*LState, context.CancelFunc) {
thread := newLState(ls.Options)
thread.G = ls.G
thread.Env = ls.Env
return thread
var f context.CancelFunc = nil
if ls.ctx != nil {
thread.mainLoop = mainLoopWithContext
thread.ctx, f = context.WithCancel(ls.ctx)
}
return thread, f
}

func (ls *LState) NewUserData() *LUserData {
Expand Down Expand Up @@ -1827,6 +1837,25 @@ func (ls *LState) SetMx(mx int) {
}()
}

// SetContext set a context ctx to this LState. The provided ctx must be non-nil.
func (ls *LState) SetContext(ctx context.Context) {
ls.mainLoop = mainLoopWithContext
ls.ctx = ctx
}

// Context returns the LState's context. To change the context, use WithContext.
func (ls *LState) Context() context.Context {
return ls.ctx
}

// RemoveContext removes the context associated with this LState and returns this context.
func (ls *LState) RemoveContext() context.Context {
oldctx := ls.ctx
ls.mainLoop = mainLoop
ls.ctx = nil
return oldctx
}

// Converts the Lua value at the given acceptable index to the chan LValue.
func (ls *LState) ToChannel(n int) chan LValue {
if lv, ok := ls.Get(n).(LChannel); ok {
Expand Down
82 changes: 80 additions & 2 deletions state_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package lua

import (
"golang.org/x/net/context"
"strings"
"testing"
"time"
)

func TestCallStackOverflow(t *testing.T) {
Expand Down Expand Up @@ -265,7 +267,7 @@ func TestPCall(t *testing.T) {
func TestCoroutineApi1(t *testing.T) {
L := NewState()
defer L.Close()
co := L.NewThread()
co, _ := L.NewThread()
errorIfScriptFail(t, L, `
function coro(v)
assert(v == 10)
Expand Down Expand Up @@ -308,7 +310,7 @@ func TestCoroutineApi1(t *testing.T) {
end
`)
fn = L.GetGlobal("coro_error").(*LFunction)
co = L.NewThread()
co, _ = L.NewThread()
st, err, values = L.Resume(co, fn)
errorIfNotEqual(t, ResumeYield, st)
errorIfNotNil(t, err)
Expand All @@ -333,3 +335,79 @@ func TestCoroutineApi1(t *testing.T) {
errorIfFalse(t, strings.Contains(err.Error(), "can not resume a dead thread"), "can not resume a dead thread")

}

func TestContextTimeout(t *testing.T) {
L := NewState()
defer L.Close()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
L.SetContext(ctx)
errorIfNotEqual(t, ctx, L.Context())
err := L.DoString(`
local clock = os.clock
function sleep(n) -- seconds
local t0 = clock()
while clock() - t0 <= n do end
end
sleep(3)
`)
errorIfNil(t, err)
errorIfFalse(t, strings.Contains(err.Error(), "context deadline exceeded"), "execution must be canceled")

oldctx := L.RemoveContext()
errorIfNotEqual(t, ctx, oldctx)
errorIfNotNil(t, L.ctx)
}

func TestContextCancel(t *testing.T) {
L := NewState()
defer L.Close()
ctx, cancel := context.WithCancel(context.Background())
errch := make(chan error, 1)
L.SetContext(ctx)
go func() {
errch <- L.DoString(`
local clock = os.clock
function sleep(n) -- seconds
local t0 = clock()
while clock() - t0 <= n do end
end
sleep(3)
`)
}()
time.Sleep(1 * time.Second)
cancel()
err := <-errch
errorIfNil(t, err)
errorIfFalse(t, strings.Contains(err.Error(), "context canceled"), "execution must be canceled")
}

func TestContextWithCroutine(t *testing.T) {
L := NewState()
defer L.Close()
ctx, cancel := context.WithCancel(context.Background())
L.SetContext(ctx)
defer cancel()
L.DoString(`
function coro()
local i = 0
while true do
coroutine.yield(i)
i = i+1
end
return i
end
`)
co, cocancel := L.NewThread()
defer cocancel()
fn := L.GetGlobal("coro").(*LFunction)
_, err, values := L.Resume(co, fn)
errorIfNotNil(t, err)
errorIfNotEqual(t, LNumber(0), values[0])
// cancel the parent context
cancel()
_, err, values = L.Resume(co, fn)
errorIfNil(t, err)
errorIfFalse(t, strings.Contains(err.Error(), "context canceled"), "coroutine execution must be canceled when the parent context is canceled")

}
Loading

1 comment on commit 0bbb393

@riking
Copy link

@riking riking commented on 0bbb393 Feb 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I can remove the fork in my vendor now!

Please sign in to comment.