diff --git a/actor.go b/actor.go index a313082..730fd98 100644 --- a/actor.go +++ b/actor.go @@ -134,3 +134,15 @@ type RunShutdownable interface { type Cleanupable interface { Cleanup(ctx context.Context) error } + +// MainRunnable is any object with a function that must be bound to the main +// thread. Only one actor may implement this at a time, and it must have been +// added at the start of the environment as an argument to the run function. +type MainRunnable interface { + // RunMain is run in the main thread and must stop once ShutdownMain + // is called. + RunMain() error + // ShutdownMain is like [RunShutdownable.Shutdown], but unblocks RunMain + // instead of Run. + ShutdownMain(ctx context.Context) error +} diff --git a/environment.go b/environment.go index 909f140..4d62485 100644 --- a/environment.go +++ b/environment.go @@ -30,6 +30,7 @@ type environment struct { name string description string actors usync.RWMonitor[*actorSets] + main MainRunnable ctx context.Context done context.CancelCauseFunc group sync.WaitGroup diff --git a/phases.go b/phases.go index 4ad3aba..e4ffbe0 100644 --- a/phases.go +++ b/phases.go @@ -228,6 +228,41 @@ func (this *environment) phase60Initialization() bool { } func (this *environment) phase70Running() bool { + for actor := range this.All() { + if actor, ok := actor.(MainRunnable); ok { + this.main = actor + } + } + + result := make(chan bool) + go func() { + result <- this.phase70RunningBody() + if this.main != nil { + shutdownCtx, done := context.WithTimeout( + context.Background(), + defaul(this.timing.shutdownTimeout.Load(), + defaultShutdownTimeout)) + defer done() + this.main.ShutdownMain(shutdownCtx) + } + }() + + if this.main != nil { + mainActor := this.main.(Actor) + if this.Verb() { log.Printf("(i) (70) binding %s to main thread", mainActor.Type()) } + runtime.LockOSThread() + defer runtime.UnlockOSThread() + err := this.main.RunMain() + if err != nil { + log.Printf("XXX [%s] main thread failed: %v", mainActor.Type(), err) + } + if this.Verb() { log.Printf("(i) (70) main thread exited") } + } + + return <- result +} + +func (this *environment) phase70RunningBody() bool { defer this.Done(nil) if this.Verb() { log.Println("... (70) starting up") } this.running.Store(true) diff --git a/run.go b/run.go index 05adf30..08a8bd9 100644 --- a/run.go +++ b/run.go @@ -57,7 +57,9 @@ var env environment // goroutine. If an actor does not run for a meaningful amount of time // after resetting/initialization before failing, it is considered erratic // and further attempts to restart it will be spaced by a limited, -// constantly increasing time interval. The timing is configurable, but by +// constantly increasing time interval. +// +// The timing is configurable, but by // default the threshold for a meaningful amount of runtime is 16 seconds, // the initial delay interval is 8 seconds, the interval increase per // attempt is 8 seconds, and the maximum interval is one hour. @@ -66,6 +68,12 @@ var env environment // configurable, but by default it is once every minute. When an actor // which implements [Resettable] is reset, it is given a configurable // timeout, which is 8 minutes by default. +// +// If one of the actors directly passed to Run implements MainRunnable, +// it will be bound to the main thread and the CAMFISH environment will +// be run in a different thread. If more than one actor implementing +// MainRunnable is passed to the Run function, only the first one is +// considered. // // 80. Shutdown: This can be triggered by all actors being removed from the // environment, a catastrophic error, [Done] being called, or the program