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