github.com/emcfarlane/larking@v0.0.0-20220605172417-1704b45ee6c3/starlib/load.go (about)

     1  // Copyright 2022 Edward McFarlane. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package starlib
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"embed"
    11  	"errors"
    12  	"fmt"
    13  	"io/fs"
    14  	"runtime/debug"
    15  	"sync"
    16  
    17  	"github.com/emcfarlane/larking/starlib/encoding/starlarkproto"
    18  	"github.com/emcfarlane/larking/starlib/net/starlarkhttp"
    19  	"github.com/emcfarlane/larking/starlib/net/starlarkopenapi"
    20  	"github.com/emcfarlane/larking/starlib/starext"
    21  	"github.com/go-logr/logr"
    22  
    23  	//"github.com/emcfarlane/larking/starlib/net/starlarkgrpc"
    24  	"github.com/emcfarlane/larking/starlib/starlarkblob"
    25  	"github.com/emcfarlane/larking/starlib/starlarkdocstore"
    26  	"github.com/emcfarlane/larking/starlib/starlarkerrors"
    27  	"github.com/emcfarlane/larking/starlib/starlarkpubsub"
    28  	"github.com/emcfarlane/larking/starlib/starlarkrule"
    29  	"github.com/emcfarlane/larking/starlib/starlarkruntimevar"
    30  	"github.com/emcfarlane/larking/starlib/starlarksql"
    31  	"github.com/emcfarlane/larking/starlib/starlarkstruct"
    32  	"github.com/emcfarlane/larking/starlib/starlarkthread"
    33  	starlarkjson "go.starlark.net/lib/json"
    34  	starlarkmath "go.starlark.net/lib/math"
    35  	starlarktime "go.starlark.net/lib/time"
    36  	"go.starlark.net/starlark"
    37  )
    38  
    39  // content holds our static web server content.
    40  //go:embed rules/*
    41  var local embed.FS
    42  
    43  func makeDict(module *starlarkstruct.Module) starlark.StringDict {
    44  	dict := make(starlark.StringDict, len(module.Members)+1)
    45  	for key, val := range module.Members {
    46  		dict[key] = val
    47  	}
    48  	// Add module if no module name.
    49  	if _, ok := dict[module.Name]; !ok {
    50  		dict[module.Name] = module
    51  	}
    52  	return dict
    53  }
    54  
    55  var (
    56  	stdLibMu sync.Mutex
    57  	stdLib   = map[string]starlark.StringDict{
    58  		//"archive/container.star":           makeDict(starlarkcontainer.NewModule()),
    59  		//"archive/tar.star":           makeDict(starlarktar.NewModule()),
    60  		//"archive/zip.star":           makeDict(starlarkzip.NewModule()),
    61  
    62  		"blob.star":           makeDict(starlarkblob.NewModule()),
    63  		"docstore.star":       makeDict(starlarkdocstore.NewModule()),
    64  		"encoding/json.star":  makeDict(starlarkjson.Module), // starlark
    65  		"encoding/proto.star": makeDict(starlarkproto.NewModule()),
    66  		"errors.star":         makeDict(starlarkerrors.NewModule()),
    67  		"math.star":           makeDict(starlarkmath.Module), // starlark
    68  		"net/http.star":       makeDict(starlarkhttp.NewModule()),
    69  		"net/openapi.star":    makeDict(starlarkopenapi.NewModule()),
    70  		"pubsub.star":         makeDict(starlarkpubsub.NewModule()),
    71  		"runtimevar.star":     makeDict(starlarkruntimevar.NewModule()),
    72  		"sql.star":            makeDict(starlarksql.NewModule()),
    73  		"time.star":           makeDict(starlarktime.Module), // starlark
    74  		"thread.star":         makeDict(starlarkthread.NewModule()),
    75  		"rule.star":           makeDict(starlarkrule.NewModule()),
    76  		//"net/grpc.star": makeDict(starlarkgrpc.NewModule()),
    77  	}
    78  )
    79  
    80  // StdLoad loads files from the standard library.
    81  func (l *Loader) StdLoad(thread *starlark.Thread, module string) (starlark.StringDict, error) {
    82  	stdLibMu.Lock()
    83  	if e, ok := stdLib[module]; ok {
    84  		stdLibMu.Unlock()
    85  		return e, nil
    86  	}
    87  	stdLibMu.Unlock()
    88  
    89  	// Load and eval the file.
    90  	src, err := local.ReadFile(module)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	v, err := starlark.ExecFile(thread, module, src, l.globals)
    95  	if err != nil {
    96  		return nil, fmt.Errorf("exec file: %v", err)
    97  	}
    98  
    99  	stdLibMu.Lock()
   100  	stdLib[module] = v // cache
   101  	stdLibMu.Unlock()
   102  	return v, nil
   103  }
   104  
   105  // call is an in-flight or completed load call
   106  type call struct {
   107  	wg  sync.WaitGroup
   108  	val starlark.StringDict
   109  	err error
   110  
   111  	// callers are idle threads waiting on the call
   112  	callers map[*starlark.Thread]bool
   113  }
   114  
   115  // Loader is a cloid.Blob backed loader. It uses thread.Name to figure out the
   116  // current bucket and module.
   117  type Loader struct {
   118  	starext.Blobs
   119  
   120  	mu sync.Mutex       // protects m
   121  	m  map[string]*call // lazily initialized
   122  
   123  	// Predeclared globals
   124  	globals starlark.StringDict
   125  }
   126  
   127  func NewLoader(globals starlark.StringDict) *Loader {
   128  	return &Loader{
   129  		globals: globals,
   130  	}
   131  }
   132  
   133  // errCycle indicates the load caused a cycle.
   134  var errCycle = errors.New("cycle in loading module")
   135  
   136  // A panicError is an arbitrary value recovered from a panic
   137  // with the stack trace during the execution of given function.
   138  type panicError struct {
   139  	value interface{}
   140  	stack []byte
   141  }
   142  
   143  // Error implements error interface.
   144  func (p *panicError) Error() string {
   145  	return fmt.Sprintf("%v\n\n%s", p.value, p.stack)
   146  }
   147  func newPanicError(v interface{}) error {
   148  	stack := debug.Stack()
   149  
   150  	// The first line of the stack trace is of the form "goroutine N [status]:"
   151  	// but by the time the panic reaches Do the goroutine may no longer exist
   152  	// and its status will have changed. Trim out the misleading line.
   153  	if line := bytes.IndexByte(stack[:], '\n'); line >= 0 {
   154  		stack = stack[line+1:]
   155  	}
   156  	return &panicError{value: v, stack: stack}
   157  }
   158  
   159  // Load checks the standard library before loading from buckets.
   160  func (l *Loader) Load(thread *starlark.Thread, module string) (starlark.StringDict, error) {
   161  	ctx := starlarkthread.GetContext(thread)
   162  	log := logr.FromContextOrDiscard(ctx)
   163  
   164  	log.Info("loading module", "module", module)
   165  	defer log.Info("finished loading", "module", module)
   166  
   167  	l.mu.Lock()
   168  	if l.m == nil {
   169  		l.m = make(map[string]*call)
   170  	}
   171  	key := module // TODO: hash file contents?
   172  	if c, ok := l.m[key]; ok {
   173  		if c.callers[thread] {
   174  			l.mu.Unlock()
   175  			return nil, errCycle
   176  		}
   177  		l.mu.Unlock()
   178  		c.wg.Wait()
   179  		return c.val, c.err
   180  	}
   181  	c := new(call)
   182  	c.wg.Add(1)
   183  	c.callers = map[*starlark.Thread]bool{thread: true}
   184  	l.m[key] = c
   185  	l.mu.Unlock()
   186  
   187  	func() {
   188  		defer func() {
   189  			if r := recover(); r != nil {
   190  				c.err = newPanicError(r)
   191  			}
   192  		}()
   193  		c.val, c.err = l.load(thread, module)
   194  	}()
   195  	c.wg.Done()
   196  
   197  	l.mu.Lock()
   198  	delete(l.m, key) // TODO: hash files.
   199  	l.mu.Unlock()
   200  
   201  	return c.val, c.err
   202  }
   203  
   204  // LoadSource fetches the source file in bytes from a bucket.
   205  func (l *Loader) LoadSource(ctx context.Context, bktURL string, key string) ([]byte, error) {
   206  	bkt, err := l.OpenBucket(ctx, bktURL)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	return bkt.ReadAll(ctx, key)
   211  }
   212  
   213  func (l *Loader) load(thread *starlark.Thread, module string) (starlark.StringDict, error) {
   214  	ctx := starlarkthread.GetContext(thread)
   215  
   216  	v, err := l.StdLoad(thread, module)
   217  	if err == nil {
   218  		return v, nil
   219  	}
   220  	if !errors.Is(err, fs.ErrNotExist) {
   221  		return nil, err
   222  	}
   223  
   224  	label, err := starlarkrule.ParseRelativeLabel(thread.Name, module)
   225  	if err != nil {
   226  		return nil, err
   227  	}
   228  
   229  	bktURL := label.BucketURL()
   230  	key := label.Key()
   231  
   232  	src, err := l.LoadSource(ctx, bktURL, key)
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	oldName, newName := thread.Name, label.String()
   238  	thread.Name = newName
   239  	defer func() { thread.Name = oldName }()
   240  
   241  	v, err = starlark.ExecFile(thread, key, src, l.globals)
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  	return v, nil
   246  }