Skip to content

ADR-015: One bot process — long polling, worker and schedulers together, guarded by a lock

Developers — read this before splitting the worker out, adding webhooks or running a second bot instance.

  • main.py runs polling, the queue worker, the delivery sweeper, the TTL cleanup and the leads loop as tasks of one asyncio program.
  • Telegram updates arrive by long polling, not webhooks.
  • A lock file prevents a second instance. Shutdown is ordered and returns unfinished jobs to the queue.
  • Status: accepted for one station.

Two bot processes with the same token fight over updates, and two workers with different configuration take jobs from the same queue. This happened during development: an old process started before a configuration change picked up a job and rendered it in the wrong mode. Separately, the process used to hang on stop until it was killed, because only polling stopped while the other loops kept running.

  • One process, several tasks. If any task ends or crashes, the process shuts down with a non-zero code and systemd restarts it (bot/main.py, serve).
  • Long polling. The bot needs no public endpoint; the API process sends only one-off messages with the same token and never polls.
  • Instance lock. data/main.lock is taken at start; a second process logs that one is already running and exits (bot/instance_lock.py).
  • Ordered shutdown on SIGINT/SIGTERM: stop polling → take no new jobs → give active jobs 15 seconds → return the rest to the queue without spending an attempt → cancel the loops → cancel delivery checks (jobs stay delivering and are picked up after restart) → close the bot session. systemd waits 30 seconds.
  • Blocking work off the loop. AI requests run in daemon threads; image processing runs in worker threads with its own concurrency limit.
Alternative Why not (now)
Webhooks Need a public route and secret handling; polling is enough for the message volume of an event.
Worker as a separate service Cleaner scaling, but one more unit, and SQLite keeps everything on one machine anyway.
Several bot instances Impossible with polling on one token.
  • A crash in any loop restarts everything; loops that must not die catch their own errors (cleanup, leads).
  • Image processing shares memory and CPU with the bot; the unit’s memory limits are sized for the pack peak.
  • Before starting a local bot with the production token, the server’s service must be stopped — and the other way round. The event checklist includes this.
  • Scaling out means separating the worker and replacing polling or keeping exactly one poller — see Scaling scenarios.

Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.