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  }