control/controlclient: use less battery when stopped, stop map requests

Updates #604

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
reviewable/pr664/r1
Brad Fitzpatrick 4 years ago committed by Brad Fitzpatrick
parent dd97111d06
commit a275b9d7aa

@ -117,13 +117,15 @@ type Client struct {
mu sync.Mutex // mutex guards the following fields mu sync.Mutex // mutex guards the following fields
statusFunc func(Status) // called to update Client status statusFunc func(Status) // called to update Client status
loggedIn bool // true if currently logged in paused bool // whether we should stop making HTTP requests
loginGoal *LoginGoal // non-nil if some login activity is desired unpauseWaiters []chan struct{}
synced bool // true if our netmap is up-to-date loggedIn bool // true if currently logged in
hostinfo *tailcfg.Hostinfo loginGoal *LoginGoal // non-nil if some login activity is desired
inPollNetMap bool // true if currently running a PollNetMap synced bool // true if our netmap is up-to-date
inSendStatus int // number of sendStatus calls currently in progress hostinfo *tailcfg.Hostinfo
state State inPollNetMap bool // true if currently running a PollNetMap
inSendStatus int // number of sendStatus calls currently in progress
state State
authCtx context.Context // context used for auth requests authCtx context.Context // context used for auth requests
mapCtx context.Context // context used for netmap requests mapCtx context.Context // context used for netmap requests
@ -169,6 +171,27 @@ func NewNoStart(opts Options) (*Client, error) {
return c, nil return c, nil
} }
// SetPaused controls whether HTTP activity should be paused.
//
// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.
func (c *Client) SetPaused(paused bool) {
c.mu.Lock()
defer c.mu.Unlock()
if paused == c.paused {
return
}
c.paused = paused
if paused {
// Just cancel the map routine. The auth routine isn't expensive.
c.cancelMapLocked()
} else {
for _, ch := range c.unpauseWaiters {
close(ch)
}
c.unpauseWaiters = nil
}
}
// Start starts the client's goroutines. // Start starts the client's goroutines.
// //
// It should only be called for clients created by NewNoStart. // It should only be called for clients created by NewNoStart.
@ -272,6 +295,7 @@ func (c *Client) authRoutine() {
if goal == nil { if goal == nil {
// Wait for something interesting to happen // Wait for something interesting to happen
var exp <-chan time.Time var exp <-chan time.Time
var expTimer *time.Timer
if expiry != nil && !expiry.IsZero() { if expiry != nil && !expiry.IsZero() {
// if expiry is in the future, don't delay // if expiry is in the future, don't delay
// past that time. // past that time.
@ -284,11 +308,15 @@ func (c *Client) authRoutine() {
if delay > 5*time.Second { if delay > 5*time.Second {
delay = time.Second delay = time.Second
} }
exp = time.After(delay) expTimer = time.NewTimer(delay)
exp = expTimer.C
} }
} }
select { select {
case <-ctx.Done(): case <-ctx.Done():
if expTimer != nil {
expTimer.Stop()
}
c.logf("authRoutine: context done.") c.logf("authRoutine: context done.")
case <-exp: case <-exp:
// Unfortunately the key expiry isn't provided // Unfortunately the key expiry isn't provided
@ -310,7 +338,7 @@ func (c *Client) authRoutine() {
} }
} }
} else if !goal.wantLoggedIn { } else if !goal.wantLoggedIn {
err := c.direct.TryLogout(c.authCtx) err := c.direct.TryLogout(ctx)
if err != nil { if err != nil {
report(err, "TryLogout") report(err, "TryLogout")
bo.BackOff(ctx, err) bo.BackOff(ctx, err)
@ -399,12 +427,35 @@ func (c *Client) Direct() *Direct {
return c.direct return c.direct
} }
// unpausedChanLocked returns a new channel that is closed when the
// current Client pause is unpaused.
//
// c.mu must be held
func (c *Client) unpausedChanLocked() <-chan struct{} {
unpaused := make(chan struct{})
c.unpauseWaiters = append(c.unpauseWaiters, unpaused)
return unpaused
}
func (c *Client) mapRoutine() { func (c *Client) mapRoutine() {
defer close(c.mapDone) defer close(c.mapDone)
bo := backoff.NewBackoff("mapRoutine", c.logf, 30*time.Second) bo := backoff.NewBackoff("mapRoutine", c.logf, 30*time.Second)
for { for {
c.mu.Lock() c.mu.Lock()
if c.paused {
unpaused := c.unpausedChanLocked()
c.mu.Unlock()
c.logf("mapRoutine: awaiting unpause")
select {
case <-unpaused:
c.logf("mapRoutine: unpaused")
case <-c.quit:
c.logf("mapRoutine: quit")
return
}
continue
}
c.logf("mapRoutine: %s", c.state) c.logf("mapRoutine: %s", c.state)
loggedIn := c.loggedIn loggedIn := c.loggedIn
ctx := c.mapCtx ctx := c.mapCtx
@ -487,8 +538,14 @@ func (c *Client) mapRoutine() {
if c.state == StateSynchronized { if c.state == StateSynchronized {
c.state = StateAuthenticated c.state = StateAuthenticated
} }
paused := c.paused
c.mu.Unlock() c.mu.Unlock()
if paused {
c.logf("mapRoutine: paused")
continue
}
if err != nil { if err != nil {
report(err, "PollNetMap") report(err, "PollNetMap")
bo.BackOff(ctx, err) bo.BackOff(ctx, err)

@ -1069,6 +1069,7 @@ func (b *LocalBackend) enterState(newState State) {
b.state = newState b.state = newState
prefs := b.prefs prefs := b.prefs
notify := b.notify notify := b.notify
bc := b.c
b.mu.Unlock() b.mu.Unlock()
if state == newState { if state == newState {
@ -1080,6 +1081,10 @@ func (b *LocalBackend) enterState(newState State) {
b.send(Notify{State: &newState}) b.send(Notify{State: &newState})
} }
if bc != nil {
bc.SetPaused(newState == Stopped)
}
switch newState { switch newState {
case NeedsLogin: case NeedsLogin:
b.blockEngineUpdates(true) b.blockEngineUpdates(true)

Loading…
Cancel
Save