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 }