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 }