go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/async/interval.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package async
     9  
    10  import (
    11  	"context"
    12  	"sync"
    13  	"time"
    14  )
    15  
    16  /*
    17  NewInterval returns a new worker that runs an action on an interval.
    18  
    19  Example:
    20  
    21  	iw := &Interval{
    22  		Action: func(ctx context.Context) error {
    23  			fmt.Println("running!")
    24  			return nil
    25  		},
    26  		Interval: 500*time.Millisecond,
    27  	}
    28  	go iw.Start()
    29  	<-iw.Started()
    30  
    31  */
    32  
    33  // Interval is a background worker that performs an action on an interval.
    34  type Interval struct {
    35  	Context     context.Context
    36  	Interval    time.Duration
    37  	Action      func(context.Context) error
    38  	Delay       time.Duration
    39  	StopOnError bool
    40  	Errors      chan error
    41  
    42  	latchMu sync.Mutex
    43  	latch   *Latch
    44  }
    45  
    46  // Interval defaults
    47  const (
    48  	DefaultInterval = 500 * time.Millisecond
    49  )
    50  
    51  // Background returns the provided context or context.Background()
    52  func (i *Interval) Background() context.Context {
    53  	if i.Context != nil {
    54  		return i.Context
    55  	}
    56  	return context.Background()
    57  }
    58  
    59  // IntervalOrDefault returns the interval or a default.
    60  func (i *Interval) IntervalOrDefault() time.Duration {
    61  	if i.Interval > 0 {
    62  		return i.Interval
    63  	}
    64  	return DefaultInterval
    65  }
    66  
    67  // Started returns the channel to notify when the worker starts.
    68  func (i *Interval) Started() <-chan struct{} {
    69  	return i.latch.NotifyStarted()
    70  }
    71  
    72  /*
    73  Start starts the worker.
    74  
    75  This will start the internal ticker, with a default initial delay of the given interval, and will return an ErrCannotStart if the interval worker is already started.
    76  
    77  This call will block.
    78  */
    79  func (i *Interval) Start() error {
    80  	i.ensureLatch()
    81  	if !i.latch.CanStart() {
    82  		return ErrCannotStart
    83  	}
    84  	if i.Action == nil {
    85  		return ErrCannotStartActionRequired
    86  	}
    87  	i.latch.Starting()
    88  	return i.Dispatch()
    89  }
    90  
    91  // Stop stops the worker.
    92  func (i *Interval) Stop() error {
    93  	if !i.latch.CanStop() {
    94  		return ErrCannotStop
    95  	}
    96  	i.latch.Stopping()
    97  	<-i.latch.NotifyStopped()
    98  	i.latch.Reset() // reset the latch in case we have to start again
    99  	return nil
   100  }
   101  
   102  // Dispatch is the main dispatch loop.
   103  func (i *Interval) Dispatch() (err error) {
   104  	i.latch.Started()
   105  
   106  	if i.Delay > 0 {
   107  		alarm := time.NewTimer(i.Delay)
   108  		stopping := i.latch.NotifyStopping()
   109  		select {
   110  		case <-i.Context.Done():
   111  			alarm.Stop()
   112  			return
   113  		case <-stopping:
   114  			alarm.Stop()
   115  			return
   116  		case <-alarm.C:
   117  			alarm.Stop()
   118  		}
   119  	}
   120  
   121  	tick := time.NewTicker(i.IntervalOrDefault())
   122  	defer func() {
   123  		tick.Stop()
   124  		i.latch.Stopped()
   125  	}()
   126  
   127  	var stopping <-chan struct{}
   128  	for {
   129  		stopping = i.latch.NotifyStopping()
   130  		select {
   131  		case <-i.Context.Done():
   132  			return
   133  		case <-stopping:
   134  			return
   135  		default:
   136  		}
   137  
   138  		select {
   139  		case <-i.Context.Done():
   140  			return
   141  		case <-stopping:
   142  			return
   143  		case <-tick.C:
   144  			err = i.Action(i.Background())
   145  			if err != nil {
   146  				if i.StopOnError {
   147  					return
   148  				}
   149  				if i.Errors != nil {
   150  					select {
   151  					case <-stopping:
   152  						return
   153  					case <-i.Context.Done():
   154  						return
   155  					case i.Errors <- err:
   156  					}
   157  				}
   158  			}
   159  		}
   160  	}
   161  }
   162  
   163  func (i *Interval) ensureLatch() {
   164  	i.latchMu.Lock()
   165  	defer i.latchMu.Unlock()
   166  	if i.latch == nil {
   167  		i.latch = NewLatch()
   168  	}
   169  }