singleflight: fix hangs after first Do panic
When first Do panic, the related wait group will never be done, and all the subsequent calls would block on the same wait group forever. Fixes golang/go#41133 Change-Id: I0ad9bfb387b6133b10766a34fc0040f200eae27e Reviewed-on: https://go-review.googlesource.com/c/sync/+/251677 Run-TryBot: Bryan C. Mills <bcmills@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@golang.org> Reviewed-by: Bryan C. Mills <bcmills@google.com> Trust: Ian Lance Taylor <iant@golang.org> Trust: Bryan C. Mills <bcmills@google.com>
This commit is contained in:
		
					parent
					
						
							
								6e8e738ad2
							
						
					
				
			
			
				commit
				
					
						30421366ff
					
				
			
		
					 2 changed files with 284 additions and 10 deletions
				
			
		|  | @ -6,7 +6,42 @@ | |||
| // mechanism. | ||||
| package singleflight // import "golang.org/x/sync/singleflight" | ||||
| 
 | ||||
| import "sync" | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"runtime" | ||||
| 	"runtime/debug" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| // errGoexit indicates the runtime.Goexit was called in | ||||
| // the user given function. | ||||
| var errGoexit = errors.New("runtime.Goexit was called") | ||||
| 
 | ||||
| // A panicError is an arbitrary value recovered from a panic | ||||
| // with the stack trace during the execution of given function. | ||||
| type panicError struct { | ||||
| 	value interface{} | ||||
| 	stack []byte | ||||
| } | ||||
| 
 | ||||
| // Error implements error interface. | ||||
| func (p *panicError) Error() string { | ||||
| 	return fmt.Sprintf("%v\n\n%s", p.value, p.stack) | ||||
| } | ||||
| 
 | ||||
| func newPanicError(v interface{}) error { | ||||
| 	stack := debug.Stack() | ||||
| 
 | ||||
| 	// The first line of the stack trace is of the form "goroutine N [status]:" | ||||
| 	// but by the time the panic reaches Do the goroutine may no longer exist | ||||
| 	// and its status will have changed. Trim out the misleading line. | ||||
| 	if line := bytes.IndexByte(stack[:], '\n'); line >= 0 { | ||||
| 		stack = stack[line+1:] | ||||
| 	} | ||||
| 	return &panicError{value: v, stack: stack} | ||||
| } | ||||
| 
 | ||||
| // call is an in-flight or completed singleflight.Do call | ||||
| type call struct { | ||||
|  | @ -57,6 +92,12 @@ func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, e | |||
| 		c.dups++ | ||||
| 		g.mu.Unlock() | ||||
| 		c.wg.Wait() | ||||
| 
 | ||||
| 		if e, ok := c.err.(*panicError); ok { | ||||
| 			panic(e) | ||||
| 		} else if c.err == errGoexit { | ||||
| 			runtime.Goexit() | ||||
| 		} | ||||
| 		return c.val, c.err, true | ||||
| 	} | ||||
| 	c := new(call) | ||||
|  | @ -70,6 +111,8 @@ func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, e | |||
| 
 | ||||
| // DoChan is like Do but returns a channel that will receive the | ||||
| // results when they are ready. | ||||
| // | ||||
| // The returned channel will not be closed. | ||||
| func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result { | ||||
| 	ch := make(chan Result, 1) | ||||
| 	g.mu.Lock() | ||||
|  | @ -94,17 +137,66 @@ func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result | |||
| 
 | ||||
| // doCall handles the single call for a key. | ||||
| func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) { | ||||
| 	c.val, c.err = fn() | ||||
| 	c.wg.Done() | ||||
| 	normalReturn := false | ||||
| 	recovered := false | ||||
| 
 | ||||
| 	g.mu.Lock() | ||||
| 	if !c.forgotten { | ||||
| 		delete(g.m, key) | ||||
| 	// use double-defer to distinguish panic from runtime.Goexit, | ||||
| 	// more details see https://golang.org/cl/134395 | ||||
| 	defer func() { | ||||
| 		// the given function invoked runtime.Goexit | ||||
| 		if !normalReturn && !recovered { | ||||
| 			c.err = errGoexit | ||||
| 		} | ||||
| 
 | ||||
| 		c.wg.Done() | ||||
| 		g.mu.Lock() | ||||
| 		defer g.mu.Unlock() | ||||
| 		if !c.forgotten { | ||||
| 			delete(g.m, key) | ||||
| 		} | ||||
| 
 | ||||
| 		if e, ok := c.err.(*panicError); ok { | ||||
| 			// In order to prevent the waiting channels from being blocked forever, | ||||
| 			// needs to ensure that this panic cannot be recovered. | ||||
| 			if len(c.chans) > 0 { | ||||
| 				go panic(e) | ||||
| 				select {} // Keep this goroutine around so that it will appear in the crash dump. | ||||
| 			} else { | ||||
| 				panic(e) | ||||
| 			} | ||||
| 		} else if c.err == errGoexit { | ||||
| 			// Already in the process of goexit, no need to call again | ||||
| 		} else { | ||||
| 			// Normal return | ||||
| 			for _, ch := range c.chans { | ||||
| 				ch <- Result{c.val, c.err, c.dups > 0} | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	func() { | ||||
| 		defer func() { | ||||
| 			if !normalReturn { | ||||
| 				// Ideally, we would wait to take a stack trace until we've determined | ||||
| 				// whether this is a panic or a runtime.Goexit. | ||||
| 				// | ||||
| 				// Unfortunately, the only way we can distinguish the two is to see | ||||
| 				// whether the recover stopped the goroutine from terminating, and by | ||||
| 				// the time we know that, the part of the stack trace relevant to the | ||||
| 				// panic has been discarded. | ||||
| 				if r := recover(); r != nil { | ||||
| 					c.err = newPanicError(r) | ||||
| 				} | ||||
| 			} | ||||
| 		}() | ||||
| 
 | ||||
| 		c.val, c.err = fn() | ||||
| 		normalReturn = true | ||||
| 	}() | ||||
| 
 | ||||
| 	if !normalReturn { | ||||
| 		recovered = true | ||||
| 	} | ||||
| 	for _, ch := range c.chans { | ||||
| 		ch <- Result{c.val, c.err, c.dups > 0} | ||||
| 	} | ||||
| 	g.mu.Unlock() | ||||
| } | ||||
| 
 | ||||
| // Forget tells the singleflight to forget about a key.  Future calls | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue