2024-10-17 13:11:27 +02:00
|
|
|
package fn
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"sync"
|
2024-12-10 08:44:40 +02:00
|
|
|
"sync/atomic"
|
2024-10-17 13:11:27 +02:00
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// DefaultTimeout is the default timeout used for context operations.
|
|
|
|
DefaultTimeout = 30 * time.Second
|
|
|
|
)
|
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// ContextGuard is a struct that provides a wait group and main quit channel
|
|
|
|
// that can be used to create guarded contexts.
|
2024-10-17 13:11:27 +02:00
|
|
|
type ContextGuard struct {
|
2024-12-10 08:44:40 +02:00
|
|
|
mu sync.Mutex
|
|
|
|
wg sync.WaitGroup
|
|
|
|
|
|
|
|
quit chan struct{}
|
|
|
|
stopped sync.Once
|
|
|
|
|
|
|
|
// id is used to generate unique ids for each context that should be
|
|
|
|
// cancelled when the main quit signal is triggered.
|
|
|
|
id atomic.Uint32
|
|
|
|
|
|
|
|
// cancelFns is a map of cancel functions that can be used to cancel
|
|
|
|
// any context that should be cancelled when the main quit signal is
|
|
|
|
// triggered. The key is the id of the context. The mutex must be held
|
|
|
|
// when accessing this map.
|
|
|
|
cancelFns map[uint32]context.CancelFunc
|
2024-10-17 13:11:27 +02:00
|
|
|
}
|
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// NewContextGuard constructs and returns a new instance of ContextGuard.
|
2024-10-17 13:11:27 +02:00
|
|
|
func NewContextGuard() *ContextGuard {
|
|
|
|
return &ContextGuard{
|
2024-12-10 08:44:40 +02:00
|
|
|
quit: make(chan struct{}),
|
|
|
|
cancelFns: make(map[uint32]context.CancelFunc),
|
2024-10-17 13:11:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// Quit is used to signal the main quit channel, which will cancel all
|
|
|
|
// non-blocking contexts derived from the ContextGuard.
|
|
|
|
func (g *ContextGuard) Quit() {
|
|
|
|
g.stopped.Do(func() {
|
|
|
|
g.mu.Lock()
|
|
|
|
defer g.mu.Unlock()
|
2024-10-17 13:11:27 +02:00
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
for _, cancel := range g.cancelFns {
|
|
|
|
cancel()
|
|
|
|
}
|
2024-10-17 13:11:27 +02:00
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
close(g.quit)
|
|
|
|
})
|
|
|
|
}
|
2024-10-17 13:11:27 +02:00
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// Done returns a channel that will be closed when the main quit signal is
|
|
|
|
// triggered.
|
|
|
|
func (g *ContextGuard) Done() <-chan struct{} {
|
|
|
|
return g.quit
|
|
|
|
}
|
2024-10-17 13:11:27 +02:00
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// WgAdd is used to add delta to the internal wait group of the ContextGuard.
|
|
|
|
func (g *ContextGuard) WgAdd(delta int) {
|
|
|
|
g.wg.Add(delta)
|
|
|
|
}
|
2024-10-17 13:11:27 +02:00
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// WgDone is used to decrement the internal wait group of the ContextGuard.
|
|
|
|
func (g *ContextGuard) WgDone() {
|
|
|
|
g.wg.Done()
|
|
|
|
}
|
2024-10-17 13:11:27 +02:00
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// WgWait is used to block until the internal wait group of the ContextGuard is
|
|
|
|
// empty.
|
|
|
|
func (g *ContextGuard) WgWait() {
|
|
|
|
g.wg.Wait()
|
|
|
|
}
|
2024-10-17 13:11:27 +02:00
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// ctxGuardOptions is used to configure the behaviour of the context derived
|
|
|
|
// via the WithCtx method of the ContextGuard.
|
|
|
|
type ctxGuardOptions struct {
|
|
|
|
blocking bool
|
|
|
|
withTimeout bool
|
|
|
|
timeout time.Duration
|
2024-10-17 13:11:27 +02:00
|
|
|
}
|
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// ContextGuardOption defines the signature of a functional option that can be
|
|
|
|
// used to configure the behaviour of the context derived via the WithCtx method
|
|
|
|
// of the ContextGuard.
|
|
|
|
type ContextGuardOption func(*ctxGuardOptions)
|
|
|
|
|
|
|
|
// WithBlockingCG is used to create a cancellable context that will NOT be
|
2024-10-17 13:11:27 +02:00
|
|
|
// cancelled if the main quit signal is triggered, to block shutdown of
|
2024-12-10 08:44:40 +02:00
|
|
|
// important tasks.
|
|
|
|
func WithBlockingCG() ContextGuardOption {
|
|
|
|
return func(o *ctxGuardOptions) {
|
|
|
|
o.blocking = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithCustomTimeoutCG is used to create a cancellable context with a custom
|
|
|
|
// timeout. Such a context will be cancelled if either the parent context is
|
|
|
|
// cancelled, the timeout is reached or, if the Blocking option is not provided,
|
|
|
|
// the main quit signal is triggered.
|
|
|
|
func WithCustomTimeoutCG(timeout time.Duration) ContextGuardOption {
|
|
|
|
return func(o *ctxGuardOptions) {
|
|
|
|
o.withTimeout = true
|
|
|
|
o.timeout = timeout
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithTimeoutCG is used to create a cancellable context with a default timeout.
|
|
|
|
// Such a context will be cancelled if either the parent context is cancelled,
|
|
|
|
// the timeout is reached or, if the Blocking option is not provided, the main
|
|
|
|
// quit signal is triggered.
|
|
|
|
func WithTimeoutCG() ContextGuardOption {
|
|
|
|
return func(o *ctxGuardOptions) {
|
|
|
|
o.withTimeout = true
|
|
|
|
o.timeout = DefaultTimeout
|
|
|
|
}
|
2024-10-17 13:11:27 +02:00
|
|
|
}
|
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// Create is used to derive a cancellable context from the parent. Various
|
|
|
|
// options can be provided to configure the behaviour of the derived context.
|
|
|
|
func (g *ContextGuard) Create(ctx context.Context,
|
|
|
|
options ...ContextGuardOption) (context.Context, context.CancelFunc) {
|
2024-10-17 13:11:27 +02:00
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// Exit early if the parent context has already been cancelled.
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx, func() {}
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
var opts ctxGuardOptions
|
|
|
|
for _, o := range options {
|
|
|
|
o(&opts)
|
|
|
|
}
|
2024-10-17 13:11:27 +02:00
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
g.mu.Lock()
|
|
|
|
defer g.mu.Unlock()
|
|
|
|
|
|
|
|
var cancel context.CancelFunc
|
|
|
|
if opts.withTimeout {
|
|
|
|
ctx, cancel = context.WithTimeout(ctx, opts.timeout)
|
|
|
|
} else {
|
|
|
|
ctx, cancel = context.WithCancel(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
if opts.blocking {
|
|
|
|
g.ctxBlocking(ctx, cancel)
|
|
|
|
|
|
|
|
return ctx, cancel
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the call is non-blocking, then we can exit early if the main quit
|
|
|
|
// signal has been triggered.
|
|
|
|
select {
|
|
|
|
case <-g.quit:
|
|
|
|
cancel()
|
|
|
|
|
|
|
|
return ctx, cancel
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
cancel = g.ctxQuitUnsafe(ctx, cancel)
|
|
|
|
|
|
|
|
return ctx, cancel
|
|
|
|
}
|
|
|
|
|
|
|
|
// ctxQuitUnsafe spins off a goroutine that will block until the passed context
|
|
|
|
// is cancelled or until the quit channel has been signaled after which it will
|
|
|
|
// call the passed cancel function and decrement the wait group.
|
|
|
|
//
|
|
|
|
// NOTE: the caller must hold the ContextGuard's mutex before calling this
|
|
|
|
// function.
|
|
|
|
func (g *ContextGuard) ctxQuitUnsafe(ctx context.Context,
|
|
|
|
cancel context.CancelFunc) context.CancelFunc {
|
|
|
|
|
|
|
|
cancel = g.addCancelFnUnsafe(cancel)
|
|
|
|
|
|
|
|
g.wg.Add(1)
|
2024-10-17 13:11:27 +02:00
|
|
|
go func() {
|
|
|
|
defer cancel()
|
2024-12-10 08:44:40 +02:00
|
|
|
defer g.wg.Done()
|
2024-10-17 13:11:27 +02:00
|
|
|
|
|
|
|
select {
|
2024-12-10 08:44:40 +02:00
|
|
|
case <-g.quit:
|
2024-10-17 13:11:27 +02:00
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
return cancel
|
2024-10-17 13:11:27 +02:00
|
|
|
}
|
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// ctxBlocking spins off a goroutine that will block until the passed context
|
|
|
|
// is cancelled after which it will call the passed cancel function and
|
|
|
|
// decrement the wait group.
|
|
|
|
func (g *ContextGuard) ctxBlocking(ctx context.Context,
|
|
|
|
cancel context.CancelFunc) {
|
2024-10-17 13:11:27 +02:00
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
g.wg.Add(1)
|
2024-10-17 13:11:27 +02:00
|
|
|
go func() {
|
|
|
|
defer cancel()
|
2024-12-10 08:44:40 +02:00
|
|
|
defer g.wg.Done()
|
2024-10-17 13:11:27 +02:00
|
|
|
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
}
|
|
|
|
}()
|
2024-12-10 08:44:40 +02:00
|
|
|
}
|
2024-10-17 13:11:27 +02:00
|
|
|
|
2024-12-10 08:44:40 +02:00
|
|
|
// addCancelFnUnsafe adds a context cancel function to the manager and returns a
|
|
|
|
// call-back which can safely be used to cancel the context.
|
|
|
|
//
|
|
|
|
// NOTE: the caller must hold the ContextGuard's mutex before calling this
|
|
|
|
// function.
|
|
|
|
func (g *ContextGuard) addCancelFnUnsafe(
|
|
|
|
cancel context.CancelFunc) context.CancelFunc {
|
|
|
|
|
|
|
|
id := g.id.Add(1)
|
|
|
|
g.cancelFns[id] = cancel
|
|
|
|
|
|
|
|
return g.cancelCtxFn(id)
|
|
|
|
}
|
|
|
|
|
|
|
|
// cancelCtxFn returns a call-back that can be used to cancel the context
|
|
|
|
// associated with the passed id.
|
|
|
|
func (g *ContextGuard) cancelCtxFn(id uint32) context.CancelFunc {
|
|
|
|
return func() {
|
|
|
|
g.mu.Lock()
|
|
|
|
|
|
|
|
fn, ok := g.cancelFns[id]
|
|
|
|
if !ok {
|
|
|
|
g.mu.Unlock()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
delete(g.cancelFns, id)
|
|
|
|
g.mu.Unlock()
|
|
|
|
|
|
|
|
fn()
|
|
|
|
}
|
2024-10-17 13:11:27 +02:00
|
|
|
}
|