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
10package engine
11
12import (
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.
25type TaskFunc func(m libkb.MetaContext) error
26
27type 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.
41type 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
53type 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.
64func 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.
73func (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.
81func (e *BackgroundTask) Prereqs() Prereqs {
82	return Prereqs{}
83}
84
85// RequiredUIs returns the required UIs.
86func (e *BackgroundTask) RequiredUIs() []libkb.UIKind {
87	return []libkb.UIKind{}
88}
89
90// SubConsumers returns the other UI consumers for this engine.
91func (e *BackgroundTask) SubConsumers() []libkb.UIConsumer {
92	return []libkb.UIConsumer{}
93}
94
95// Run starts the engine.
96// Returns immediately, kicks off a background goroutine.
97func (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
128func (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
137func (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
185func (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
197func (e *BackgroundTask) meta(s string) {
198	if e.args.testingMetaCh != nil {
199		e.args.testingMetaCh <- s
200	}
201}
202
203func (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}
207