github.com/amp-space/amp-sdk-go@v0.7.6/stdlib/task/task.go (about)

     1  package task
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  	"sync"
     9  	"sync/atomic"
    10  	"time"
    11  
    12  	"github.com/amp-space/amp-sdk-go/stdlib/log"
    13  )
    14  
    15  // ctx implements Context
    16  type ctx struct {
    17  	log.Logger
    18  
    19  	task Task
    20  
    21  	id             int64
    22  	state          int32
    23  	idle           bool
    24  	idleCloseRetry atomic.Int64 // time.Duration
    25  	idleCloseMin   time.Time
    26  
    27  	chClosing chan struct{}  // signals Close() has been called and close execution has begun.
    28  	chClosed  chan struct{}  // signals Close() has been called and all close execution is done.
    29  	err       error          // See context.Err() for spec
    30  	busy      sync.WaitGroup // blocks until all execution is complete
    31  	subsMu    sync.Mutex     // Locked when .subs is being accessed
    32  	subs      []Context
    33  }
    34  
    35  // Errors
    36  var (
    37  	ErrAlreadyStarted = errors.New("already started")
    38  	ErrUnstarted      = errors.New("unstarted")
    39  	ErrClosed         = errors.New("closed")
    40  )
    41  
    42  var gSpawnCounter = int64(0)
    43  
    44  func (p *ctx) Close() error {
    45  	first := atomic.CompareAndSwapInt32(&p.state, Running, Closing)
    46  	if first {
    47  		close(p.chClosing)
    48  	}
    49  	return nil
    50  }
    51  
    52  func (p *ctx) PreventIdleClose(delay time.Duration) bool {
    53  	p.subsMu.Lock()
    54  	p.idleCloseMin = time.Now().Add(delay)
    55  	p.idle = false
    56  	p.subsMu.Unlock()
    57  
    58  	select {
    59  	case <-p.Closing():
    60  		return false
    61  	default:
    62  		return true
    63  	}
    64  }
    65  
    66  func (p *ctx) CloseWhenIdle(delay time.Duration) {
    67  	if delay <= 0 {
    68  		delay = 0
    69  	}
    70  
    71  	// Can this be folded into the main go routine in StartChild() to save a goroutine?
    72  	prevDelay := p.idleCloseRetry.Swap(int64(delay))
    73  
    74  	// Only spawn a new timer when the delay is changed from 0
    75  	if prevDelay > 0 {
    76  		return
    77  	}
    78  
    79  	go func() {
    80  		var timer *time.Timer
    81  
    82  		for idleClose := true; idleClose; {
    83  			p.idle = true
    84  			p.busy.Wait() // wait until there is a chance of catching ctx idle
    85  
    86  			retry := false
    87  
    88  			p.subsMu.Lock()
    89  			delay := time.Duration(p.idleCloseRetry.Load())
    90  			if !p.idle {
    91  				retry = true
    92  			} else if delay <= 0 {
    93  				idleClose = false
    94  			} else {
    95  				if !p.idleCloseMin.IsZero() {
    96  					minDelay := time.Until(p.idleCloseMin)
    97  					if minDelay <= 0 {
    98  						p.idleCloseMin = time.Time{}
    99  					}
   100  					// Wait for the more restrictive time constraint
   101  					if delay < minDelay {
   102  						delay = minDelay
   103  					}
   104  				}
   105  			}
   106  			p.subsMu.Unlock()
   107  
   108  			if retry || !idleClose {
   109  				continue
   110  			}
   111  
   112  			if delay > 0 {
   113  				if timer == nil {
   114  					timer = time.NewTimer(delay)
   115  				} else {
   116  					timer.Reset(delay)
   117  				}
   118  				select {
   119  				case <-timer.C:
   120  				case <-p.Closing():
   121  					idleClose = false
   122  				}
   123  			}
   124  
   125  			// If no new children were added while we were waiting, then we have been idle and can close.
   126  			// Note in the case that we're closing, the below has no effect
   127  			if idleClose {
   128  				p.subsMu.Lock()
   129  				if p.idle {
   130  					p.Close()
   131  					idleClose = false
   132  				}
   133  				p.subsMu.Unlock()
   134  			}
   135  		}
   136  	}()
   137  }
   138  
   139  func (p *ctx) Deadline() (deadline time.Time, ok bool) {
   140  	return time.Time{}, false
   141  }
   142  
   143  func (p *ctx) Err() error {
   144  	select {
   145  	case <-p.Done():
   146  		if p.err == nil {
   147  			return context.Canceled
   148  		}
   149  		return p.err
   150  	default:
   151  		return nil
   152  	}
   153  }
   154  
   155  func (p *ctx) Value(key interface{}) interface{} {
   156  	return nil
   157  }
   158  
   159  func (p *ctx) TaskRef() interface{} {
   160  	return p.task.TaskRef
   161  }
   162  
   163  func (p *ctx) ContextID() int64 {
   164  	return p.id
   165  }
   166  
   167  func (p *ctx) Label() string {
   168  	return p.task.Label
   169  }
   170  
   171  func printContextTree(ctx Context, out *strings.Builder, depth int, prefix []rune, lastChild bool) {
   172  	icon := ' '
   173  	if depth > 0 {
   174  		icon = '┣'
   175  		if lastChild {
   176  			icon = '┗'
   177  		}
   178  	}
   179  	prefix = append(prefix, icon, ' ')
   180  		
   181  	out.WriteString(fmt.Sprintf("%04d%s%s\n", ctx.ContextID(), string(prefix), ctx.Label()))
   182  	
   183  	icon = '┃'
   184  	if lastChild { 
   185  		icon = ' '
   186  	}
   187  	prefix = append(prefix[:len(prefix)-2], icon, ' ', ' ', ' ', ' ')
   188  
   189  	var subBuf [20]Context
   190  	children := ctx.GetChildren(subBuf[:0])
   191  	for i, ci := range children {
   192  		printContextTree(ci, out, depth+1, prefix, i == len(children)-1)
   193  	}
   194  }
   195  
   196  func (p *ctx) GetChildren(in []Context) []Context {
   197  	p.subsMu.Lock()
   198  	defer p.subsMu.Unlock()
   199  	return append(in, p.subs...)
   200  }
   201  
   202  // StartChild starts the given child Context as a "sub" task.
   203  func (p *ctx) StartChild(task *Task) (Context, error) {
   204  	child := &ctx{
   205  		state:     Running,
   206  		id:        atomic.AddInt64(&gSpawnCounter, 1),
   207  		chClosing: make(chan struct{}),
   208  		chClosed:  make(chan struct{}),
   209  	}
   210  	if task != nil {
   211  		child.task = *task
   212  	}
   213  	if child.task.Label == "" {
   214  		child.task.Label = fmt.Sprintf("ctx_%d", child.id)
   215  	}
   216  	child.Logger = log.NewLogger(child.task.Label)
   217  
   218  	// If a parent is given, add the child to the parent's list of children.
   219  	if p != nil {
   220  
   221  		var err error
   222  		p.subsMu.Lock()
   223  		if p.state == Running {
   224  			p.busy.Add(1)
   225  			p.idle = false
   226  			p.subs = append(p.subs, child)
   227  		} else {
   228  			err = ErrUnstarted
   229  		}
   230  		p.subsMu.Unlock()
   231  
   232  		if err != nil {
   233  			return nil, err
   234  		}
   235  	}
   236  
   237  	go func() {
   238  
   239  		// If there is a parent, wait until child.Close() *or* p.Close()
   240  		// TODO: merge CloseWhenIdle() into this block?
   241  		if p != nil {
   242  			select {
   243  			case <-p.Closing():
   244  				child.Close()
   245  			case <-child.Closing():
   246  			}
   247  		}
   248  
   249  		// Wait for child to begin closing phase
   250  		<-child.Closing()
   251  
   252  		// Fire callback if given
   253  		if child.task.OnClosing != nil {
   254  			child.task.OnClosing()
   255  		}
   256  
   257  		if p != nil && p.task.OnChildClosing != nil {
   258  			p.task.OnChildClosing(child)
   259  		}
   260  
   261  		// Once all child's children are closed, proceed with completion.
   262  		child.busy.Wait()
   263  
   264  		var idleClose time.Duration
   265  
   266  		if p != nil {
   267  
   268  			p.subsMu.Lock()
   269  			{
   270  				// remove the child from its parent
   271  				N := len(p.subs)
   272  				for i := 0; i < N; i++ {
   273  					if p.subs[i] == child {
   274  						copy(p.subs[i:], p.subs[i+1:N])
   275  						N--
   276  						p.subs[N] = nil // show GC some love
   277  						p.subs = p.subs[:N]
   278  						break
   279  					}
   280  				}
   281  
   282  				// If removing the last child and in IdleClose mode, queue the parent to be closed
   283  				if N == 0 {
   284  					idleClose = p.task.IdleClose
   285  				}
   286  			}
   287  			p.subsMu.Unlock()
   288  		}
   289  
   290  		// Move to Closed state now that all all that remains is the OnClosed callback and release of the chClosed chan.
   291  		child.state = Closed
   292  		if child.task.OnClosed != nil {
   293  			child.task.OnClosed()
   294  		}
   295  		close(child.chClosed)
   296  
   297  		// With the child now fully closed, the parent is no longer waiting on this child
   298  		if p != nil {
   299  			p.busy.Done()
   300  		}
   301  
   302  		if idleClose > 0 {
   303  			p.CloseWhenIdle(idleClose)
   304  		}
   305  	}()
   306  
   307  	if child.task.OnStart != nil {
   308  		err := child.task.OnStart(child)
   309  		child.task.OnStart = nil
   310  		if err != nil {
   311  			child.Close()
   312  			return nil, err
   313  		}
   314  	}
   315  
   316  	if child.task.OnRun != nil {
   317  		child.busy.Add(1)
   318  		go func() {
   319  			child.task.OnRun(child)
   320  			child.task.OnRun = nil
   321  			child.busy.Done()
   322  
   323  			// If idleclose is set, try to do so
   324  			if child.task.IdleClose > 0 {
   325  				child.CloseWhenIdle(child.task.IdleClose)
   326  			}
   327  		}()
   328  	}
   329  
   330  	return child, nil
   331  }
   332  
   333  func (p *ctx) Go(label string, fn func(ctx Context)) (Context, error) {
   334  	return p.StartChild(&Task{
   335  		Label:     label,
   336  		IdleClose: time.Nanosecond,
   337  		OnRun:     fn,
   338  	})
   339  }
   340  
   341  func (p *ctx) Closing() <-chan struct{} {
   342  	return p.chClosing
   343  }
   344  
   345  func (p *ctx) Done() <-chan struct{} {
   346  	return p.chClosed
   347  }
   348  
   349  const (
   350  	Unstarted int32 = iota
   351  	Running
   352  	Closing
   353  	Closed
   354  )