REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY
6
Bot Lifecycle
ScuroNeko edited this page 2026-05-20 13:19:27 +03:00

Bot Lifecycle

Russian version: Bot-Lifecycle-RU

This page explains how a Bot is created, configured, started, stopped, and retired. The important rule is that a Bot instance is single-use: configure it fully, run it once, then create a new instance for the next session.

Lifecycle at a glance

  1. Build BotOpts and call NewBot.
  2. Configure the bot instance: prefixes, plugins, middleware, runners, localization, payload defaults, and optional app data.
  3. Start it with RunWithContext(ctx), Run(), or RunWebhookWithContext(...).
  4. Stop runtime by canceling the context or letting the run method return.
  5. Call Close() to release local resources.
  6. Create a new Bot if you need another run.

Creating a bot

NewBot[T](opts) is the construction step. It validates the required options, creates the internal Telegram API client and uploader, initializes loggers, prepares a default draft provider, and calls getMe to verify the token and fetch the bot username.

Construction fails immediately when:

  • opts is nil;
  • opts.Token is empty;
  • Telegram rejects the token or getMe fails during startup.

Useful defaults applied during construction:

  • command prefixes default to "/" when BotOpts.Prefixes is empty;
  • the default callback payload encoding is Base64;
  • MaxWorkers defaults to 32 when not set;
  • the default error template is "%s";
  • a random draft provider is installed unless you replace it.

Configuration before runtime

The normal pattern is to finish all structural configuration before starting the bot:

  • SetAppData(...) injects your shared dependency container or database handle.
  • AddPrefixes(...) extends the accepted command prefixes.
  • SetPayloadType(...) and SetStrictPayloadType(...) control callback payload decoding defaults.
  • SetUpdateTypes(...) and AddUpdateType(...) control which Telegram update kinds are requested.
  • AddPlugins(...) registers command, payload, update, and scene handlers.
  • AddMiddleware(...) adds bot-level middleware, sorted by order and then by name.
  • AddRunner(...) registers background or one-time tasks.
  • SetL10n(...) replaces the localization provider.
  • SetDraftProvider(...), SetSessionStore(...), and SetSceneScopePriority(...) replace runtime helpers.
  • SetErrorTemplate(...) adjusts centralized user-facing error text.

For an overview of handlers and plugins, see Commands-and-Plugins. For context helpers available inside handlers, see MessageContext.

AddPlugins(...) is a configuration commit point

Bot.AddPlugins(...) clones the plugin configuration into the bot. That means the original *Plugin should be treated as finished before registration.

In practice:

  • add commands, payloads, update handlers, scenes, middleware, logger, and OnClose callbacks before AddPlugins(...);
  • do not rely on mutating the original plugin after registration;
  • later edits to the original plugin may not appear in the bot, because the bot keeps its own internal snapshot.

This is especially important for:

  • command descriptions used by auto-generated command metadata;
  • plugin middleware;
  • custom plugin loggers;
  • plugin shutdown hooks.

Configuration freeze phases

Laniakea has three practical configuration phases:

  1. Construction and bot setup after NewBot[T](opts).
  2. Plugin snapshotting at AddPlugins(...).
  3. Runtime freeze after the first Run(), RunWithContext(...), or RunWebhookWithContext(...).

That means:

  • finish plugin structure before AddPlugins(...);
  • finish bot-level structure before the first runtime entry point;
  • treat later structural bot mutations as intentionally ignored.

After runtime starts, structural calls such as AddPlugins(...), AddMiddleware(...), AddRunner(...), AddPrefixes(...), SetPayloadType(...), SetStrictPayloadType(...), SetDraftProvider(...), SetSessionStore(...), SetSceneScopePriority(...), SetL10n(...), SetAppData(...), SetUpdateTypes(...), AddUpdateType(...), and SetErrorTemplate(...) are ignored.

What “ignored” means

Ignored means the method returns without changing the bot's structural runtime state.

Laniakea prefers a predictable no-op over partially applying runtime configuration changes while updates may already be queued or handlers may already be running. That avoids:

  • unclear ordering between configuration edits and update processing;
  • races around shared bot state;
  • confusion about whether a change affects only future updates or also already accepted work;
  • different mental models for plugin snapshots and bot-level state.

Minimal startup pattern

opts := &laniakea.BotOpts{Token: "TOKEN"}

bot, err := laniakea.NewBot[laniakea.NoData](opts)
if err != nil {
	return err
}
defer bot.Close()

