mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-03 07:40:28 +08:00
Fixes #7606, where a node doesn't notice when the tunnel port changes. Imagine you have a cluster with a node connected in via a tunnel through a proxy `proxy.example.com` on port `3024` Now change the proxy config so that `tunnel_public_address` is `proxy.example.com:4024`. You either restart the proxy, or reload the proxy config with a `SIGHUP`. ...and then the node a) loses its connection to auth (because the tunnel is gone), and b) _doesn't reconnect_, because even though the proxy address hasn't changed, the node has cached the old tunnel_public_address and keeps trying to connect to that. You can always manually restart the node to have it reconnect, but that would be a pain if you have thousands of nodes. In order to not have to manually restart all nodes, this change implements a check for a connection failures to the auth server, and re-starts the node if there are multiple connection failures in a given period of time. The check as-implemented piggybacks on the node's "common.rotate" service, which can already restart the node in certain circumstances, and uses the success of the periodic rotation sync as a proxy for the health of the node's connection to the auth server. See-Also: #7606
290 lines
7.2 KiB
Go
290 lines
7.2 KiB
Go
/*
|
|
Copyright 2020 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package workpool
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"go.uber.org/atomic"
|
|
)
|
|
|
|
// Pool manages a collection of work groups by key and is the primary means
|
|
// by which groups are managed. Each work group has an adjustable target value
|
|
// which is the number of target leases which should be active for the given
|
|
// group.
|
|
type Pool struct {
|
|
mu sync.Mutex
|
|
leaseIDs *atomic.Uint64
|
|
groups map[interface{}]*group
|
|
// grantC is an unbuffered channel that funnels available leases from the
|
|
// workgroups to the outside world
|
|
grantC chan Lease
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
func NewPool(ctx context.Context) *Pool {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
return &Pool{
|
|
leaseIDs: atomic.NewUint64(0),
|
|
groups: make(map[interface{}]*group),
|
|
grantC: make(chan Lease),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
// Acquire is the channel which must be received on to acquire
|
|
// new leases. Each lease acquired in this way *must* have its
|
|
// Release method called when the lease is no longer needed.
|
|
// Note this channel will deliver leases from all active work
|
|
// groups. It's up to the receiver to differentiate what group
|
|
// the lease refers to and act accordingly.
|
|
func (p *Pool) Acquire() <-chan Lease {
|
|
return p.grantC
|
|
}
|
|
|
|
// Done signals pool closure.
|
|
func (p *Pool) Done() <-chan struct{} {
|
|
return p.ctx.Done()
|
|
}
|
|
|
|
// Get gets the current counts for the specified key.
|
|
func (p *Pool) Get(key interface{}) Counts {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if g, ok := p.groups[key]; ok {
|
|
return g.loadCounts()
|
|
}
|
|
return Counts{}
|
|
}
|
|
|
|
// Set sets the target for the specified key.
|
|
func (p *Pool) Set(key interface{}, target uint64) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if target < 1 {
|
|
p.del(key)
|
|
return
|
|
}
|
|
g, ok := p.groups[key]
|
|
if !ok {
|
|
p.start(key, target)
|
|
return
|
|
}
|
|
g.setTarget(target)
|
|
}
|
|
|
|
// Start starts a new work group with the specified initial target.
|
|
// If Start returns false, the group already exists.
|
|
func (p *Pool) start(key interface{}, target uint64) {
|
|
ctx, cancel := context.WithCancel(p.ctx)
|
|
notifyC := make(chan struct{}, 1)
|
|
g := &group{
|
|
counts: Counts{
|
|
Active: 0,
|
|
Target: target,
|
|
},
|
|
leaseIDs: p.leaseIDs,
|
|
key: key,
|
|
grantC: p.grantC,
|
|
notifyC: notifyC,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
p.groups[key] = g
|
|
|
|
// Start a routine to monitor the group's lease acquisition
|
|
// and handle notifications when a lease is returned to the
|
|
// pool
|
|
go g.run()
|
|
}
|
|
|
|
func (p *Pool) del(key interface{}) (ok bool) {
|
|
group, ok := p.groups[key]
|
|
if !ok {
|
|
return false
|
|
}
|
|
group.cancel()
|
|
delete(p.groups, key)
|
|
return true
|
|
}
|
|
|
|
// Stop permanently halts all associated groups.
|
|
func (p *Pool) Stop() {
|
|
p.cancel()
|
|
}
|
|
|
|
// Counts holds the target and active counts for a
|
|
// key/group.
|
|
type Counts struct {
|
|
// Target is the number of active leases that we would
|
|
// like to converge toward.
|
|
Target uint64
|
|
// Active is the current active lease count.
|
|
Active uint64
|
|
}
|
|
|
|
// group is a work group for a particular key in the pool. It tracks the number of
|
|
// active and target leases and adds leases when active drops below target.
|
|
type group struct {
|
|
cmu sync.Mutex
|
|
counts Counts
|
|
leaseIDs *atomic.Uint64
|
|
key interface{}
|
|
grantC chan Lease
|
|
notifyC chan struct{}
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// notify ensures that group is in a notified state.
|
|
// if the group is already in a notified state, this
|
|
// method has no effect. This function should be called
|
|
// any time state is changed to ensure that the group's
|
|
// goroutine unblocks & handles the updated state.
|
|
func (g *group) notify() {
|
|
select {
|
|
case g.notifyC <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// loadCounts loads the current lease counts.
|
|
func (g *group) loadCounts() Counts {
|
|
g.cmu.Lock()
|
|
defer g.cmu.Unlock()
|
|
return g.counts
|
|
}
|
|
|
|
// incrActive increases the active lease count by 1
|
|
// and ensures group is in the notified state.
|
|
func (g *group) incrActive() Counts {
|
|
g.cmu.Lock()
|
|
defer g.cmu.Unlock()
|
|
g.counts.Active++
|
|
g.notify()
|
|
return g.counts
|
|
}
|
|
|
|
// decrActive decreases the active lease count by 1
|
|
// and ensures group is in the notified state.
|
|
func (g *group) decrActive() Counts {
|
|
g.cmu.Lock()
|
|
defer g.cmu.Unlock()
|
|
g.counts.Active--
|
|
g.notify()
|
|
return g.counts
|
|
}
|
|
|
|
// setTarget sets the target lease count that the group should
|
|
// attempt to converge toward.
|
|
func (g *group) setTarget(target uint64) {
|
|
g.cmu.Lock()
|
|
defer g.cmu.Unlock()
|
|
g.counts.Target = target
|
|
g.notify()
|
|
}
|
|
|
|
// run manages the issuing of leases and handling their return to the pool for
|
|
// a given work group.
|
|
func (g *group) run() {
|
|
var counts Counts
|
|
var nextLease Lease
|
|
var grant chan Lease
|
|
for {
|
|
// Are we able to grant new leases? We are only allowed to grant a lease
|
|
// while the number of outstanding leases is less than the target value.
|
|
// If anyone else wants one after that, they will have to wait until a
|
|
// lease is returned to the pool.
|
|
counts = g.loadCounts()
|
|
if counts.Active < counts.Target {
|
|
// We are in a "granting" state; prepare to issue the lease to the
|
|
// next person who asks.
|
|
grant = g.grantC
|
|
if nextLease.id == 0 {
|
|
nextLease = newLease(g)
|
|
}
|
|
} else {
|
|
// we are in a non-"granting" state; prepare to block until a new
|
|
// event wakes the routine.
|
|
grant = nil
|
|
}
|
|
|
|
// Remembering that writes to a `nil` channel block indefinitely,
|
|
// if we're in a non-granting state this select blocks until `notify()`
|
|
// is called (usually by a lease being returned), or the context is
|
|
// canceled.
|
|
//
|
|
// Otherwise we post the lease to the outside world and block on
|
|
// someone picking it up (or cancellation)
|
|
select {
|
|
case grant <- nextLease:
|
|
g.incrActive()
|
|
nextLease = Lease{}
|
|
case <-g.notifyC:
|
|
// some event has woken the dispatch routine. Go back around.
|
|
case <-g.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lease grants access to a resource or group. When the lease is received,
|
|
// work can begin. Leases are held by workers and must be released when
|
|
// the worker has finished its work.
|
|
type Lease struct {
|
|
*group
|
|
id uint64
|
|
relOnce *sync.Once
|
|
}
|
|
|
|
func newLease(group *group) Lease {
|
|
return Lease{
|
|
group: group,
|
|
id: group.leaseIDs.Add(1),
|
|
relOnce: new(sync.Once),
|
|
}
|
|
}
|
|
|
|
// ID returns the unique ID of this lease.
|
|
func (l Lease) ID() uint64 {
|
|
return l.id
|
|
}
|
|
|
|
// Key returns the key that this lease is associated with.
|
|
func (l Lease) Key() interface{} {
|
|
return l.key
|
|
}
|
|
|
|
// IsZero checks if this is the zero value of Lease.
|
|
func (l Lease) IsZero() bool {
|
|
return l == Lease{}
|
|
}
|
|
|
|
// Release relinquishes this lease. Each lease is unique,
|
|
// so double-calling Release() on the same Lease has no effect.
|
|
func (l Lease) Release() {
|
|
if l.IsZero() {
|
|
return
|
|
}
|
|
l.relOnce.Do(func() {
|
|
l.decrActive()
|
|
})
|
|
}
|