FILE / ScuroNeko/Laniakea
runners.go
Исходный файл и его история в репозитории.
(fix): polling panic ErrorEvent, nil-logger guard (refactor): inline webhook errors → sentinels, dead Runner branch (doc): Runner godoc, Error godoc, drafts cleanup
174 lines
5.7 KiB
Go
174 lines
5.7 KiB
Go
package laniakea
|
|
|
|
import (
|
|
"context"
|
|
"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 AppData] 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 Async and Every. Once the
|
|
// bot's runtime has started executing the runner, it should not be modified.
|
|
//
|
|
// Execution semantics:
|
|
// - every=0, async=true: Run once in a goroutine (non-blocking, default).
|
|
// - every=0, async=false: Run once synchronously (blocks runtime startup).
|
|
// - every>0, async=true: Run repeatedly in a goroutine with the given interval.
|
|
// - every>0, async=false: Invalid configuration — skipped with a warning.
|
|
type Runner[T AppData] struct {
|
|
name string // Human-readable name for logging
|
|
async bool // If true, runs in a goroutine; else, runs synchronously
|
|
every time.Duration // Interval between periodic executions; zero means one-shot
|
|
fn RunnerFn[T] // The function to execute
|
|
}
|
|
|
|
// NewRunner creates a new Runner with the given name and function.
|
|
//
|
|
// The default configuration is async=true and every=0, i.e. a one-shot
|
|
// goroutine that fires once when the bot runtime starts. Use Async and Every
|
|
// to customize this. Do not call builder methods concurrently or after the
|
|
// bot runtime has begun executing runners.
|
|
func NewRunner[T AppData](name string, fn RunnerFn[T]) Runner[T] {
|
|
return Runner[T]{
|
|
name: name,
|
|
fn: fn,
|
|
async: true,
|
|
every: 0,
|
|
}
|
|
}
|
|
|
|
// 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: periodic runners (Every > 0) require async=true and are skipped with
|
|
// a warning when async=false.
|
|
func (r Runner[T]) Async(async bool) Runner[T] {
|
|
r.async = async
|
|
return r
|
|
}
|
|
|
|
// Every sets the interval between repeated executions of a periodic runner.
|
|
//
|
|
// A zero value (the default) keeps the runner one-shot. A positive value
|
|
// schedules the runner to fire repeatedly with the given interval and
|
|
// requires async=true; periodic sync runners are skipped with a warning.
|
|
func (r Runner[T]) Every(timeout time.Duration) Runner[T] {
|
|
r.every = timeout
|
|
return r
|
|
}
|
|
|
|
// ExecRunners executes all runners registered on the Bot with context-based lifecycle management.
|
|
//
|
|
// Execution semantics by configuration:
|
|
// - every=0, async=true: Runs once in a goroutine (fire and forget).
|
|
// - every=0, async=false: Runs once synchronously; warns if slower than 2 seconds.
|
|
// - every>0, async=true: Runs in a loop with the configured interval until ctx.Done().
|
|
// - every>0, async=false: Skipped with a warning (invalid configuration).
|
|
//
|
|
// Background runners listen for ctx.Done() and gracefully shut down when the context is canceled.
|
|
//
|
|
// This method is typically called once during bot startup from RunWithContext or
|
|
// RunWebhookWithContext.
|
|
func (bot *Bot[T]) ExecRunners(ctx context.Context) {
|
|
bot.logger.Infoln("Executing runners...")
|
|
for _, runner := range bot.runners {
|
|
if runner.every > 0 && !runner.async {
|
|
bot.logger.Warnf("Runner %q is periodic but sync; skipping (use Async(true))\n", runner.name)
|
|
continue
|
|
}
|
|
|
|
if runner.every == 0 && runner.async {
|
|
// One-time async: fire and forget
|
|
bot.runnerOnceWG.Add(1)
|
|
go func(r Runner[T]) {
|
|
defer bot.runnerOnceWG.Done()
|
|
startedAt := time.Now()
|
|
err := r.fn(bot)
|
|
bot.safeEmitEvent(ctx, RunnerFinishedEvent{
|
|
Name: r.name,
|
|
Duration: time.Since(startedAt),
|
|
Err: err,
|
|
})
|
|
if err != nil {
|
|
bot.safeEmitEvent(ctx, ErrorEvent{
|
|
Plugin: "bot",
|
|
HandlerKind: HandlerRunnerKind,
|
|
HandlerName: r.name,
|
|
Err: err,
|
|
UserFacing: false,
|
|
})
|
|
bot.logger.Warnf("Runner %s failed: %s\n", r.name, err)
|
|
}
|
|
}(runner)
|
|
} else if runner.every == 0 && !runner.async {
|
|
// One-time sync: block until done
|
|
t := time.Now()
|
|
err := runner.fn(bot)
|
|
elapsed := time.Since(t)
|
|
bot.safeEmitEvent(ctx, RunnerFinishedEvent{
|
|
Name: runner.name,
|
|
Duration: elapsed,
|
|
Err: err,
|
|
})
|
|
if err != nil {
|
|
bot.safeEmitEvent(ctx, ErrorEvent{
|
|
Plugin: "bot",
|
|
HandlerKind: HandlerRunnerKind,
|
|
HandlerName: runner.name,
|
|
Err: err,
|
|
UserFacing: false,
|
|
})
|
|
bot.logger.Warnf("Runner %s failed: %s\n", runner.name, err)
|
|
}
|
|
if elapsed > time.Second*2 {
|
|
bot.logger.Warnf("Runner %s too slow. Elapsed time %v >= 2s\n", runner.name, elapsed)
|
|
}
|
|
} else if runner.every > 0 && runner.async {
|
|
// Background loop: periodic execution with graceful shutdown
|
|
bot.runnerBgWG.Add(1)
|
|
go func(r Runner[T]) {
|
|
defer bot.runnerBgWG.Done()
|
|
ticker := time.NewTicker(r.every)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
}
|
|
// When both ctx.Done() and ticker.C are ready at the same
|
|
// time, Go's select picks one at random. Re-check ctx so a
|
|
// late tick after cancellation does not fire one extra
|
|
// invocation past shutdown.
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
startedAt := time.Now()
|
|
err := r.fn(bot)
|
|
bot.safeEmitEvent(ctx, RunnerFinishedEvent{
|
|
Name: r.name,
|
|
Duration: time.Since(startedAt),
|
|
Err: err,
|
|
})
|
|
if err != nil {
|
|
bot.safeEmitEvent(ctx, ErrorEvent{
|
|
Plugin: "bot",
|
|
HandlerKind: HandlerRunnerKind,
|
|
HandlerName: r.name,
|
|
Err: err,
|
|
UserFacing: false,
|
|
})
|
|
bot.logger.Warnf("Runner %s failed: %s\n", r.name, err)
|
|
}
|
|
}
|
|
}(runner)
|
|
}
|
|
}
|
|
}
|