// Package routines provides utilities for managing long-running goroutines. package routines import "io" import "fmt" import "log" import "time" import "sync" // Routine is a long-running function that does not return until it is finished. // An error is returned if the routine exited due to an error. type Routine func () error // Manager is a system capable of managing multiple routines, and restarting // them if they fail. type Manager struct { // Routines specifies a list of routines to manage. These are started // when Run() is called. Routines []Routine // RestartDeadline specifies the amount of time a routine has to be // running before failing to be restarted. This is to prevent routines // that immediately fail from just being restarted over and over again. RestartDeadline time.Duration // Logger, if non-nil, is where log messages will be written to. If it // is nil, messages will be written to the standard logger. To disable // logging altogether, this can be set to io.Discard. Logger io.Writer } // Run spawns all routines in the Routines slice. If a routine exits with an // error and it was running for longer than RestartDeadline, it is restarted. // Run returns only when all routines have exited. func (manager *Manager) Run () error { var waitGroup sync.WaitGroup var errExit error for _, routine := range manager.Routines { if routine != nil { waitGroup.Add(1) go manager.runRoutine(routine, &waitGroup, &errExit) } } waitGroup.Wait() return errExit } // Append adds one or more routines to the Routines slice. This has no effect if // the manager is already running. func (manager *Manager) Append (routines ...Routine) { manager.Routines = append(manager.Routines, routines...) } func (manager *Manager) log (message ...any) { if manager.Logger == nil { log.Println(message...) } else { fmt.Fprintln(manager.Logger, message...) } } func (manager *Manager) runRoutine (routine Routine, group *sync.WaitGroup, errExit *error) { defer group.Done() var err error for { // TODO: recover from panics lastStart := time.Now() err = routine() if err == nil { manager.log("(i) routine exited") break } else { manager.log("XXX routine failed:", err) } if time.Since(lastStart) < manager.RestartDeadline { manager.log("!!! not restarting routine, failed too soon") break } else { manager.log("(i) routine is being restarted") } } if err != nil { *errExit = err } }