Files
teleport/lib/utils/workpool/workpool.go
T
Zac Bergquist 09de0ba530 Revert "Remove [direct] dependency on go.uber.org/atomic (#16884)" (#16918)
This reverts commit 4f3aa9a3f2.

We're unable to build for 32-bit Linux due to
https://github.com/golang/go/issues/55152,
which looks like it will be fixed with Go 1.19.2 next week.

We'll re-evaluate with the next Go release and reintroduce this change
as soon as we can.
2022-09-30 22:41:45 +00:00

285 lines
6.8 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 group by key and is the primary means
// by which group 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
group *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),
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
// group. 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() Counts {
p.mu.Lock()
defer p.mu.Unlock()
if p.group == nil {
return Counts{}
}
return p.group.loadCounts()
}
// Set sets the target for the specified key.
func (p *Pool) Set(target uint64) {
p.mu.Lock()
defer p.mu.Unlock()
if target < 1 {
p.del()
return
}
if p.group == nil {
p.start(target)
return
}
p.group.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(target uint64) {
ctx, cancel := context.WithCancel(p.ctx)
notifyC := make(chan struct{}, 1)
g := &group{
counts: Counts{
Active: 0,
Target: target,
},
leaseIDs: p.leaseIDs,
grantC: p.grantC,
notifyC: notifyC,
ctx: ctx,
cancel: cancel,
}
p.group = 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() (ok bool) {
if p.group == nil {
return false
}
p.group.cancel()
p.group = nil
return true
}
// Stop permanently halts all associated group.
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
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
}
// 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()
})
}