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

     1  /*
     2  © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package parl
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"time"
    12  
    13  	"github.com/haraldrudell/parl/pruntime"
    14  )
    15  
    16  // Thread interface
    17  
    18  // Go provides the four needs of a running goroutione thread.
    19  // The Go is provided as a function argument in the go statement function call
    20  // that launches the thread.
    21  //   - the four needs:
    22  //   - — to be waited upon via [Go.Done]
    23  //   - — to submit non-fatal errors via [Go.AddError]
    24  //   - — to detect and initiate cancel via [Go.Context] [Go.Cancel]
    25  //   - [Go.Cancel] cancels:
    26  //   - — this Go thread’s context
    27  //   - — this Go’s parent thread-group’s context and
    28  //   - — this Go’s parent thread-group’s subordinate thread-groups’ contexts
    29  //   - The Go Context is canceled when
    30  //   - — the parent GoGroup thread-group’s context is Canceled or
    31  //   - —a thread in the parent GoGroup thread-group initiates Cancel
    32  //   - Cancel by threads in sub ordinate thread-groups do not Cancel this Go thread
    33  //
    34  // Usage:
    35  //
    36  //	var threadGroup = g0.NewGoGroup(context.Background())
    37  //	go someFunc(threadGroup.Go())
    38  //	…
    39  //	func someFunc(g parl.Go) {
    40  //	  var err error
    41  //	  defer g.Register().Done(&err)
    42  //	  defer parl.RecoverErr(func() parl.DA { return parl.A() }, &err)
    43  //	  …
    44  type Go interface {
    45  	// Register performs no function but allows the Go object to collect
    46  	// information on the new thread.
    47  	// - label is an optional name that can be assigned to a Go goroutine thread
    48  	Register(label ...string) (g0 Go)
    49  	// AddError emits a non-fatal errors
    50  	AddError(err error)
    51  	// Go returns a Go object to be provided as a go-statement function-argument
    52  	//	in a function call invocation launching a new gorotuine thread.
    53  	//	- the new thread belongs to the same GoGroup thread-group as the Go
    54  	//		object whose Go method was invoked.
    55  	Go() (g0 Go)
    56  	// SubGo returns a GoGroup thread-group whose fatal and non-fatel errors go to
    57  	//	the Go object’s parent thread-group.
    58  	//	- a SubGo is used to ensure sub-threads exiting prior to their parent thread
    59  	//		or to facilitate separate cancelation of the threads in the subordinate thread-group.
    60  	//	- fatal errors from SubGo threads are handled in the same way as those of the
    61  	//		Go object, typically terminating the application.
    62  	//   - the SubGo thread-group terminates when both its own threads have exited and
    63  	//	- the threads of its subordinate thread-groups.
    64  	SubGo(onFirstFatal ...GoFatalCallback) (subGo SubGo)
    65  	// SubGroup returns a thread-group with its own error channel.
    66  	//	- a SubGroup is used for threads whose fatal errors should be handled
    67  	//		in the Go thread.
    68  	//	- The threads of the Subgroup can be canceled separately.
    69  	//		- SubGroup’s error channel collects fatal thread terminations
    70  	//		- the SubGroup’s error channel needs to be read in real-time or after
    71  	//		SubGroup termination
    72  	//   - non-fatal errors in SubGroup threads are sent to the Go object’s parent
    73  	//		similar to the AddError method
    74  	//   - the SubGroup thread-group terminates when both its own threads have exited and
    75  	//	- the threads of its subordinate thread-groups.
    76  	SubGroup(onFirstFatal ...GoFatalCallback) (subGroup SubGroup)
    77  	// Done indicates that this goroutine is exiting
    78  	//	- err == nil means successful exit
    79  	//	- non-nil err indicates fatal error
    80  	// 	- deferrable
    81  	Done(errp *error)
    82  	// Wait awaits exit of this Go thread.
    83  	Wait()
    84  	WaitCh() (ch AwaitableCh)
    85  	// Cancel signals for the threads in this Go thread’s parent GoGroup thread-group
    86  	// and any subordinate thread-groups to exit.
    87  	Cancel()
    88  	// Context will Cancel when the parent thread-group Cancels
    89  	//	or Cancel is invoked on this Go object.
    90  	//	- Subordinate thread-groups do not Cancel the context of the Go thread.
    91  	Context() (ctx context.Context)
    92  	// ThreadInfo returns thread data that is partially or fully populated
    93  	//	- ThreadID may be invalid: threadID.IsValid.
    94  	//	- goFunction may be zero-value: goFunction.IsSet
    95  	//	- those values present after public methods of parl.Go has been invoked by
    96  	//		the new goroutine
    97  	ThreadInfo() (threadData ThreadData)
    98  	// values always present
    99  	Creator() (threadID ThreadID, createLocation *pruntime.CodeLocation)
   100  	//	- ThreadID may be invalid: threadID.IsValid.
   101  	//	- goFunction may be zero-value: goFunction.IsSet
   102  	//	- those values present after public methods of parl.Go has been invoked by
   103  	//		the new goroutine
   104  	GoRoutine() (threadID ThreadID, goFunction *pruntime.CodeLocation)
   105  	// GoID efficiently returns the goroutine ID that may be invalid
   106  	//	- valid after public methods of parl.Go has been invoked by
   107  	//		the new goroutine
   108  	GoID() (threadID ThreadID)
   109  	// EntityID returns a value unique for this Go
   110  	//	- ordered: usable as map key or for sorting
   111  	//	- always valid, has .String method
   112  	EntityID() (goEntityID GoEntityID)
   113  	fmt.Stringer
   114  }
   115  
   116  // GoFatalCallback receives the thread-group on its first fatal thread-exit
   117  //   - GoFatalCallback is an optional onFirstFatal argument to
   118  //   - — NewGoGroup
   119  //   - — SubGo
   120  //   - — SubGroup
   121  type GoFatalCallback func(goGen GoGen)
   122  
   123  // GoGen allows for new Go threads, new SubGo and SubGroup thread-groups and
   124  // cancel of threads in the thread-group and its subordinate thread-groups.
   125  //   - GoGen is value from NewGoGroup GoGroup SubGo SubGroup Go,
   126  //     ie. any Go-interface object
   127  type GoGen interface {
   128  	// Go returns a Go object to be provided as a go statement function argument.
   129  	Go() (g0 Go)
   130  	// SubGo returns a thread-group whose fatal errors go to GoGen’s parent.
   131  	//   - both non-fatal and fatal errors in SubGo threads are sent to GoGen’s parent
   132  	// 		like Go.AddError and Go.Done.
   133  	//		- therefore, when a SubGo thread fails, the application will typically exit.
   134  	//		- by awaiting SubGo, Go can delay its exit until SubGo has terminated
   135  	//   - the SubGo thread-group terminates when the its thread exits
   136  	SubGo(onFirstFatal ...GoFatalCallback) (subGo SubGo)
   137  	// SubGroup creates a sub-ordinate thread-group
   138  	SubGroup(onFirstFatal ...GoFatalCallback) (subGroup SubGroup)
   139  	// Cancel terminates the threads in the Go consumer thread-group.
   140  	Cancel()
   141  	// Context will Cancel when the parent thread-group Cancels.
   142  	// Subordinate thread-groups do not Cancel this context.
   143  	Context() (ctx context.Context)
   144  }
   145  
   146  // Thread Group interfaces and Factory
   147  
   148  // GoGroup manages a hierarchy of threads
   149  //   - GoGroup only terminates when:
   150  //   - — the last thread in its hierarchy exits
   151  //   - — [GoGroup.EnableTermination] is set to true when
   152  //     no Go threads exist in the hierarchy
   153  //   - the GoGroup hierarchy consists of:
   154  //   - — managed goroutines returned by [GoGroup.Go]
   155  //   - — a [SubGo] subordinate thread-group hierarchy returned by [GoGroup.SubGo]
   156  //     that allows for a group of threads to be canceled or waited upon separately
   157  //   - — a [SubGroup] subordinate thread-group hierarchy returned by [GoGroup.SubGroup]
   158  //     that allows for a group of threads to exit with fatal errors without
   159  //     canceling the GoGroup and for those threads to be
   160  //     canceled or waited upon separately
   161  //   - — each subordinate Go thread or SubGo or SubGroup subordinate thread-groups
   162  //     can create additional threads and subordinate thread-groups.
   163  //   - [GoGroup.Context] returns a context that is the context or parent context
   164  //     of all the Go threads, SubGo and SubGroup subordinate thread-groups
   165  //     in its hierarchy
   166  //   - [GoGroup.Cancel] cancels the GoGroup Context,
   167  //     thereby signaling to all threads in the GoGroup hierarchy to exit.
   168  //     This will eventually terminate the GoGroup
   169  //   - providing a parent context to the GoGroup implementation allows
   170  //     for terminating the GoGroup via this parent context
   171  //   - A thread invoking [Go.Cancel] will signal to all threads in its
   172  //     GoGroup or SubGo or SubGroup thread-groups to exit.
   173  //     It will also signal to all threads in its subordinate thread-groups to exit.
   174  //     This will eventually terminate its threadgroup and all that threadgroup’s
   175  //     subordinate threadgroups.
   176  //   - Alternatives to [parl.Go] is [parl.NewGoResult] and [parl.NewGoResult2]
   177  //     that only provides being awaitable to a goroutine
   178  //   - —
   179  //   - from this thread-group will terminate all threads in this
   180  //     and subordinate thread-groups if this thread-group was provided
   181  //     the FirstFailTerminates option, which is default.
   182  //   - A fatal thread-termination in a sub thread-group only affects this
   183  //     thread-group if the sub thread-group was provided a nil fatal function,
   184  //     the FirstFailTerminates option, which is default, and no explicit
   185  //     FailChannel option.
   186  //   - Fatal thread terminations will propagate to parent thread-groups if
   187  //     this thread group did not have a fatal function provided and was not
   188  //     explicitly provided the FailChannel option.
   189  //   - A Cancel in this thread-group or in a parent context cancels threads in
   190  //     this and all subordinate thread-groups.
   191  //   - A Cancel in a subordinate thread-group does not affect this thread-group.
   192  //   - Wait in this thread-group wait for threads in this and all subordinate
   193  //     thread-groups.
   194  type GoGroup interface {
   195  	// Go returns a Go object to be provided as a go statement function argument.
   196  	Go() (g0 Go)
   197  	// SubGo returns athread-group whose fatal errors go to Go’s parent.
   198  	//   - both non-fatal and fatal errors in SubGo threads are sent to Go’s parent
   199  	// 		like Go.AddError and Go.Done.
   200  	//		- therefore, when a SubGo thread fails, the application will typically exit.
   201  	//		- by awaiting SubGo, Go can delay its exit until SubGo has terminated
   202  	//   - the SubGo thread-group terminates when the its thread exits
   203  	SubGo(onFirstFatal ...GoFatalCallback) (subGo SubGo)
   204  	// SubGroup creates a sub-ordinate GoGroup.
   205  	//	- SubGroup fatal and non-fatal errors are sent to the parent GoGroup.
   206  	//	-	SubGroup-context initiated Cancel only affect threads in the SubGroup thread-group
   207  	//	- parent-initiated Cancel terminates SubGroup threads
   208  	//	- SubGroup only awaits SubGroup threads
   209  	//	- parent await also awaits SubGroup threads
   210  	SubGroup(onFirstFatal ...GoFatalCallback) (subGroup SubGroup)
   211  	// Ch returns a channel sending the all fatal termination errors when
   212  	// the FailChannel option is present, or only the first when both
   213  	// FailChannel and StoreSubsequentFail options are present.
   214  	Ch() (ch <-chan GoError)
   215  	// Wait waits for this thread-group to terminate.
   216  	Wait()
   217  	// EnableTermination controls temporarily preventing the GoGroup from
   218  	// terminating.
   219  	// EnableTermination is initially true.
   220  	//	- invoked with no argument returns the current state of EnableTermination
   221  	//	- invoked with [AllowTermination] again allows for termination and
   222  	//		immediately terminates the thread-group if no threads are currently running.
   223  	//	- invoked with [PreventTermination] allows for the number of managed
   224  	//		threads to be temporarily zero without terminating the thread-group
   225  	EnableTermination(allowTermination ...bool) (mayTerminate bool)
   226  	// Cancel terminates the threads in this and subordinate thread-groups.
   227  	Cancel()
   228  	// Context will Cancel when the parent context Cancels.
   229  	// Subordinate thread-groups do not Cancel this context.
   230  	Context() (ctx context.Context)
   231  	// the available data for all threads
   232  	Threads() (threads []ThreadData)
   233  	// threads that have been named ordered by name
   234  	NamedThreads() (threads []ThreadData)
   235  	// SetDebug enables debug logging on this particular instance
   236  	//	- parl.NoDebug
   237  	//	- parl.DebugPrint
   238  	//	- parl.AggregateThread
   239  	SetDebug(debug GoDebug, log ...PrintfFunc)
   240  	fmt.Stringer
   241  }
   242  
   243  type SubGo interface {
   244  	// Go returns a [Go] object managing a thread of the GoGroup thread-group
   245  	// by providing the g value as a go-statement function argument.
   246  	Go() (g Go)
   247  	// SubGo returns a thread-group whose fatal errors go to Go’s parent.
   248  	//   - both non-fatal and fatal errors in SubGo threads are sent to Go’s parent
   249  	// 		like Go.AddError and Go.Done.
   250  	//		- therefore, when a SubGo thread fails, the application will typically exit.
   251  	//		- by awaiting SubGo, Go can delay its exit until SubGo has terminated
   252  	//   - the SubGo thread-group terminates when the its thread exits
   253  	SubGo(onFirstFatal ...GoFatalCallback) (subGo SubGo)
   254  	// SubGroup creates a sub-ordinate GoGroup.
   255  	//	- SubGroup fatal and non-fatal errors are sent to the parent GoGroup.
   256  	//	-	SubGroup-context initiated Cancel only affect threads in the SubGroup thread-group
   257  	//	- parent-initiated Cancel terminates SubGroup threads
   258  	//	- SubGroup only awaits SubGroup threads
   259  	//	- parent await also awaits SubGroup threads
   260  	SubGroup(onFirstFatal ...GoFatalCallback) (subGroup SubGroup)
   261  	// Wait waits for all threads of this thread-group to terminate.
   262  	Wait()
   263  	// returns a channel that closes on subGo end similar to Wait
   264  	WaitCh() (ch AwaitableCh)
   265  	// EnableTermination controls temporarily preventing the GoGroup from
   266  	// terminating.
   267  	// EnableTermination is initially true.
   268  	//	- invoked with no argument returns the current state of EnableTermination
   269  	//	- invoked with [AllowTermination] again allows for termination and
   270  	//		immediately terminates the thread-group if no threads are currently running.
   271  	//	- invoked with [PreventTermination] allows for the number of managed
   272  	//		threads to be temporarily zero without terminating the thread-group
   273  	EnableTermination(allowTermination ...bool) (mayTerminate bool)
   274  	// Cancel terminates the threads in this and subordinate thread-groups.
   275  	Cancel()
   276  	// Context will Cancel when the parent context Cancels.
   277  	// Subordinate thread-groups do not Cancel this context.
   278  	Context() (ctx context.Context)
   279  	// the available data for all threads
   280  	Threads() (threads []ThreadData)
   281  	// threads that have been named ordered by name
   282  	NamedThreads() (threads []ThreadData)
   283  	// SetDebug enables debug logging on this particular instance
   284  	//   - parl.NoDebug
   285  	//   - parl.DebugPrint
   286  	//   - parl.AggregateThread
   287  	SetDebug(debug GoDebug, log ...PrintfFunc)
   288  	fmt.Stringer
   289  }
   290  
   291  type SubGroup interface {
   292  	SubGo
   293  	// Ch returns a receive channel for fatal errors if this SubGo has LocalChannel option.
   294  	Ch() (ch <-chan GoError)
   295  	// FirstFatal allows to await or inspect the first thread terminating with error.
   296  	// it is valid if this SubGo has LocalSubGo or LocalChannel options.
   297  	// To wait for first fatal error using multiple-semaphore mechanic:
   298  	//  firstFatal := g0.FirstFatal()
   299  	//  for {
   300  	//    select {
   301  	//    case <-firstFatal.Ch():
   302  	//    …
   303  	// To inspect first fatal:
   304  	//  if firstFatal.DidOccur() …
   305  	FirstFatal() (firstFatal *OnceWaiterRO)
   306  }
   307  
   308  type GoFactory interface {
   309  	// NewGo returns a light-weight thread-group.
   310  	//	- GoGroup only receives Cancel from ctx, it does not cancel this context.
   311  	NewGoGroup(ctx context.Context, onFirstFatal ...GoFatalCallback) (g0 GoGroup)
   312  }
   313  
   314  const (
   315  	AllowTermination   = true
   316  	PreventTermination = false
   317  )
   318  
   319  // data types
   320  
   321  // GoError is an error or a thread exit associated with a goroutine
   322  //   - GoError encapsulates the original unadulterated error
   323  //   - GoError provides context for taking action on the error
   324  //   - Go provides the thread associated with the error. All GoErrors are associated with
   325  //     a Go object
   326  //   - because GoError is both error and fmt.Stringer, to print its string representation
   327  //     requires using the String() method, otherwise fmt.Printf defaults to the Error()
   328  //     method
   329  type GoError interface {
   330  	error // Error() string
   331  	// Err retrieves the original error value
   332  	Err() (err error)
   333  	// ErrString returns string representation of error
   334  	//   - if no error “OK”
   335  	//   - if not debug or panic, short error with location
   336  	//   - otherwise error with stack trace
   337  	ErrString() (errString string)
   338  	// Time provides when this error occurred
   339  	Time() time.Time
   340  	// IsThreadExit determines if this error is a thread exit, ie. GeExit GePreDoneExit
   341  	//	- thread exits may have err nil
   342  	//	- fatals are non-nil thread exits that may require specific actions such as
   343  	//		application termination
   344  	IsThreadExit() (isThreadExit bool)
   345  	// IsFatal determines if this error is a fatal thread-exit, ie. a thread exiting with non-nil error
   346  	IsFatal() (isThreadExit bool)
   347  	// ErrContext returns in what situation this error occurred
   348  	ErrContext() (errContext GoErrorContext)
   349  	// Go provides the thread and goroutine emitting this error
   350  	Go() (g0 Go)
   351  	fmt.Stringer
   352  }
   353  
   354  // ThreadData is information about a Go object thread.
   355  //   - initially, only Create is present
   356  //   - Name is only present for threads that have been named
   357  type ThreadData interface {
   358  	// threadID is the ID of the running thread assigned by the go runtime
   359  	//	- IsValid method checks if value is present
   360  	//	- zero value is empty string
   361  	//	- .ThreadID().String(): "5"
   362  	ThreadID() (threadID ThreadID)
   363  	// createLocation is the code line of the go statement function-call
   364  	// creating the goroutine thread
   365  	// - IsSet method checks if value is present
   366  	//	- Create().Short(): "g0.(*SomeType).SomeCode()-thread-data_test.go:73"
   367  	Create() (createLocation *pruntime.CodeLocation)
   368  	// Func returns the code line of the function of the running thread.
   369  	// - IsSet method checks if value is present
   370  	//	- .Func().Short(): "g0.(*SomeType).SomeFunction()-thread-data_test.go:80"
   371  	Func() (funcLocation *pruntime.CodeLocation)
   372  	// optional thread-name assigned by consumer
   373  	//	- zero-value: empty string "" for threads that have not been named
   374  	Name() (label string)
   375  	// Short returns a short description of the thread "label:threadID" or fmt.Stringer
   376  	//	- "myThreadName:4"
   377  	//	- zero-value: "[empty]" ThreadDataEmpty
   378  	//	- nil value: "threadData:nil" ThreadDataNil
   379  	Short() (short string)
   380  	// all non-empty fields: [label]:[threadID]_func:[Func]_cre:[Create]
   381  	//	- "myThreadName:5_func:g0.(*SomeType).SomeFunction()-thread-data_test.go:80_cre:g0.(*SomeType).SomeCode()-thread-data_test.go:73"
   382  	//	- zero-value: "[empty]" ThreadDataEmpty
   383  	fmt.Stringer
   384  }
   385  
   386  const (
   387  	// GeNonFatal indicates a non-fatal error ocurring during processing.
   388  	// err is non-nil
   389  	GeNonFatal GoErrorContext = iota + 1
   390  	// GePreDoneExit indicates an exit value of a subordinate goroutine,
   391  	// other than the final exit of the last running goroutine.
   392  	// err may be nil
   393  	GePreDoneExit
   394  	// A SubGroup with its own error channel is sending a
   395  	// locally fatal error not intended to terminate the app
   396  	GeLocalChan
   397  	// A thread is requesting app termination without a fatal error.
   398  	//	- this could be a callback
   399  	GeTerminate
   400  	// GeExit indicates exit of the last goroutine.
   401  	// err may be nil.
   402  	// The error channel may close after GeExit.
   403  	GeExit
   404  )
   405  
   406  const (
   407  	NoDebug GoDebug = iota
   408  	DebugPrint
   409  	AggregateThread
   410  )
   411  
   412  type GoDebug uint8