Skip to content

Commit

Permalink
all: support capturing panics
Browse files Browse the repository at this point in the history
  • Loading branch information
changkun committed Sep 20, 2021
1 parent 7eebd51 commit ae74ec1
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 21 deletions.
50 changes: 46 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,70 @@ import "golang.design/x/mainthread"

func main() { mainthread.Init(fn) }

// fn is the actual main function
// fn is the actual main function
func fn() {
// ... do whatever you want to do ...
// ... do stuff ...

// mainthread.Call returns when f1 returns. Note that if f1 blocks
// it will also block the execution of any subsequent calls on the
// main thread.
mainthread.Call(f1)

// ... do whatever you want to do ...
// ... do stuff ...


// mainthread.Go returns immediately and f2 is scheduled to be
// executed in the future.
mainthread.Go(f2)

// ... do whatever you want to do ...
// ... do stuff ...
}

func f1() { ... }
func f2() { ... }
```

If the given function triggers a panic, and called via `mainthread.Call`,
then the panic will be propagated to the same goroutine. One can capture
that panic, when possible:

```go
defer func() {
if r := recover(); r != nil {
println(r)
}
}()

mainthread.Call(func() { ... }) // if panic
```

If the given function triggers a panic, and called via `mainthread.Go`,
then the panic will be cached internally, until a call to the `Error()` method:

```go
mainthread.Go(func() { ... }) // if panics

// ... do stuff ...

if err := mainthread.Error(); err != nil { // can be captured here.
println(err)
}
```

Note that a panic happens before `mainthread.Error()` returning the
panicked error. If one needs to guarantee `mainthread.Error()` indeed
captured the panic, a dummy function can be used as synchornization:

```go
mainthread.Go(func() { panic("die") }) // if panics
mainthread.Call(func() {}) // for execution synchronization
err := mainthread.Error() // err must be non-nil
```


It is possible to cache up to a maximum of 42 panicked errors.
More errors are ignored.

## When do you need this package?

Read this to learn more about the design purpose of this package:
Expand Down
104 changes: 87 additions & 17 deletions mainthread.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,63 @@
//
// // fn is the actual main function
// func fn() {
// // ... do whatever you want to do ...
// // ... do stuff ...
//
// // mainthread.Call returns when f1 returns. Note that if f1
// // blocks it will also block the execution of any subsequent
// // calls on the main thread.
// // mainthread.Call returns when f1 returns. Note that if f1 blocks
// // it will also block the execution of any subsequent calls on the
// // main thread.
// mainthread.Call(f1)
//
// // ... do whatever you want to do ...
// // ... do stuff ...
//
//
// // mainthread.Go returns immediately and f2 is scheduled to be
// // executed in the future.
// mainthread.Go(f2)
//
// // ... do whatever you want to do ...
// // ... do stuff ...
// }
//
// func f1() { ... }
// func f2() { ... }
//
// If the given function triggers a panic, and called via `mainthread.Call`,
// then the panic will be propagated to the same goroutine. One can capture
// that panic, when possible:
//
// defer func() {
// if r := recover(); r != nil {
// println(r)
// }
// }()
//
// mainthread.Call(func() { ... }) // if panic
//
// If the given function triggers a panic, and called via `mainthread.Go`,
// then the panic will be cached internally, until a call to the `Error()` method:
//
// mainthread.Go(func() { ... }) // if panics
//
// // ... do stuff ...
//
// if err := mainthread.Error(); err != nil { // can be captured here.
// println(err)
// }
//
// Note that a panic happens before `mainthread.Error()` returning the
// panicked error. If one needs to guarantee `mainthread.Error()` indeed
// captured the panic, a dummy function can be used as synchornization:
//
// mainthread.Go(func() { panic("die") }) // if panics
// mainthread.Call(func() {}) // for execution synchronization
// err := mainthread.Error() // err must be non-nil
//
// It is possible to cache up to a maximum of 42 panicked errors.
// More errors are ignored.
package mainthread // import "golang.design/x/mainthread"

import (
"fmt"
"runtime"
"sync"
)
Expand All @@ -50,23 +86,39 @@ func init() {
//
// Init must be called in the main.main function.
func Init(main func()) {
done := donePool.Get().(chan struct{})
done := donePool.Get().(chan error)
defer donePool.Put(done)

go func() {
defer func() {
done <- struct{}{}
done <- nil
}()
main()
}()

for {
select {
case f := <-funcQ:
f.fn()
if f.done != nil {
f.done <- struct{}{}
}
func() {
defer func() {
r := recover()
if f.done != nil {
if r != nil {
f.done <- fmt.Errorf("%v", r)
} else {
f.done <- nil
}
} else {
if r != nil {
select {
case erroQ <- fmt.Errorf("%v", r):
default:
}
}
}
}()
f.fn()
}()
case <-done:
return
}
Expand All @@ -75,26 +127,44 @@ func Init(main func()) {

// Call calls f on the main thread and blocks until f finishes.
func Call(f func()) {
done := donePool.Get().(chan struct{})
done := donePool.Get().(chan error)
defer donePool.Put(done)

funcQ <- funcData{fn: f, done: done}
<-done
data := funcData{fn: f, done: done}
funcQ <- data
if err := <-done; err != nil {
panic(err)
}
}

// Go schedules f to be called on the main thread.
func Go(f func()) {
funcQ <- funcData{fn: f}
}

// Error returns an error that is captured if there are any panics
// happened on the mainthread.
//
// It is possible to cache up to a maximum of 42 panicked errors.
// More errors are ignored.
func Error() error {
select {
case err := <-erroQ:
return err
default:
return nil
}
}

var (
funcQ = make(chan funcData, runtime.GOMAXPROCS(0))
erroQ = make(chan error, 42)
donePool = sync.Pool{New: func() interface{} {
return make(chan struct{})
return make(chan error)
}}
)

type funcData struct {
fn func()
done chan struct{}
done chan error
}
25 changes: 25 additions & 0 deletions mainthread_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,28 @@ func TestGo(t *testing.T) {
case <-done:
}
}

func TestPanickedFuncCall(t *testing.T) {
defer func() {
if r := recover(); r != nil {
return
}
t.Fatalf("expected to panic, but actually not")
}()

mainthread.Call(func() {
panic("die")
})
}

func TestPanickedFuncGo(t *testing.T) {
defer func() {
if err := mainthread.Error(); err != nil {
return
}
t.Fatalf("expected to panic, but actually not")
}()

mainthread.Go(func() { panic("die") })
mainthread.Call(func() {}) // for sync
}

0 comments on commit ae74ec1

Please sign in to comment.