plugin := laniakea.NewPlugin[laniakea.NoData]("main")
plugin.NewCommand(func(ctx *laniakea.MessageContext, db laniakea.NoData) error {
	ctx.Answer("pong")
	return nil
}, "ping")

bot.AddPlugins(plugin)

if err := bot.Run(); err != nil {
	return err
}

RunWithContext(...), Run(), and RunWebhookWithContext(...)

RunWithContext(ctx) is the main polling runtime entry point.

It:

  • rejects startup when no prefixes are defined;
  • rejects startup when no plugins are registered;
  • rejects a second start with ErrBotAlreadyRun;
  • starts registered runners;
  • begins long polling with getUpdates;
  • dispatches updates through a worker pool;
  • stops polling when the context is canceled.

Run() is only a shorthand for RunWithContext(context.Background()).

RunWebhookWithContext(...) is the webhook runtime entry point. It shares the same:

  • single-use rule;
  • runner startup behavior;
  • internal update queue;
  • worker-pool dispatch model;
  • graceful shutdown semantics.

Use RunWithContext(...) for production services that poll Telegram directly. Use RunWebhookWithContext(...) when Telegram should deliver updates through your HTTP endpoint.

If you switch an existing deployment from webhook delivery to polling, remove the current webhook first with CloseWebhook() or low-level tgapi.DeleteWebhook(...). Telegram keeps webhook delivery active until the webhook is deleted.

For the webhook-specific option model, transport behavior, and operational guidance, see Webhook-Runtime.

What happens during runtime

After startup, the bot runs three main pieces of work:

  • an update ingress path receives updates from polling or webhook transport;
  • an internal queue buffers updates before processing;
  • a worker pool handles updates concurrently.

Behavior worth knowing:

  • polling retry uses exponential backoff, capped at 30 seconds, when getUpdates keeps failing;
  • the worker pool size is controlled by BotOpts.MaxWorkers;
  • when the shutdown context is canceled, update ingress stops first, then queued work is drained, then runners are awaited.

Runners and shutdown semantics

Runners start from runtime entry points, not from NewBot.

Supported runner modes:

  • one-time async runners start in their own goroutine;
  • one-time sync runners block startup until they finish;
  • repeating async runners run on a ticker until the bot context is canceled.

Important caveats:

  • repeating synchronous runners are treated as invalid and skipped;
  • repeating async runners without a timeout are skipped with a warning;
  • one-time synchronous runners that take more than two seconds log a warning because they delay startup.

Graceful shutdown

Canceling the runtime context tells the bot to stop accepting new work and finish work already accepted into the queue. The runtime then waits for:

  • all queued update handlers to finish;
  • one-time async runners to finish;
  • background runners to exit after noticing ctx.Done().

RunWithContext(...) and RunWebhookWithContext(...) do not automatically release API, uploader, or logger resources. You still need to call Close().

Close() versus CloseRemote()

Close() releases local process resources owned by the bot. It closes, in order:

  • registered plugins via Plugin.Close();
  • the webhook logger when webhook runtime initialized it;
  • the uploader;
  • the local API client internals;
  • the optional request logger;
  • the main bot logger.

Call Close() once you are done with the bot instance, usually with defer right after successful construction.

CloseRemote(ctx) is different. It sends Telegram's Bot API close request for the remote session. It does not replace Close(), and Close() does not replace it.

Use CloseRemote(ctx) only when you specifically need Telegram-side session shutdown behavior. Most bots only need Close().

Single-use rule

A Bot cannot be started twice. After Run(), RunWithContext(...), or RunWebhookWithContext(...) returns, later start attempts fail with ErrBotAlreadyRun.

That means:

  • do not call a runtime entry point again after a graceful stop;
  • do not reuse a bot instance across tests that start it;
  • create a fresh Bot for every new run cycle.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

bot, err := laniakea.NewBot[*sql.DB](opts)
if err != nil {
	return err
}
defer bot.Close()

bot.SetAppData(db)
bot.AddPlugins(plugin)

if err := bot.RunWithContext(ctx); err != nil {
	return err
}

Common mistakes

  • Mutating a plugin after AddPlugins(...) and expecting the bot to see the change.
  • Calling a runtime entry point twice on the same bot instance.
  • Continuing to mutate bot-level structure after runtime already started.
  • Forgetting to call Close() after runtime returns.
  • Registering background runners without a timeout.