github.com/haraldrudell/parl@v0.4.176/g0/go-group.go (about)

     1  /*
     2  © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package g0
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"strings"
    12  	"sync"
    13  	"sync/atomic"
    14  
    15  	"github.com/haraldrudell/parl"
    16  	"github.com/haraldrudell/parl/parli"
    17  	"github.com/haraldrudell/parl/perrors"
    18  	"github.com/haraldrudell/parl/pmaps"
    19  	"github.com/haraldrudell/parl/pruntime"
    20  	"golang.org/x/exp/slices"
    21  )
    22  
    23  const (
    24  	// 1 is for NewGoGroup/.SubGo/.SubGroup
    25  	//	1 is for new
    26  	goGroupNewObjectFrames = 2
    27  	// 1 is for .Go
    28  	// 1 is for newGo
    29  	goGroupStackFrames  = 2
    30  	goFromGoStackFrames = goGroupStackFrames + 1
    31  	// 1 is for Go method
    32  	// 1 is for NewGoGroup/.SubGo/.SubGroup/.Go
    33  	//	1 is for new
    34  	fromGoNewFrames = goGroupNewObjectFrames + 1
    35  )
    36  
    37  // GoGroup is a Go thread-group. Thread-safe.
    38  //   - GoGroup has its own error channel and waitgroup and no parent thread-group.
    39  //   - thread exits are processed by G1Done and the g1WaitGroup
    40  //   - the thread-group terminates when its erropr channel closes
    41  //   - non-fatal erors are processed by ConsumeError and the error channel
    42  //   - new Go threads are handled by the g1WaitGroup
    43  //   - SubGroup creates a subordinate thread-group using this threadgroup’s error channel
    44  type GoGroup struct {
    45  	// creator is the code line that invoked new for this GoGroup SubGo or SubGroup
    46  	creator pruntime.CodeLocation
    47  	// parent for SubGo SubGroup, nil for GoGroup
    48  	parent goGroupParent
    49  	// true if instance has error channel, ie. is GoGroup or SubGroup
    50  	hasErrorChannel bool
    51  	// true if instance is SubGroup and not GoGroup or SubGo
    52  	isSubGroup bool
    53  	// invoked on first fatal thread-exit
    54  	onFirstFatal parl.GoFatalCallback
    55  	// gos is a map from goEntityId to subordinate SubGo SunGroup Go
    56  	gos parli.ThreadSafeMap[parl.GoEntityID, *ThreadData]
    57  	// unbound error channel used when instance is GoGroup or SubGroup
    58  	errCh parl.NBChan[parl.GoError]
    59  	// channel that closes when this threadGroup ends
    60  	endCh parl.Awaitable
    61  	// provides Go entity ID, sub-object waitgroup, cancel-context
    62  	//	- Cancel() Context() EntityID()
    63  	goContext
    64  
    65  	// whether a fatal exit has occurred
    66  	hadFatal atomic.Bool
    67  	// whether thread-group termination is allowed
    68  	//	- set by EnableTermination
    69  	isNoTermination atomic.Bool
    70  	// controls whether debug information is printed
    71  	//	- set by SetDebug
    72  	isDebug atomic.Bool
    73  	// controls whether trean information is stroed in gos
    74  	//	- set by SetDebug
    75  	isAggregateThreads atomic.Bool
    76  	onceWaiter         atomic.Pointer[parl.OnceWaiter]
    77  	// debug-log set by SetDebug
    78  	log atomic.Pointer[parl.PrintfFunc]
    79  
    80  	// doneLock ensures:
    81  	//	- critical section for:
    82  	//	- — closing of error channel
    83  	//	- — change of number of child objects or ending that waitGroup
    84  	//	- — change in enableTermination state
    85  	//	- order of:
    86  	//	- — parent Add GoDone
    87  	//	- — emitted termination-goErrors by [GoGroup.GoDone]
    88  	//	- mutual exclusion of:
    89  	//	- — [GoGroup.GoDone]
    90  	//	- — [GoGroup.Cancel]
    91  	//	- — [GoGroup.Add]
    92  	//	- — [GoGroup.EnableTermination]
    93  	//	- the context can be canceled at any time
    94  	doneLock sync.Mutex
    95  }
    96  
    97  var _ goGroupParent = &GoGroup{}
    98  var _ goParent = &GoGroup{}
    99  
   100  // NewGoGroup returns a stand-alone thread-group with its own error channel. Thread-safe.
   101  //   - ctx is not canceled by the thread-group
   102  //   - ctx may initiate thread-group Cancel
   103  //   - a stand-alone GoGroup thread-group has goGroupParent nil
   104  //   - non-fatal and fatal errors from the thread-group’s threads are sent on the GoGroup’s
   105  //     error channel
   106  //   - the GoGroup processes Go invocations and thread-exits from its own threads and
   107  //     the threads of its subordinate thread-groups
   108  //     wait-group and that of its parent
   109  //   - cancel of the GoGroup’s context signals termination to its own threads and all threads of its
   110  //     subordinate thread-groups
   111  //   - the GoGroup’s context is canceled when its provided parent context is canceled or any of its
   112  //     threads invoke the GoGroup’s Cancel method
   113  //   - the GoGroup terminates when its error channel closes from all threads in its own
   114  //     thread-group and that of any subordinate thread-groups have exited.
   115  func NewGoGroup(ctx context.Context, onFirstFatal ...parl.GoFatalCallback) (g0 parl.GoGroup) {
   116  	return new(nil, ctx, true, false, goGroupNewObjectFrames, onFirstFatal...)
   117  }
   118  
   119  // Go returns a parl.Go thread-features object
   120  //   - Go is invoked by a g0-package consumer
   121  //   - the Go return value is to be used as a function argument in a go-statement
   122  //     function-call launching a goroutine thread
   123  func (g *GoGroup) Go() (g2 parl.Go) { return g.newGo(goGroupStackFrames) }
   124  
   125  // FromGoGo returns a parl.Go thread-features object invoked from another
   126  // parl.Go object
   127  //   - the Go return value is to be used as a function argument in a go-statement
   128  //     function-call launching a goroutine thread
   129  func (g *GoGroup) FromGoGo() (g2 parl.Go) { return g.newGo(goFromGoStackFrames) }
   130  
   131  // newGo creates parl.Go objects
   132  func (g *GoGroup) newGo(frames int) (g2 parl.Go) {
   133  	// At this point, Go invocation is accessible so retrieve it
   134  	// the goroutine has not been created yet, so there is no creator
   135  	// instead, use top of the stack, the invocation location for the Go() function call
   136  	var goInvocation = pruntime.NewCodeLocation(frames)
   137  
   138  	if g.isEnd() {
   139  		panic(perrors.ErrorfPF(g.panicString(".Go(): "+goInvocation.Short(), nil, nil, false, nil)))
   140  	}
   141  
   142  	// the only location creating Go objects
   143  	var threadData *ThreadData
   144  	var goEntityID parl.GoEntityID
   145  	g2, goEntityID, threadData = newGo(g, goInvocation)
   146  
   147  	// count the running thread in this thread-group and its parents
   148  	g.Add(goEntityID, threadData)
   149  
   150  	return
   151  }
   152  
   153  // newSubGo returns a subordinate thread-group witthout an error channel. Thread-safe.
   154  //   - a SubGo has goGroupParent non-nil and isSubGo true
   155  //   - the SubGo thread’s fatal and non-fatal errors are forwarded to its parent
   156  //   - SubGo has FirstFatal mechanic but no error channel of its own.
   157  //   - the SubGo’s Go invocations and thread-exits are processed by the SubGo’s wait-group
   158  //     and the thread-group of its parent
   159  //   - cancel of the SubGo’s context signals termination to its own threads and all threads of its
   160  //     subordinate thread-groups
   161  //   - the SubGo’s context is canceled when its parent’s context is canceled or any of its
   162  //     threads invoke the SubGo’s Cancel method
   163  //   - the SubGo thread-group terminates when all threads in its own thread-group and
   164  //     that of any subordinate thread-groups have exited.
   165  func (g *GoGroup) SubGo(onFirstFatal ...parl.GoFatalCallback) (g2 parl.SubGo) {
   166  	return new(g, nil, false, false, goGroupNewObjectFrames, onFirstFatal...)
   167  }
   168  
   169  // FromGoSubGo returns a subordinate thread-group witthout an error channel. Thread-safe.
   170  func (g *GoGroup) FromGoSubGo(onFirstFatal ...parl.GoFatalCallback) (g1 parl.SubGo) {
   171  	return new(g, nil, false, false, fromGoNewFrames, onFirstFatal...)
   172  }
   173  
   174  // newSubGroup returns a subordinate thread-group with an error channel handling fatal
   175  // errors only. Thread-safe.
   176  //   - a SubGroup has goGroupParent non-nil and isSubGo false
   177  //   - fatal errors from the SubGroup’s threads are sent on its own error channel
   178  //   - non-fatal errors from the SubGroup’s threads are forwarded to the parent
   179  //   - the SubGroup’s Go invocations and thread-exits are processed in the SubGroup’s
   180  //     wait-group and that of its parent
   181  //   - cancel of the SubGroup’s context signals termination to its own threads and all threads of its
   182  //     subordinate thread-groups
   183  //   - the SubGroup’s context is canceled when its parent’s context is canceled or any of its
   184  //     threads invoke the SubGroup’s Cancel method
   185  //   - SubGroup thread-group terminates when its error channel closes after all of its threads
   186  //     and threads of its subordinate thread-groups have exited.
   187  func (g *GoGroup) SubGroup(onFirstFatal ...parl.GoFatalCallback) (g2 parl.SubGroup) {
   188  	return new(g, nil, true, true, goGroupNewObjectFrames, onFirstFatal...)
   189  }
   190  
   191  // FromGoSubGroup returns a subordinate thread-group with an error channel handling fatal
   192  // errors only. Thread-safe.
   193  func (g *GoGroup) FromGoSubGroup(onFirstFatal ...parl.GoFatalCallback) (g2 parl.SubGroup) {
   194  	return new(g, nil, true, true, fromGoNewFrames, onFirstFatal...)
   195  }
   196  
   197  // new returns a new GoGroup as parl.GoGroup
   198  func new(
   199  	parent goGroupParent, ctx context.Context,
   200  	hasErrorChannel, isSubGroup bool,
   201  	stackOffset int,
   202  	onFirstFatal ...parl.GoFatalCallback,
   203  ) (g2 *GoGroup) {
   204  	if ctx == nil && parent != nil {
   205  		ctx = parent.Context()
   206  	}
   207  	g := GoGroup{
   208  		creator: *pruntime.NewCodeLocation(stackOffset),
   209  		parent:  parent,
   210  		gos:     pmaps.NewRWMap[parl.GoEntityID, *ThreadData](),
   211  	}
   212  	newGoContext(&g.goContext, ctx)
   213  	if parl.IsThisDebug() {
   214  		g.isDebug.Store(true)
   215  		var log parl.PrintfFunc = parl.Log
   216  		g.log.CompareAndSwap(nil, &log)
   217  	}
   218  	if len(onFirstFatal) > 0 {
   219  		g.onFirstFatal = onFirstFatal[0]
   220  	}
   221  	if hasErrorChannel {
   222  		g.hasErrorChannel = true
   223  	}
   224  	if isSubGroup {
   225  		g.isSubGroup = true
   226  	}
   227  	if g.isDebug.Load() {
   228  		s := "new:" + g.typeString()
   229  		if parent != nil {
   230  			if p, ok := parent.(*GoGroup); ok {
   231  				s += "(" + p.typeString() + ")"
   232  			}
   233  		}
   234  		(*g.log.Load())(s)
   235  	}
   236  	return &g
   237  }
   238  
   239  // Add processes a thread from this or a subordinate thread-group
   240  func (g *GoGroup) Add(goEntityID parl.GoEntityID, threadData *ThreadData) {
   241  	g.doneLock.Lock() // Add
   242  	defer g.doneLock.Unlock()
   243  
   244  	g.wg.Add(1)
   245  	if g.isDebug.Load() {
   246  		(*g.log.Load())("goGroup#%s:Add(new:Go#%s.Go():%s)#%d",
   247  			g.EntityID(),
   248  			goEntityID, threadData.Short(), g.goContext.wg.Count())
   249  	}
   250  	if g.isAggregateThreads.Load() {
   251  		g.gos.Put(goEntityID, threadData)
   252  	}
   253  	if g.parent != nil {
   254  		g.parent.Add(goEntityID, threadData)
   255  	}
   256  }
   257  
   258  // UpdateThread recursively updates thread information for a parl.Go object
   259  // invoked when that Go fiorst obtains the information
   260  func (g *GoGroup) UpdateThread(goEntityID parl.GoEntityID, threadData *ThreadData) {
   261  	if g.isAggregateThreads.Load() {
   262  		g.gos.Put(goEntityID, threadData)
   263  	}
   264  	if g.parent != nil {
   265  		g.parent.UpdateThread(goEntityID, threadData)
   266  	}
   267  }
   268  
   269  // Done receives thread exits from threads in subordinate thread-groups
   270  func (g *GoGroup) GoDone(thread parl.Go, err error) {
   271  	if g.endCh.IsClosed() {
   272  		panic(perrors.ErrorfPF(g.panicString("", thread, &err, false, nil)))
   273  	}
   274  
   275  	// first fatal thread-exit of this thread-group
   276  	if err != nil && g.hadFatal.CompareAndSwap(false, true) {
   277  
   278  		// handle FirstFatal()
   279  		g.setFirstFatal()
   280  
   281  		// onFirstFatal callback
   282  		if g.onFirstFatal != nil {
   283  			var errPanic error
   284  			if errPanic = g.invokeOnFirstFatal(); errPanic != nil {
   285  				g.ConsumeError(NewGoError(
   286  					perrors.ErrorfPF("onFatal callback: %w", errPanic), parl.GeNonFatal, thread))
   287  			}
   288  		}
   289  	}
   290  
   291  	// atomic operation: DoneBool and g0.ch.Close
   292  	g.doneLock.Lock() // GoDone
   293  	defer g.doneLock.Unlock()
   294  
   295  	// check inside lock
   296  	if g.endCh.IsClosed() {
   297  		panic(perrors.ErrorfPF(g.panicString("", thread, &err, false, nil)))
   298  	}
   299  
   300  	// debug print termination-start
   301  	if g.isDebug.Load() {
   302  		var threadData parl.ThreadData
   303  		var id string
   304  		if thread != nil {
   305  			threadData = thread.ThreadInfo()
   306  			id = thread.EntityID().String()
   307  		}
   308  		(*g.log.Load())("goGroup#%s:GoDone(Label-ThreadID:%sGo#%s_exit:‘%s’)after#:%d",
   309  			g.EntityID(),
   310  			threadData.Short(), id, perrors.Short(err),
   311  			g.goContext.wg.Count()-1,
   312  		)
   313  	}
   314  
   315  	// indicates that this GoGroup is about to terminate
   316  	//	- DoneBool invokes Done and returns status
   317  	var isTermination = g.goContext.wg.DoneBool()
   318  
   319  	// delete thread from thread-map
   320  	g.gos.Delete(thread.EntityID(), parli.MapDeleteWithZeroValue)
   321  
   322  	// SubGroup with its own error channel with fatals not affecting parent
   323  	//	- send fatal error to parent as non-fatal error with
   324  	//		error context GeLocalChan
   325  	if g.isSubGroup {
   326  		if err != nil {
   327  			g.ConsumeError(NewGoError(err, parl.GeLocalChan, thread))
   328  		}
   329  		// pretend good thread exit to parent
   330  		g.parent.GoDone(thread, nil)
   331  	}
   332  
   333  	// emit on local error channel: GoGroup, SubGroup
   334  	if g.hasErrorChannel {
   335  		var goErrorContext parl.GoErrorContext
   336  		if isTermination {
   337  			goErrorContext = parl.GeExit
   338  		} else {
   339  			goErrorContext = parl.GePreDoneExit
   340  		}
   341  		g.errCh.Send(NewGoError(err, goErrorContext, thread))
   342  	} else {
   343  
   344  		// SubGo: forward error to parent
   345  		g.parent.GoDone(thread, err)
   346  	}
   347  
   348  	// debug print termination end
   349  	if g.isDebug.Load() {
   350  		var actionS string
   351  		if isTermination {
   352  			actionS = fmt.Sprintf("TERMINATED:isSubGroup:%t:hasEc:%t", g.isSubGroup, g.hasErrorChannel)
   353  		} else {
   354  			actionS = "remaining:" + Shorts(g.Threads())
   355  		}
   356  		(*g.log.Load())(fmt.Sprintf("%s:%s", g.typeString(), actionS))
   357  	}
   358  
   359  	if !isTermination {
   360  		return // GoGroup not yet terminated return
   361  	}
   362  	g.endGoGroup() // GoDone
   363  }
   364  
   365  // ConsumeError receives non-fatal errors from a Go thread.
   366  //   - Go.AddError delegates to this method
   367  func (g *GoGroup) ConsumeError(goError parl.GoError) {
   368  	if g.errCh.DidClose() {
   369  		panic(perrors.ErrorfPF(g.panicString("", nil, nil, true, goError)))
   370  	}
   371  	if goError == nil {
   372  		panic(perrors.NewPF("goError cannot be nil"))
   373  	}
   374  	// non-fatal errors are:
   375  	//	- parl.GeNonFatal or
   376  	//	- parl.GeLocalChan when a SubGroup send fatal errors as non-fatal
   377  	if goError.ErrContext() != parl.GeNonFatal && // it is a non-fatal error
   378  		goError.ErrContext() != parl.GeLocalChan { // it is a fatal error store in a local error channel
   379  		panic(perrors.ErrorfPF(g.panicString("received termination as non-fatal error", nil, nil, true, goError)))
   380  	}
   381  
   382  	// it is a non-fatal error that should be processed
   383  
   384  	// if we have a parent GoGroup, send it there
   385  	if g.parent != nil {
   386  		g.parent.ConsumeError(goError)
   387  		return
   388  	}
   389  
   390  	// send the error to the channel of this stand-alone G1Group
   391  	g.errCh.Send(goError)
   392  }
   393  
   394  // Ch returns a channel sending the all fatal termination errors when
   395  // the FailChannel option is present, or only the first when both
   396  // FailChannel and StoreSubsequentFail options are present.
   397  func (g *GoGroup) Ch() (ch <-chan parl.GoError) { return g.errCh.Ch() }
   398  
   399  // FirstFatal allows to await or inspect the first thread terminating with error.
   400  // it is valid if this SubGo has LocalSubGo or LocalChannel options.
   401  // To wait for first fatal error using multiple-semaphore mechanic:
   402  //
   403  //	firstFatal := g0.FirstFatal()
   404  //	for {
   405  //	  select {
   406  //	  case <-firstFatal.Ch():
   407  //	  …
   408  //
   409  // To inspect first fatal:
   410  //
   411  //	if firstFatal.DidOccur() …
   412  func (g *GoGroup) FirstFatal() (firstFatal *parl.OnceWaiterRO) {
   413  	var onceWaiter *parl.OnceWaiter
   414  	for {
   415  		if onceWaiter0 := g.onceWaiter.Load(); onceWaiter0 != nil {
   416  			return parl.NewOnceWaiterRO(onceWaiter0)
   417  		}
   418  		if onceWaiter == nil {
   419  			onceWaiter = parl.NewOnceWaiter(context.Background())
   420  		}
   421  		if g.onceWaiter.CompareAndSwap(nil, onceWaiter) {
   422  			onceWaiter.Cancel()
   423  			return
   424  		}
   425  	}
   426  }
   427  
   428  // EnableTermination controls whether the thread-droup is allowed to terminate
   429  //   - true is default
   430  //   - period of false prevents terminating even if child-object count reaches zero
   431  //   - invoking with true while child-object count is zero,
   432  //     terminates the thread-group regardless of previous enableTermination state.
   433  //     This is used prior to Wait when a thread-group was not used.
   434  //     Using the alternative Cancel would signal to threads to exit.
   435  func (g *GoGroup) EnableTermination(allowTermination ...bool) (mayTerminate bool) {
   436  	if g.isDebug.Load() {
   437  		(*g.log.Load())("%s:EnableTermination:%t", g.typeString(), allowTermination)
   438  	}
   439  
   440  	// if no argument or the thread-group already terminated
   441  	//	- just return current state
   442  	if len(allowTermination) == 0 || g.endCh.IsClosed() {
   443  		return !g.isNoTermination.Load()
   444  	}
   445  
   446  	// prevent termination case
   447  	if !allowTermination[0] {
   448  		// cascade if it is a state change
   449  		if g.isNoTermination.CompareAndSwap(false, true) {
   450  			// add a fake count to parent waitgroup preventing iut from terminating
   451  			g.CascadeEnableTermination(1)
   452  		}
   453  		return // prevent termination complete: mayTerminate: false
   454  	}
   455  
   456  	// allow termination case
   457  	//	- must always be cascaded
   458  	//	- either to change the state or
   459  	//	- for unused thread-group termination
   460  	var delta int
   461  	if g.isNoTermination.Load() {
   462  		if g.isNoTermination.CompareAndSwap(true, false) {
   463  			// remove the fake count from parent
   464  			delta = -1
   465  			g.CascadeEnableTermination(delta)
   466  		}
   467  	}
   468  	if delta == 0 {
   469  		if p := g.parent; p != nil {
   470  			p.CascadeEnableTermination(0)
   471  		}
   472  	}
   473  
   474  	mayTerminate = true
   475  
   476  	// check if this thread-group should be terminated
   477  	// atomic operation: DoneBool and g0.ch.Close
   478  	g.doneLock.Lock() // EnableTermination
   479  	defer g.doneLock.Unlock()
   480  
   481  	// if there are subordinate objects, termination will be done by GoDone
   482  	if !g.wg.IsZero() {
   483  		return // GoGroup is not in pending termination
   484  	}
   485  	// all threads have exited, so this ends the thread-group
   486  	if g.isDebug.Load() {
   487  		(*g.log.Load())("%s:TERMINATED:EnableTermination", g.typeString())
   488  	}
   489  	g.endGoGroup() // EnableTermination
   490  
   491  	return
   492  }
   493  
   494  // CascadeEnableTermination manipulates wait groups of this goGroup and
   495  // those of its parents to allow or prevent termination
   496  func (g *GoGroup) CascadeEnableTermination(delta int) {
   497  	g.wg.Add(delta)
   498  	if g.parent != nil {
   499  		g.parent.CascadeEnableTermination(delta)
   500  	}
   501  	// make EnableTermination Allow cascade
   502  	if delta == 0 && !g.isNoTermination.Load() {
   503  		g.EnableTermination(parl.AllowTermination)
   504  	}
   505  }
   506  
   507  // ThreadsInternal returns values with the internal parl.GoEntityID key
   508  func (g *GoGroup) ThreadsInternal() (m parli.ThreadSafeMap[parl.GoEntityID, *ThreadData]) {
   509  	return g.gos.Clone()
   510  }
   511  
   512  // Internals returns methods used by [g0debug.ThreadLogger]
   513  func (g *GoGroup) Internals() (
   514  	isEnd func() bool,
   515  	isAggregateThreads *atomic.Bool,
   516  	setCancelListener func(f func()),
   517  	endCh <-chan struct{},
   518  ) {
   519  	if g.hasErrorChannel {
   520  		endCh = g.errCh.WaitForCloseCh()
   521  	} else {
   522  		endCh = g.endCh.Ch()
   523  	}
   524  	return g.isEnd, &g.isAggregateThreads, g.goContext.setCancelListener, endCh
   525  }
   526  
   527  // the available data for all threads
   528  func (g *GoGroup) Threads() (threads []parl.ThreadData) {
   529  	// the pointer can be updated at any time, but the value does not change
   530  	var list = g.gos.List()
   531  	threads = make([]parl.ThreadData, len(list))
   532  	for i, tp := range list {
   533  		threads[i] = tp
   534  	}
   535  	return
   536  }
   537  
   538  // threads that have been named ordered by name
   539  func (g *GoGroup) NamedThreads() (threads []parl.ThreadData) {
   540  	// the pointer can be updated at any time, but the value does not change
   541  	//	- slice of struct pointer
   542  	var list = g.gos.List()
   543  
   544  	// remove unnamed threads
   545  	for i := 0; i < len(list); {
   546  		if list[i].label == "" {
   547  			list = slices.Delete(list, i, i+1)
   548  		} else {
   549  			i++
   550  		}
   551  	}
   552  
   553  	// sort pointers
   554  	slices.SortFunc(list, g.cmpNames)
   555  
   556  	// return slice of interface
   557  	threads = make([]parl.ThreadData, len(list))
   558  	for i, tp := range list {
   559  		threads[i] = tp
   560  	}
   561  
   562  	return
   563  }
   564  
   565  // SetDebug enables debug logging on this particular instance
   566  //   - parl.NoDebug
   567  //   - parl.DebugPrint
   568  //   - parl.AggregateThread
   569  func (g *GoGroup) SetDebug(debug parl.GoDebug, log ...parl.PrintfFunc) {
   570  
   571  	// ensure g.log
   572  	var logF parl.PrintfFunc
   573  	if len(log) > 0 {
   574  		logF = log[0]
   575  	}
   576  	if logF != nil {
   577  		g.log.Store(&logF)
   578  	} else if g.log.Load() == nil {
   579  		logF = parl.Log
   580  		g.log.Store(&logF)
   581  	}
   582  
   583  	if debug == parl.DebugPrint {
   584  		g.isDebug.Store(true)
   585  		g.isAggregateThreads.Store(true)
   586  		return
   587  	}
   588  	g.isDebug.Store(false)
   589  
   590  	if debug == parl.AggregateThread {
   591  		g.isAggregateThreads.Store(true)
   592  		return
   593  	}
   594  
   595  	g.isAggregateThreads.Store(false)
   596  }
   597  
   598  // Cancel signals shutdown to all threads of a thread-group.
   599  func (g *GoGroup) Cancel() {
   600  
   601  	// cancel the context
   602  	g.goContext.Cancel()
   603  
   604  	// check outside lock: done if:
   605  	// - if GoGroup/SubGroup/SubGo already terminated
   606  	//	- subordinate objects exist
   607  	//	- termination is temporarily disabled
   608  	if g.isEnd() || g.goContext.wg.Count() > 0 || g.isNoTermination.Load() {
   609  		return // already ended or have child object or termination off return
   610  	}
   611  
   612  	// special case: Cancel before any Go SubGo SubGroup
   613  	//	- normally, GoDone or EnableTermination
   614  	// atomic operation: DoneBool and g0.ch.Close
   615  	g.doneLock.Lock() // Cancel
   616  	defer g.doneLock.Unlock()
   617  
   618  	// repeat check inside lock
   619  	if g.isEnd() || g.goContext.wg.Count() > 0 || g.isNoTermination.Load() {
   620  		return // already ended or have child objects or termination off return
   621  	}
   622  	if g.isDebug.Load() {
   623  		(*g.log.Load())("%s:TERMINATED:Cancel", g.typeString())
   624  	}
   625  	g.endGoGroup() // Cancel
   626  }
   627  
   628  // Wait waits for all threads of this thread-group to terminate.
   629  func (g *GoGroup) Wait() {
   630  	<-g.endCh.Ch()
   631  }
   632  
   633  // returns a channel that closes on subGo end similar to Wait
   634  func (g *GoGroup) WaitCh() (ch parl.AwaitableCh) {
   635  	return g.endCh.Ch()
   636  }
   637  
   638  func (g *GoGroup) panicString(
   639  	text string,
   640  	thread parl.Go,
   641  	errp *error,
   642  	hasGoE bool, goError parl.GoError,
   643  ) (s string) {
   644  	var sL = []string{fmt.Sprintf("after %s termination.", g.typeString())}
   645  	if text != "" {
   646  		sL = append(sL, text)
   647  	}
   648  	if thread != nil {
   649  		var _, goFunction = thread.GoRoutine()
   650  		if goFunction.IsSet() {
   651  			sL = append(sL, "goFunc: "+goFunction.Short())
   652  		} else {
   653  			var _, creator = thread.Creator()
   654  			sL = append(sL, "go-statement: "+creator.Short())
   655  		}
   656  	}
   657  	if errp != nil {
   658  		sL = append(sL, fmt.Sprintf("err: ‘%s’", perrors.Short(*errp)))
   659  	}
   660  	if hasGoE {
   661  		sL = append(sL, goError.String())
   662  	}
   663  	sL = append(sL, "newGroup: "+g.creator.Short())
   664  	return strings.Join(sL, "\x20")
   665  }
   666  
   667  // invoked while holding g.doneLock
   668  //   - closes error channel if GoGroup or SubGroup
   669  //   - closes endCh
   670  //   - cancels context
   671  func (g *GoGroup) endGoGroup() {
   672  	if g.hasErrorChannel {
   673  		g.errCh.Close() // close local error channel
   674  	}
   675  	// mark GoGroup terminated
   676  	g.endCh.Close()
   677  	// cancel the context
   678  	g.goContext.Cancel()
   679  }
   680  
   681  // cmpNames is a slice comparison function for thread names
   682  func (g *GoGroup) cmpNames(a *ThreadData, b *ThreadData) (result int) {
   683  	if a.label < b.label {
   684  		return -1
   685  	} else if a.label > b.label {
   686  		return 1
   687  	}
   688  	return 0
   689  }
   690  
   691  // setFirstFatal triggers a possible onFirstFatal
   692  func (g *GoGroup) setFirstFatal() {
   693  	var onceWaiter = g.onceWaiter.Load()
   694  	if onceWaiter == nil {
   695  		return // FirstFatal not invoked return
   696  	}
   697  	onceWaiter.Cancel()
   698  }
   699  
   700  // isEnd determines if this goGroup has ended
   701  //   - if goGroup has error channel, the goGroup ends when its error channel closes
   702  //   - — goGroups without a parent
   703  //   - — subGroups with error channel
   704  //   - — a subGo, having no error channel, ends when all threads have exited
   705  //   - if the GoGroup or any of its subordinate thread-groups have EnableTermination false
   706  //     GoGroups will not end until EnableTermination true
   707  func (g *GoGroup) isEnd() (isEnd bool) {
   708  
   709  	// SubGo termination flag
   710  	if !g.hasErrorChannel {
   711  		return g.endCh.IsClosed()
   712  	} else {
   713  		// others is when error channel closes
   714  		return g.errCh.IsClosed()
   715  	}
   716  }
   717  
   718  // "goGroup#1" "subGroup#2" "subGo#3"
   719  func (g *GoGroup) typeString() (s string) {
   720  	if g.parent == nil {
   721  		s = "goGroup"
   722  	} else if g.isSubGroup {
   723  		s = "subGroup"
   724  	} else {
   725  		s = "subGo"
   726  	}
   727  	return s + "#" + g.goEntityID.EntityID().String()
   728  }
   729  
   730  // g1Group#3threads:1(1)g0.TestNewG1Group-g1-group_test.go:60
   731  func (g *GoGroup) String() (s string) {
   732  	return parl.Sprintf("%s_threads:%s_New:%s",
   733  		g.typeString(), // "goGroup#1"
   734  		g.goContext.wg.String(),
   735  		g.creator.Short(),
   736  	)
   737  }
   738  
   739  func (g *GoGroup) invokeOnFirstFatal() (err error) {
   740  	defer parl.RecoverErr(func() parl.DA { return parl.A() }, &err)
   741  
   742  	g.onFirstFatal(g)
   743  
   744  	return
   745  }