github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/component/run_component_test.go (about)

     1  package component_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"math"
     7  	"sync"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/onflow/flow-go/module/component"
    14  	"github.com/onflow/flow-go/module/irrecoverable"
    15  )
    16  
    17  var ErrFatal = errors.New("fatal")
    18  var ErrCouldNotCreateComponent = errors.New("failed to create component")
    19  
    20  var _ component.Component = (*StartupErroringComponent)(nil)
    21  var _ component.Component = (*RunningErroringComponent)(nil)
    22  var _ component.Component = (*ShutdownErroringComponent)(nil)
    23  var _ component.Component = (*ConcurrentErroringComponent)(nil)
    24  
    25  type StartupErroringComponent struct {
    26  	ready chan struct{}
    27  	done  chan struct{}
    28  }
    29  
    30  func NewStartupErroringComponent() *StartupErroringComponent {
    31  	return &StartupErroringComponent{
    32  		ready: make(chan struct{}),
    33  		done:  make(chan struct{}),
    34  	}
    35  }
    36  
    37  func (c *StartupErroringComponent) Start(ctx irrecoverable.SignalerContext) {
    38  	go func() {
    39  		defer close(c.done)
    40  
    41  		// throw fatal error during startup
    42  		ctx.Throw(ErrFatal)
    43  
    44  		// close(c.ready)
    45  	}()
    46  }
    47  
    48  func (c *StartupErroringComponent) Ready() <-chan struct{} {
    49  	return c.ready
    50  }
    51  
    52  func (c *StartupErroringComponent) Done() <-chan struct{} {
    53  	return c.done
    54  }
    55  
    56  type RunningErroringComponent struct {
    57  	ready    chan struct{}
    58  	done     chan struct{}
    59  	duration time.Duration
    60  }
    61  
    62  func NewRunningErroringComponent(duration time.Duration) *RunningErroringComponent {
    63  	return &RunningErroringComponent{
    64  		ready:    make(chan struct{}),
    65  		done:     make(chan struct{}),
    66  		duration: duration,
    67  	}
    68  }
    69  
    70  func (c *RunningErroringComponent) Start(ctx irrecoverable.SignalerContext) {
    71  	go func() {
    72  		defer close(c.done)
    73  		close(c.ready)
    74  
    75  		// do some work...
    76  		select {
    77  		case <-time.After(c.duration):
    78  		case <-ctx.Done():
    79  			return
    80  		}
    81  
    82  		// throw fatal error
    83  		ctx.Throw(ErrFatal)
    84  	}()
    85  }
    86  
    87  func (c *RunningErroringComponent) Ready() <-chan struct{} {
    88  	return c.ready
    89  }
    90  
    91  func (c *RunningErroringComponent) Done() <-chan struct{} {
    92  	return c.done
    93  }
    94  
    95  type ShutdownErroringComponent struct {
    96  	ready    sync.WaitGroup
    97  	done     sync.WaitGroup
    98  	started  chan struct{}
    99  	duration time.Duration
   100  }
   101  
   102  func NewShutdownErroringComponent(duration time.Duration) *ShutdownErroringComponent {
   103  	return &ShutdownErroringComponent{
   104  		started:  make(chan struct{}),
   105  		duration: duration,
   106  	}
   107  }
   108  
   109  func (c *ShutdownErroringComponent) Start(ctx irrecoverable.SignalerContext) {
   110  	c.ready.Add(2)
   111  	c.done.Add(2)
   112  
   113  	go func() {
   114  		c.ready.Done()
   115  		defer c.done.Done()
   116  
   117  		// do some work...
   118  		select {
   119  		case <-time.After(c.duration):
   120  		case <-ctx.Done():
   121  			return
   122  		}
   123  
   124  		// encounter fatal error
   125  		ctx.Throw(ErrFatal)
   126  	}()
   127  
   128  	go func() {
   129  		c.ready.Done()
   130  		defer c.done.Done()
   131  
   132  		// wait for shutdown signal triggered by fatal error
   133  		<-ctx.Done()
   134  
   135  		// encounter error during shutdown
   136  		ctx.Throw(ErrFatal)
   137  	}()
   138  
   139  	close(c.started)
   140  }
   141  
   142  func (c *ShutdownErroringComponent) Ready() <-chan struct{} {
   143  	ready := make(chan struct{})
   144  	go func() {
   145  		<-c.started
   146  		c.ready.Wait()
   147  		close(ready)
   148  	}()
   149  	return ready
   150  }
   151  
   152  func (c *ShutdownErroringComponent) Done() <-chan struct{} {
   153  	done := make(chan struct{})
   154  	go func() {
   155  		<-c.started
   156  		c.done.Wait()
   157  		close(done)
   158  	}()
   159  	return done
   160  }
   161  
   162  type ConcurrentErroringComponent struct {
   163  	ready    sync.WaitGroup
   164  	done     sync.WaitGroup
   165  	started  chan struct{}
   166  	duration time.Duration
   167  }
   168  
   169  func NewConcurrentErroringComponent(duration time.Duration) *ConcurrentErroringComponent {
   170  	return &ConcurrentErroringComponent{
   171  		started:  make(chan struct{}),
   172  		duration: duration,
   173  	}
   174  }
   175  
   176  func (c *ConcurrentErroringComponent) Start(ctx irrecoverable.SignalerContext) {
   177  	c.ready.Add(2)
   178  	c.done.Add(2)
   179  
   180  	for i := 0; i < 2; i++ {
   181  		go func() {
   182  			c.ready.Done()
   183  			defer c.done.Done()
   184  
   185  			// do some work...
   186  			select {
   187  			case <-time.After(c.duration):
   188  			case <-ctx.Done():
   189  				return
   190  			}
   191  
   192  			// encounter fatal error
   193  			ctx.Throw(ErrFatal)
   194  		}()
   195  	}
   196  
   197  	close(c.started)
   198  }
   199  
   200  func (c *ConcurrentErroringComponent) Ready() <-chan struct{} {
   201  	ready := make(chan struct{})
   202  	go func() {
   203  		<-c.started
   204  		c.ready.Wait()
   205  		close(ready)
   206  	}()
   207  	return ready
   208  }
   209  
   210  func (c *ConcurrentErroringComponent) Done() <-chan struct{} {
   211  	done := make(chan struct{})
   212  	go func() {
   213  		<-c.started
   214  		c.done.Wait()
   215  		close(done)
   216  	}()
   217  	return done
   218  }
   219  
   220  type NonErroringComponent struct {
   221  	ready    chan struct{}
   222  	done     chan struct{}
   223  	duration time.Duration
   224  }
   225  
   226  func NewNonErroringComponent(duration time.Duration) *NonErroringComponent {
   227  	return &NonErroringComponent{
   228  		ready:    make(chan struct{}),
   229  		done:     make(chan struct{}),
   230  		duration: duration,
   231  	}
   232  }
   233  
   234  func (c *NonErroringComponent) Start(ctx irrecoverable.SignalerContext) {
   235  	go func() {
   236  		defer close(c.done)
   237  
   238  		// do some work...
   239  		select {
   240  		case <-time.After(c.duration):
   241  		case <-ctx.Done():
   242  			return
   243  		}
   244  
   245  		// exit gracefully
   246  	}()
   247  }
   248  
   249  func (c *NonErroringComponent) Ready() <-chan struct{} {
   250  	return c.ready
   251  }
   252  
   253  func (c *NonErroringComponent) Done() <-chan struct{} {
   254  	return c.done
   255  }
   256  
   257  // tests that after starting a component that generates, an expected error is received
   258  func TestRunComponentStartupError(t *testing.T) {
   259  	componentFactory := func() (component.Component, error) {
   260  		return NewStartupErroringComponent(), nil
   261  	}
   262  
   263  	called := false
   264  	onError := func(err error) component.ErrorHandlingResult {
   265  		called = true
   266  		require.ErrorIs(t, err, ErrFatal)  //check that really got the fatal error we were expecting
   267  		return component.ErrorHandlingStop //stop component after receiving the error (don't restart it)
   268  	}
   269  
   270  	//irrelevant what context we use - test won't be using it
   271  	err := component.RunComponent(context.Background(), componentFactory, onError)
   272  	require.ErrorIs(t, err, ErrFatal)
   273  	require.True(t, called)
   274  }
   275  
   276  // tests repeatedly restarting a component during an error that occurs while shutting down
   277  func TestRunComponentShutdownError(t *testing.T) {
   278  	componentFactory := func() (component.Component, error) {
   279  		//shutdown the component after some time - simulate an error during shutdown
   280  		return NewShutdownErroringComponent(100 * time.Millisecond), nil
   281  	}
   282  
   283  	fatals := 0
   284  	onError := func(err error) component.ErrorHandlingResult {
   285  		fatals++
   286  		require.ErrorIs(t, err, ErrFatal)
   287  		if fatals < 3 { //restart component after first and second error
   288  			return component.ErrorHandlingRestart
   289  		} else { //stop component after third error
   290  			return component.ErrorHandlingStop
   291  		}
   292  	}
   293  
   294  	//irrelevant what context we use - test won't be using it
   295  	err := component.RunComponent(context.Background(), componentFactory, onError)
   296  	require.ErrorIs(t, err, ErrFatal)
   297  	require.Equal(t, 3, fatals)
   298  }
   299  
   300  func TestRunComponentConcurrentError(t *testing.T) {
   301  	ctx, cancel := context.WithCancel(context.Background())
   302  	defer cancel()
   303  
   304  	componentFactory := func() (component.Component, error) {
   305  		return NewConcurrentErroringComponent(100 * time.Millisecond), nil
   306  	}
   307  
   308  	fatals := 0
   309  	onError := func(err error) component.ErrorHandlingResult {
   310  		fatals++
   311  		require.ErrorIs(t, err, ErrFatal)
   312  		if fatals < 2 {
   313  			return component.ErrorHandlingRestart
   314  		} else {
   315  			return component.ErrorHandlingStop
   316  		}
   317  	}
   318  
   319  	err := component.RunComponent(ctx, componentFactory, onError)
   320  	require.ErrorIs(t, err, ErrFatal)
   321  	require.Equal(t, 2, fatals)
   322  }
   323  
   324  func TestRunComponentNoError(t *testing.T) {
   325  	ctx, cancel := context.WithCancel(context.Background())
   326  	defer cancel()
   327  
   328  	componentFactory := func() (component.Component, error) {
   329  		return NewNonErroringComponent(100 * time.Millisecond), nil
   330  	}
   331  
   332  	onError := func(err error) component.ErrorHandlingResult {
   333  		require.Fail(t, "error handler should not have been called")
   334  		return component.ErrorHandlingStop
   335  	}
   336  
   337  	err := component.RunComponent(ctx, componentFactory, onError)
   338  	require.NoError(t, err)
   339  }
   340  
   341  func TestRunComponentCancel(t *testing.T) {
   342  	componentFactory := func() (component.Component, error) {
   343  		return NewNonErroringComponent(math.MaxInt64), nil
   344  	}
   345  
   346  	onError := func(err error) component.ErrorHandlingResult {
   347  		require.Fail(t, "error handler should not have been called")
   348  		return component.ErrorHandlingStop
   349  	}
   350  
   351  	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
   352  	defer cancel()
   353  
   354  	err := component.RunComponent(ctx, componentFactory, onError)
   355  	require.ErrorIs(t, err, ctx.Err())
   356  }
   357  
   358  func TestRunComponentFactoryError(t *testing.T) {
   359  	componentFactory := func() (component.Component, error) {
   360  		return nil, ErrCouldNotCreateComponent
   361  	}
   362  
   363  	onError := func(err error) component.ErrorHandlingResult {
   364  		require.Fail(t, "error handler should not have been called")
   365  		return component.ErrorHandlingStop
   366  	}
   367  
   368  	ctx, cancel := context.WithCancel(context.Background())
   369  	defer cancel()
   370  
   371  	err := component.RunComponent(ctx, componentFactory, onError)
   372  	require.ErrorIs(t, err, ErrCouldNotCreateComponent)
   373  }