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 }