REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY
4
Runners
ScuroNeko edited this page 2026-05-20 13:19:27 +03:00

Runners

Russian version: Runners-RU

Runners are background or one-time tasks that start with the bot and live alongside update processing. They are useful for periodic cleanup, maintenance jobs, health checks, and startup work that belongs to the bot process but not to any single update handler.

Overview

Runners are registered with Bot.AddRunner(...) and executed from the bot runtime entry points.

Each runner is built from:

  • a name;
  • a function func(*Bot[T]) error;
  • execution flags configured through builder methods.

Builder methods:

  • Every(time.Duration) — sets the repeat interval. Zero (default) means run once; positive means repeat.
  • Async(bool) — if true (default), the runner runs in a goroutine; if false, it blocks runtime startup.

Creating a runner

Use NewRunner(name, fn) to create a runner.

cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error {
	return cleanupExpiredState(bot.GetAppData())
})

By default, a new runner is:

  • asynchronous (Async(true));
  • one-shot (Every(0)).

That default fires the runner once in a goroutine when the bot starts.

Runner execution modes

There are three meaningful configurations.

One-time asynchronous (default)

runner := laniakea.NewRunner("prefetch", fn)
// or explicitly:
runner := laniakea.NewRunner("prefetch", fn).Every(0).Async(true)

Behavior:

  • runs once;
  • starts in a goroutine;
  • does not block bot startup.

Use this for fire-and-forget startup work that is useful but not required before handling updates.

One-time synchronous

runner := laniakea.NewRunner("warmup", fn).Async(false)

Behavior:

  • runs once;
  • blocks startup until it finishes;
  • logs a warning if it takes longer than two seconds.

Use this for startup work that must complete before the bot is considered ready.

Repeating asynchronous

runner := laniakea.NewRunner("cleanup", fn).Every(time.Minute)

Behavior:

  • runs on a ticker with the configured interval;
  • keeps running until ctx.Done() from the bot runtime context;
  • is awaited during graceful shutdown.

Use this for recurring background jobs.

Invalid configurations

One configuration is intentionally treated as invalid and skipped with a warning:

  • Every(d > 0).Async(false) — a periodic sync runner blocks startup indefinitely, which is never correct.

Registration

Add runners with Bot.AddRunner(...).

bot.AddRunner(cleanupRunner)

Runners with an empty name are skipped with a warning, so always give them a stable, readable name.

Lifecycle

Runners are not started by NewBot(...). They start from RunWithContext(...) or RunWebhookWithContext(...), right before the bot begins polling or webhook ingestion.

That means runner execution belongs to the bot's runtime lifecycle, not to its configuration phase.

High-level order:

  1. validate bot startup;
  2. start runners;
  3. begin polling or webhook ingress;
  4. process updates concurrently;
  5. cancel context to stop ingress and let runners exit.

For the broader bot runtime model, see Bot-Lifecycle.

Error handling

Runner functions return error.

When a runner returns a non-nil error:

  • the bot logs a warning;
  • an ErrorEvent is emitted through the observer;
  • the process continues;
  • the bot does not crash automatically.

This is useful for periodic jobs where failure should be visible but not fatal.

If a runner must be fatal for startup, make it one-time synchronous and return an error that your process architecture handles outside the runner itself.

Shutdown behavior

RunWithContext(...) and RunWebhookWithContext(...) wait for runner completion in two groups:

  • one-time async runners;
  • background repeating runners.

Repeating runners observe ctx.Done() and exit when the bot context is canceled.

This means graceful shutdown includes runner shutdown, but only if your runner function itself returns promptly and does not ignore cancellation indirectly.

Practical patterns

Periodic cleanup

cleanup := laniakea.NewRunner("cleanup", func(bot *laniakea.Bot[*App]) error {
	return bot.GetAppData().CleanupExpired()
}).Every(5 * time.Minute)

Startup warmup (blocking)

warmup := laniakea.NewRunner("warmup", func(bot *laniakea.Bot[*App]) error {
	return bot.GetAppData().WarmCaches()
}).Async(false)

Background metrics push

metrics := laniakea.NewRunner("metrics", func(bot *laniakea.Bot[*App]) error {
	return pushMetrics(bot.GetAppData())
}).Every(30 * time.Second)

Recommendations

  • Use one-time sync runners only for short startup-critical work.
  • Use repeating async runners for periodic jobs.
  • Always set Every(...) on repeating runners.
  • Keep runner bodies small and delegate complex work to regular application services.
  • Treat runner names as operational identifiers that should make sense in logs.

Caveats

  • Runners do not receive context.Context directly; they receive *Bot[T].
  • Periodic sync runners are skipped with a warning.
  • Slow one-time sync runners delay bot startup.