mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-04 02:21:02 +08:00
* Vendor slack plugin and supporting libraries * Fix up plugin integration tests (wip) * Run GCI on vendored code * Use newtype instead of type alias golangci-lint currently panics on this, "skip-files" et al don't help, as it is a linter panic, not an error See https://github.com/dominikh/go-tools/commit/d717045480130461e1e777ead7f2981621f0d9f0 * Remove long-runing plugins tests from difftest * Move access plugin tests to unit-tests-integrations
292 lines
5.9 KiB
Go
292 lines
5.9 KiB
Go
// Copyright 2023 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 lib
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/gravitational/trace"
|
|
)
|
|
|
|
type Job interface {
|
|
DoJob(context.Context) error
|
|
}
|
|
|
|
type ServiceJob interface {
|
|
Job
|
|
IsReady() bool
|
|
SetReady(ready bool)
|
|
WaitReady(ctx context.Context) (bool, error)
|
|
Done() <-chan struct{}
|
|
Err() error
|
|
}
|
|
|
|
type serviceJob struct {
|
|
mu sync.Mutex
|
|
do func(context.Context) error
|
|
ready bool
|
|
err error
|
|
readyCh chan struct{}
|
|
doneCh chan struct{}
|
|
}
|
|
|
|
type Process struct {
|
|
sync.Mutex
|
|
// doneCh is closed when all the jobs are completed.
|
|
doneCh chan struct{}
|
|
// spawn runs a goroutine in the app's context as a job with waiting for
|
|
// its completion on shutdown.
|
|
spawn func(Job, bool)
|
|
// terminate signals the app to terminate gracefully.
|
|
terminate func()
|
|
// cancel signals the app to terminate immediately
|
|
cancel context.CancelFunc
|
|
// onTerminate is a list of callbacks called on terminate.
|
|
onTerminate []jobFunc
|
|
// terminated flags out that process has been signaled for termination.
|
|
terminated bool
|
|
// criticalErrors is a list of errors returned by critical jobs.
|
|
criticalErrors []error
|
|
}
|
|
|
|
type jobFunc func(context.Context) error
|
|
|
|
type processKey struct{}
|
|
type jobKey struct{}
|
|
|
|
var closedChan = make(chan struct{})
|
|
|
|
func init() {
|
|
close(closedChan)
|
|
}
|
|
|
|
func NewProcess(ctx context.Context) *Process {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
doneCh := make(chan struct{})
|
|
process := &Process{
|
|
doneCh: doneCh,
|
|
cancel: cancel,
|
|
onTerminate: make([]jobFunc, 0),
|
|
}
|
|
ctx = context.WithValue(ctx, processKey{}, process)
|
|
|
|
var jobs sync.WaitGroup
|
|
|
|
jobs.Add(1) // Start the main "job". We have to do it for Wait() not being returned beforehand.
|
|
go func() {
|
|
jobs.Wait()
|
|
close(doneCh)
|
|
}()
|
|
process.spawn = func(job Job, critical bool) {
|
|
jobs.Add(1)
|
|
jctx, jcancel := context.WithCancel(context.WithValue(ctx, jobKey{}, job))
|
|
go func() {
|
|
err := job.DoJob(jctx)
|
|
jcancel()
|
|
jobs.Done()
|
|
if err != nil && critical {
|
|
process.Terminate()
|
|
}
|
|
}()
|
|
}
|
|
|
|
var once sync.Once
|
|
process.terminate = func() {
|
|
once.Do(func() {
|
|
process.Lock()
|
|
process.terminated = true
|
|
for _, j := range process.onTerminate {
|
|
process.spawn(j, false)
|
|
}
|
|
process.onTerminate = nil
|
|
process.Unlock()
|
|
jobs.Done() // Stop the main "job".
|
|
})
|
|
}
|
|
|
|
return process
|
|
}
|
|
|
|
func (p *Process) SpawnJob(job Job) {
|
|
if p == nil {
|
|
panic("spawning a job on a nil process")
|
|
}
|
|
select {
|
|
case <-p.doneCh:
|
|
panic("spawning a job on a finished process")
|
|
default:
|
|
p.spawn(job, false)
|
|
}
|
|
}
|
|
|
|
func (p *Process) SpawnCriticalJob(job Job) {
|
|
if p == nil {
|
|
panic("spawning a job on a nil process")
|
|
}
|
|
select {
|
|
case <-p.doneCh:
|
|
panic("spawning a job on a finished process")
|
|
default:
|
|
p.spawn(job, true)
|
|
}
|
|
}
|
|
|
|
func (p *Process) Spawn(fn func(ctx context.Context) error) {
|
|
p.SpawnJob(jobFunc(fn))
|
|
}
|
|
|
|
func (p *Process) SpawnCritical(fn func(ctx context.Context) error) {
|
|
p.SpawnCriticalJob(jobFunc(fn))
|
|
}
|
|
|
|
func (p *Process) OnTerminate(fn func(ctx context.Context) error) {
|
|
if p == nil {
|
|
panic("calling OnTerminate a nil process")
|
|
}
|
|
p.Lock()
|
|
defer p.Unlock()
|
|
if p.terminated {
|
|
p.Spawn(fn)
|
|
} else {
|
|
p.onTerminate = append(p.onTerminate, fn)
|
|
}
|
|
}
|
|
|
|
// Done channel is used to wait for jobs completion.
|
|
func (p *Process) Done() <-chan struct{} {
|
|
if p == nil {
|
|
return closedChan
|
|
}
|
|
return p.doneCh
|
|
}
|
|
|
|
// Terminate signals a process to terminate. You should avoid spawning new jobs after termination.
|
|
func (p *Process) Terminate() {
|
|
if p == nil {
|
|
return
|
|
}
|
|
p.terminate()
|
|
}
|
|
|
|
// Shutdown signals a process to terminate and waits for completion of all jobs.
|
|
func (p *Process) Shutdown(ctx context.Context) error {
|
|
p.Terminate()
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-p.Done():
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Close shuts down all process jobs immediately.
|
|
func (p *Process) Close() {
|
|
if p == nil {
|
|
return
|
|
}
|
|
p.cancel()
|
|
<-p.doneCh
|
|
}
|
|
|
|
func (p *Process) CriticalError() error {
|
|
return trace.NewAggregate(p.criticalErrors...)
|
|
}
|
|
|
|
func (j jobFunc) DoJob(ctx context.Context) error {
|
|
return j(ctx)
|
|
}
|
|
|
|
func MustGetProcess(ctx context.Context) *Process {
|
|
return ctx.Value(processKey{}).(*Process)
|
|
}
|
|
|
|
func MustGetJob(ctx context.Context) Job {
|
|
return ctx.Value(jobKey{}).(Job)
|
|
}
|
|
|
|
func MustGetServiceJob(ctx context.Context) ServiceJob {
|
|
return MustGetJob(ctx).(ServiceJob)
|
|
}
|
|
|
|
func NewServiceJob(fn func(ctx context.Context) error) ServiceJob {
|
|
job := &serviceJob{
|
|
readyCh: make(chan struct{}),
|
|
doneCh: make(chan struct{}),
|
|
}
|
|
job.do = func(ctx context.Context) error {
|
|
err := fn(ctx)
|
|
job.finish(err)
|
|
return err
|
|
}
|
|
return job
|
|
}
|
|
|
|
func (job *serviceJob) finish(err error) {
|
|
job.mu.Lock()
|
|
defer job.mu.Unlock()
|
|
|
|
select {
|
|
case <-job.readyCh:
|
|
default:
|
|
close(job.readyCh)
|
|
}
|
|
job.err = err
|
|
close(job.doneCh)
|
|
}
|
|
|
|
func (job *serviceJob) DoJob(ctx context.Context) error {
|
|
return job.do(ctx)
|
|
}
|
|
|
|
func (job *serviceJob) IsReady() bool {
|
|
job.mu.Lock()
|
|
defer job.mu.Unlock()
|
|
|
|
return job.ready
|
|
}
|
|
|
|
func (job *serviceJob) SetReady(ready bool) {
|
|
job.mu.Lock()
|
|
defer job.mu.Unlock()
|
|
|
|
job.ready = ready
|
|
select {
|
|
case <-job.readyCh:
|
|
default:
|
|
close(job.readyCh)
|
|
}
|
|
}
|
|
|
|
func (job *serviceJob) WaitReady(ctx context.Context) (bool, error) {
|
|
select {
|
|
case <-job.readyCh:
|
|
return job.IsReady(), nil
|
|
case <-ctx.Done():
|
|
return false, trace.Wrap(ctx.Err())
|
|
}
|
|
}
|
|
|
|
func (job *serviceJob) Done() <-chan struct{} {
|
|
return job.doneCh
|
|
}
|
|
|
|
func (job *serviceJob) Err() error {
|
|
job.mu.Lock()
|
|
defer job.mu.Unlock()
|
|
|
|
return job.err
|
|
}
|