gobot.io/x/gobot/v2@v2.1.0/robot_work.go (about)

     1  package gobot
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	"sync"
     9  
    10  	"github.com/gofrs/uuid"
    11  )
    12  
    13  // RobotWorkRegistry contains all the work units registered on a Robot
    14  type RobotWorkRegistry struct {
    15  	sync.RWMutex
    16  
    17  	r map[string]*RobotWork
    18  }
    19  
    20  const (
    21  	EveryWorkKind = "every"
    22  	AfterWorkKind = "after"
    23  )
    24  
    25  // RobotWork and the RobotWork registry represent units of executing computation
    26  // managed at the Robot level. Unlike the utility functions gobot.After and gobot.Every,
    27  // RobotWork units require a context.Context, and can be cancelled externally by calling code.
    28  //
    29  // Usage:
    30  //
    31  //	someWork := myRobot.Every(context.Background(), time.Second * 2, func(){
    32  //		fmt.Println("Here I am doing work")
    33  // 	})
    34  //
    35  //	someWork.CallCancelFunc() // Cancel next tick and remove from work registry
    36  //
    37  // goroutines for Every and After are run on their own WaitGroups for synchronization:
    38  //
    39  //	someWork2 := myRobot.Every(context.Background(), time.Second * 2, func(){
    40  //		fmt.Println("Here I am doing more work")
    41  // 	})
    42  //
    43  //	somework2.CallCancelFunc()
    44  //
    45  //	// wait for both Every calls to finish
    46  //	robot.WorkEveryWaitGroup().Wait()
    47  type RobotWork struct {
    48  	id         uuid.UUID
    49  	kind       string
    50  	tickCount  int
    51  	ctx        context.Context
    52  	cancelFunc context.CancelFunc
    53  	function   func()
    54  	ticker     *time.Ticker
    55  	duration   time.Duration
    56  }
    57  
    58  // ID returns the UUID of the RobotWork
    59  func (rw *RobotWork) ID() uuid.UUID {
    60  	return rw.id
    61  }
    62  
    63  // CancelFunc returns the context.CancelFunc used to cancel the work
    64  func (rw *RobotWork) CancelFunc() context.CancelFunc {
    65  	return rw.cancelFunc
    66  }
    67  
    68  // CallCancelFunc calls the context.CancelFunc used to cancel the work
    69  func (rw *RobotWork) CallCancelFunc() {
    70  	rw.cancelFunc()
    71  }
    72  
    73  // Ticker returns the time.Ticker used in an Every so that calling code can sync on the same channel
    74  func (rw *RobotWork) Ticker() *time.Ticker {
    75  	if rw.kind == AfterWorkKind {
    76  		return nil
    77  	}
    78  	return rw.ticker
    79  }
    80  
    81  // TickCount returns the number of times the function successfully ran
    82  func (rw *RobotWork) TickCount() int {
    83  	return rw.tickCount
    84  }
    85  
    86  // Duration returns the timeout until an After fires or the period of an Every
    87  func (rw *RobotWork) Duration() time.Duration {
    88  	return rw.duration
    89  }
    90  
    91  func (rw *RobotWork) String() string {
    92  	format := `ID: %s
    93  Kind: %s
    94  TickCount: %d
    95  
    96  `
    97  	return fmt.Sprintf(format, rw.id, rw.kind, rw.tickCount)
    98  }
    99  
   100  // WorkRegistry returns the Robot's WorkRegistry
   101  func (r *Robot) WorkRegistry() *RobotWorkRegistry {
   102  	return r.workRegistry
   103  }
   104  
   105  // Every calls the given function for every tick of the provided duration.
   106  func (r *Robot) Every(ctx context.Context, d time.Duration, f func()) *RobotWork {
   107  	rw := r.workRegistry.registerEvery(ctx, d, f)
   108  	r.WorkEveryWaitGroup.Add(1)
   109  	go func() {
   110  	EVERYWORK:
   111  		for {
   112  			select {
   113  			case <-rw.ctx.Done():
   114  				r.workRegistry.delete(rw.id)
   115  				rw.ticker.Stop()
   116  				break EVERYWORK
   117  			case <-rw.ticker.C:
   118  				f()
   119  				rw.tickCount++
   120  			}
   121  		}
   122  		r.WorkEveryWaitGroup.Done()
   123  	}()
   124  	return rw
   125  }
   126  
   127  // After calls the given function after the provided duration has elapsed
   128  func (r *Robot) After(ctx context.Context, d time.Duration, f func()) *RobotWork {
   129  	rw := r.workRegistry.registerAfter(ctx, d, f)
   130  	ch := time.After(d)
   131  	r.WorkAfterWaitGroup.Add(1)
   132  	go func() {
   133  	AFTERWORK:
   134  		for {
   135  			select {
   136  			case <-rw.ctx.Done():
   137  				r.workRegistry.delete(rw.id)
   138  				break AFTERWORK
   139  			case <-ch:
   140  				f()
   141  			}
   142  		}
   143  		r.WorkAfterWaitGroup.Done()
   144  	}()
   145  	return rw
   146  }
   147  
   148  // Get returns the RobotWork specified by the provided ID. To delete something from the registry, it's
   149  // necessary to call its context.CancelFunc, which will perform a goroutine-safe delete on the underlying
   150  // map.
   151  func (rwr *RobotWorkRegistry) Get(id uuid.UUID) *RobotWork {
   152  	rwr.Lock()
   153  	defer rwr.Unlock()
   154  	return rwr.r[id.String()]
   155  }
   156  
   157  // Delete returns the RobotWork specified by the provided ID
   158  func (rwr *RobotWorkRegistry) delete(id uuid.UUID) {
   159  	rwr.Lock()
   160  	defer rwr.Unlock()
   161  	delete(rwr.r, id.String())
   162  }
   163  
   164  // registerAfter creates a new unit of RobotWork and sets up its context/cancellation
   165  func (rwr *RobotWorkRegistry) registerAfter(ctx context.Context, d time.Duration, f func()) *RobotWork {
   166  	rwr.Lock()
   167  	defer rwr.Unlock()
   168  
   169  	id, _ := uuid.NewV4()
   170  	rw := &RobotWork{
   171  		id:       id,
   172  		kind:     AfterWorkKind,
   173  		function: f,
   174  		duration: d,
   175  	}
   176  
   177  	rw.ctx, rw.cancelFunc = context.WithCancel(ctx)
   178  	rwr.r[id.String()] = rw
   179  	return rw
   180  }
   181  
   182  // registerEvery creates a new unit of RobotWork and sets up its context/cancellation
   183  func (rwr *RobotWorkRegistry) registerEvery(ctx context.Context, d time.Duration, f func()) *RobotWork {
   184  	rwr.Lock()
   185  	defer rwr.Unlock()
   186  
   187  	id, _ := uuid.NewV4()
   188  	rw := &RobotWork{
   189  		id:       id,
   190  		kind:     EveryWorkKind,
   191  		function: f,
   192  		duration: d,
   193  		ticker:   time.NewTicker(d),
   194  	}
   195  
   196  	rw.ctx, rw.cancelFunc = context.WithCancel(ctx)
   197  
   198  	rwr.r[id.String()] = rw
   199  	return rw
   200  }