github.com/yunabe/lgo@v0.0.0-20190709125917-42c42d410fdf/core/core.go (about) 1 package core 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "reflect" 9 "runtime" 10 "runtime/debug" 11 "strings" 12 "sync" 13 "sync/atomic" 14 "time" 15 ) 16 17 // SelfPkgPath is the package path of this package 18 const SelfPkgPath = "github.com/yunabe/lgo/core" 19 20 // How long time we should wait for goroutines after a cancel operation. 21 var execWaitDuration = time.Second 22 23 // isRunning indicates lgo execution is running. 24 // This var is used to improve the performance of ExitIfCtxDone. 25 // To access this var, use atomic.Store/LoadUint32. 26 var isRunning uint32 27 28 // A LgoContext carries a context of lgo execution. 29 type LgoContext struct { 30 // Go context.Context. 31 context.Context 32 // Display displays non-text content in Jupyter Notebook. 33 Display DataDisplayer 34 } 35 36 func lgoCtxWithCancel(ctx LgoContext) (LgoContext, context.CancelFunc) { 37 goctx, cancel := context.WithCancel(ctx.Context) 38 return LgoContext{goctx, ctx.Display}, cancel 39 } 40 41 // DataDisplayer is the interface that wraps Jupyter Notebook display_data protocol. 42 // The list of supported content types are based on Jupyter Notebook implementation[2]. 43 // Each method receives a content and an display id. If id is nil, the method does not use id. 44 // If id is not nil and it points an empty string, the method reserves a new display ID and stores it to id. 45 // If id is not nil and it points a non-empty string, the method overwrites a content with the same ID in Jupyter Notebooks. 46 // 47 // Please note that JavaScript output is disabled in JupyterLab[3]. 48 // 49 // References: 50 // [1] http://jupyter-client.readthedocs.io/en/latest/messaging.html#display-data 51 // [2] https://github.com/jupyter/notebook/blob/master/notebook/static/notebook/js/outputarea.js 52 // [3] https://github.com/jupyterlab/jupyterlab/issues/3748 53 type DataDisplayer interface { 54 JavaScript(s string, id *string) 55 HTML(s string, id *string) 56 Markdown(s string, id *string) 57 Latex(s string, id *string) 58 SVG(s string, id *string) 59 PNG(b []byte, id *string) 60 JPEG(b []byte, id *string) 61 GIF(b []byte, id *string) 62 PDF(b []byte, id *string) 63 Text(s string, id *string) 64 Raw(contentType string, v interface{}, id *string) error 65 } 66 67 type resultCounter struct { 68 active uint 69 fail uint 70 cancel uint 71 mu sync.Mutex 72 } 73 74 func (c *resultCounter) add() { 75 c.mu.Lock() 76 defer c.mu.Unlock() 77 c.active++ 78 } 79 80 // recordResult records a result of a routine based on the value of recover(). 81 func (c *resultCounter) recordResult(r interface{}) { 82 c.mu.Lock() 83 defer c.mu.Unlock() 84 c.active-- 85 if c.active < 0 { 86 panic("active is negative") 87 } 88 if r == nil { 89 return 90 } 91 if r == Bailout { 92 c.cancel++ 93 return 94 } 95 fmt.Fprintf(os.Stderr, "panic: %v\n\n%s", r, debug.Stack()) 96 c.fail++ 97 } 98 99 func (c *resultCounter) recordResultInDefer() { 100 c.recordResult(recover()) 101 } 102 103 // ExecutionState maintains the state of the current code execution in lgo. 104 type ExecutionState struct { 105 Context LgoContext 106 cancelCtx func() 107 canceled bool 108 cancelMu sync.Mutex 109 110 mainCounter resultCounter 111 subCounter resultCounter 112 routineWait sync.WaitGroup 113 } 114 115 func newExecutionState(parent LgoContext) *ExecutionState { 116 ctx, cancel := lgoCtxWithCancel(parent) 117 e := &ExecutionState{ 118 Context: ctx, 119 cancelCtx: cancel, 120 } 121 go func() { 122 <-parent.Done() 123 e.cancel() 124 }() 125 return e 126 } 127 128 func (e *ExecutionState) cancel() { 129 e.cancelMu.Lock() 130 if e.canceled { 131 e.cancelMu.Unlock() 132 return 133 } 134 e.canceled = true 135 e.cancelMu.Unlock() 136 137 if getExecState() == e { 138 atomic.StoreUint32(&isRunning, 0) 139 } 140 e.cancelCtx() 141 } 142 143 func (e *ExecutionState) counterMessage() string { 144 var msgs []string 145 func() { 146 e.mainCounter.mu.Lock() 147 defer e.mainCounter.mu.Unlock() 148 if e.mainCounter.fail > 0 { 149 msgs = append(msgs, "main routine failed") 150 } else if e.mainCounter.cancel > 0 { 151 msgs = append(msgs, "main routine canceled") 152 } else if e.mainCounter.active > 0 { 153 msgs = append(msgs, "main routine is hanging") 154 } 155 }() 156 func() { 157 e.subCounter.mu.Lock() 158 defer e.subCounter.mu.Unlock() 159 if c := e.subCounter.fail; c > 1 { 160 msgs = append(msgs, fmt.Sprintf("%d goroutines failed", c)) 161 } else if c == 1 { 162 msgs = append(msgs, fmt.Sprintf("%d goroutine failed", c)) 163 } 164 if c := e.subCounter.cancel; c > 1 { 165 msgs = append(msgs, fmt.Sprintf("%d goroutines canceled", c)) 166 } else if c == 1 { 167 msgs = append(msgs, fmt.Sprintf("%d goroutine canceled", c)) 168 } 169 if c := e.subCounter.active; c > 1 { 170 msgs = append(msgs, fmt.Sprintf("%d goroutines are hanging", c)) 171 } else if c == 1 { 172 msgs = append(msgs, fmt.Sprintf("%d goroutine is hanging", c)) 173 } 174 }() 175 return strings.Join(msgs, ", ") 176 } 177 178 func (e *ExecutionState) waitRoutines() { 179 ctx, done := context.WithCancel(context.Background()) 180 go func() { 181 e.routineWait.Wait() 182 done() 183 // Don't forget to cancel the current ctx to avoid ctx leak. 184 e.cancel() 185 }() 186 go func() { 187 <-e.Context.Done() 188 time.Sleep(execWaitDuration) 189 done() 190 }() 191 // Wait done is called. 192 <-ctx.Done() 193 } 194 195 // execState should be protected with a mutex because 196 // InitGoroutine, FinalizeGoroutine and ExitIfCtxDone might be called after 197 // a lgo execution finishes and execState is modified if there are goroutines which 198 // are not terminated properly when the context is canceled. 199 var execState *ExecutionState 200 var execStateMu sync.Mutex 201 202 // canceledCtx is used to return an canceled context when GetExecContext() is invoked when execState is nil. 203 var canceledCtx LgoContext 204 205 func init() { 206 ctx, cancel := context.WithCancel(context.Background()) 207 cancel() 208 canceledCtx = LgoContext{Context: ctx} 209 } 210 211 // GetExecContext returns the context of the current code execution. 212 // It returns a canceled context when lgo does not execute any code blocks. 213 // _ctx in lgo is converted to this function internally. 214 func GetExecContext() LgoContext { 215 if e := getExecState(); e != nil { 216 return e.Context 217 } 218 return canceledCtx 219 } 220 221 func getExecState() *ExecutionState { 222 execStateMu.Lock() 223 defer execStateMu.Unlock() 224 return execState 225 } 226 227 func setExecState(e *ExecutionState) { 228 execStateMu.Lock() 229 defer execStateMu.Unlock() 230 execState = e 231 } 232 233 func resetExecState(e *ExecutionState) { 234 execStateMu.Lock() 235 defer execStateMu.Unlock() 236 if execState == e { 237 execState = nil 238 } 239 } 240 241 // ExecLgoEntryPoint executes main under a new code execution context which is derived from parent. 242 func ExecLgoEntryPoint(parent LgoContext, main func()) error { 243 return finalizeExec(startExec(parent, main)) 244 } 245 246 func startExec(parent LgoContext, main func()) *ExecutionState { 247 atomic.StoreUint32(&isRunning, 1) 248 e := newExecutionState(parent) 249 setExecState(e) 250 251 e.routineWait.Add(1) 252 e.mainCounter.add() 253 go func() { 254 defer e.routineWait.Done() 255 defer e.mainCounter.recordResultInDefer() 256 main() 257 }() 258 return e 259 } 260 261 func finalizeExec(e *ExecutionState) error { 262 e.waitRoutines() 263 resetExecState(e) 264 if msg := e.counterMessage(); msg != "" { 265 return errors.New(msg) 266 } 267 return nil 268 } 269 270 // InitGoroutine is called internally before lgo starts a new goroutine 271 // so that lgo can manage goroutines. 272 func InitGoroutine() *ExecutionState { 273 e := getExecState() 274 if e == nil { 275 return nil 276 } 277 e.routineWait.Add(1) 278 e.subCounter.add() 279 return e 280 } 281 282 // FinalizeGoroutine is called when a goroutine invoked in lgo quits. 283 func FinalizeGoroutine(e *ExecutionState) { 284 r := recover() 285 e.subCounter.recordResult(r) 286 e.routineWait.Done() 287 if r != nil { 288 // paniced, cancel other routines. 289 e.cancel() 290 } 291 return 292 } 293 294 // LgoPrinter is the interface that prints the result of the last lgo expression. 295 type LgoPrinter interface { 296 Println(args ...interface{}) 297 } 298 299 var lgoPrinters = make(map[LgoPrinter]bool) 300 301 // Bailout is thrown to cancel lgo code execution internally. 302 // Bailout is exported to be used from converted code (See converter/autoexit.go). 303 var Bailout = errors.New("canceled") 304 305 // ExitIfCtxDone checkes the current code execution status and throws Bailout to exit the execution 306 // if the execution is canceled. 307 func ExitIfCtxDone() { 308 running := atomic.LoadUint32(&isRunning) 309 if running == 1 { 310 // If running, do nothing. 311 return 312 } 313 // Slow operation 314 select { 315 case <-GetExecContext().Done(): 316 panic(Bailout) 317 default: 318 } 319 } 320 321 // RegisterLgoPrinter registers a LgoPrinter to print the result of the last lgo expression. 322 func RegisterLgoPrinter(p LgoPrinter) { 323 lgoPrinters[p] = true 324 } 325 326 // UnregisterLgoPrinter removes a registered LgoPrinter. 327 func UnregisterLgoPrinter(p LgoPrinter) { 328 delete(lgoPrinters, p) 329 } 330 331 // LgoPrintln prints args with registered LgoPrinters. 332 func LgoPrintln(args ...interface{}) { 333 for p := range lgoPrinters { 334 p.Println(args...) 335 } 336 } 337 338 // AllVars keeps pointers to all variables defined in the current lgo process. 339 // AllVars is keyed by variable names. 340 var AllVars = make(map[string][]interface{}) 341 342 // ZeroClearAllVars clear all existing variables defined in lgo with zero-values. 343 // You can release memory holded from old variables easily with this function. 344 func ZeroClearAllVars() { 345 for _, vars := range AllVars { 346 for _, p := range vars { 347 v := reflect.ValueOf(p) 348 v.Elem().Set(reflect.New(v.Type().Elem()).Elem()) 349 } 350 } 351 // Return memory to OS. 352 debug.FreeOSMemory() 353 runtime.GC() 354 } 355 356 // LgoRegisterVar is used to register a variable to AllVars internally. 357 func LgoRegisterVar(name string, p interface{}) { 358 v := reflect.ValueOf(p) 359 if v.Kind() != reflect.Ptr { 360 panic("cannot register a non-pointer") 361 } 362 AllVars[name] = append(AllVars[name], p) 363 }