...

Source file src/golang.org/x/sync/singleflight/singleflight_test.go

Documentation: golang.org/x/sync/singleflight

     1  // Copyright 2013 The Go Authors. 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 singleflight
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"os/exec"
    13  	"runtime"
    14  	"runtime/debug"
    15  	"strings"
    16  	"sync"
    17  	"sync/atomic"
    18  	"testing"
    19  	"time"
    20  )
    21  
    22  type errValue struct{}
    23  
    24  func (err *errValue) Error() string {
    25  	return "error value"
    26  }
    27  
    28  func TestPanicErrorUnwrap(t *testing.T) {
    29  	t.Parallel()
    30  
    31  	testCases := []struct {
    32  		name             string
    33  		panicValue       interface{}
    34  		wrappedErrorType bool
    35  	}{
    36  		{
    37  			name:             "panicError wraps non-error type",
    38  			panicValue:       &panicError{value: "string value"},
    39  			wrappedErrorType: false,
    40  		},
    41  		{
    42  			name:             "panicError wraps error type",
    43  			panicValue:       &panicError{value: new(errValue)},
    44  			wrappedErrorType: false,
    45  		},
    46  	}
    47  
    48  	for _, tc := range testCases {
    49  		tc := tc
    50  
    51  		t.Run(tc.name, func(t *testing.T) {
    52  			t.Parallel()
    53  
    54  			var recovered interface{}
    55  
    56  			group := &Group{}
    57  
    58  			func() {
    59  				defer func() {
    60  					recovered = recover()
    61  					t.Logf("after panic(%#v) in group.Do, recovered %#v", tc.panicValue, recovered)
    62  				}()
    63  
    64  				_, _, _ = group.Do(tc.name, func() (interface{}, error) {
    65  					panic(tc.panicValue)
    66  				})
    67  			}()
    68  
    69  			if recovered == nil {
    70  				t.Fatal("expected a non-nil panic value")
    71  			}
    72  
    73  			err, ok := recovered.(error)
    74  			if !ok {
    75  				t.Fatalf("recovered non-error type: %T", recovered)
    76  			}
    77  
    78  			if !errors.Is(err, new(errValue)) && tc.wrappedErrorType {
    79  				t.Errorf("unexpected wrapped error type %T; want %T", err, new(errValue))
    80  			}
    81  		})
    82  	}
    83  }
    84  
    85  func TestDo(t *testing.T) {
    86  	var g Group
    87  	v, err, _ := g.Do("key", func() (interface{}, error) {
    88  		return "bar", nil
    89  	})
    90  	if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
    91  		t.Errorf("Do = %v; want %v", got, want)
    92  	}
    93  	if err != nil {
    94  		t.Errorf("Do error = %v", err)
    95  	}
    96  }
    97  
    98  func TestDoErr(t *testing.T) {
    99  	var g Group
   100  	someErr := errors.New("Some error")
   101  	v, err, _ := g.Do("key", func() (interface{}, error) {
   102  		return nil, someErr
   103  	})
   104  	if err != someErr {
   105  		t.Errorf("Do error = %v; want someErr %v", err, someErr)
   106  	}
   107  	if v != nil {
   108  		t.Errorf("unexpected non-nil value %#v", v)
   109  	}
   110  }
   111  
   112  func TestDoDupSuppress(t *testing.T) {
   113  	var g Group
   114  	var wg1, wg2 sync.WaitGroup
   115  	c := make(chan string, 1)
   116  	var calls int32
   117  	fn := func() (interface{}, error) {
   118  		if atomic.AddInt32(&calls, 1) == 1 {
   119  			// First invocation.
   120  			wg1.Done()
   121  		}
   122  		v := <-c
   123  		c <- v // pump; make available for any future calls
   124  
   125  		time.Sleep(10 * time.Millisecond) // let more goroutines enter Do
   126  
   127  		return v, nil
   128  	}
   129  
   130  	const n = 10
   131  	wg1.Add(1)
   132  	for i := 0; i < n; i++ {
   133  		wg1.Add(1)
   134  		wg2.Add(1)
   135  		go func() {
   136  			defer wg2.Done()
   137  			wg1.Done()
   138  			v, err, _ := g.Do("key", fn)
   139  			if err != nil {
   140  				t.Errorf("Do error: %v", err)
   141  				return
   142  			}
   143  			if s, _ := v.(string); s != "bar" {
   144  				t.Errorf("Do = %T %v; want %q", v, v, "bar")
   145  			}
   146  		}()
   147  	}
   148  	wg1.Wait()
   149  	// At least one goroutine is in fn now and all of them have at
   150  	// least reached the line before the Do.
   151  	c <- "bar"
   152  	wg2.Wait()
   153  	if got := atomic.LoadInt32(&calls); got <= 0 || got >= n {
   154  		t.Errorf("number of calls = %d; want over 0 and less than %d", got, n)
   155  	}
   156  }
   157  
   158  // Test that singleflight behaves correctly after Forget called.
   159  // See https://github.com/golang/go/issues/31420
   160  func TestForget(t *testing.T) {
   161  	var g Group
   162  
   163  	var (
   164  		firstStarted  = make(chan struct{})
   165  		unblockFirst  = make(chan struct{})
   166  		firstFinished = make(chan struct{})
   167  	)
   168  
   169  	go func() {
   170  		g.Do("key", func() (i interface{}, e error) {
   171  			close(firstStarted)
   172  			<-unblockFirst
   173  			close(firstFinished)
   174  			return
   175  		})
   176  	}()
   177  	<-firstStarted
   178  	g.Forget("key")
   179  
   180  	unblockSecond := make(chan struct{})
   181  	secondResult := g.DoChan("key", func() (i interface{}, e error) {
   182  		<-unblockSecond
   183  		return 2, nil
   184  	})
   185  
   186  	close(unblockFirst)
   187  	<-firstFinished
   188  
   189  	thirdResult := g.DoChan("key", func() (i interface{}, e error) {
   190  		return 3, nil
   191  	})
   192  
   193  	close(unblockSecond)
   194  	<-secondResult
   195  	r := <-thirdResult
   196  	if r.Val != 2 {
   197  		t.Errorf("We should receive result produced by second call, expected: 2, got %d", r.Val)
   198  	}
   199  }
   200  
   201  func TestDoChan(t *testing.T) {
   202  	var g Group
   203  	ch := g.DoChan("key", func() (interface{}, error) {
   204  		return "bar", nil
   205  	})
   206  
   207  	res := <-ch
   208  	v := res.Val
   209  	err := res.Err
   210  	if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
   211  		t.Errorf("Do = %v; want %v", got, want)
   212  	}
   213  	if err != nil {
   214  		t.Errorf("Do error = %v", err)
   215  	}
   216  }
   217  
   218  // Test singleflight behaves correctly after Do panic.
   219  // See https://github.com/golang/go/issues/41133
   220  func TestPanicDo(t *testing.T) {
   221  	var g Group
   222  	fn := func() (interface{}, error) {
   223  		panic("invalid memory address or nil pointer dereference")
   224  	}
   225  
   226  	const n = 5
   227  	waited := int32(n)
   228  	panicCount := int32(0)
   229  	done := make(chan struct{})
   230  	for i := 0; i < n; i++ {
   231  		go func() {
   232  			defer func() {
   233  				if err := recover(); err != nil {
   234  					t.Logf("Got panic: %v\n%s", err, debug.Stack())
   235  					atomic.AddInt32(&panicCount, 1)
   236  				}
   237  
   238  				if atomic.AddInt32(&waited, -1) == 0 {
   239  					close(done)
   240  				}
   241  			}()
   242  
   243  			g.Do("key", fn)
   244  		}()
   245  	}
   246  
   247  	select {
   248  	case <-done:
   249  		if panicCount != n {
   250  			t.Errorf("Expect %d panic, but got %d", n, panicCount)
   251  		}
   252  	case <-time.After(time.Second):
   253  		t.Fatalf("Do hangs")
   254  	}
   255  }
   256  
   257  func TestGoexitDo(t *testing.T) {
   258  	var g Group
   259  	fn := func() (interface{}, error) {
   260  		runtime.Goexit()
   261  		return nil, nil
   262  	}
   263  
   264  	const n = 5
   265  	waited := int32(n)
   266  	done := make(chan struct{})
   267  	for i := 0; i < n; i++ {
   268  		go func() {
   269  			var err error
   270  			defer func() {
   271  				if err != nil {
   272  					t.Errorf("Error should be nil, but got: %v", err)
   273  				}
   274  				if atomic.AddInt32(&waited, -1) == 0 {
   275  					close(done)
   276  				}
   277  			}()
   278  			_, err, _ = g.Do("key", fn)
   279  		}()
   280  	}
   281  
   282  	select {
   283  	case <-done:
   284  	case <-time.After(time.Second):
   285  		t.Fatalf("Do hangs")
   286  	}
   287  }
   288  
   289  func executable(t testing.TB) string {
   290  	exe, err := os.Executable()
   291  	if err != nil {
   292  		t.Skipf("skipping: test executable not found")
   293  	}
   294  
   295  	// Control case: check whether exec.Command works at all.
   296  	// (For example, it might fail with a permission error on iOS.)
   297  	cmd := exec.Command(exe, "-test.list=^$")
   298  	cmd.Env = []string{}
   299  	if err := cmd.Run(); err != nil {
   300  		t.Skipf("skipping: exec appears not to work on %s: %v", runtime.GOOS, err)
   301  	}
   302  
   303  	return exe
   304  }
   305  
   306  func TestPanicDoChan(t *testing.T) {
   307  	if os.Getenv("TEST_PANIC_DOCHAN") != "" {
   308  		defer func() {
   309  			recover()
   310  		}()
   311  
   312  		g := new(Group)
   313  		ch := g.DoChan("", func() (interface{}, error) {
   314  			panic("Panicking in DoChan")
   315  		})
   316  		<-ch
   317  		t.Fatalf("DoChan unexpectedly returned")
   318  	}
   319  
   320  	t.Parallel()
   321  
   322  	cmd := exec.Command(executable(t), "-test.run="+t.Name(), "-test.v")
   323  	cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
   324  	out := new(bytes.Buffer)
   325  	cmd.Stdout = out
   326  	cmd.Stderr = out
   327  	if err := cmd.Start(); err != nil {
   328  		t.Fatal(err)
   329  	}
   330  
   331  	err := cmd.Wait()
   332  	t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out)
   333  	if err == nil {
   334  		t.Errorf("Test subprocess passed; want a crash due to panic in DoChan")
   335  	}
   336  	if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) {
   337  		t.Errorf("Test subprocess failed with an unexpected failure mode.")
   338  	}
   339  	if !bytes.Contains(out.Bytes(), []byte("Panicking in DoChan")) {
   340  		t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in DoChan")
   341  	}
   342  }
   343  
   344  func TestPanicDoSharedByDoChan(t *testing.T) {
   345  	if os.Getenv("TEST_PANIC_DOCHAN") != "" {
   346  		blocked := make(chan struct{})
   347  		unblock := make(chan struct{})
   348  
   349  		g := new(Group)
   350  		go func() {
   351  			defer func() {
   352  				recover()
   353  			}()
   354  			g.Do("", func() (interface{}, error) {
   355  				close(blocked)
   356  				<-unblock
   357  				panic("Panicking in Do")
   358  			})
   359  		}()
   360  
   361  		<-blocked
   362  		ch := g.DoChan("", func() (interface{}, error) {
   363  			panic("DoChan unexpectedly executed callback")
   364  		})
   365  		close(unblock)
   366  		<-ch
   367  		t.Fatalf("DoChan unexpectedly returned")
   368  	}
   369  
   370  	t.Parallel()
   371  
   372  	cmd := exec.Command(executable(t), "-test.run="+t.Name(), "-test.v")
   373  	cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
   374  	out := new(bytes.Buffer)
   375  	cmd.Stdout = out
   376  	cmd.Stderr = out
   377  	if err := cmd.Start(); err != nil {
   378  		t.Fatal(err)
   379  	}
   380  
   381  	err := cmd.Wait()
   382  	t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out)
   383  	if err == nil {
   384  		t.Errorf("Test subprocess passed; want a crash due to panic in Do shared by DoChan")
   385  	}
   386  	if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) {
   387  		t.Errorf("Test subprocess failed with an unexpected failure mode.")
   388  	}
   389  	if !bytes.Contains(out.Bytes(), []byte("Panicking in Do")) {
   390  		t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in Do")
   391  	}
   392  }
   393  
   394  func ExampleGroup() {
   395  	g := new(Group)
   396  
   397  	block := make(chan struct{})
   398  	res1c := g.DoChan("key", func() (interface{}, error) {
   399  		<-block
   400  		return "func 1", nil
   401  	})
   402  	res2c := g.DoChan("key", func() (interface{}, error) {
   403  		<-block
   404  		return "func 2", nil
   405  	})
   406  	close(block)
   407  
   408  	res1 := <-res1c
   409  	res2 := <-res2c
   410  
   411  	// Results are shared by functions executed with duplicate keys.
   412  	fmt.Println("Shared:", res2.Shared)
   413  	// Only the first function is executed: it is registered and started with "key",
   414  	// and doesn't complete before the second function is registered with a duplicate key.
   415  	fmt.Println("Equal results:", res1.Val.(string) == res2.Val.(string))
   416  	fmt.Println("Result:", res1.Val)
   417  
   418  	// Output:
   419  	// Shared: true
   420  	// Equal results: true
   421  	// Result: func 1
   422  }
   423  

View as plain text