Files
teleport/integrations/lib/process.go
T
Justinas Stankevičius 6af6e7f566 Vendor slack plugin and supporting libraries (#23045)
* 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
2023-03-22 19:39:07 +00:00

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
}