You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

323 lines
9.0 KiB

// Copyright 2017 The Bazel Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package starlark_test
import (
"fmt"
"log"
"reflect"
"sort"
"strings"
"sync"
"sync/atomic"
"testing"
"unsafe"
"go.starlark.net/starlark"
)
// ExampleExecFile demonstrates a simple embedding
// of the Starlark interpreter into a Go program.
func ExampleExecFile() {
const data = `
print(greeting + ", world")
print(repeat("one"))
print(repeat("mur", 2))
squares = [x*x for x in range(10)]
`
// repeat(str, n=1) is a Go function called from Starlark.
// It behaves like the 'string * int' operation.
repeat := func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var s string
var n int = 1
if err := starlark.UnpackArgs(b.Name(), args, kwargs, "s", &s, "n?", &n); err != nil {
return nil, err
}
return starlark.String(strings.Repeat(s, n)), nil
}
// The Thread defines the behavior of the built-in 'print' function.
thread := &starlark.Thread{
Name: "example",
Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
}
// This dictionary defines the pre-declared environment.
predeclared := starlark.StringDict{
"greeting": starlark.String("hello"),
"repeat": starlark.NewBuiltin("repeat", repeat),
}
// Execute a program.
globals, err := starlark.ExecFile(thread, "apparent/filename.star", data, predeclared)
if err != nil {
if evalErr, ok := err.(*starlark.EvalError); ok {
log.Fatal(evalErr.Backtrace())
}
log.Fatal(err)
}
// Print the global environment.
fmt.Println("\nGlobals:")
for _, name := range globals.Keys() {
v := globals[name]
fmt.Printf("%s (%s) = %s\n", name, v.Type(), v.String())
}
// Output:
// hello, world
// one
// murmur
//
// Globals:
// squares (list) = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
}
// ExampleThread_Load_sequential demonstrates a simple caching
// implementation of 'load' that works sequentially.
func ExampleThread_Load_sequential() {
fakeFilesystem := map[string]string{
"c.star": `load("b.star", "b"); c = b + "!"`,
"b.star": `load("a.star", "a"); b = a + ", world"`,
"a.star": `a = "Hello"`,
}
type entry struct {
globals starlark.StringDict
err error
}
cache := make(map[string]*entry)
var load func(_ *starlark.Thread, module string) (starlark.StringDict, error)
load = func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
e, ok := cache[module]
if e == nil {
if ok {
// request for package whose loading is in progress
return nil, fmt.Errorf("cycle in load graph")
}
// Add a placeholder to indicate "load in progress".
cache[module] = nil
// Load and initialize the module in a new thread.
data := fakeFilesystem[module]
thread := &starlark.Thread{Name: "exec " + module, Load: load}
globals, err := starlark.ExecFile(thread, module, data, nil)
e = &entry{globals, err}
// Update the cache.
cache[module] = e
}
return e.globals, e.err
}
globals, err := load(nil, "c.star")
if err != nil {
log.Fatal(err)
}
fmt.Println(globals["c"])
// Output:
// "Hello, world!"
}
// ExampleThread_Load_parallel demonstrates a parallel implementation
// of 'load' with caching, duplicate suppression, and cycle detection.
func ExampleThread_Load_parallel() {
cache := &cache{
cache: make(map[string]*entry),
fakeFilesystem: map[string]string{
"c.star": `load("a.star", "a"); c = a * 2`,
"b.star": `load("a.star", "a"); b = a * 3`,
"a.star": `a = 1; print("loaded a")`,
},
}
// We load modules b and c in parallel by concurrent calls to
// cache.Load. Both of them load module a, but a is executed
// only once, as witnessed by the sole output of its print
// statement.
ch := make(chan string)
for _, name := range []string{"b", "c"} {
go func(name string) {
globals, err := cache.Load(name + ".star")
if err != nil {
log.Fatal(err)
}
ch <- fmt.Sprintf("%s = %s", name, globals[name])
}(name)
}
got := []string{<-ch, <-ch}
sort.Strings(got)
fmt.Println(strings.Join(got, "\n"))
// Output:
// loaded a
// b = 3
// c = 2
}
// TestThread_Load_parallelCycle demonstrates detection
// of cycles during parallel loading.
func TestThreadLoad_ParallelCycle(t *testing.T) {
cache := &cache{
cache: make(map[string]*entry),
fakeFilesystem: map[string]string{
"c.star": `load("b.star", "b"); c = b * 2`,
"b.star": `load("a.star", "a"); b = a * 3`,
"a.star": `load("c.star", "c"); a = c * 5; print("loaded a")`,
},
}
ch := make(chan string)
for _, name := range "bc" {
name := string(name)
go func() {
_, err := cache.Load(name + ".star")
if err == nil {
log.Fatalf("Load of %s.star succeeded unexpectedly", name)
}
ch <- err.Error()
}()
}
got := []string{<-ch, <-ch}
sort.Strings(got)
// Typically, the c goroutine quickly blocks behind b;
// b loads a, and a then fails to load c because it forms a cycle.
// The errors observed by the two goroutines are:
want1 := []string{
"cannot load a.star: cannot load c.star: cycle in load graph", // from b
"cannot load b.star: cannot load a.star: cannot load c.star: cycle in load graph", // from c
}
// But if the c goroutine is slow to start, b loads a,
// and a loads c; then c fails to load b because it forms a cycle.
// The errors this time are:
want2 := []string{
"cannot load a.star: cannot load c.star: cannot load b.star: cycle in load graph", // from b
"cannot load b.star: cycle in load graph", // from c
}
if !reflect.DeepEqual(got, want1) && !reflect.DeepEqual(got, want2) {
t.Error(got)
}
}
// cache is a concurrency-safe, duplicate-suppressing,
// non-blocking cache of the doLoad function.
// See Section 9.7 of gopl.io for an explanation of this structure.
// It also features online deadlock (load cycle) detection.
type cache struct {
cacheMu sync.Mutex
cache map[string]*entry
fakeFilesystem map[string]string
}
type entry struct {
owner unsafe.Pointer // a *cycleChecker; see cycleCheck
globals starlark.StringDict
err error
ready chan struct{}
}
func (c *cache) Load(module string) (starlark.StringDict, error) {
return c.get(new(cycleChecker), module)
}
// get loads and returns an entry (if not already loaded).
func (c *cache) get(cc *cycleChecker, module string) (starlark.StringDict, error) {
c.cacheMu.Lock()
e := c.cache[module]
if e != nil {
c.cacheMu.Unlock()
// Some other goroutine is getting this module.
// Wait for it to become ready.
// Detect load cycles to avoid deadlocks.
if err := cycleCheck(e, cc); err != nil {
return nil, err
}
cc.setWaitsFor(e)
<-e.ready
cc.setWaitsFor(nil)
} else {
// First request for this module.
e = &entry{ready: make(chan struct{})}
c.cache[module] = e
c.cacheMu.Unlock()
e.setOwner(cc)
e.globals, e.err = c.doLoad(cc, module)
e.setOwner(nil)
// Broadcast that the entry is now ready.
close(e.ready)
}
return e.globals, e.err
}
func (c *cache) doLoad(cc *cycleChecker, module string) (starlark.StringDict, error) {
thread := &starlark.Thread{
Name: "exec " + module,
Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
Load: func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
// Tunnel the cycle-checker state for this "thread of loading".
return c.get(cc, module)
},
}
data := c.fakeFilesystem[module]
return starlark.ExecFile(thread, module, data, nil)
}
// -- concurrent cycle checking --
// A cycleChecker is used for concurrent deadlock detection.
// Each top-level call to Load creates its own cycleChecker,
// which is passed to all recursive calls it makes.
// It corresponds to a logical thread in the deadlock detection literature.
type cycleChecker struct {
waitsFor unsafe.Pointer // an *entry; see cycleCheck
}
func (cc *cycleChecker) setWaitsFor(e *entry) {
atomic.StorePointer(&cc.waitsFor, unsafe.Pointer(e))
}
func (e *entry) setOwner(cc *cycleChecker) {
atomic.StorePointer(&e.owner, unsafe.Pointer(cc))
}
// cycleCheck reports whether there is a path in the waits-for graph
// from resource 'e' to thread 'me'.
//
// The waits-for graph (WFG) is a bipartite graph whose nodes are
// alternately of type entry and cycleChecker. Each node has at most
// one outgoing edge. An entry has an "owner" edge to a cycleChecker
// while it is being readied by that cycleChecker, and a cycleChecker
// has a "waits-for" edge to an entry while it is waiting for that entry
// to become ready.
//
// Before adding a waits-for edge, the cache checks whether the new edge
// would form a cycle. If so, this indicates that the load graph is
// cyclic and that the following wait operation would deadlock.
func cycleCheck(e *entry, me *cycleChecker) error {
for e != nil {
cc := (*cycleChecker)(atomic.LoadPointer(&e.owner))
if cc == nil {
break
}
if cc == me {
return fmt.Errorf("cycle in load graph")
}
e = (*entry)(atomic.LoadPointer(&cc.waitsFor))
}
return nil
}