145 lines
5.2 KiB
Go
145 lines
5.2 KiB
Go
// Package laniakea provides a system for managing background and one-time
|
|
// runner functions that operate on a Bot instance, with support for
|
|
// asynchronous execution, timeouts, and lifecycle control.
|
|
//
|
|
// Runners are used for periodic tasks (e.g., cleanup, stats updates) or
|
|
// one-time initialization logic. They are executed via Bot.ExecRunners().
|
|
//
|
|
// Important: Runners are not thread-safe for concurrent modification.
|
|
// Builder methods (Onetime, Async, Timeout) must be called sequentially
|
|
// and only before Execute().
|
|
package laniakea
|
|
|
|
import (
|
|
"time"
|
|
)
|
|
|
|
// RunnerFn is the function type for a runner. It receives a pointer to
|
|
// the Bot and returns an error if execution fails.
|
|
type RunnerFn[T DbContext] func(*Bot[T]) error
|
|
|
|
// Runner represents a configurable background or one-time task to be
|
|
// executed by a Bot.
|
|
//
|
|
// Runners are configured using builder methods: Onetime(), Async(), Timeout().
|
|
// Once Execute() is called, the Runner should not be modified.
|
|
//
|
|
// Execution semantics:
|
|
// - onetime=true, async=false: Run once synchronously (blocks).
|
|
// - onetime=true, async=true: Run once in a goroutine (non-blocking).
|
|
// - onetime=false, async=true: Run repeatedly in a goroutine with timeout.
|
|
// - onetime=false, async=false: Invalid configuration — ignored with warning.
|
|
type Runner[T DbContext] struct {
|
|
name string // Human-readable name for logging
|
|
onetime bool // If true, runs once; if false, runs periodically
|
|
async bool // If true, runs in a goroutine; else, runs synchronously
|
|
timeout time.Duration // Duration to wait between periodic executions (ignored if onetime=true)
|
|
fn RunnerFn[T] // The function to execute
|
|
}
|
|
|
|
// NewRunner creates a new Runner with the given name and function.
|
|
// By default, the Runner is configured as async=true (non-blocking).
|
|
//
|
|
// Builder methods (Onetime, Async, Timeout) can be chained to customize behavior.
|
|
// DO NOT call builder methods concurrently or after Execute().
|
|
func NewRunner[T DbContext](name string, fn RunnerFn[T]) *Runner[T] {
|
|
return &Runner[T]{
|
|
name: name,
|
|
fn: fn,
|
|
async: true, // Default: run asynchronously
|
|
timeout: 0, // Default: no timeout (ignored if onetime=true)
|
|
}
|
|
}
|
|
|
|
// Onetime sets whether the runner executes once or repeatedly.
|
|
// If true, the runner runs only once.
|
|
// If false, the runner runs in a loop with the configured timeout.
|
|
func (r *Runner[T]) Onetime(onetime bool) *Runner[T] {
|
|
r.onetime = onetime
|
|
return r
|
|
}
|
|
|
|
// Async sets whether the runner executes synchronously or asynchronously.
|
|
// If true, the runner runs in a goroutine (non-blocking).
|
|
// If false, the runner blocks the caller during execution.
|
|
//
|
|
// Note: If onetime=false and async=false, the runner will be skipped with a warning.
|
|
func (r *Runner[T]) Async(async bool) *Runner[T] {
|
|
r.async = async
|
|
return r
|
|
}
|
|
|
|
// Timeout sets the duration to wait between repeated executions for
|
|
// non-onetime runners.
|
|
//
|
|
// If onetime=true, this value is ignored.
|
|
// If onetime=false and async=true, this timeout determines the sleep interval
|
|
// between loop iterations.
|
|
//
|
|
// A zero value (time.Duration(0)) is allowed but may trigger a warning
|
|
// if used with a background (non-onetime) async runner.
|
|
func (r *Runner[T]) Timeout(timeout time.Duration) *Runner[T] {
|
|
r.timeout = timeout
|
|
return r
|
|
}
|
|
|
|
// ExecRunners executes all runners registered on the Bot.
|
|
//
|
|
// It logs warnings for misconfigured runners:
|
|
// - Sync, non-onetime runners are skipped (invalid configuration).
|
|
// - Background (non-onetime, async) runners without a timeout trigger a warning.
|
|
//
|
|
// Execution logic:
|
|
// - onetime + async: Runs once in a goroutine.
|
|
// - onetime + sync: Runs once synchronously; warns if slower than 2 seconds.
|
|
// - !onetime + async: Runs in an infinite loop with timeout between iterations.
|
|
// - !onetime + sync: Skipped with warning.
|
|
//
|
|
// This method is typically called once during bot startup.
|
|
func (bot *Bot[T]) ExecRunners() {
|
|
bot.logger.Infoln("Executing runners...")
|
|
for _, runner := range bot.runners {
|
|
// Validate configuration
|
|
if !runner.onetime && !runner.async {
|
|
bot.logger.Warnf("Runner %s not onetime, but sync — skipping\n", runner.name)
|
|
continue
|
|
}
|
|
if !runner.onetime && runner.async && runner.timeout == 0 {
|
|
bot.logger.Warnf("Background runner \"%s\" has no timeout — may cause tight loop\n", runner.name)
|
|
}
|
|
|
|
if runner.onetime && runner.async {
|
|
// One-time async: fire and forget
|
|
go func(r Runner[T]) {
|
|
err := r.fn(bot)
|
|
if err != nil {
|
|
bot.logger.Warnf("Runner %s failed: %s\n", r.name, err)
|
|
}
|
|
}(runner)
|
|
} else if runner.onetime && !runner.async {
|
|
// One-time sync: block until done
|
|
t := time.Now()
|
|
err := runner.fn(bot)
|
|
if err != nil {
|
|
bot.logger.Warnf("Runner %s failed: %s\n", runner.name, err)
|
|
}
|
|
elapsed := time.Since(t)
|
|
if elapsed > time.Second*2 {
|
|
bot.logger.Warnf("Runner %s too slow. Elapsed time %v >= 2s\n", runner.name, elapsed)
|
|
}
|
|
} else if !runner.onetime && runner.async {
|
|
// Background loop: periodic execution
|
|
go func(r Runner[T]) {
|
|
for {
|
|
err := r.fn(bot)
|
|
if err != nil {
|
|
bot.logger.Warnf("Runner %s failed: %s\n", r.name, err)
|
|
}
|
|
time.Sleep(r.timeout)
|
|
}
|
|
}(runner)
|
|
}
|
|
// Note: !onetime && !async is already skipped above
|
|
}
|
|
}
|