github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/engine/background_task.go (about)

     1  // Copyright 2017 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  // BackgroundTask runs a function in the background once in a while.
     5  // Note that this engine is long-lived and potentially has to deal with being
     6  // logged out and logged in as a different user, etc.
     7  // The timer uses the clock to sleep. So if there is a timezone change
     8  // it will probably wake up early or sleep for the extra hours.
     9  
    10  package engine
    11  
    12  import (
    13  	"fmt"
    14  	insecurerand "math/rand"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/keybase/client/go/libkb"
    19  	"github.com/keybase/client/go/protocol/keybase1"
    20  	context "golang.org/x/net/context"
    21  )
    22  
    23  // Function to run periodically.
    24  // The error is logged but otherwise ignored.
    25  type TaskFunc func(m libkb.MetaContext) error
    26  
    27  type BackgroundTaskSettings struct {
    28  	Start time.Duration // Wait after starting the app
    29  	// Additional wait after starting the mobile app, but only on foreground
    30  	// (i.e., does not get triggered when service starts during background fetch/BACKGROUND_ACTIVE mode)
    31  	MobileForegroundStartAddition time.Duration
    32  	StartStagger                  time.Duration // Wait an additional random amount.
    33  	// When waking up on mobile lots of timers will go off at once. We wait an additional
    34  	// delay so as not to add to that herd and slow down the mobile experience when opening the app.
    35  	WakeUp   time.Duration
    36  	Interval time.Duration // Wait between runs
    37  	Limit    time.Duration // Time limit on each round
    38  }
    39  
    40  // BackgroundTask is an engine.
    41  type BackgroundTask struct {
    42  	libkb.Contextified
    43  	sync.Mutex
    44  
    45  	args *BackgroundTaskArgs
    46  
    47  	shutdown bool
    48  	// Function to cancel the background context.
    49  	// Can be nil before RunEngine exits
    50  	shutdownFunc context.CancelFunc
    51  }
    52  
    53  type BackgroundTaskArgs struct {
    54  	Name     string
    55  	F        TaskFunc
    56  	Settings BackgroundTaskSettings
    57  
    58  	// Channels used for testing. Normally nil.
    59  	testingMetaCh     chan<- string
    60  	testingRoundResCh chan<- error
    61  }
    62  
    63  // NewBackgroundTask creates a BackgroundTask engine.
    64  func NewBackgroundTask(g *libkb.GlobalContext, args *BackgroundTaskArgs) *BackgroundTask {
    65  	return &BackgroundTask{
    66  		Contextified: libkb.NewContextified(g),
    67  		args:         args,
    68  		shutdownFunc: nil,
    69  	}
    70  }
    71  
    72  // Name is the unique engine name.
    73  func (e *BackgroundTask) Name() string {
    74  	if e.args != nil {
    75  		return fmt.Sprintf("BackgroundTask(%v)", e.args.Name)
    76  	}
    77  	return "BackgroundTask"
    78  }
    79  
    80  // GetPrereqs returns the engine prereqs.
    81  func (e *BackgroundTask) Prereqs() Prereqs {
    82  	return Prereqs{}
    83  }
    84  
    85  // RequiredUIs returns the required UIs.
    86  func (e *BackgroundTask) RequiredUIs() []libkb.UIKind {
    87  	return []libkb.UIKind{}
    88  }
    89  
    90  // SubConsumers returns the other UI consumers for this engine.
    91  func (e *BackgroundTask) SubConsumers() []libkb.UIConsumer {
    92  	return []libkb.UIConsumer{}
    93  }
    94  
    95  // Run starts the engine.
    96  // Returns immediately, kicks off a background goroutine.
    97  func (e *BackgroundTask) Run(m libkb.MetaContext) (err error) {
    98  	defer m.Trace(e.Name(), &err)()
    99  
   100  	// use a new background context with a saved cancel function
   101  	var cancel func()
   102  	m, cancel = m.BackgroundWithCancel()
   103  
   104  	e.Lock()
   105  	defer e.Unlock()
   106  
   107  	e.shutdownFunc = cancel
   108  	if e.shutdown {
   109  		// Shutdown before started
   110  		cancel()
   111  		e.meta("early-shutdown")
   112  		return nil
   113  	}
   114  
   115  	// start the loop and return
   116  	go func() {
   117  		err := e.loop(m)
   118  		if err != nil {
   119  			e.log(m, "loop error: %s", err)
   120  		}
   121  		cancel()
   122  		e.meta("loop-exit")
   123  	}()
   124  
   125  	return nil
   126  }
   127  
   128  func (e *BackgroundTask) Shutdown() {
   129  	e.Lock()
   130  	defer e.Unlock()
   131  	e.shutdown = true
   132  	if e.shutdownFunc != nil {
   133  		e.shutdownFunc()
   134  	}
   135  }
   136  
   137  func (e *BackgroundTask) loop(mctx libkb.MetaContext) error {
   138  	// wakeAt times are calculated before a meta before their corresponding sleep.
   139  	// To avoid the race where the testing goroutine calls advance before
   140  	// this routine decides when to wake up. That led to this routine never waking.
   141  	wakeAt := mctx.G().Clock().Now().Add(e.args.Settings.Start)
   142  	if e.args.Settings.StartStagger > 0 {
   143  		wakeAt = wakeAt.Add(time.Duration(insecurerand.Int63n(int64(e.args.Settings.StartStagger))))
   144  	}
   145  	if e.args.Settings.MobileForegroundStartAddition > 0 && mctx.G().IsMobileAppType() {
   146  		appState := mctx.G().MobileAppState.State()
   147  		if appState == keybase1.MobileAppState_FOREGROUND {
   148  			mctx.Debug("Since starting on mobile and foregrounded, waiting an additional %v", e.args.Settings.MobileForegroundStartAddition)
   149  			wakeAt = wakeAt.Add(e.args.Settings.MobileForegroundStartAddition)
   150  		}
   151  	}
   152  	e.meta("loop-start")
   153  	if err := libkb.SleepUntilWithContext(mctx.Ctx(), mctx.G().Clock(), wakeAt); err != nil {
   154  		return err
   155  	}
   156  	e.meta("woke-start")
   157  	var i int
   158  	for {
   159  		i++
   160  		mctx := mctx.WithLogTag("BGT") // Background Task
   161  		e.log(mctx, "round(%v) start", i)
   162  		err := e.round(mctx)
   163  		if err != nil {
   164  			e.log(mctx, "round(%v) error: %s", i, err)
   165  		} else {
   166  			e.log(mctx, "round(%v) complete", i)
   167  		}
   168  		if e.args.testingRoundResCh != nil {
   169  			e.args.testingRoundResCh <- err
   170  		}
   171  		wakeAt = mctx.G().Clock().Now().Add(e.args.Settings.Interval)
   172  		e.meta("loop-round-complete")
   173  		if err := libkb.SleepUntilWithContext(mctx.Ctx(), mctx.G().Clock(), wakeAt); err != nil {
   174  			return err
   175  		}
   176  		wakeAt = mctx.G().Clock().Now().Add(e.args.Settings.WakeUp)
   177  		e.meta("woke-interval")
   178  		if err := libkb.SleepUntilWithContext(mctx.Ctx(), mctx.G().Clock(), wakeAt); err != nil {
   179  			return err
   180  		}
   181  		e.meta("woke-wakeup")
   182  	}
   183  }
   184  
   185  func (e *BackgroundTask) round(m libkb.MetaContext) error {
   186  	var cancel func()
   187  	m, cancel = m.WithTimeout(e.args.Settings.Limit)
   188  	defer cancel()
   189  
   190  	// Run the function.
   191  	if e.args.F == nil {
   192  		return fmt.Errorf("nil task function")
   193  	}
   194  	return e.args.F(m)
   195  }
   196  
   197  func (e *BackgroundTask) meta(s string) {
   198  	if e.args.testingMetaCh != nil {
   199  		e.args.testingMetaCh <- s
   200  	}
   201  }
   202  
   203  func (e *BackgroundTask) log(m libkb.MetaContext, format string, args ...interface{}) {
   204  	content := fmt.Sprintf(format, args...)
   205  	m.Debug("%s %s", e.Name(), content)
   206  }